mirror of
https://github.com/serai-dex/serai.git
synced 2025-03-23 23:58:53 +00:00
Finish binding Tendermint, bar the P2P layer
This commit is contained in:
parent
997dd611d5
commit
03a6470a5b
7 changed files with 192 additions and 37 deletions
|
@ -21,8 +21,14 @@ transcript = { package = "flexible-transcript", path = "../../crypto/transcript"
|
||||||
ciphersuite = { package = "ciphersuite", path = "../../crypto/ciphersuite", features = ["ristretto"] }
|
ciphersuite = { package = "ciphersuite", path = "../../crypto/ciphersuite", features = ["ristretto"] }
|
||||||
schnorr = { package = "schnorr-signatures", path = "../../crypto/schnorr" }
|
schnorr = { package = "schnorr-signatures", path = "../../crypto/schnorr" }
|
||||||
|
|
||||||
|
hex = "0.4"
|
||||||
|
log = "0.4"
|
||||||
|
|
||||||
|
scale = { package = "parity-scale-codec", version = "3", features = ["derive"] }
|
||||||
tendermint = { package = "tendermint-machine", path = "./tendermint" }
|
tendermint = { package = "tendermint-machine", path = "./tendermint" }
|
||||||
|
|
||||||
|
tokio = { version = "1", features = ["macros", "sync", "time", "rt"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
zeroize = "^1.5"
|
zeroize = "^1.5"
|
||||||
rand_core = "0.6"
|
rand_core = "0.6"
|
||||||
|
|
|
@ -48,7 +48,7 @@ impl ReadWrite for BlockHeader {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BlockHeader {
|
impl BlockHeader {
|
||||||
fn hash(&self) -> [u8; 32] {
|
pub fn hash(&self) -> [u8; 32] {
|
||||||
Blake2s256::digest([b"tributary_block".as_ref(), &self.serialize()].concat()).into()
|
Blake2s256::digest([b"tributary_block".as_ref(), &self.serialize()].concat()).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,12 +55,8 @@ impl<T: Transaction> Blockchain<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a block.
|
/// Add a block.
|
||||||
#[must_use]
|
pub fn add_block(&mut self, block: &Block<T>) -> Result<(), BlockError> {
|
||||||
pub fn add_block(&mut self, block: &Block<T>) -> bool {
|
self.verify_block(block)?;
|
||||||
// TODO: Handle desyncs re: provided transactions properly
|
|
||||||
if self.verify_block(block).is_err() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// None of the following assertions should be reachable since we verified the block
|
// None of the following assertions should be reachable since we verified the block
|
||||||
self.tip = block.hash();
|
self.tip = block.hash();
|
||||||
|
@ -85,6 +81,6 @@ impl<T: Transaction> Blockchain<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
use core::ops::Deref;
|
use core::ops::Deref;
|
||||||
|
use std::{sync::Arc, collections::HashMap};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use subtle::ConstantTimeEq;
|
use subtle::ConstantTimeEq;
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
@ -14,7 +17,18 @@ use ciphersuite::{
|
||||||
};
|
};
|
||||||
use schnorr::SchnorrSignature;
|
use schnorr::SchnorrSignature;
|
||||||
|
|
||||||
use tendermint::ext::{Signer as SignerTrait, SignatureScheme as SignatureSchemeTrait};
|
use scale::{Encode, Decode};
|
||||||
|
use tendermint::{
|
||||||
|
SignedMessageFor,
|
||||||
|
ext::{
|
||||||
|
BlockNumber, RoundNumber, Signer as SignerTrait, SignatureScheme, Weights, Block as BlockTrait,
|
||||||
|
BlockError as TendermintBlockError, Commit, Network as NetworkTrait,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use tokio::time::{Duration, sleep};
|
||||||
|
|
||||||
|
use crate::{ReadWrite, Transaction, TransactionError, BlockHeader, Block, BlockError, Blockchain};
|
||||||
|
|
||||||
fn challenge(
|
fn challenge(
|
||||||
genesis: [u8; 32],
|
genesis: [u8; 32],
|
||||||
|
@ -37,7 +51,7 @@ struct Signer {
|
||||||
key: Zeroizing<<Ristretto as Ciphersuite>::F>,
|
key: Zeroizing<<Ristretto as Ciphersuite>::F>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait]
|
||||||
impl SignerTrait for Signer {
|
impl SignerTrait for Signer {
|
||||||
type ValidatorId = [u8; 32];
|
type ValidatorId = [u8; 32];
|
||||||
type Signature = [u8; 64];
|
type Signature = [u8; 64];
|
||||||
|
@ -85,19 +99,25 @@ impl SignerTrait for Signer {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
struct SignatureScheme {
|
struct Validators {
|
||||||
genesis: [u8; 32],
|
genesis: [u8; 32],
|
||||||
|
weight: u64,
|
||||||
|
weights: HashMap<[u8; 32], u64>,
|
||||||
|
robin: Vec<[u8; 32]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignatureSchemeTrait for SignatureScheme {
|
impl SignatureScheme for Validators {
|
||||||
type ValidatorId = [u8; 32];
|
type ValidatorId = [u8; 32];
|
||||||
type Signature = [u8; 64];
|
type Signature = [u8; 64];
|
||||||
// TODO: Use half-aggregation.
|
// TODO: Use half-aggregation.
|
||||||
type AggregateSignature = Vec<[u8; 64]>;
|
type AggregateSignature = Vec<[u8; 64]>;
|
||||||
type Signer = Signer;
|
type Signer = Arc<Signer>;
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn verify(&self, validator: Self::ValidatorId, msg: &[u8], sig: &Self::Signature) -> bool {
|
fn verify(&self, validator: Self::ValidatorId, msg: &[u8], sig: &Self::Signature) -> bool {
|
||||||
|
if !self.weights.contains_key(&validator) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let Ok(validator_point) = Ristretto::read_G::<&[u8]>(&mut validator.as_ref()) else {
|
let Ok(validator_point) = Ristretto::read_G::<&[u8]>(&mut validator.as_ref()) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
@ -126,3 +146,122 @@ impl SignatureSchemeTrait for SignatureScheme {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Weights for Validators {
|
||||||
|
type ValidatorId = [u8; 32];
|
||||||
|
|
||||||
|
fn total_weight(&self) -> u64 {
|
||||||
|
self.weight
|
||||||
|
}
|
||||||
|
fn weight(&self, validator: Self::ValidatorId) -> u64 {
|
||||||
|
self.weights[&validator]
|
||||||
|
}
|
||||||
|
fn proposer(&self, block: BlockNumber, round: RoundNumber) -> Self::ValidatorId {
|
||||||
|
let block = usize::try_from(block.0).unwrap();
|
||||||
|
let round = usize::try_from(round.0).unwrap();
|
||||||
|
// If multiple rounds are used, a naive block + round would cause the same index to be chosen
|
||||||
|
// in quick succesion.
|
||||||
|
// Accordingly, if we use additional rounds, jump halfway around.
|
||||||
|
// While this is still game-able, it's not explicitly reusing indexes immediately after each
|
||||||
|
// other.
|
||||||
|
self.robin
|
||||||
|
[(block + (if round == 0 { 0 } else { round + (self.robin.len() / 2) })) % self.robin.len()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
|
||||||
|
struct TendermintBlock(Vec<u8>);
|
||||||
|
impl BlockTrait for TendermintBlock {
|
||||||
|
type Id = [u8; 32];
|
||||||
|
fn id(&self) -> Self::Id {
|
||||||
|
BlockHeader::read::<&[u8]>(&mut self.0.as_ref()).unwrap().hash()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
struct Network<T: Transaction> {
|
||||||
|
genesis: [u8; 32],
|
||||||
|
signer: Arc<Signer>,
|
||||||
|
validators: Arc<Validators>,
|
||||||
|
blockchain: Blockchain<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T: Transaction> NetworkTrait for Network<T> {
|
||||||
|
type ValidatorId = [u8; 32];
|
||||||
|
type SignatureScheme = Arc<Validators>;
|
||||||
|
type Weights = Arc<Validators>;
|
||||||
|
type Block = TendermintBlock;
|
||||||
|
|
||||||
|
const BLOCK_PROCESSING_TIME: u32 = 3;
|
||||||
|
const LATENCY_TIME: u32 = 1;
|
||||||
|
|
||||||
|
fn signer(&self) -> Arc<Signer> {
|
||||||
|
self.signer.clone()
|
||||||
|
}
|
||||||
|
fn signature_scheme(&self) -> Arc<Validators> {
|
||||||
|
self.validators.clone()
|
||||||
|
}
|
||||||
|
fn weights(&self) -> Arc<Validators> {
|
||||||
|
self.validators.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn broadcast(&mut self, _msg: SignedMessageFor<Self>) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
async fn slash(&mut self, validator: Self::ValidatorId) {
|
||||||
|
log::error!(
|
||||||
|
"validator {} was slashed on tributary {}",
|
||||||
|
hex::encode(validator),
|
||||||
|
hex::encode(self.genesis)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate(&mut self, block: &Self::Block) -> Result<(), TendermintBlockError> {
|
||||||
|
let block =
|
||||||
|
Block::read::<&[u8]>(&mut block.0.as_ref()).map_err(|_| TendermintBlockError::Fatal)?;
|
||||||
|
self.blockchain.verify_block(&block).map_err(|e| match e {
|
||||||
|
BlockError::TransactionError(TransactionError::MissingProvided(_)) => {
|
||||||
|
TendermintBlockError::Temporal
|
||||||
|
}
|
||||||
|
_ => TendermintBlockError::Fatal,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_block(
|
||||||
|
&mut self,
|
||||||
|
block: Self::Block,
|
||||||
|
_commit: Commit<Self::SignatureScheme>,
|
||||||
|
) -> Option<Self::Block> {
|
||||||
|
let invalid_block = || {
|
||||||
|
// There's a fatal flaw in the code, it's behind a hard fork, or the validators turned
|
||||||
|
// malicious
|
||||||
|
// All justify a halt to then achieve social consensus from
|
||||||
|
// TODO: Under multiple validator sets, a small validator set turning malicious knocks
|
||||||
|
// off the entire network. That's an unacceptable DoS.
|
||||||
|
panic!("validators added invalid block to tributary {}", hex::encode(self.genesis));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(block) = Block::read::<&[u8]>(&mut block.0.as_ref()) else {
|
||||||
|
return invalid_block();
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match self.blockchain.add_block(&block) {
|
||||||
|
Ok(()) => break,
|
||||||
|
Err(BlockError::TransactionError(TransactionError::MissingProvided(hash))) => {
|
||||||
|
log::error!(
|
||||||
|
"missing provided transaction {} which other validators on tributary {} had",
|
||||||
|
hex::encode(hash),
|
||||||
|
hex::encode(self.genesis)
|
||||||
|
);
|
||||||
|
sleep(Duration::from_secs(30)).await;
|
||||||
|
}
|
||||||
|
_ => return invalid_block(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle the commit and return the next proposal
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ fn block_addition() {
|
||||||
assert_eq!(block.header.parent, genesis);
|
assert_eq!(block.header.parent, genesis);
|
||||||
assert_eq!(block.header.transactions, [0; 32]);
|
assert_eq!(block.header.transactions, [0; 32]);
|
||||||
blockchain.verify_block(&block).unwrap();
|
blockchain.verify_block(&block).unwrap();
|
||||||
assert!(blockchain.add_block(&block));
|
assert!(blockchain.add_block(&block).is_ok());
|
||||||
assert_eq!(blockchain.tip(), block.hash());
|
assert_eq!(blockchain.tip(), block.hash());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,7 +155,7 @@ fn signed_transaction() {
|
||||||
|
|
||||||
// Verify and add the block
|
// Verify and add the block
|
||||||
blockchain.verify_block(&block).unwrap();
|
blockchain.verify_block(&block).unwrap();
|
||||||
assert!(blockchain.add_block(&block));
|
assert!(blockchain.add_block(&block).is_ok());
|
||||||
assert_eq!(blockchain.tip(), block.hash());
|
assert_eq!(blockchain.tip(), block.hash());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -194,11 +194,11 @@ fn provided_transaction() {
|
||||||
blockchain.verify_block(&block).unwrap();
|
blockchain.verify_block(&block).unwrap();
|
||||||
|
|
||||||
// add_block should work for verified blocks
|
// add_block should work for verified blocks
|
||||||
assert!(blockchain.add_block(&block));
|
assert!(blockchain.add_block(&block).is_ok());
|
||||||
|
|
||||||
let block = Block::new(blockchain.tip(), &txs, HashMap::new());
|
let block = Block::new(blockchain.tip(), &txs, HashMap::new());
|
||||||
// The provided transaction should no longer considered provided, causing this error
|
// The provided transaction should no longer considered provided, causing this error
|
||||||
assert!(blockchain.verify_block(&block).is_err());
|
assert!(blockchain.verify_block(&block).is_err());
|
||||||
// add_block should fail for unverified provided transactions if told to add them
|
// add_block should fail for unverified provided transactions if told to add them
|
||||||
assert!(!blockchain.add_block(&block));
|
assert!(blockchain.add_block(&block).is_err());
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,18 @@ use crate::ReadWrite;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
||||||
pub enum TransactionError {
|
pub enum TransactionError {
|
||||||
/// This transaction was perceived as invalid against the current state.
|
/// A provided transaction wasn't locally provided.
|
||||||
#[error("transaction temporally invalid")]
|
#[error("provided transaction wasn't locally provided")]
|
||||||
Temporal,
|
MissingProvided([u8; 32]),
|
||||||
/// This transaction is definitively invalid.
|
/// This transaction's signer isn't a participant.
|
||||||
#[error("transaction definitively invalid")]
|
#[error("invalid signer")]
|
||||||
Fatal,
|
InvalidSigner,
|
||||||
|
/// This transaction's nonce isn't the prior nonce plus one.
|
||||||
|
#[error("invalid nonce")]
|
||||||
|
InvalidNonce,
|
||||||
|
/// This transaction's signature is invalid.
|
||||||
|
#[error("invalid signature")]
|
||||||
|
InvalidSignature,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data for a signed transaction.
|
/// Data for a signed transaction.
|
||||||
|
@ -100,24 +106,25 @@ pub(crate) fn verify_transaction<T: Transaction>(
|
||||||
|
|
||||||
match tx.kind() {
|
match tx.kind() {
|
||||||
TransactionKind::Provided => {
|
TransactionKind::Provided => {
|
||||||
if !locally_provided.remove(&tx.hash()) {
|
let hash = tx.hash();
|
||||||
Err(TransactionError::Temporal)?;
|
if !locally_provided.remove(&hash) {
|
||||||
|
Err(TransactionError::MissingProvided(hash))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TransactionKind::Unsigned => {}
|
TransactionKind::Unsigned => {}
|
||||||
TransactionKind::Signed(Signed { signer, nonce, signature }) => {
|
TransactionKind::Signed(Signed { signer, nonce, signature }) => {
|
||||||
if let Some(next_nonce) = next_nonces.get(signer) {
|
if let Some(next_nonce) = next_nonces.get(signer) {
|
||||||
if nonce != next_nonce {
|
if nonce != next_nonce {
|
||||||
Err(TransactionError::Temporal)?;
|
Err(TransactionError::InvalidNonce)?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not a participant
|
// Not a participant
|
||||||
Err(TransactionError::Fatal)?;
|
Err(TransactionError::InvalidSigner)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Use Schnorr half-aggregation and a batch verification here
|
// TODO: Use Schnorr half-aggregation and a batch verification here
|
||||||
if !signature.verify(*signer, tx.sig_hash(genesis)) {
|
if !signature.verify(*signer, tx.sig_hash(genesis)) {
|
||||||
Err(TransactionError::Fatal)?;
|
Err(TransactionError::InvalidSignature)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
next_nonces.insert(*signer, nonce + 1);
|
next_nonces.insert(*signer, nonce + 1);
|
||||||
|
|
|
@ -241,23 +241,30 @@ pub trait Network: Send + Sync {
|
||||||
commit.validators.iter().map(|v| weights.weight(*v)).sum::<u64>() >= weights.threshold()
|
commit.validators.iter().map(|v| weights.weight(*v)).sum::<u64>() >= weights.threshold()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Broadcast a message to the other validators. If authenticated channels have already been
|
/// Broadcast a message to the other validators.
|
||||||
/// established, this will double-authenticate. Switching to unauthenticated channels in a system
|
///
|
||||||
/// already providing authenticated channels is not recommended as this is a minor, temporal
|
/// If authenticated channels have already been established, this will double-authenticate.
|
||||||
/// inefficiency while downgrading channels may have wider implications.
|
/// Switching to unauthenticated channels in a system already providing authenticated channels is
|
||||||
|
/// not recommended as this is a minor, temporal inefficiency, while downgrading channels may
|
||||||
|
/// have wider implications.
|
||||||
async fn broadcast(&mut self, msg: SignedMessageFor<Self>);
|
async fn broadcast(&mut self, msg: SignedMessageFor<Self>);
|
||||||
|
|
||||||
/// Trigger a slash for the validator in question who was definitively malicious.
|
/// Trigger a slash for the validator in question who was definitively malicious.
|
||||||
|
///
|
||||||
/// The exact process of triggering a slash is undefined and left to the network as a whole.
|
/// The exact process of triggering a slash is undefined and left to the network as a whole.
|
||||||
async fn slash(&mut self, validator: Self::ValidatorId);
|
async fn slash(&mut self, validator: Self::ValidatorId);
|
||||||
|
|
||||||
/// Validate a block.
|
/// Validate a block.
|
||||||
async fn validate(&mut self, block: &Self::Block) -> Result<(), BlockError>;
|
async fn validate(&mut self, block: &Self::Block) -> Result<(), BlockError>;
|
||||||
/// Add a block, returning the proposal for the next one. It's possible a block, which was never
|
|
||||||
/// validated or even failed validation, may be passed here if a supermajority of validators did
|
/// Add a block, returning the proposal for the next one.
|
||||||
/// consider it valid and created a commit for it. This deviates from the paper which will have a
|
///
|
||||||
/// local node refuse to decide on a block it considers invalid. This library acknowledges the
|
/// It's possible a block, which was never validated or even failed validation, may be passed
|
||||||
/// network did decide on it, leaving handling of it to the network, and outside of this scope.
|
/// here if a supermajority of validators did consider it valid and created a commit for it.
|
||||||
|
///
|
||||||
|
/// This deviates from the paper which will have a local node refuse to decide on a block it
|
||||||
|
/// considers invalid. This library acknowledges the network did decide on it, leaving handling
|
||||||
|
/// of it to the network, and outside of this scope.
|
||||||
async fn add_block(
|
async fn add_block(
|
||||||
&mut self,
|
&mut self,
|
||||||
block: Self::Block,
|
block: Self::Block,
|
||||||
|
|
Loading…
Reference in a new issue