diff --git a/substrate/tendermint/README.md b/substrate/tendermint/README.md index 8f0fd5e1..0135adbb 100644 --- a/substrate/tendermint/README.md +++ b/substrate/tendermint/README.md @@ -3,10 +3,34 @@ An implementation of the Tendermint state machine in Rust. This is solely the state machine, intended to be mapped to any arbitrary system. -It supports an arbitrary hash function, signature protocol, and block -definition accordingly. It is not intended to work with the Cosmos SDK, solely -be an implementation of the -[academic protocol](https://arxiv.org/pdf/1807.04938.pdf). +It supports an arbitrary signature scheme, weighting, and block definition +accordingly. It is not intended to work with the Cosmos SDK, solely to be an +implementation of the [academic protocol](https://arxiv.org/pdf/1807.04938.pdf). + +### Caveats + +- Only SCALE serialization is supported currently. Ideally, everything from + SCALE to borsh to bincode would be supported. SCALE was chosen due to this + being under Serai, which uses Substrate, which uses SCALE. Accordingly, when + deciding which of the three (mutually incompatible) options to support... + +- tokio is explicitly used for the asynchronous task which runs the Tendermint + machine. Ideally, `futures-rs` would be used enabling any async runtime to be + used. + +- It is possible for `add_block` to be called on a block which failed (or never + went through in the first place) validation. This is a break from the paper + which is accepted here. This is for two reasons. + + 1) Serai needing this functionality. + 2) If a block is committed which is invalid, either there's a malicious + majority now defining consensus OR the local node is malicious by virtue of + being faulty. Considering how either represents a fatal circumstance, + except with regards to system like Serai which have their own logic for + pseudo-valid blocks, it is accepted as a possible behavior with the caveat + any consumers must be aware of it. No machine will vote nor precommit to a + block it considers invalid, so for a network with an honest majority, this + is a non-issue. ### Paper @@ -41,3 +65,6 @@ The included pseudocode segments can be minimally described as follows: 61-64 on timeout prevote 65-67 on timeout precommit ``` + +The corresponding Rust code implementing these tasks are marked with their +related line numbers. diff --git a/substrate/tendermint/src/ext.rs b/substrate/tendermint/src/ext.rs index 748150d7..8bc19f8c 100644 --- a/substrate/tendermint/src/ext.rs +++ b/substrate/tendermint/src/ext.rs @@ -5,6 +5,8 @@ use parity_scale_codec::{Encode, Decode}; use crate::SignedMessage; +/// An alias for a series of traits required for a type to be usable as a validator ID, +/// automatically implemented for all types satisfying those traits. pub trait ValidatorId: Send + Sync + Clone + Copy + PartialEq + Eq + Hash + Debug + Encode + Decode { @@ -14,48 +16,73 @@ impl Signature for S {} // Type aliases which are distinct according to the type system -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode)] -pub struct BlockNumber(pub u32); -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode)] -pub struct Round(pub u16); +/// A struct containing a Block Number, wrapped to have a distinct type. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode)] +pub struct BlockNumber(pub u64); +/// A struct containing a round number, wrapped to have a distinct type. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode)] +pub struct Round(pub u32); + +/// A signature scheme used by validators. pub trait SignatureScheme: Send + Sync { + // Type used to identify validators. type ValidatorId: ValidatorId; + /// Signature type. type Signature: Signature; + /// Type representing an aggregate signature. This would presumably be a BLS signature, + /// yet even with Schnorr signatures + /// [half-aggregation is possible](https://eprint.iacr.org/2021/350). + /// It could even be a threshold signature scheme, though that's currently unexpected. type AggregateSignature: Signature; + /// Sign a signature with the current validator's private key. fn sign(&self, msg: &[u8]) -> Self::Signature; + /// Verify a signature from the validator in question. #[must_use] fn verify(&self, validator: Self::ValidatorId, msg: &[u8], sig: Self::Signature) -> bool; + /// Aggregate signatures. fn aggregate(sigs: &[Self::Signature]) -> Self::AggregateSignature; + /// Verify an aggregate signature for the list of signers. #[must_use] fn verify_aggregate( &self, msg: &[u8], signers: &[Self::ValidatorId], - sig: Self::AggregateSignature, + sig: &Self::AggregateSignature, ) -> bool; } +/// A commit for a specific block. The list of validators have weight exceeding the threshold for +/// a valid commit. #[derive(Clone, PartialEq, Debug, Encode, Decode)] pub struct Commit { + /// Validators participating in the signature. pub validators: Vec, + /// Aggregate signature. pub signature: S::AggregateSignature, } +/// Weights for the validators present. pub trait Weights: Send + Sync { type ValidatorId: ValidatorId; + /// Total weight of all validators. fn total_weight(&self) -> u64; + /// Weight for a specific validator. fn weight(&self, validator: Self::ValidatorId) -> u64; + /// Threshold needed for BFT consensus. fn threshold(&self) -> u64 { ((self.total_weight() * 2) / 3) + 1 } + /// Threshold preventing BFT consensus. fn fault_thresold(&self) -> u64 { (self.total_weight() - self.threshold()) + 1 } @@ -64,41 +91,59 @@ pub trait Weights: Send + Sync { fn proposer(&self, number: BlockNumber, round: Round) -> Self::ValidatorId; } +/// Simplified error enum representing a block's validity. #[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode)] pub enum BlockError { - // Invalid behavior entirely + /// Malformed block which is wholly invalid. Fatal, - // Potentially valid behavior dependent on unsynchronized state + /// Valid block by syntax, with semantics which may or may not be valid yet are locally + /// considered invalid. If a block fails to validate with this, a slash will not be triggered. Temporal, } +/// Trait representing a Block. pub trait Block: Send + Sync + Clone + PartialEq + Debug + Encode + Decode { + // Type used to identify blocks. Presumably a cryptographic hash of the block. type Id: Send + Sync + Copy + Clone + PartialEq + Debug + Encode + Decode; + /// Return the deterministic, unique ID for this block. fn id(&self) -> Self::Id; } +/// Trait representing the distributed system Tendermint is providing consensus over. #[async_trait::async_trait] pub trait Network: Send + Sync { + // Type used to identify validators. type ValidatorId: ValidatorId; + /// Signature scheme used by validators. type SignatureScheme: SignatureScheme; + /// Object representing the weights of validators. type Weights: Weights; + /// Type used for ordered blocks of information. type Block: Block; // Block time in seconds const BLOCK_TIME: u32; + /// Return the signature scheme in use. The instance is expected to have the validators' public + /// keys, along with an instance of the private key of the current validator. fn signature_scheme(&self) -> Arc; + /// Return a reference to the validators' weights. fn weights(&self) -> Arc; + /// Verify a commit for a given block. Intended for use when syncing or when not an active + /// validator. #[must_use] fn verify_commit( &self, id: ::Id, - commit: Commit, + commit: &Commit, ) -> bool { - if !self.signature_scheme().verify_aggregate(&id.encode(), &commit.validators, commit.signature) - { + if !self.signature_scheme().verify_aggregate( + &id.encode(), + &commit.validators, + &commit.signature, + ) { return false; } @@ -106,6 +151,10 @@ pub trait Network: Send + Sync { commit.validators.iter().map(|v| weights.weight(*v)).sum::() >= weights.threshold() } + /// Broadcast a message to the other validators. If authenticated channels have already been + /// 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 + /// inefficiency while downgrading channels may have wider implications. async fn broadcast( &mut self, msg: SignedMessage< @@ -115,11 +164,17 @@ pub trait Network: Send + Sync { >, ); - // TODO: Should this take a verifiable reason? + /// 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. async fn slash(&mut self, validator: Self::ValidatorId); + /// Validate a block. fn validate(&mut self, block: &Self::Block) -> Result<(), BlockError>; - // Add a block and return the proposal for the next one + /// 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 + /// 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. fn add_block(&mut self, block: Self::Block, commit: Commit) -> Self::Block; } diff --git a/substrate/tendermint/src/lib.rs b/substrate/tendermint/src/lib.rs index 7b398bfc..75f9de30 100644 --- a/substrate/tendermint/src/lib.rs +++ b/substrate/tendermint/src/lib.rs @@ -16,6 +16,7 @@ use tokio::{ }, }; +/// Traits and types of the external network being integrated with to provide consensus over. pub mod ext; use ext::*; @@ -59,7 +60,7 @@ impl Data { } #[derive(Clone, PartialEq, Debug, Encode, Decode)] -pub struct Message { +struct Message { sender: V, number: BlockNumber, @@ -68,18 +69,20 @@ pub struct Message { data: Data, } +/// A signed Tendermint consensus message to be broadcast to the other validators. #[derive(Clone, PartialEq, Debug, Encode, Decode)] pub struct SignedMessage { msg: Message, sig: S, } -#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode)] -pub enum TendermintError { +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum TendermintError { Malicious(V), Temporal, } +/// A machine executing the Tendermint protocol. pub struct TendermintMachine { network: Arc>, signer: Arc, @@ -100,19 +103,21 @@ pub struct TendermintMachine { timeouts: HashMap, } +/// A handle to an asynchronous task, along with a channel to inform of it of messages received. pub struct TendermintHandle { - // Messages received + /// Channel to send messages received from the P2P layer. pub messages: mpsc::Sender< SignedMessage::Signature>, >, - // Async task executing the machine + /// Handle for the asynchronous task executing the machine. The task will automatically exit + /// when the channel is dropped. pub handle: JoinHandle<()>, } impl TendermintMachine { fn timeout(&self, step: Step) -> Instant { let mut round_time = Duration::from_secs(N::BLOCK_TIME.into()); - round_time *= (self.round.0 + 1).into(); + round_time *= self.round.0 + 1; let step_time = round_time / 3; let offset = match step { @@ -183,6 +188,8 @@ impl TendermintMachine { self.round(Round(0)).await; } + /// Create a new Tendermint machine, for the specified proposer, from the specified block, with + /// the specified block as the one to propose next, returning a handle for the machine. // 10 #[allow(clippy::new_ret_no_self)] pub fn new( @@ -262,10 +269,10 @@ impl TendermintMachine { sigs.push(sig); } - let proposal = machine.network.write().await.add_block( - block, - Commit { validators, signature: N::SignatureScheme::aggregate(&sigs) }, - ); + let commit = + Commit { validators, signature: N::SignatureScheme::aggregate(&sigs) }; + debug_assert!(machine.network.read().await.verify_commit(block.id(), &commit)); + let proposal = machine.network.write().await.add_block(block, commit); machine.reset(proposal).await } Err(TendermintError::Malicious(validator)) => { @@ -385,12 +392,10 @@ impl TendermintMachine { } } else { // 22-27 - self - .network - .write() - .await - .validate(block) - .map_err(|_| TendermintError::Malicious(msg.sender))?; + self.network.write().await.validate(block).map_err(|e| match e { + BlockError::Temporal => TendermintError::Temporal, + BlockError::Fatal => TendermintError::Malicious(msg.sender), + })?; debug_assert!(self .broadcast(Data::Prevote(Some(block.id()).filter(|_| self.locked.is_none() || self.locked.as_ref().map(|locked| locked.1.id()) == Some(block.id())))) diff --git a/substrate/tendermint/tests/ext.rs b/substrate/tendermint/tests/ext.rs index 478e213e..16bf2bce 100644 --- a/substrate/tendermint/tests/ext.rs +++ b/substrate/tendermint/tests/ext.rs @@ -32,7 +32,12 @@ impl SignatureScheme for TestSignatureScheme { } #[must_use] - fn verify_aggregate(&self, msg: &[u8], signers: &[TestValidatorId], sigs: Vec<[u8; 32]>) -> bool { + fn verify_aggregate( + &self, + msg: &[u8], + signers: &[TestValidatorId], + sigs: &Vec<[u8; 32]>, + ) -> bool { assert_eq!(signers.len(), sigs.len()); for sig in signers.iter().zip(sigs.iter()) { assert!(self.verify(*sig.0, msg, *sig.1)); @@ -53,7 +58,7 @@ impl Weights for TestWeights { } fn proposer(&self, number: BlockNumber, round: Round) -> TestValidatorId { - TestValidatorId::try_from((number.0 + u32::from(round.0)) % 4).unwrap() + TestValidatorId::try_from((number.0 + u64::from(round.0)) % 4).unwrap() } } @@ -108,7 +113,7 @@ impl Network for TestNetwork { fn add_block(&mut self, block: TestBlock, commit: Commit) -> TestBlock { dbg!("Adding ", &block); assert!(block.valid.is_ok()); - assert!(self.verify_commit(block.id(), commit)); + assert!(self.verify_commit(block.id(), &commit)); TestBlock { id: block.id + 1, valid: Ok(()) } } }