From 03a6470a5b53d7e1377191a3d974bbd7b2d5398e Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Wed, 12 Apr 2023 18:04:28 -0400 Subject: [PATCH] Finish binding Tendermint, bar the P2P layer --- coordinator/tributary/Cargo.toml | 6 + coordinator/tributary/src/block.rs | 2 +- coordinator/tributary/src/blockchain.rs | 10 +- coordinator/tributary/src/tendermint.rs | 149 +++++++++++++++++- coordinator/tributary/src/tests/blockchain.rs | 8 +- coordinator/tributary/src/transaction.rs | 29 ++-- coordinator/tributary/tendermint/src/ext.rs | 25 +-- 7 files changed, 192 insertions(+), 37 deletions(-) diff --git a/coordinator/tributary/Cargo.toml b/coordinator/tributary/Cargo.toml index e38a06fe..6ca0da73 100644 --- a/coordinator/tributary/Cargo.toml +++ b/coordinator/tributary/Cargo.toml @@ -21,8 +21,14 @@ transcript = { package = "flexible-transcript", path = "../../crypto/transcript" ciphersuite = { package = "ciphersuite", path = "../../crypto/ciphersuite", features = ["ristretto"] } 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" } +tokio = { version = "1", features = ["macros", "sync", "time", "rt"] } + [dev-dependencies] zeroize = "^1.5" rand_core = "0.6" diff --git a/coordinator/tributary/src/block.rs b/coordinator/tributary/src/block.rs index e9b0529a..e7c51bde 100644 --- a/coordinator/tributary/src/block.rs +++ b/coordinator/tributary/src/block.rs @@ -48,7 +48,7 @@ impl ReadWrite for 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() } } diff --git a/coordinator/tributary/src/blockchain.rs b/coordinator/tributary/src/blockchain.rs index f73c874b..79801e28 100644 --- a/coordinator/tributary/src/blockchain.rs +++ b/coordinator/tributary/src/blockchain.rs @@ -55,12 +55,8 @@ impl Blockchain { } /// Add a block. - #[must_use] - pub fn add_block(&mut self, block: &Block) -> bool { - // TODO: Handle desyncs re: provided transactions properly - if self.verify_block(block).is_err() { - return false; - } + pub fn add_block(&mut self, block: &Block) -> Result<(), BlockError> { + self.verify_block(block)?; // None of the following assertions should be reachable since we verified the block self.tip = block.hash(); @@ -85,6 +81,6 @@ impl Blockchain { } } - true + Ok(()) } } diff --git a/coordinator/tributary/src/tendermint.rs b/coordinator/tributary/src/tendermint.rs index 452ae4f6..919df6c7 100644 --- a/coordinator/tributary/src/tendermint.rs +++ b/coordinator/tributary/src/tendermint.rs @@ -1,4 +1,7 @@ use core::ops::Deref; +use std::{sync::Arc, collections::HashMap}; + +use async_trait::async_trait; use subtle::ConstantTimeEq; use zeroize::{Zeroize, Zeroizing}; @@ -14,7 +17,18 @@ use ciphersuite::{ }; 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( genesis: [u8; 32], @@ -37,7 +51,7 @@ struct Signer { key: Zeroizing<::F>, } -#[async_trait::async_trait] +#[async_trait] impl SignerTrait for Signer { type ValidatorId = [u8; 32]; type Signature = [u8; 64]; @@ -85,19 +99,25 @@ impl SignerTrait for Signer { } #[derive(Clone, PartialEq, Eq, Debug)] -struct SignatureScheme { +struct Validators { 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 Signature = [u8; 64]; // TODO: Use half-aggregation. type AggregateSignature = Vec<[u8; 64]>; - type Signer = Signer; + type Signer = Arc; #[must_use] 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 { return false; }; @@ -126,3 +146,122 @@ impl SignatureSchemeTrait for SignatureScheme { 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); +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 { + genesis: [u8; 32], + signer: Arc, + validators: Arc, + blockchain: Blockchain, +} + +#[async_trait] +impl NetworkTrait for Network { + type ValidatorId = [u8; 32]; + type SignatureScheme = Arc; + type Weights = Arc; + type Block = TendermintBlock; + + const BLOCK_PROCESSING_TIME: u32 = 3; + const LATENCY_TIME: u32 = 1; + + fn signer(&self) -> Arc { + self.signer.clone() + } + fn signature_scheme(&self) -> Arc { + self.validators.clone() + } + fn weights(&self) -> Arc { + self.validators.clone() + } + + async fn broadcast(&mut self, _msg: SignedMessageFor) { + 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, + ) -> Option { + 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!() + } +} diff --git a/coordinator/tributary/src/tests/blockchain.rs b/coordinator/tributary/src/tests/blockchain.rs index c4c49f43..1bb3bd58 100644 --- a/coordinator/tributary/src/tests/blockchain.rs +++ b/coordinator/tributary/src/tests/blockchain.rs @@ -35,7 +35,7 @@ fn block_addition() { assert_eq!(block.header.parent, genesis); assert_eq!(block.header.transactions, [0; 32]); blockchain.verify_block(&block).unwrap(); - assert!(blockchain.add_block(&block)); + assert!(blockchain.add_block(&block).is_ok()); assert_eq!(blockchain.tip(), block.hash()); } @@ -155,7 +155,7 @@ fn signed_transaction() { // Verify and add the block blockchain.verify_block(&block).unwrap(); - assert!(blockchain.add_block(&block)); + assert!(blockchain.add_block(&block).is_ok()); assert_eq!(blockchain.tip(), block.hash()); }; @@ -194,11 +194,11 @@ fn provided_transaction() { blockchain.verify_block(&block).unwrap(); // 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()); // The provided transaction should no longer considered provided, causing this error assert!(blockchain.verify_block(&block).is_err()); // 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()); } diff --git a/coordinator/tributary/src/transaction.rs b/coordinator/tributary/src/transaction.rs index d4e9aa0e..c0fa48f3 100644 --- a/coordinator/tributary/src/transaction.rs +++ b/coordinator/tributary/src/transaction.rs @@ -15,12 +15,18 @@ use crate::ReadWrite; #[derive(Clone, PartialEq, Eq, Debug, Error)] pub enum TransactionError { - /// This transaction was perceived as invalid against the current state. - #[error("transaction temporally invalid")] - Temporal, - /// This transaction is definitively invalid. - #[error("transaction definitively invalid")] - Fatal, + /// A provided transaction wasn't locally provided. + #[error("provided transaction wasn't locally provided")] + MissingProvided([u8; 32]), + /// This transaction's signer isn't a participant. + #[error("invalid signer")] + 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. @@ -100,24 +106,25 @@ pub(crate) fn verify_transaction( match tx.kind() { TransactionKind::Provided => { - if !locally_provided.remove(&tx.hash()) { - Err(TransactionError::Temporal)?; + let hash = tx.hash(); + if !locally_provided.remove(&hash) { + Err(TransactionError::MissingProvided(hash))?; } } TransactionKind::Unsigned => {} TransactionKind::Signed(Signed { signer, nonce, signature }) => { if let Some(next_nonce) = next_nonces.get(signer) { if nonce != next_nonce { - Err(TransactionError::Temporal)?; + Err(TransactionError::InvalidNonce)?; } } else { // Not a participant - Err(TransactionError::Fatal)?; + Err(TransactionError::InvalidSigner)?; } // TODO: Use Schnorr half-aggregation and a batch verification here if !signature.verify(*signer, tx.sig_hash(genesis)) { - Err(TransactionError::Fatal)?; + Err(TransactionError::InvalidSignature)?; } next_nonces.insert(*signer, nonce + 1); diff --git a/coordinator/tributary/tendermint/src/ext.rs b/coordinator/tributary/tendermint/src/ext.rs index 91c27248..0670d5da 100644 --- a/coordinator/tributary/tendermint/src/ext.rs +++ b/coordinator/tributary/tendermint/src/ext.rs @@ -241,23 +241,30 @@ 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. + /// 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: SignedMessageFor); /// 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. 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 - /// 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. + + /// 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. async fn add_block( &mut self, block: Self::Block,