From 8c8232516d42a365e4e5fd50ebe1c907883978a8 Mon Sep 17 00:00:00 2001
From: Luke Parker <lukeparker5132@gmail.com>
Date: Wed, 12 Apr 2023 12:42:23 -0400
Subject: [PATCH] Only allow designated participants to send transactions

---
 coordinator/tributary/src/blockchain.rs       | 28 ++++++---
 coordinator/tributary/src/mempool.rs          | 16 +++--
 coordinator/tributary/src/tests/block.rs      |  8 +--
 coordinator/tributary/src/tests/blockchain.rs | 61 +++++++++++++++----
 coordinator/tributary/src/tests/mempool.rs    | 21 +++----
 coordinator/tributary/src/transaction.rs      | 12 ++--
 6 files changed, 99 insertions(+), 47 deletions(-)

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