Document tendermint

This commit is contained in:
Luke Parker 2022-10-17 08:07:23 -04:00
parent 0501ff259e
commit f28d412f78
No known key found for this signature in database
GPG key ID: F9F1386DB1E119B6
4 changed files with 127 additions and 35 deletions

View file

@ -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.

View file

@ -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<V: Send + Sync + Clone + Copy + PartialEq + Eq + Hash + Debug + Encode + De
{
}
/// An alias for a series of traits required for a type to be usable as a signature,
/// automatically implemented for all types satisfying those traits.
pub trait Signature: Send + Sync + Clone + PartialEq + Debug + Encode + Decode {}
impl<S: Send + Sync + Clone + PartialEq + Debug + Encode + Decode> 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<S: SignatureScheme> {
/// Validators participating in the signature.
pub validators: Vec<S::ValidatorId>,
/// 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<ValidatorId = Self::ValidatorId>;
/// Object representing the weights of validators.
type Weights: Weights<ValidatorId = Self::ValidatorId>;
/// 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<Self::SignatureScheme>;
/// Return a reference to the validators' weights.
fn weights(&self) -> Arc<Self::Weights>;
/// 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: <Self::Block as Block>::Id,
commit: Commit<Self::SignatureScheme>,
commit: &Commit<Self::SignatureScheme>,
) -> 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::<u64>() >= 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::SignatureScheme>)
-> Self::Block;
}

View file

@ -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<B: Block, S: Signature> Data<B, S> {
}
#[derive(Clone, PartialEq, Debug, Encode, Decode)]
pub struct Message<V: ValidatorId, B: Block, S: Signature> {
struct Message<V: ValidatorId, B: Block, S: Signature> {
sender: V,
number: BlockNumber,
@ -68,18 +69,20 @@ pub struct Message<V: ValidatorId, B: Block, S: Signature> {
data: Data<B, S>,
}
/// A signed Tendermint consensus message to be broadcast to the other validators.
#[derive(Clone, PartialEq, Debug, Encode, Decode)]
pub struct SignedMessage<V: ValidatorId, B: Block, S: Signature> {
msg: Message<V, B, S>,
sig: S,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode)]
pub enum TendermintError<V: ValidatorId> {
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum TendermintError<V: ValidatorId> {
Malicious(V),
Temporal,
}
/// A machine executing the Tendermint protocol.
pub struct TendermintMachine<N: Network> {
network: Arc<RwLock<N>>,
signer: Arc<N::SignatureScheme>,
@ -100,19 +103,21 @@ pub struct TendermintMachine<N: Network> {
timeouts: HashMap<Step, Instant>,
}
/// A handle to an asynchronous task, along with a channel to inform of it of messages received.
pub struct TendermintHandle<N: Network> {
// Messages received
/// Channel to send messages received from the P2P layer.
pub messages: mpsc::Sender<
SignedMessage<N::ValidatorId, N::Block, <N::SignatureScheme as SignatureScheme>::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<N: Network + 'static> TendermintMachine<N> {
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<N: Network + 'static> TendermintMachine<N> {
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<N: Network + 'static> TendermintMachine<N> {
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<N: Network + 'static> TendermintMachine<N> {
}
} 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()))))

View file

@ -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<TestSignatureScheme>) -> 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(()) }
}
}