Reloaded provided transactions from the disk

Also resolves a race condition by asserting provided transactions must be
unique, allowing them to be safely provided multiple times.
This commit is contained in:
Luke Parker 2023-04-14 15:03:01 -04:00
parent 63318cb728
commit 695d923593
No known key found for this signature in database
4 changed files with 109 additions and 38 deletions

View file

@ -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<D: Db, T: Transaction> {
tip: [u8; 32],
next_nonces: HashMap<<Ristretto as Ciphersuite>::G, u32>,
provided: ProvidedTransactions<T>,
provided: ProvidedTransactions<D, T>,
mempool: Mempool<T>,
}
@ -50,7 +50,7 @@ impl<D: Db, T: Transaction> Blockchain<D, T> {
genesis: [u8; 32],
participants: &[<Ristretto as Ciphersuite>::G],
) -> Self {
// TODO: Reload provided/mempool
// TODO: Reload mempool
let mut next_nonces = HashMap::new();
for participant in participants {
@ -58,14 +58,14 @@ impl<D: Db, T: Transaction> Blockchain<D, T> {
}
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<D: Db, T: Transaction> Blockchain<D, T> {
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<D: Db, T: Transaction> Blockchain<D, T> {
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, .. }) => {

View file

@ -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<D: Db, T: Transaction, P: P2p> Tributary<D, T, P> {
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)
}

View file

@ -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<T: Transaction> {
pub struct ProvidedTransactions<D: Db, T: Transaction> {
db: D,
genesis: [u8; 32],
pub(crate) transactions: VecDeque<T>,
}
impl<T: Transaction> Default for ProvidedTransactions<T> {
fn default() -> Self {
ProvidedTransactions { transactions: VecDeque::new() }
impl<D: Db, T: Transaction> ProvidedTransactions<D, T> {
fn provided_key(&self, hash: &[u8]) -> Vec<u8> {
D::key(b"tributary", b"provided", [self.genesis.as_ref(), hash].concat())
}
fn currently_provided_key(&self) -> Vec<u8> {
D::key(b"tributary", b"currently_provided", self.genesis)
}
}
impl<T: Transaction> ProvidedTransactions<T> {
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(&currently_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(&currently_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(&currently_provided_key).unwrap();
assert_eq!(&currently_provided.drain(.. 32).collect::<Vec<_>>(), &tx);
txn.put(currently_provided_key, currently_provided);
}
}

View file

@ -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::<ProvidedTransaction>(new_genesis(), &[]);
let genesis = new_genesis();
let mut blockchain = new_blockchain::<ProvidedTransaction>(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