diff --git a/coordinator/tributary/src/blockchain.rs b/coordinator/tributary/src/blockchain.rs index 5bf0be34..c7d5dae9 100644 --- a/coordinator/tributary/src/blockchain.rs +++ b/coordinator/tributary/src/blockchain.rs @@ -5,8 +5,8 @@ use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto}; use serai_db::{DbTxn, Db}; use crate::{ - ReadWrite, Signed, TransactionKind, Transaction, verify_transaction, ProvidedTransactions, - BlockError, Block, Mempool, + ReadWrite, Signed, TransactionKind, Transaction, ProvidedError, ProvidedTransactions, BlockError, + Block, Mempool, }; #[derive(Clone, PartialEq, Eq, Debug)] @@ -18,7 +18,7 @@ pub(crate) struct Blockchain { tip: [u8; 32], next_nonces: HashMap<::G, u32>, - provided: ProvidedTransactions, + provided: ProvidedTransactions, mempool: Mempool, } @@ -50,7 +50,7 @@ impl Blockchain { genesis: [u8; 32], participants: &[::G], ) -> Self { - // TODO: Reload provided/mempool + // TODO: Reload mempool let mut next_nonces = HashMap::new(); for participant in participants { @@ -58,14 +58,14 @@ impl Blockchain { } let mut res = Self { - db: Some(db), + db: Some(db.clone()), genesis, block_number: 0, tip: genesis, next_nonces, - provided: ProvidedTransactions::new(), + provided: ProvidedTransactions::new(db, genesis), mempool: Mempool::new(genesis), }; @@ -102,13 +102,8 @@ impl Blockchain { self.mempool.add(&self.next_nonces, internal, tx) } - pub(crate) fn provide_transaction(&mut self, tx: T) -> bool { - // TODO: Should this check be internal to ProvidedTransactions? - if verify_transaction(&tx, self.genesis, &mut HashMap::new()).is_err() { - return false; - } - self.provided.provide(tx); - true + pub(crate) fn provide_transaction(&mut self, tx: T) -> Result<(), ProvidedError> { + self.provided.provide(tx) } /// Returns the next nonce for signing, or None if they aren't a participant. @@ -159,7 +154,7 @@ impl Blockchain { for tx in &block.transactions { match tx.kind() { TransactionKind::Provided => { - self.provided.complete(tx.hash()); + self.provided.complete(&mut txn, tx.hash()); } TransactionKind::Unsigned => {} TransactionKind::Signed(Signed { signer, nonce, .. }) => { diff --git a/coordinator/tributary/src/lib.rs b/coordinator/tributary/src/lib.rs index 09eb79b2..ec386f80 100644 --- a/coordinator/tributary/src/lib.rs +++ b/coordinator/tributary/src/lib.rs @@ -29,6 +29,7 @@ pub use transaction::*; mod provided; pub(crate) use provided::*; +pub use provided::ProvidedError; mod block; pub use block::*; @@ -125,9 +126,7 @@ impl Tributary { Self { network, synced_block, messages } } - // TODO: Is there a race condition with providing these? Since the same provided TX provided - // twice counts as two transactions - pub fn provide_transaction(&self, tx: T) -> bool { + pub fn provide_transaction(&self, tx: T) -> Result<(), ProvidedError> { self.network.blockchain.write().unwrap().provide_transaction(tx) } diff --git a/coordinator/tributary/src/provided.rs b/coordinator/tributary/src/provided.rs index b86f0cfd..957034f1 100644 --- a/coordinator/tributary/src/provided.rs +++ b/coordinator/tributary/src/provided.rs @@ -1,32 +1,95 @@ -use std::collections::VecDeque; +use std::collections::{VecDeque, HashMap}; -use crate::{TransactionKind, Transaction}; +use thiserror::Error; + +use serai_db::{Get, DbTxn, Db}; + +use crate::{TransactionKind, TransactionError, Transaction, verify_transaction}; + +#[derive(Clone, PartialEq, Eq, Debug, Error)] +pub enum ProvidedError { + /// The provided transaction's kind wasn't Provided + #[error("transaction wasn't a provided transaction")] + NotProvided, + /// The provided transaction was invalid. + #[error("provided transaction was invalid")] + InvalidProvided(TransactionError), + /// Transaction was already provided + #[error("transaction was already provided")] + AlreadyProvided, +} #[derive(Clone, PartialEq, Eq, Debug)] -pub struct ProvidedTransactions { +pub struct ProvidedTransactions { + db: D, + genesis: [u8; 32], + pub(crate) transactions: VecDeque, } -impl Default for ProvidedTransactions { - fn default() -> Self { - ProvidedTransactions { transactions: VecDeque::new() } +impl ProvidedTransactions { + fn provided_key(&self, hash: &[u8]) -> Vec { + D::key(b"tributary", b"provided", [self.genesis.as_ref(), hash].concat()) + } + fn currently_provided_key(&self) -> Vec { + D::key(b"tributary", b"currently_provided", self.genesis) } -} -impl ProvidedTransactions { - pub(crate) fn new() -> Self { - ProvidedTransactions::default() + pub(crate) fn new(db: D, genesis: [u8; 32]) -> Self { + let mut res = ProvidedTransactions { db, genesis, transactions: VecDeque::new() }; + + let currently_provided = res.db.get(res.currently_provided_key()).unwrap_or(vec![]); + let mut i = 0; + while i < currently_provided.len() { + res.transactions.push_back( + T::read::<&[u8]>( + &mut res.db.get(res.provided_key(¤tly_provided[i .. (i + 32)])).unwrap().as_ref(), + ) + .unwrap(), + ); + i += 32; + } + + res } /// Provide a transaction for inclusion in a block. - pub(crate) fn provide(&mut self, tx: T) { - // TODO: Make an error out of this - assert_eq!(tx.kind(), TransactionKind::Provided, "provided a non-provided transaction"); + pub(crate) fn provide(&mut self, tx: T) -> Result<(), ProvidedError> { + if tx.kind() != TransactionKind::Provided { + Err(ProvidedError::NotProvided)?; + } + + match verify_transaction(&tx, self.genesis, &mut HashMap::new()) { + Ok(()) => {} + Err(e) => Err(ProvidedError::InvalidProvided(e))?, + } + + let tx_hash = tx.hash(); + let provided_key = self.provided_key(&tx_hash); + if self.db.get(&provided_key).is_some() { + Err(ProvidedError::AlreadyProvided)?; + } + + let currently_provided_key = self.currently_provided_key(); + let mut currently_provided = self.db.get(¤tly_provided_key).unwrap_or(vec![]); + + let mut txn = self.db.txn(); + txn.put(provided_key, tx.serialize()); + currently_provided.extend(tx_hash); + txn.put(currently_provided_key, currently_provided); + txn.commit(); + self.transactions.push_back(tx); + Ok(()) } /// Complete a provided transaction, no longer proposing it nor voting for its validity. - pub(crate) fn complete(&mut self, tx: [u8; 32]) { + pub(crate) fn complete(&mut self, txn: &mut D::Transaction<'_>, tx: [u8; 32]) { assert_eq!(self.transactions.pop_front().unwrap().hash(), tx); + + let currently_provided_key = self.currently_provided_key(); + let mut currently_provided = txn.get(¤tly_provided_key).unwrap(); + assert_eq!(¤tly_provided.drain(.. 32).collect::>(), &tx); + txn.put(currently_provided_key, currently_provided); } } diff --git a/coordinator/tributary/src/tests/blockchain.rs b/coordinator/tributary/src/tests/blockchain.rs index 3c139fdc..de0406f3 100644 --- a/coordinator/tributary/src/tests/blockchain.rs +++ b/coordinator/tributary/src/tests/blockchain.rs @@ -1,3 +1,5 @@ +use std::collections::VecDeque; + use zeroize::Zeroizing; use rand::{RngCore, rngs::OsRng}; @@ -5,10 +7,10 @@ use blake2::{Digest, Blake2s256}; use ciphersuite::{group::ff::Field, Ciphersuite, Ristretto}; -use serai_db::MemDb; +use serai_db::{DbTxn, Db, MemDb}; use crate::{ - merkle, Transaction, ProvidedTransactions, Block, Blockchain, + merkle, Transaction, ProvidedError, ProvidedTransactions, Block, Blockchain, tests::{ProvidedTransaction, SignedTransaction, random_provided_transaction}, }; @@ -173,21 +175,33 @@ fn signed_transaction() { #[test] fn provided_transaction() { - let mut blockchain = new_blockchain::(new_genesis(), &[]); + let genesis = new_genesis(); + let mut blockchain = new_blockchain::(genesis, &[]); let tx = random_provided_transaction(&mut OsRng); // This should be provideable - let mut txs = ProvidedTransactions::new(); - txs.provide(tx.clone()); - txs.complete(tx.hash()); + let mut db = MemDb::new(); + let mut txs = ProvidedTransactions::<_, ProvidedTransaction>::new(db.clone(), genesis); + txs.provide(tx.clone()).unwrap(); + assert_eq!(txs.provide(tx.clone()), Err(ProvidedError::AlreadyProvided)); + assert_eq!( + ProvidedTransactions::<_, ProvidedTransaction>::new(db.clone(), genesis).transactions, + VecDeque::from([tx.clone()]), + ); + let mut txn = db.txn(); + txs.complete(&mut txn, tx.hash()); + txn.commit(); + assert!(ProvidedTransactions::<_, ProvidedTransaction>::new(db.clone(), genesis) + .transactions + .is_empty()); // Non-provided transactions should fail verification let block = Block::new(blockchain.tip(), vec![tx.clone()], vec![]); assert!(blockchain.verify_block(&block).is_err()); // Provided transactions should pass verification - blockchain.provide_transaction(tx.clone()); + blockchain.provide_transaction(tx.clone()).unwrap(); blockchain.verify_block(&block).unwrap(); // add_block should work for verified blocks