From 7c7f17aac66112a2405d6e77bcbac5b0fcf81662 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Wed, 12 Apr 2023 11:13:48 -0400 Subject: [PATCH] Test the blockchain --- coordinator/tributary/src/block.rs | 18 +- coordinator/tributary/src/blockchain.rs | 73 ++++++++ coordinator/tributary/src/lib.rs | 3 + coordinator/tributary/src/tests/block.rs | 27 +-- coordinator/tributary/src/tests/blockchain.rs | 169 ++++++++++++++++++ coordinator/tributary/src/tests/mod.rs | 5 +- .../tributary/src/tests/transaction/mod.rs | 36 ++-- coordinator/tributary/src/transaction.rs | 6 + 8 files changed, 301 insertions(+), 36 deletions(-) create mode 100644 coordinator/tributary/src/blockchain.rs create mode 100644 coordinator/tributary/src/tests/blockchain.rs diff --git a/coordinator/tributary/src/block.rs b/coordinator/tributary/src/block.rs index a3e70788..e9b0529a 100644 --- a/coordinator/tributary/src/block.rs +++ b/coordinator/tributary/src/block.rs @@ -29,8 +29,8 @@ use crate::{ #[derive(Clone, PartialEq, Eq, Debug)] pub struct BlockHeader { - parent: [u8; 32], - transactions: [u8; 32], + pub parent: [u8; 32], + pub transactions: [u8; 32], } impl ReadWrite for BlockHeader { @@ -55,8 +55,8 @@ impl BlockHeader { #[derive(Clone, PartialEq, Eq, Debug)] pub struct Block { - header: BlockHeader, - transactions: Vec, + pub header: BlockHeader, + pub transactions: Vec, } impl ReadWrite for Block { @@ -89,7 +89,7 @@ impl Block { /// Create a new block. /// /// mempool is expected to only have valid, non-conflicting transactions. - pub fn new( + pub(crate) fn new( parent: [u8; 32], provided: &ProvidedTransactions, mempool: HashMap<[u8; 32], T>, @@ -106,7 +106,7 @@ impl Block { // Sort txs by nonces. let nonce = |tx: &T| { if let TransactionKind::Signed(Signed { nonce, .. }) = tx.kind() { - nonce + *nonce } else { 0 } @@ -135,8 +135,8 @@ impl Block { &self, genesis: [u8; 32], last_block: [u8; 32], - locally_provided: &mut HashSet<[u8; 32]>, - next_nonces: &mut HashMap<::G, u32>, + mut locally_provided: HashSet<[u8; 32]>, + mut next_nonces: HashMap<::G, u32>, ) -> Result<(), BlockError> { if self.header.parent != last_block { Err(BlockError::InvalidParent)?; @@ -144,7 +144,7 @@ impl Block { let mut txs = Vec::with_capacity(self.transactions.len()); for tx in &self.transactions { - match verify_transaction(tx, genesis, locally_provided, next_nonces) { + match verify_transaction(tx, genesis, &mut locally_provided, &mut next_nonces) { Ok(()) => {} Err(e) => Err(BlockError::TransactionError(e))?, } diff --git a/coordinator/tributary/src/blockchain.rs b/coordinator/tributary/src/blockchain.rs new file mode 100644 index 00000000..627efadc --- /dev/null +++ b/coordinator/tributary/src/blockchain.rs @@ -0,0 +1,73 @@ +use std::collections::{HashSet, HashMap}; + +use ciphersuite::{Ciphersuite, Ristretto}; + +use crate::{Signed, TransactionKind, Transaction, ProvidedTransactions, BlockError, Block}; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Blockchain { + genesis: [u8; 32], + // TODO: db + tip: [u8; 32], + provided: ProvidedTransactions, + // TODO: Mempool + nonces: HashMap<::G, u32>, +} + +impl Blockchain { + pub fn new(genesis: [u8; 32]) -> Self { + // TODO: Reload provided/nonces + Self { genesis, tip: genesis, provided: ProvidedTransactions::new(), nonces: HashMap::new() } + } + + pub fn tip(&self) -> [u8; 32] { + self.tip + } + + pub fn provide_transaction(&mut self, tx: T) { + self.provided.provide(tx) + } + + pub fn next_nonce(&self, key: ::G) -> u32 { + self.nonces.get(&key).cloned().unwrap_or(0) + } + + // TODO: Embed mempool + pub fn build_block(&self, txs: HashMap<[u8; 32], T>) -> Block { + let block = Block::new(self.tip, &self.provided, txs); + // build_block should not return invalid blocks + self.verify_block(&block).unwrap(); + block + } + + pub fn verify_block(&self, block: &Block) -> Result<(), BlockError> { + let mut locally_provided = HashSet::new(); + for provided in self.provided.transactions.keys() { + locally_provided.insert(*provided); + } + block.verify(self.genesis, self.tip, locally_provided, self.nonces.clone()) + } + + /// Add a block, assuming it's valid. + /// + /// Do not call this without either verifying the block or having it confirmed under consensus. + /// Doing so will cause a panic or action an invalid transaction. + pub fn add_block(&mut self, block: &Block) { + self.tip = block.hash(); + for tx in &block.transactions { + match tx.kind() { + TransactionKind::Provided => { + self.provided.withdraw(tx.hash()); + } + TransactionKind::Unsigned => {} + TransactionKind::Signed(Signed { signer, nonce, .. }) => { + if let Some(prev) = self.nonces.insert(*signer, nonce + 1) { + if prev != *nonce { + panic!("block had an invalid nonce"); + } + } + } + } + } + } +} diff --git a/coordinator/tributary/src/lib.rs b/coordinator/tributary/src/lib.rs index 6400b51a..446af37e 100644 --- a/coordinator/tributary/src/lib.rs +++ b/coordinator/tributary/src/lib.rs @@ -12,6 +12,9 @@ pub use provided::*; mod block; pub use block::*; +mod blockchain; +pub use blockchain::*; + #[cfg(any(test, feature = "tests"))] pub mod tests; diff --git a/coordinator/tributary/src/tests/block.rs b/coordinator/tributary/src/tests/block.rs index 1e2c7454..7f5d02b8 100644 --- a/coordinator/tributary/src/tests/block.rs +++ b/coordinator/tributary/src/tests/block.rs @@ -75,7 +75,7 @@ fn empty_block() { const GENESIS: [u8; 32] = [0xff; 32]; const LAST: [u8; 32] = [0x01; 32]; Block::new(LAST, &ProvidedTransactions::::new(), HashMap::new()) - .verify(GENESIS, LAST, &mut HashSet::new(), &mut HashMap::new()) + .verify(GENESIS, LAST, HashSet::new(), HashMap::new()) .unwrap(); } @@ -89,19 +89,18 @@ fn duplicate_nonces() { for i in [1, 0] { let mut mempool = HashMap::new(); let mut insert = |tx: NonceTransaction| mempool.insert(tx.hash(), tx); - insert(NonceTransaction(0, 0)); - insert(NonceTransaction(i, 1)); + insert(NonceTransaction::new(0, 0)); + insert(NonceTransaction::new(i, 1)); - let mut nonces = HashMap::new(); + let nonces = HashMap::new(); let res = Block::new(LAST, &ProvidedTransactions::new(), mempool).verify( GENESIS, LAST, - &mut HashSet::new(), - &mut nonces, + HashSet::new(), + nonces, ); if i == 1 { res.unwrap(); - assert_eq!(nonces[&::G::identity()], 2); } else { assert!(res.is_err()); } @@ -119,18 +118,20 @@ fn unsorted_nonces() { let nonce = nonces.swap_remove( usize::try_from(OsRng.next_u64() % u64::try_from(nonces.len()).unwrap()).unwrap(), ); - let tx = NonceTransaction(nonce, 0); + let tx = NonceTransaction::new(nonce, 0); mempool.insert(tx.hash(), tx); } // Create and verify the block const GENESIS: [u8; 32] = [0xff; 32]; const LAST: [u8; 32] = [0x01; 32]; - let mut nonces = HashMap::new(); - Block::new(LAST, &ProvidedTransactions::new(), mempool) - .verify(GENESIS, LAST, &mut HashSet::new(), &mut nonces) + Block::new(LAST, &ProvidedTransactions::new(), mempool.clone()) + .verify(GENESIS, LAST, HashSet::new(), HashMap::new()) .unwrap(); - // Make sure the nonce was properly set - assert_eq!(nonces[&::G::identity()], 64); + let skip = NonceTransaction::new(65, 0); + mempool.insert(skip.hash(), skip); + assert!(Block::new(LAST, &ProvidedTransactions::new(), mempool) + .verify(GENESIS, LAST, HashSet::new(), HashMap::new()) + .is_err()); } diff --git a/coordinator/tributary/src/tests/blockchain.rs b/coordinator/tributary/src/tests/blockchain.rs new file mode 100644 index 00000000..1029107b --- /dev/null +++ b/coordinator/tributary/src/tests/blockchain.rs @@ -0,0 +1,169 @@ +use std::collections::{HashSet, HashMap}; + +use zeroize::Zeroizing; +use rand_core::{RngCore, OsRng}; + +use blake2::{Digest, Blake2s256}; + +use ciphersuite::{group::ff::Field, Ciphersuite, Ristretto}; + +use crate::{ + merkle, Transaction, ProvidedTransactions, Block, Blockchain, + tests::{ProvidedTransaction, SignedTransaction, random_provided_transaction}, +}; + +fn new_blockchain() -> ([u8; 32], Blockchain) { + let mut genesis = [0; 32]; + OsRng.fill_bytes(&mut genesis); + + let blockchain = Blockchain::new(genesis); + assert_eq!(blockchain.tip(), genesis); + + (genesis, blockchain) +} + +#[test] +fn block_addition() { + let (genesis, mut blockchain) = new_blockchain::(); + let block = blockchain.build_block(HashMap::new()); + assert_eq!(block.header.parent, genesis); + assert_eq!(block.header.transactions, [0; 32]); + blockchain.verify_block(&block).unwrap(); + blockchain.add_block(&block); + assert_eq!(blockchain.tip(), block.hash()); +} + +#[test] +fn invalid_block() { + let (genesis, blockchain) = new_blockchain::(); + + let block = blockchain.build_block(HashMap::new()); + + // Mutate parent + { + #[allow(clippy::redundant_clone)] // False positive + let mut block = block.clone(); + block.header.parent = Blake2s256::digest(block.header.parent).into(); + assert!(blockchain.verify_block(&block).is_err()); + } + + // Mutate tranactions merkle + { + let mut block = block; + block.header.transactions = Blake2s256::digest(block.header.transactions).into(); + assert!(blockchain.verify_block(&block).is_err()); + } + + let key = Zeroizing::new(::F::random(&mut OsRng)); + + { + // Add a valid transaction + let tx = crate::tests::signed_transaction(&mut OsRng, genesis, &key, 0); + let mut block = blockchain.build_block(HashMap::from([(tx.hash(), tx.clone())])); + assert_eq!(block.header.transactions, merkle(&[tx.hash()])); + blockchain.verify_block(&block).unwrap(); + + // And verify mutating the transactions merkle now causes a failure + block.header.transactions = merkle(&[]); + assert!(blockchain.verify_block(&block).is_err()); + } + + { + // Invalid nonce + let tx = crate::tests::signed_transaction(&mut OsRng, genesis, &key, 5); + // Manually create the block to bypass build_block's checks + let block = + Block::new(blockchain.tip(), &ProvidedTransactions::new(), HashMap::from([(tx.hash(), tx)])); + assert!(blockchain.verify_block(&block).is_err()); + } + + { + // Invalid signature + let tx = crate::tests::signed_transaction(&mut OsRng, genesis, &key, 0); + let mut block = blockchain.build_block(HashMap::from([(tx.hash(), tx)])); + blockchain.verify_block(&block).unwrap(); + block.transactions[0].1.signature.s += ::F::ONE; + assert!(blockchain.verify_block(&block).is_err()); + + // Make sure this isn't because the merkle changed due to the transaction hash including the + // signature (which it explicitly isn't allowed to anyways) + assert_eq!(block.header.transactions, merkle(&[block.transactions[0].hash()])); + } +} + +#[test] +fn signed_transaction() { + let (genesis, mut blockchain) = new_blockchain::(); + let key = Zeroizing::new(::F::random(&mut OsRng)); + let tx = crate::tests::signed_transaction(&mut OsRng, genesis, &key, 0); + let signer = tx.1.signer; + assert_eq!(blockchain.next_nonce(signer), 0); + + let test = |blockchain: &mut Blockchain, mempool: HashMap<_, _>| { + let mut hashes = mempool.keys().cloned().collect::>(); + + let tip = blockchain.tip(); + let block = blockchain.build_block(mempool); + assert_eq!(blockchain.tip(), tip); + assert_eq!(block.header.parent, tip); + + // Make sure all transactions were included + let mut ordered_hashes = vec![]; + assert_eq!(hashes.len(), block.transactions.len()); + for transaction in &block.transactions { + let hash = transaction.hash(); + assert!(hashes.remove(&hash)); + ordered_hashes.push(hash); + } + // Make sure the merkle was correct + assert_eq!(block.header.transactions, merkle(&ordered_hashes)); + + // Verify and add the block + blockchain.verify_block(&block).unwrap(); + blockchain.add_block(&block); + assert_eq!(blockchain.tip(), block.hash()); + }; + + // Test with a single nonce + test(&mut blockchain, HashMap::from([(tx.hash(), tx)])); + assert_eq!(blockchain.next_nonce(signer), 1); + + // Test with a flood of nonces + let mut mempool = HashMap::new(); + let mut nonces = (1 .. 64).collect::>(); + // Randomize insertion order into HashMap, even though it should already have unordered iteration + while !nonces.is_empty() { + let nonce = nonces.swap_remove( + usize::try_from(OsRng.next_u64() % u64::try_from(nonces.len()).unwrap()).unwrap(), + ); + let tx = crate::tests::signed_transaction(&mut OsRng, genesis, &key, nonce); + mempool.insert(tx.hash(), tx); + } + test(&mut blockchain, mempool); + assert_eq!(blockchain.next_nonce(signer), 64); +} + +#[test] +fn provided_transaction() { + let (_, mut blockchain) = new_blockchain::(); + + let tx = random_provided_transaction(&mut OsRng); + let mut txs = ProvidedTransactions::new(); + txs.provide(tx.clone()); + // Non-provided transactions should fail verification + let block = Block::new(blockchain.tip(), &txs, HashMap::new()); + assert!(blockchain.verify_block(&block).is_err()); + + // Provided transactions should pass verification + blockchain.provide_transaction(tx); + blockchain.verify_block(&block).unwrap(); + + // add_block should work for verified blocks + blockchain.add_block(&block); + + 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 also work for unverified provided transactions if told to add them + blockchain.add_block(&block); +} diff --git a/coordinator/tributary/src/tests/mod.rs b/coordinator/tributary/src/tests/mod.rs index 9867de21..01361e90 100644 --- a/coordinator/tributary/src/tests/mod.rs +++ b/coordinator/tributary/src/tests/mod.rs @@ -1,7 +1,10 @@ mod transaction; pub use transaction::*; +#[cfg(test)] +mod merkle; + #[cfg(test)] mod block; #[cfg(test)] -pub use block::*; +mod blockchain; diff --git a/coordinator/tributary/src/tests/transaction/mod.rs b/coordinator/tributary/src/tests/transaction/mod.rs index 6da9793e..eeafd7e5 100644 --- a/coordinator/tributary/src/tests/transaction/mod.rs +++ b/coordinator/tributary/src/tests/transaction/mod.rs @@ -3,6 +3,7 @@ use std::{ collections::{HashSet, HashMap}, }; +use zeroize::Zeroizing; use rand_core::{RngCore, CryptoRng}; use blake2::{Digest, Blake2s256}; @@ -105,33 +106,42 @@ impl Transaction for SignedTransaction { } } -pub fn random_signed_transaction( +pub fn signed_transaction( rng: &mut R, -) -> ([u8; 32], SignedTransaction) { - use zeroize::Zeroizing; - + genesis: [u8; 32], + key: &Zeroizing<::F>, + nonce: u32, +) -> SignedTransaction { let mut data = vec![0; 512]; rng.fill_bytes(&mut data); - let key = ::F::random(&mut *rng); - let signer = ::generator() * key; - // Shift over an additional bit to ensure it won't overflow when incremented - let nonce = u32::try_from(rng.next_u64() >> 32 >> 1).unwrap(); + let signer = ::generator() * **key; let mut tx = SignedTransaction(data, Signed { signer, nonce, signature: random_signed(rng).signature }); - let mut genesis = [0; 32]; - rng.fill_bytes(&mut genesis); tx.1.signature = SchnorrSignature::sign( - &Zeroizing::new(key), + key, Zeroizing::new(::F::random(rng)), tx.sig_hash(genesis), ); - let mut nonces = HashMap::from([(tx.1.signer, tx.1.nonce)]); + let mut nonces = HashMap::from([(signer, nonce)]); verify_transaction(&tx, genesis, &mut HashSet::new(), &mut nonces).unwrap(); assert_eq!(nonces, HashMap::from([(tx.1.signer, tx.1.nonce.wrapping_add(1))])); - (genesis, tx) + tx +} + +pub fn random_signed_transaction( + rng: &mut R, +) -> ([u8; 32], SignedTransaction) { + let mut genesis = [0; 32]; + rng.fill_bytes(&mut genesis); + + let key = Zeroizing::new(::F::random(&mut *rng)); + // Shift over an additional bit to ensure it won't overflow when incremented + let nonce = u32::try_from(rng.next_u64() >> 32 >> 1).unwrap(); + + (genesis, signed_transaction(rng, genesis, &key, nonce)) } diff --git a/coordinator/tributary/src/transaction.rs b/coordinator/tributary/src/transaction.rs index 1d4416f5..8a63b852 100644 --- a/coordinator/tributary/src/transaction.rs +++ b/coordinator/tributary/src/transaction.rs @@ -67,14 +67,20 @@ pub enum TransactionKind<'a> { } pub trait Transaction: Send + Sync + Clone + Eq + Debug + ReadWrite { + /// Return what type of transaction this is. fn kind(&self) -> TransactionKind<'_>; + /// Return the hash of this transaction. /// /// The hash must NOT commit to the signature. fn hash(&self) -> [u8; 32]; + /// Perform transaction-specific verification. fn verify(&self) -> Result<(), TransactionError>; + /// Obtain the challenge for this transaction's signature. + /// + /// Do not override this unless you know what you're doing. fn sig_hash(&self, genesis: [u8; 32]) -> ::F { ::F::from_bytes_mod_order_wide( &Blake2b512::digest([genesis, self.hash()].concat()).into(),