diff --git a/coordinator/tributary/src/blockchain.rs b/coordinator/tributary/src/blockchain.rs index 627efadc..86b7f37b 100644 --- a/coordinator/tributary/src/blockchain.rs +++ b/coordinator/tributary/src/blockchain.rs @@ -11,13 +11,18 @@ pub struct Blockchain<T: Transaction> { tip: [u8; 32], provided: ProvidedTransactions<T>, // TODO: Mempool - nonces: HashMap<<Ristretto as Ciphersuite>::G, u32>, + next_nonces: HashMap<<Ristretto as Ciphersuite>::G, u32>, } impl<T: Transaction> Blockchain<T> { - pub fn new(genesis: [u8; 32]) -> Self { + pub fn new(genesis: [u8; 32], participants: &[<Ristretto as Ciphersuite>::G]) -> Self { // TODO: Reload provided/nonces - Self { genesis, tip: genesis, provided: ProvidedTransactions::new(), nonces: HashMap::new() } + + let mut next_nonces = HashMap::new(); + for participant in participants { + next_nonces.insert(*participant, 0); + } + Self { genesis, tip: genesis, provided: ProvidedTransactions::new(), next_nonces } } pub fn tip(&self) -> [u8; 32] { @@ -28,8 +33,9 @@ impl<T: Transaction> Blockchain<T> { self.provided.provide(tx) } - pub fn next_nonce(&self, key: <Ristretto as Ciphersuite>::G) -> u32 { - self.nonces.get(&key).cloned().unwrap_or(0) + /// Returns the next nonce, or None if they aren't a participant. + pub fn next_nonce(&self, key: <Ristretto as Ciphersuite>::G) -> Option<u32> { + self.next_nonces.get(&key).cloned() } // TODO: Embed mempool @@ -45,7 +51,7 @@ impl<T: Transaction> Blockchain<T> { for provided in self.provided.transactions.keys() { locally_provided.insert(*provided); } - block.verify(self.genesis, self.tip, locally_provided, self.nonces.clone()) + block.verify(self.genesis, self.tip, locally_provided, self.next_nonces.clone()) } /// Add a block, assuming it's valid. @@ -61,10 +67,12 @@ impl<T: Transaction> Blockchain<T> { } 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"); - } + let prev = self + .next_nonces + .insert(*signer, nonce + 1) + .expect("block had signed transaction from non-participant"); + if prev != *nonce { + panic!("block had an invalid nonce"); } } } diff --git a/coordinator/tributary/src/mempool.rs b/coordinator/tributary/src/mempool.rs index 9f647b00..70f01486 100644 --- a/coordinator/tributary/src/mempool.rs +++ b/coordinator/tributary/src/mempool.rs @@ -19,15 +19,18 @@ impl<T: Transaction> Mempool<T> { /// Returns true if this is a valid, new transaction. pub fn add( &mut self, - blockchain_nonces: &HashMap<<Ristretto as Ciphersuite>::G, u32>, + blockchain_next_nonces: &HashMap<<Ristretto as Ciphersuite>::G, u32>, tx: T, ) -> bool { match tx.kind() { TransactionKind::Signed(Signed { signer, nonce, .. }) => { // If the mempool doesn't have a nonce tracked, grab it from the blockchain if !self.next_nonces.contains_key(signer) { - // TODO: Same commentary here as present in verify_transaction about a whitelist - self.next_nonces.insert(*signer, blockchain_nonces.get(signer).cloned().unwrap_or(0)); + let Some(blockchain_next_nonces) = blockchain_next_nonces.get(signer).cloned() else { + // Not a participant + return false; + }; + self.next_nonces.insert(*signer, blockchain_next_nonces); } if verify_transaction(&tx, self.genesis, &mut HashSet::new(), &mut self.next_nonces) @@ -44,6 +47,9 @@ impl<T: Transaction> Mempool<T> { } } + // Returns None if the mempool doesn't have a nonce tracked. + // The nonce to use when signing should be: + // max(blockchain.next_nonce().unwrap(), mempool.next_nonce().unwrap_or(0)) pub fn next_nonce(&self, signer: &<Ristretto as Ciphersuite>::G) -> Option<u32> { self.next_nonces.get(signer).cloned() } @@ -51,7 +57,7 @@ impl<T: Transaction> Mempool<T> { /// Get transactions to include in a block. pub fn block( &mut self, - blockchain_nonces: &HashMap<<Ristretto as Ciphersuite>::G, u32>, + blockchain_next_nonces: &HashMap<<Ristretto as Ciphersuite>::G, u32>, ) -> HashMap<[u8; 32], T> { let mut res = HashMap::new(); for hash in self.txs.keys().cloned().collect::<Vec<_>>() { @@ -59,7 +65,7 @@ impl<T: Transaction> Mempool<T> { // Verify this hasn't gone stale match tx.kind() { TransactionKind::Signed(Signed { signer, nonce, .. }) => { - if blockchain_nonces.get(signer).cloned().unwrap_or(0) > *nonce { + if blockchain_next_nonces[signer] > *nonce { self.txs.remove(&hash); continue; } diff --git a/coordinator/tributary/src/tests/block.rs b/coordinator/tributary/src/tests/block.rs index 7f5d02b8..ba2fffd1 100644 --- a/coordinator/tributary/src/tests/block.rs +++ b/coordinator/tributary/src/tests/block.rs @@ -92,12 +92,11 @@ fn duplicate_nonces() { insert(NonceTransaction::new(0, 0)); insert(NonceTransaction::new(i, 1)); - let nonces = HashMap::new(); let res = Block::new(LAST, &ProvidedTransactions::new(), mempool).verify( GENESIS, LAST, HashSet::new(), - nonces, + HashMap::from([(<Ristretto as Ciphersuite>::G::identity(), 0)]), ); if i == 1 { res.unwrap(); @@ -125,13 +124,14 @@ fn unsorted_nonces() { // Create and verify the block const GENESIS: [u8; 32] = [0xff; 32]; const LAST: [u8; 32] = [0x01; 32]; + let nonces = HashMap::from([(<Ristretto as Ciphersuite>::G::identity(), 0)]); Block::new(LAST, &ProvidedTransactions::new(), mempool.clone()) - .verify(GENESIS, LAST, HashSet::new(), HashMap::new()) + .verify(GENESIS, LAST, HashSet::new(), nonces.clone()) .unwrap(); 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()) + .verify(GENESIS, LAST, HashSet::new(), nonces) .is_err()); } diff --git a/coordinator/tributary/src/tests/blockchain.rs b/coordinator/tributary/src/tests/blockchain.rs index 1029107b..4ccd793a 100644 --- a/coordinator/tributary/src/tests/blockchain.rs +++ b/coordinator/tributary/src/tests/blockchain.rs @@ -12,19 +12,25 @@ use crate::{ tests::{ProvidedTransaction, SignedTransaction, random_provided_transaction}, }; -fn new_blockchain<T: Transaction>() -> ([u8; 32], Blockchain<T>) { +fn new_genesis() -> [u8; 32] { let mut genesis = [0; 32]; OsRng.fill_bytes(&mut genesis); + genesis +} - let blockchain = Blockchain::new(genesis); +fn new_blockchain<T: Transaction>( + genesis: [u8; 32], + participants: &[<Ristretto as Ciphersuite>::G], +) -> Blockchain<T> { + let blockchain = Blockchain::new(genesis, participants); assert_eq!(blockchain.tip(), genesis); - - (genesis, blockchain) + blockchain } #[test] fn block_addition() { - let (genesis, mut blockchain) = new_blockchain::<SignedTransaction>(); + let genesis = new_genesis(); + let mut blockchain = new_blockchain::<SignedTransaction>(genesis, &[]); let block = blockchain.build_block(HashMap::new()); assert_eq!(block.header.parent, genesis); assert_eq!(block.header.transactions, [0; 32]); @@ -35,7 +41,8 @@ fn block_addition() { #[test] fn invalid_block() { - let (genesis, blockchain) = new_blockchain::<SignedTransaction>(); + let genesis = new_genesis(); + let blockchain = new_blockchain::<SignedTransaction>(genesis, &[]); let block = blockchain.build_block(HashMap::new()); @@ -55,10 +62,36 @@ fn invalid_block() { } let key = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(&mut OsRng)); + let tx = crate::tests::signed_transaction(&mut OsRng, genesis, &key, 0); + + // Not a participant + { + // Manually create the block to bypass build_block's checks + let block = Block::new( + blockchain.tip(), + &ProvidedTransactions::new(), + HashMap::from([(tx.hash(), tx.clone())]), + ); + assert_eq!(block.header.transactions, merkle(&[tx.hash()])); + assert!(blockchain.verify_block(&block).is_err()); + } + + // Run the rest of the tests with them as a participant + let blockchain = new_blockchain(genesis, &[tx.1.signer]); + + // Re-run the not a participant block to make sure it now works + { + let block = Block::new( + blockchain.tip(), + &ProvidedTransactions::new(), + HashMap::from([(tx.hash(), tx.clone())]), + ); + assert_eq!(block.header.transactions, merkle(&[tx.hash()])); + blockchain.verify_block(&block).unwrap(); + } { // 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(); @@ -79,7 +112,6 @@ fn invalid_block() { { // 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 += <Ristretto as Ciphersuite>::F::ONE; @@ -93,11 +125,14 @@ fn invalid_block() { #[test] fn signed_transaction() { - let (genesis, mut blockchain) = new_blockchain::<SignedTransaction>(); + let genesis = new_genesis(); + let key = Zeroizing::new(<Ristretto as Ciphersuite>::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 mut blockchain = new_blockchain::<SignedTransaction>(genesis, &[signer]); + assert_eq!(blockchain.next_nonce(signer), Some(0)); let test = |blockchain: &mut Blockchain<SignedTransaction>, mempool: HashMap<_, _>| { let mut hashes = mempool.keys().cloned().collect::<HashSet<_>>(); @@ -126,7 +161,7 @@ fn signed_transaction() { // Test with a single nonce test(&mut blockchain, HashMap::from([(tx.hash(), tx)])); - assert_eq!(blockchain.next_nonce(signer), 1); + assert_eq!(blockchain.next_nonce(signer), Some(1)); // Test with a flood of nonces let mut mempool = HashMap::new(); @@ -140,12 +175,12 @@ fn signed_transaction() { mempool.insert(tx.hash(), tx); } test(&mut blockchain, mempool); - assert_eq!(blockchain.next_nonce(signer), 64); + assert_eq!(blockchain.next_nonce(signer), Some(64)); } #[test] fn provided_transaction() { - let (_, mut blockchain) = new_blockchain::<ProvidedTransaction>(); + let mut blockchain = new_blockchain::<ProvidedTransaction>(new_genesis(), &[]); let tx = random_provided_transaction(&mut OsRng); let mut txs = ProvidedTransactions::new(); diff --git a/coordinator/tributary/src/tests/mempool.rs b/coordinator/tributary/src/tests/mempool.rs index 7556e13a..b9a68db8 100644 --- a/coordinator/tributary/src/tests/mempool.rs +++ b/coordinator/tributary/src/tests/mempool.rs @@ -27,17 +27,18 @@ fn mempool_addition() { assert_eq!(mempool.next_nonce(&signer), None); // Add TX 0 - assert!(mempool.add(&HashMap::new(), first_tx.clone())); + let mut blockchain_next_nonces = HashMap::from([(signer, 0)]); + assert!(mempool.add(&blockchain_next_nonces, first_tx.clone())); assert_eq!(mempool.next_nonce(&signer), Some(1)); // Adding it again should fail - assert!(!mempool.add(&HashMap::new(), first_tx.clone())); + assert!(!mempool.add(&blockchain_next_nonces, first_tx.clone())); // Do the same with the next nonce let second_tx = signed_transaction(&mut OsRng, genesis, &key, 1); - assert!(mempool.add(&HashMap::new(), second_tx.clone())); + assert!(mempool.add(&blockchain_next_nonces, second_tx.clone())); assert_eq!(mempool.next_nonce(&signer), Some(2)); - assert!(!mempool.add(&HashMap::new(), second_tx.clone())); + assert!(!mempool.add(&blockchain_next_nonces, second_tx.clone())); // If the mempool doesn't have a nonce for an account, it should successfully use the // blockchain's @@ -45,18 +46,16 @@ fn mempool_addition() { let tx = signed_transaction(&mut OsRng, genesis, &second_key, 2); let second_signer = tx.1.signer; assert_eq!(mempool.next_nonce(&second_signer), None); - let mut blockchain_nonces = HashMap::from([(second_signer, 2)]); - assert!(mempool.add(&blockchain_nonces, tx.clone())); + blockchain_next_nonces.insert(second_signer, 2); + assert!(mempool.add(&blockchain_next_nonces, tx.clone())); assert_eq!(mempool.next_nonce(&second_signer), Some(3)); // Getting a block should work - let block = mempool.block(&HashMap::new()); - assert_eq!(block, mempool.block(&blockchain_nonces)); - assert_eq!(block.len(), 3); + assert_eq!(mempool.block(&blockchain_next_nonces).len(), 3); // If the blockchain says an account had its nonce updated, it should cause a prune - blockchain_nonces.insert(signer, 1); - let block = mempool.block(&blockchain_nonces); + blockchain_next_nonces.insert(signer, 1); + let block = mempool.block(&blockchain_next_nonces); assert_eq!(block.len(), 2); assert!(!block.contains_key(&first_tx.hash())); assert_eq!(mempool.txs(), &block); diff --git a/coordinator/tributary/src/transaction.rs b/coordinator/tributary/src/transaction.rs index b829af83..290038d0 100644 --- a/coordinator/tributary/src/transaction.rs +++ b/coordinator/tributary/src/transaction.rs @@ -91,7 +91,7 @@ pub trait Transaction: Send + Sync + Clone + Eq + Debug + ReadWrite { } } -// This will only cause mutations when the transaction is valid. +// This will only cause mutations when the transaction is valid pub(crate) fn verify_transaction<T: Transaction>( tx: &T, genesis: [u8; 32], @@ -108,9 +108,13 @@ pub(crate) fn verify_transaction<T: Transaction>( } TransactionKind::Unsigned => {} TransactionKind::Signed(Signed { signer, nonce, signature }) => { - // TODO: Use presence as a whitelist, erroring on lack of - if next_nonces.get(signer).cloned().unwrap_or(0) != *nonce { - Err(TransactionError::Temporal)?; + if let Some(next_nonce) = next_nonces.get(signer) { + if nonce != next_nonce { + Err(TransactionError::Temporal)?; + } + } else { + // Not a participant + Err(TransactionError::Fatal)?; } // TODO: Use Schnorr half-aggregation and a batch verification here