Use multiple nonces in the Tributary

This commit is contained in:
Luke Parker 2023-12-01 12:09:22 -05:00
parent c82d1283af
commit 1ca66b846a
11 changed files with 221 additions and 200 deletions

View file

@ -7,13 +7,12 @@ use thiserror::Error;
use blake2::{Digest, Blake2s256}; use blake2::{Digest, Blake2s256};
use ciphersuite::{Ciphersuite, Ristretto};
use tendermint::ext::{Network, Commit}; use tendermint::ext::{Network, Commit};
use crate::{ use crate::{
transaction::{ transaction::{
TransactionError, Signed, TransactionKind, Transaction as TransactionTrait, verify_transaction, TransactionError, Signed, TransactionKind, Transaction as TransactionTrait, GAIN,
verify_transaction,
}, },
BLOCK_SIZE_LIMIT, ReadWrite, merkle, Transaction, BLOCK_SIZE_LIMIT, ReadWrite, merkle, Transaction,
tendermint::tx::verify_tendermint_tx, tendermint::tx::verify_tendermint_tx,
@ -122,7 +121,7 @@ impl<T: TransactionTrait> Block<T> {
let mut unsigned = vec![]; let mut unsigned = vec![];
for tx in mempool { for tx in mempool {
match tx.kind() { match tx.kind() {
TransactionKind::Signed(_) => signed.push(tx), TransactionKind::Signed(_, _) => signed.push(tx),
TransactionKind::Unsigned => unsigned.push(tx), TransactionKind::Unsigned => unsigned.push(tx),
TransactionKind::Provided(_) => panic!("provided transaction entered mempool"), TransactionKind::Provided(_) => panic!("provided transaction entered mempool"),
} }
@ -135,7 +134,7 @@ impl<T: TransactionTrait> Block<T> {
// Check TXs are sorted by nonce. // Check TXs are sorted by nonce.
let nonce = |tx: &Transaction<T>| { let nonce = |tx: &Transaction<T>| {
if let TransactionKind::Signed(Signed { nonce, .. }) = tx.kind() { if let TransactionKind::Signed(_, Signed { nonce, .. }) = tx.kind() {
*nonce *nonce
} else { } else {
0 0
@ -169,12 +168,12 @@ impl<T: TransactionTrait> Block<T> {
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(crate) fn verify<N: Network>( pub(crate) fn verify<N: Network, G: GAIN>(
&self, &self,
genesis: [u8; 32], genesis: [u8; 32],
last_block: [u8; 32], last_block: [u8; 32],
mut locally_provided: HashMap<&'static str, VecDeque<T>>, mut locally_provided: HashMap<&'static str, VecDeque<T>>,
mut next_nonces: HashMap<<Ristretto as Ciphersuite>::G, u32>, get_and_increment_nonce: &mut G,
schema: N::SignatureScheme, schema: N::SignatureScheme,
commit: impl Fn(u32) -> Option<Commit<N::SignatureScheme>>, commit: impl Fn(u32) -> Option<Commit<N::SignatureScheme>>,
unsigned_in_chain: impl Fn([u8; 32]) -> bool, unsigned_in_chain: impl Fn([u8; 32]) -> bool,
@ -259,10 +258,12 @@ impl<T: TransactionTrait> Block<T> {
Err(e) => Err(BlockError::TransactionError(e))?, Err(e) => Err(BlockError::TransactionError(e))?,
} }
} }
Transaction::Application(tx) => match verify_transaction(tx, genesis, &mut next_nonces) { Transaction::Application(tx) => {
match verify_transaction(tx, genesis, get_and_increment_nonce) {
Ok(()) => {} Ok(()) => {}
Err(e) => Err(BlockError::TransactionError(e))?, Err(e) => Err(BlockError::TransactionError(e))?,
}, }
}
} }
} }

View file

@ -1,8 +1,8 @@
use std::collections::{VecDeque, HashMap}; use std::collections::{VecDeque, HashSet};
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto}; use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
use serai_db::{DbTxn, Db}; use serai_db::{Get, DbTxn, Db};
use scale::Decode; use scale::Decode;
@ -20,7 +20,7 @@ pub(crate) struct Blockchain<D: Db, T: TransactionTrait> {
block_number: u32, block_number: u32,
tip: [u8; 32], tip: [u8; 32],
next_nonces: HashMap<<Ristretto as Ciphersuite>::G, u32>, participants: HashSet<<Ristretto as Ciphersuite>::G>,
provided: ProvidedTransactions<D, T>, provided: ProvidedTransactions<D, T>,
mempool: Mempool<D, T>, mempool: Mempool<D, T>,
@ -53,11 +53,15 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
fn provided_included_key(genesis: &[u8], hash: &[u8; 32]) -> Vec<u8> { fn provided_included_key(genesis: &[u8], hash: &[u8; 32]) -> Vec<u8> {
D::key(b"tributary_blockchain", b"provided_included", [genesis, hash].concat()) D::key(b"tributary_blockchain", b"provided_included", [genesis, hash].concat())
} }
fn next_nonce_key(&self, signer: &<Ristretto as Ciphersuite>::G) -> Vec<u8> { fn next_nonce_key(
genesis: &[u8; 32],
signer: &<Ristretto as Ciphersuite>::G,
order: &[u8],
) -> Vec<u8> {
D::key( D::key(
b"tributary_blockchain", b"tributary_blockchain",
b"next_nonce", b"next_nonce",
[self.genesis.as_ref(), signer.to_bytes().as_ref()].concat(), [genesis.as_ref(), signer.to_bytes().as_ref(), order].concat(),
) )
} }
@ -66,18 +70,13 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
genesis: [u8; 32], genesis: [u8; 32],
participants: &[<Ristretto as Ciphersuite>::G], participants: &[<Ristretto as Ciphersuite>::G],
) -> Self { ) -> Self {
let mut next_nonces = HashMap::new();
for participant in participants {
next_nonces.insert(*participant, 0);
}
let mut res = Self { let mut res = Self {
db: Some(db.clone()), db: Some(db.clone()),
genesis, genesis,
participants: participants.iter().cloned().collect(),
block_number: 0, block_number: 0,
tip: genesis, tip: genesis,
next_nonces,
provided: ProvidedTransactions::new(db.clone(), genesis), provided: ProvidedTransactions::new(db.clone(), genesis),
mempool: Mempool::new(db, genesis), mempool: Mempool::new(db, genesis),
@ -93,12 +92,6 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
res.tip.copy_from_slice(&tip); res.tip.copy_from_slice(&tip);
} }
for participant in participants {
if let Some(next_nonce) = res.db.as_ref().unwrap().get(res.next_nonce_key(participant)) {
res.next_nonces.insert(*participant, u32::from_le_bytes(next_nonce.try_into().unwrap()));
}
}
res res
} }
@ -179,27 +172,58 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
let unsigned_in_chain = let unsigned_in_chain =
|hash: [u8; 32]| db.get(Self::unsigned_included_key(&self.genesis, &hash)).is_some(); |hash: [u8; 32]| db.get(Self::unsigned_included_key(&self.genesis, &hash)).is_some();
self.mempool.add::<N>(&self.next_nonces, internal, tx, schema, unsigned_in_chain, commit) self.mempool.add::<N, _>(
|signer, order| {
if self.participants.contains(&signer) {
Some(
db.get(Self::next_nonce_key(&self.genesis, &signer, &order))
.map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap()))
.unwrap_or(0),
)
} else {
None
}
},
internal,
tx,
schema,
unsigned_in_chain,
commit,
)
} }
pub(crate) fn provide_transaction(&mut self, tx: T) -> Result<(), ProvidedError> { pub(crate) fn provide_transaction(&mut self, tx: T) -> Result<(), ProvidedError> {
self.provided.provide(tx) self.provided.provide(tx)
} }
/// Returns the next nonce for signing, or None if they aren't a participant. pub(crate) fn next_nonce(
pub(crate) fn next_nonce(&self, key: <Ristretto as Ciphersuite>::G) -> Option<u32> { &self,
Some(self.next_nonces.get(&key).cloned()?.max(self.mempool.next_nonce(&key).unwrap_or(0))) signer: &<Ristretto as Ciphersuite>::G,
order: &[u8],
) -> Option<u32> {
if let Some(next_nonce) = self.mempool.next_nonce_in_mempool(signer, order.to_vec()) {
return Some(next_nonce);
}
if self.participants.contains(signer) {
Some(
self
.db
.as_ref()
.unwrap()
.get(Self::next_nonce_key(&self.genesis, signer, order))
.map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap()))
.unwrap_or(0),
)
} else {
None
}
} }
pub(crate) fn build_block<N: Network>(&mut self, schema: N::SignatureScheme) -> Block<T> { pub(crate) fn build_block<N: Network>(&mut self, schema: N::SignatureScheme) -> Block<T> {
let db = self.db.as_ref().unwrap();
let unsigned_in_chain =
|hash: [u8; 32]| db.get(Self::unsigned_included_key(&self.genesis, &hash)).is_some();
let block = Block::new( let block = Block::new(
self.tip, self.tip,
self.provided.transactions.values().flatten().cloned().collect(), self.provided.transactions.values().flatten().cloned().collect(),
self.mempool.block(&self.next_nonces, unsigned_in_chain), self.mempool.block(),
); );
// build_block should not return invalid blocks // build_block should not return invalid blocks
self.verify_block::<N>(&block, schema, false).unwrap(); self.verify_block::<N>(&block, schema, false).unwrap();
@ -222,17 +246,34 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
// commit has to be valid if it is coming from our db // commit has to be valid if it is coming from our db
Some(Commit::<N::SignatureScheme>::decode(&mut commit.as_ref()).unwrap()) Some(Commit::<N::SignatureScheme>::decode(&mut commit.as_ref()).unwrap())
}; };
block.verify::<N>(
let mut txn_db = db.clone();
let mut txn = txn_db.txn();
let res = block.verify::<N, _>(
self.genesis, self.genesis,
self.tip, self.tip,
self.provided.transactions.clone(), self.provided.transactions.clone(),
self.next_nonces.clone(), &mut |signer, order| {
if self.participants.contains(signer) {
let key = Self::next_nonce_key(&self.genesis, signer, order);
let next = txn
.get(&key)
.map(|next_nonce| u32::from_le_bytes(next_nonce.try_into().unwrap()))
.unwrap_or(0);
txn.put(key, (next + 1).to_le_bytes());
Some(next)
} else {
None
}
},
schema, schema,
&commit, &commit,
unsigned_in_chain, unsigned_in_chain,
provided_in_chain, provided_in_chain,
allow_non_local_provided, allow_non_local_provided,
) );
drop(txn);
res
} }
/// Add a block. /// Add a block.
@ -285,18 +326,9 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
// remove from the mempool // remove from the mempool
self.mempool.remove(&hash); self.mempool.remove(&hash);
} }
TransactionKind::Signed(Signed { signer, nonce, .. }) => { TransactionKind::Signed(order, Signed { signer, nonce, .. }) => {
let next_nonce = nonce + 1; let next_nonce = nonce + 1;
let prev = self txn.put(Self::next_nonce_key(&self.genesis, signer, &order), next_nonce.to_le_bytes());
.next_nonces
.insert(*signer, next_nonce)
.expect("block had signed transaction from non-participant");
if prev != *nonce {
panic!("verified block had an invalid nonce");
}
txn.put(self.next_nonce_key(signer), next_nonce.to_le_bytes());
self.mempool.remove(&tx.hash()); self.mempool.remove(&tx.hash());
} }
} }

View file

@ -254,8 +254,12 @@ impl<D: Db, T: TransactionTrait, P: P2p> Tributary<D, T, P> {
self.network.blockchain.write().await.provide_transaction(tx) self.network.blockchain.write().await.provide_transaction(tx)
} }
pub async fn next_nonce(&self, signer: <Ristretto as Ciphersuite>::G) -> Option<u32> { pub async fn next_nonce(
self.network.blockchain.read().await.next_nonce(signer) &self,
signer: &<Ristretto as Ciphersuite>::G,
order: &[u8],
) -> Option<u32> {
self.network.blockchain.read().await.next_nonce(signer, order)
} }
// Returns Ok(true) if new, Ok(false) if an already present unsigned, or the error. // Returns Ok(true) if new, Ok(false) if an already present unsigned, or the error.

View file

@ -20,8 +20,9 @@ pub(crate) struct Mempool<D: Db, T: TransactionTrait> {
db: D, db: D,
genesis: [u8; 32], genesis: [u8; 32],
last_nonce_in_mempool: HashMap<(<Ristretto as Ciphersuite>::G, Vec<u8>), u32>,
txs: HashMap<[u8; 32], Transaction<T>>, txs: HashMap<[u8; 32], Transaction<T>>,
next_nonces: HashMap<<Ristretto as Ciphersuite>::G, u32>, txs_per_signer: HashMap<<Ristretto as Ciphersuite>::G, u32>,
} }
impl<D: Db, T: TransactionTrait> Mempool<D, T> { impl<D: Db, T: TransactionTrait> Mempool<D, T> {
@ -58,7 +59,13 @@ impl<D: Db, T: TransactionTrait> Mempool<D, T> {
} }
pub(crate) fn new(db: D, genesis: [u8; 32]) -> Self { pub(crate) fn new(db: D, genesis: [u8; 32]) -> Self {
let mut res = Mempool { db, genesis, txs: HashMap::new(), next_nonces: HashMap::new() }; let mut res = Mempool {
db,
genesis,
last_nonce_in_mempool: HashMap::new(),
txs: HashMap::new(),
txs_per_signer: HashMap::new(),
};
let current_mempool = res.db.get(res.current_mempool_key()).unwrap_or(vec![]); let current_mempool = res.db.get(res.current_mempool_key()).unwrap_or(vec![]);
@ -73,21 +80,24 @@ impl<D: Db, T: TransactionTrait> Mempool<D, T> {
Transaction::Tendermint(tx) => { Transaction::Tendermint(tx) => {
res.txs.insert(hash, Transaction::Tendermint(tx)); res.txs.insert(hash, Transaction::Tendermint(tx));
} }
Transaction::Application(tx) => { Transaction::Application(tx) => match tx.kind() {
match tx.kind() { TransactionKind::Signed(order, Signed { signer, nonce, .. }) => {
TransactionKind::Signed(Signed { signer, nonce, .. }) => { let amount = *res.txs_per_signer.get(signer).unwrap_or(&0) + 1;
if let Some(prev) = res.next_nonces.insert(*signer, nonce + 1) { res.txs_per_signer.insert(*signer, amount);
// These mempool additions should've been ordered
debug_assert!(prev < *nonce); if let Some(prior_nonce) =
res.last_nonce_in_mempool.insert((*signer, order.clone()), *nonce)
{
assert_eq!(prior_nonce, nonce - 1);
} }
res.txs.insert(hash, Transaction::Application(tx)); res.txs.insert(hash, Transaction::Application(tx));
} }
TransactionKind::Unsigned => { TransactionKind::Unsigned => {
res.txs.insert(hash, Transaction::Application(tx)); res.txs.insert(hash, Transaction::Application(tx));
} }
_ => panic!("mempool database had a provided transaction"), _ => panic!("mempool database had a provided transaction"),
} },
}
} }
} }
@ -95,9 +105,12 @@ impl<D: Db, T: TransactionTrait> Mempool<D, T> {
} }
// Returns Ok(true) if new, Ok(false) if an already present unsigned, or the error. // Returns Ok(true) if new, Ok(false) if an already present unsigned, or the error.
pub(crate) fn add<N: Network>( pub(crate) fn add<
N: Network,
F: FnOnce(<Ristretto as Ciphersuite>::G, Vec<u8>) -> Option<u32>,
>(
&mut self, &mut self,
blockchain_next_nonces: &HashMap<<Ristretto as Ciphersuite>::G, u32>, blockchain_next_nonce: F,
internal: bool, internal: bool,
tx: Transaction<T>, tx: Transaction<T>,
schema: N::SignatureScheme, schema: N::SignatureScheme,
@ -119,32 +132,31 @@ impl<D: Db, T: TransactionTrait> Mempool<D, T> {
} }
Transaction::Application(app_tx) => { Transaction::Application(app_tx) => {
match app_tx.kind() { match app_tx.kind() {
TransactionKind::Signed(Signed { signer, nonce, .. }) => { TransactionKind::Signed(order, Signed { signer, .. }) => {
// Get the nonce from the blockchain // Get the nonce from the blockchain
let Some(blockchain_next_nonce) = blockchain_next_nonces.get(signer).cloned() else { let Some(blockchain_next_nonce) = blockchain_next_nonce(*signer, order.clone()) else {
// Not a participant // Not a participant
Err(TransactionError::InvalidSigner)? Err(TransactionError::InvalidSigner)?
}; };
let mut next_nonce = blockchain_next_nonce;
// If the blockchain's nonce is greater than the mempool's, use it if let Some(mempool_last_nonce) =
// Default to true so if the mempool hasn't tracked this nonce yet, it'll be inserted self.last_nonce_in_mempool.get(&(*signer, order.clone()))
let mut blockchain_is_greater = true; {
if let Some(mempool_next_nonce) = self.next_nonces.get(signer) { assert!(*mempool_last_nonce >= blockchain_next_nonce);
blockchain_is_greater = blockchain_next_nonce > *mempool_next_nonce; next_nonce = *mempool_last_nonce + 1;
}
if blockchain_is_greater {
self.next_nonces.insert(*signer, blockchain_next_nonce);
} }
// If we have too many transactions from this sender, don't add this yet UNLESS we are // If we have too many transactions from this sender, don't add this yet UNLESS we are
// this sender // this sender
if !internal && (nonce >= &(blockchain_next_nonce + ACCOUNT_MEMPOOL_LIMIT)) { let amount_in_pool = *self.txs_per_signer.get(signer).unwrap_or(&0) + 1;
if !internal && (amount_in_pool > ACCOUNT_MEMPOOL_LIMIT) {
Err(TransactionError::TooManyInMempool)?; Err(TransactionError::TooManyInMempool)?;
} }
verify_transaction(app_tx, self.genesis, &mut self.next_nonces)?; verify_transaction(app_tx, self.genesis, &mut |_, _| Some(next_nonce))?;
debug_assert_eq!(self.next_nonces[signer], nonce + 1); self.last_nonce_in_mempool.insert((*signer, order.clone()), next_nonce);
self.txs_per_signer.insert(*signer, amount_in_pool);
} }
TransactionKind::Unsigned => { TransactionKind::Unsigned => {
// check we have the tx in the pool/chain // check we have the tx in the pool/chain
@ -165,38 +177,26 @@ impl<D: Db, T: TransactionTrait> Mempool<D, T> {
} }
// Returns None if the mempool doesn't have a nonce tracked. // Returns None if the mempool doesn't have a nonce tracked.
pub(crate) fn next_nonce(&self, signer: &<Ristretto as Ciphersuite>::G) -> Option<u32> { pub(crate) fn next_nonce_in_mempool(
self.next_nonces.get(signer).cloned() &self,
signer: &<Ristretto as Ciphersuite>::G,
order: Vec<u8>,
) -> Option<u32> {
self.last_nonce_in_mempool.get(&(*signer, order)).cloned().map(|nonce| nonce + 1)
} }
/// Get transactions to include in a block. /// Get transactions to include in a block.
pub(crate) fn block( pub(crate) fn block(&mut self) -> Vec<Transaction<T>> {
&mut self,
blockchain_next_nonces: &HashMap<<Ristretto as Ciphersuite>::G, u32>,
unsigned_in_chain: impl Fn([u8; 32]) -> bool,
) -> Vec<Transaction<T>> {
let mut unsigned = vec![]; let mut unsigned = vec![];
let mut signed = vec![]; let mut signed = vec![];
for hash in self.txs.keys().cloned().collect::<Vec<_>>() { for hash in self.txs.keys().cloned().collect::<Vec<_>>() {
let tx = &self.txs[&hash]; let tx = &self.txs[&hash];
// Verify this hasn't gone stale
match tx.kind() { match tx.kind() {
TransactionKind::Signed(Signed { signer, nonce, .. }) => { TransactionKind::Signed(_, Signed { .. }) => {
if blockchain_next_nonces[signer] > *nonce {
self.remove(&hash);
continue;
}
// Since this TX isn't stale, include it
signed.push(tx.clone()); signed.push(tx.clone());
} }
TransactionKind::Unsigned => { TransactionKind::Unsigned => {
if unsigned_in_chain(hash) {
self.remove(&hash);
continue;
}
unsigned.push(tx.clone()); unsigned.push(tx.clone());
} }
_ => panic!("provided transaction entered mempool"), _ => panic!("provided transaction entered mempool"),
@ -205,7 +205,7 @@ impl<D: Db, T: TransactionTrait> Mempool<D, T> {
// Sort signed by nonce // Sort signed by nonce
let nonce = |tx: &Transaction<T>| { let nonce = |tx: &Transaction<T>| {
if let TransactionKind::Signed(Signed { nonce, .. }) = tx.kind() { if let TransactionKind::Signed(_, Signed { nonce, .. }) = tx.kind() {
*nonce *nonce
} else { } else {
unreachable!() unreachable!()
@ -242,7 +242,16 @@ impl<D: Db, T: TransactionTrait> Mempool<D, T> {
} }
txn.commit(); txn.commit();
self.txs.remove(tx); if let Some(tx) = self.txs.remove(tx) {
if let TransactionKind::Signed(order, Signed { signer, nonce, .. }) = tx.kind() {
let amount = *self.txs_per_signer.get(signer).unwrap() - 1;
self.txs_per_signer.insert(*signer, amount);
if self.last_nonce_in_mempool.get(&(*signer, order.clone())) == Some(nonce) {
self.last_nonce_in_mempool.remove(&(*signer, order));
}
}
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -89,7 +89,7 @@ impl<D: Db, T: Transaction> ProvidedTransactions<D, T> {
pub(crate) fn provide(&mut self, tx: T) -> Result<(), ProvidedError> { pub(crate) fn provide(&mut self, tx: T) -> Result<(), ProvidedError> {
let TransactionKind::Provided(order) = tx.kind() else { Err(ProvidedError::NotProvided)? }; let TransactionKind::Provided(order) = tx.kind() else { Err(ProvidedError::NotProvided)? };
match verify_transaction(&tx, self.genesis, &mut HashMap::new()) { match verify_transaction(&tx, self.genesis, &mut |_, _| None) {
Ok(()) => {} Ok(()) => {}
Err(e) => Err(ProvidedError::InvalidProvided(e))?, Err(e) => Err(ProvidedError::InvalidProvided(e))?,
} }

View file

@ -61,7 +61,7 @@ impl ReadWrite for NonceTransaction {
impl TransactionTrait for NonceTransaction { impl TransactionTrait for NonceTransaction {
fn kind(&self) -> TransactionKind<'_> { fn kind(&self) -> TransactionKind<'_> {
TransactionKind::Signed(&self.2) TransactionKind::Signed(vec![], &self.2)
} }
fn hash(&self) -> [u8; 32] { fn hash(&self) -> [u8; 32] {
@ -84,11 +84,11 @@ fn empty_block() {
let unsigned_in_chain = |_: [u8; 32]| false; let unsigned_in_chain = |_: [u8; 32]| false;
let provided_in_chain = |_: [u8; 32]| false; let provided_in_chain = |_: [u8; 32]| false;
Block::<NonceTransaction>::new(LAST, vec![], vec![]) Block::<NonceTransaction>::new(LAST, vec![], vec![])
.verify::<N>( .verify::<N, _>(
GENESIS, GENESIS,
LAST, LAST,
HashMap::new(), HashMap::new(),
HashMap::new(), &mut |_, _| None,
validators, validators,
commit, commit,
unsigned_in_chain, unsigned_in_chain,
@ -119,11 +119,16 @@ fn duplicate_nonces() {
let unsigned_in_chain = |_: [u8; 32]| false; let unsigned_in_chain = |_: [u8; 32]| false;
let provided_in_chain = |_: [u8; 32]| false; let provided_in_chain = |_: [u8; 32]| false;
let res = Block::new(LAST, vec![], mempool).verify::<N>( let mut last_nonce = 0;
let res = Block::new(LAST, vec![], mempool).verify::<N, _>(
GENESIS, GENESIS,
LAST, LAST,
HashMap::new(), HashMap::new(),
HashMap::from([(<Ristretto as Ciphersuite>::G::identity(), 0)]), &mut |_, _| {
let res = last_nonce;
last_nonce += 1;
Some(res)
},
validators.clone(), validators.clone(),
commit, commit,
unsigned_in_chain, unsigned_in_chain,

View file

@ -156,7 +156,7 @@ fn signed_transaction() {
let signer = tx.1.signer; let signer = tx.1.signer;
let (_, mut blockchain) = new_blockchain::<SignedTransaction>(genesis, &[signer]); let (_, mut blockchain) = new_blockchain::<SignedTransaction>(genesis, &[signer]);
assert_eq!(blockchain.next_nonce(signer), Some(0)); assert_eq!(blockchain.next_nonce(&signer, &[]), Some(0));
let test = |blockchain: &mut Blockchain<MemDb, SignedTransaction>, let test = |blockchain: &mut Blockchain<MemDb, SignedTransaction>,
mempool: Vec<Transaction<SignedTransaction>>| { mempool: Vec<Transaction<SignedTransaction>>| {
@ -165,11 +165,11 @@ fn signed_transaction() {
let Transaction::Application(tx) = tx else { let Transaction::Application(tx) = tx else {
panic!("tendermint tx found"); panic!("tendermint tx found");
}; };
let next_nonce = blockchain.next_nonce(signer).unwrap(); let next_nonce = blockchain.next_nonce(&signer, &[]).unwrap();
blockchain blockchain
.add_transaction::<N>(true, Transaction::Application(tx), validators.clone()) .add_transaction::<N>(true, Transaction::Application(tx), validators.clone())
.unwrap(); .unwrap();
assert_eq!(next_nonce + 1, blockchain.next_nonce(signer).unwrap()); assert_eq!(next_nonce + 1, blockchain.next_nonce(&signer, &[]).unwrap());
} }
let block = blockchain.build_block::<N>(validators.clone()); let block = blockchain.build_block::<N>(validators.clone());
assert_eq!(block, Block::new(blockchain.tip(), vec![], mempool.clone())); assert_eq!(block, Block::new(blockchain.tip(), vec![], mempool.clone()));
@ -192,7 +192,7 @@ fn signed_transaction() {
// Test with a single nonce // Test with a single nonce
test(&mut blockchain, vec![Transaction::Application(tx)]); test(&mut blockchain, vec![Transaction::Application(tx)]);
assert_eq!(blockchain.next_nonce(signer), Some(1)); assert_eq!(blockchain.next_nonce(&signer, &[]), Some(1));
// Test with a flood of nonces // Test with a flood of nonces
let mut mempool = vec![]; let mut mempool = vec![];
@ -202,7 +202,7 @@ fn signed_transaction() {
))); )));
} }
test(&mut blockchain, mempool); test(&mut blockchain, mempool);
assert_eq!(blockchain.next_nonce(signer), Some(64)); assert_eq!(blockchain.next_nonce(&signer, &[]), Some(64));
} }
#[test] #[test]

View file

@ -36,16 +36,15 @@ async fn mempool_addition() {
let first_tx = signed_transaction(&mut OsRng, genesis, &key, 0); let first_tx = signed_transaction(&mut OsRng, genesis, &key, 0);
let signer = first_tx.1.signer; let signer = first_tx.1.signer;
assert_eq!(mempool.next_nonce(&signer), None); assert_eq!(mempool.next_nonce_in_mempool(&signer, vec![]), None);
// validators // validators
let validators = Arc::new(Validators::new(genesis, vec![(signer, 1)]).unwrap()); let validators = Arc::new(Validators::new(genesis, vec![(signer, 1)]).unwrap());
// Add TX 0 // Add TX 0
let mut blockchain_next_nonces = HashMap::from([(signer, 0)]);
assert!(mempool assert!(mempool
.add::<N>( .add::<N, _>(
&blockchain_next_nonces, &|_, _| Some(0),
true, true,
Transaction::Application(first_tx.clone()), Transaction::Application(first_tx.clone()),
validators.clone(), validators.clone(),
@ -53,15 +52,15 @@ async fn mempool_addition() {
commit, commit,
) )
.unwrap()); .unwrap());
assert_eq!(mempool.next_nonce(&signer), Some(1)); assert_eq!(mempool.next_nonce_in_mempool(&signer, vec![]), Some(1));
// add a tendermint evidence tx // add a tendermint evidence tx
let evidence_tx = let evidence_tx =
random_evidence_tx::<N>(Signer::new(genesis, key.clone()).into(), TendermintBlock(vec![])) random_evidence_tx::<N>(Signer::new(genesis, key.clone()).into(), TendermintBlock(vec![]))
.await; .await;
assert!(mempool assert!(mempool
.add::<N>( .add::<N, _>(
&blockchain_next_nonces, &|_, _| None,
true, true,
Transaction::Tendermint(evidence_tx.clone()), Transaction::Tendermint(evidence_tx.clone()),
validators.clone(), validators.clone(),
@ -75,8 +74,8 @@ async fn mempool_addition() {
// Adding them again should fail // Adding them again should fail
assert_eq!( assert_eq!(
mempool.add::<N>( mempool.add::<N, _>(
&blockchain_next_nonces, &|_, _| Some(0),
true, true,
Transaction::Application(first_tx.clone()), Transaction::Application(first_tx.clone()),
validators.clone(), validators.clone(),
@ -86,8 +85,8 @@ async fn mempool_addition() {
Err(TransactionError::InvalidNonce) Err(TransactionError::InvalidNonce)
); );
assert_eq!( assert_eq!(
mempool.add::<N>( mempool.add::<N, _>(
&blockchain_next_nonces, &|_, _| None,
true, true,
Transaction::Tendermint(evidence_tx.clone()), Transaction::Tendermint(evidence_tx.clone()),
validators.clone(), validators.clone(),
@ -100,8 +99,8 @@ async fn mempool_addition() {
// Do the same with the next nonce // Do the same with the next nonce
let second_tx = signed_transaction(&mut OsRng, genesis, &key, 1); let second_tx = signed_transaction(&mut OsRng, genesis, &key, 1);
assert_eq!( assert_eq!(
mempool.add::<N>( mempool.add::<N, _>(
&blockchain_next_nonces, &|_, _| Some(0),
true, true,
Transaction::Application(second_tx.clone()), Transaction::Application(second_tx.clone()),
validators.clone(), validators.clone(),
@ -110,10 +109,10 @@ async fn mempool_addition() {
), ),
Ok(true) Ok(true)
); );
assert_eq!(mempool.next_nonce(&signer), Some(2)); assert_eq!(mempool.next_nonce_in_mempool(&signer, vec![]), Some(2));
assert_eq!( assert_eq!(
mempool.add::<N>( mempool.add::<N, _>(
&blockchain_next_nonces, &|_, _| Some(0),
true, true,
Transaction::Application(second_tx.clone()), Transaction::Application(second_tx.clone()),
validators.clone(), validators.clone(),
@ -128,11 +127,10 @@ async fn mempool_addition() {
let second_key = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(&mut OsRng)); let second_key = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(&mut OsRng));
let tx = signed_transaction(&mut OsRng, genesis, &second_key, 2); let tx = signed_transaction(&mut OsRng, genesis, &second_key, 2);
let second_signer = tx.1.signer; let second_signer = tx.1.signer;
assert_eq!(mempool.next_nonce(&second_signer), None); assert_eq!(mempool.next_nonce_in_mempool(&second_signer, vec![]), None);
blockchain_next_nonces.insert(second_signer, 2);
assert!(mempool assert!(mempool
.add::<N>( .add::<N, _>(
&blockchain_next_nonces, &|_, _| Some(2),
true, true,
Transaction::Application(tx.clone()), Transaction::Application(tx.clone()),
validators.clone(), validators.clone(),
@ -140,24 +138,18 @@ async fn mempool_addition() {
commit commit
) )
.unwrap()); .unwrap());
assert_eq!(mempool.next_nonce(&second_signer), Some(3)); assert_eq!(mempool.next_nonce_in_mempool(&second_signer, vec![]), Some(3));
// Getting a block should work // Getting a block should work
assert_eq!(mempool.block(&blockchain_next_nonces, unsigned_in_chain).len(), 4); assert_eq!(mempool.block().len(), 4);
// If the blockchain says an account had its nonce updated, it should cause a prune // Removing should successfully prune
blockchain_next_nonces.insert(signer, 1);
let mut block = mempool.block(&blockchain_next_nonces, unsigned_in_chain);
assert_eq!(block.len(), 3);
assert!(!block.iter().any(|tx| tx.hash() == first_tx.hash()));
assert_eq!(mempool.txs(), &block.drain(..).map(|tx| (tx.hash(), tx)).collect::<HashMap<_, _>>());
// Removing should also successfully prune
mempool.remove(&tx.hash()); mempool.remove(&tx.hash());
assert_eq!( assert_eq!(
mempool.txs(), mempool.txs(),
&HashMap::from([ &HashMap::from([
(first_tx.hash(), Transaction::Application(first_tx)),
(second_tx.hash(), Transaction::Application(second_tx)), (second_tx.hash(), Transaction::Application(second_tx)),
(evidence_tx.hash(), Transaction::Tendermint(evidence_tx)) (evidence_tx.hash(), Transaction::Tendermint(evidence_tx))
]) ])
@ -173,13 +165,12 @@ fn too_many_mempool() {
}; };
let unsigned_in_chain = |_: [u8; 32]| false; let unsigned_in_chain = |_: [u8; 32]| false;
let key = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(&mut OsRng)); let key = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(&mut OsRng));
let signer = signed_transaction(&mut OsRng, genesis, &key, 0).1.signer;
// We should be able to add transactions up to the limit // We should be able to add transactions up to the limit
for i in 0 .. ACCOUNT_MEMPOOL_LIMIT { for i in 0 .. ACCOUNT_MEMPOOL_LIMIT {
assert!(mempool assert!(mempool
.add::<N>( .add::<N, _>(
&HashMap::from([(signer, 0)]), &|_, _| Some(0),
false, false,
Transaction::Application(signed_transaction(&mut OsRng, genesis, &key, i)), Transaction::Application(signed_transaction(&mut OsRng, genesis, &key, i)),
validators.clone(), validators.clone(),
@ -190,8 +181,8 @@ fn too_many_mempool() {
} }
// Yet adding more should fail // Yet adding more should fail
assert_eq!( assert_eq!(
mempool.add::<N>( mempool.add::<N, _>(
&HashMap::from([(signer, 0)]), &|_, _| Some(0),
false, false,
Transaction::Application(signed_transaction( Transaction::Application(signed_transaction(
&mut OsRng, &mut OsRng,

View file

@ -1,5 +1,5 @@
use core::ops::Deref; use core::ops::Deref;
use std::{sync::Arc, io, collections::HashMap}; use std::{sync::Arc, io};
use zeroize::Zeroizing; use zeroize::Zeroizing;
use rand::{RngCore, CryptoRng, rngs::OsRng}; use rand::{RngCore, CryptoRng, rngs::OsRng};
@ -114,7 +114,7 @@ impl ReadWrite for SignedTransaction {
impl Transaction for SignedTransaction { impl Transaction for SignedTransaction {
fn kind(&self) -> TransactionKind<'_> { fn kind(&self) -> TransactionKind<'_> {
TransactionKind::Signed(&self.1) TransactionKind::Signed(vec![], &self.1)
} }
fn hash(&self) -> [u8; 32] { fn hash(&self) -> [u8; 32] {
@ -145,9 +145,7 @@ pub fn signed_transaction<R: RngCore + CryptoRng>(
tx.1.signature.R = Ristretto::generator() * sig_nonce.deref(); tx.1.signature.R = Ristretto::generator() * sig_nonce.deref();
tx.1.signature = SchnorrSignature::sign(key, sig_nonce, tx.sig_hash(genesis)); tx.1.signature = SchnorrSignature::sign(key, sig_nonce, tx.sig_hash(genesis));
let mut nonces = HashMap::from([(signer, nonce)]); verify_transaction(&tx, genesis, &mut |_, _| Some(tx.1.nonce)).unwrap();
verify_transaction(&tx, genesis, &mut nonces).unwrap();
assert_eq!(nonces, HashMap::from([(tx.1.signer, tx.1.nonce.wrapping_add(1))]));
tx tx
} }

View file

@ -1,5 +1,3 @@
use std::collections::HashMap;
use rand::rngs::OsRng; use rand::rngs::OsRng;
use blake2::{Digest, Blake2s256}; use blake2::{Digest, Blake2s256};
@ -35,29 +33,23 @@ fn signed_transaction() {
// Mutate various properties and verify it no longer works // Mutate various properties and verify it no longer works
// Different genesis // Different genesis
assert!(verify_transaction( assert!(verify_transaction(&tx, Blake2s256::digest(genesis).into(), &mut |_, _| Some(
&tx, tx.1.nonce
Blake2s256::digest(genesis).into(), ))
&mut HashMap::from([(tx.1.signer, tx.1.nonce)]),
)
.is_err()); .is_err());
// Different data // Different data
{ {
let mut tx = tx.clone(); let mut tx = tx.clone();
tx.0 = Blake2s256::digest(tx.0).to_vec(); tx.0 = Blake2s256::digest(tx.0).to_vec();
assert!( assert!(verify_transaction(&tx, genesis, &mut |_, _| Some(tx.1.nonce)).is_err());
verify_transaction(&tx, genesis, &mut HashMap::from([(tx.1.signer, tx.1.nonce)]),).is_err()
);
} }
// Different signer // Different signer
{ {
let mut tx = tx.clone(); let mut tx = tx.clone();
tx.1.signer += Ristretto::generator(); tx.1.signer += Ristretto::generator();
assert!( assert!(verify_transaction(&tx, genesis, &mut |_, _| Some(tx.1.nonce)).is_err());
verify_transaction(&tx, genesis, &mut HashMap::from([(tx.1.signer, tx.1.nonce)]),).is_err()
);
} }
// Different nonce // Different nonce
@ -65,41 +57,28 @@ fn signed_transaction() {
#[allow(clippy::redundant_clone)] // False positive? #[allow(clippy::redundant_clone)] // False positive?
let mut tx = tx.clone(); let mut tx = tx.clone();
tx.1.nonce = tx.1.nonce.wrapping_add(1); tx.1.nonce = tx.1.nonce.wrapping_add(1);
assert!( assert!(verify_transaction(&tx, genesis, &mut |_, _| Some(tx.1.nonce)).is_err());
verify_transaction(&tx, genesis, &mut HashMap::from([(tx.1.signer, tx.1.nonce)]),).is_err()
);
} }
// Different signature // Different signature
{ {
let mut tx = tx.clone(); let mut tx = tx.clone();
tx.1.signature.R += Ristretto::generator(); tx.1.signature.R += Ristretto::generator();
assert!( assert!(verify_transaction(&tx, genesis, &mut |_, _| Some(tx.1.nonce)).is_err());
verify_transaction(&tx, genesis, &mut HashMap::from([(tx.1.signer, tx.1.nonce)]),).is_err()
);
} }
{ {
let mut tx = tx.clone(); let mut tx = tx.clone();
tx.1.signature.s += <Ristretto as Ciphersuite>::F::ONE; tx.1.signature.s += <Ristretto as Ciphersuite>::F::ONE;
assert!( assert!(verify_transaction(&tx, genesis, &mut |_, _| Some(tx.1.nonce)).is_err());
verify_transaction(&tx, genesis, &mut HashMap::from([(tx.1.signer, tx.1.nonce)]),).is_err()
);
} }
// Sanity check the original TX was never mutated and is valid // Sanity check the original TX was never mutated and is valid
let mut nonces = HashMap::from([(tx.1.signer, tx.1.nonce)]); verify_transaction(&tx, genesis, &mut |_, _| Some(tx.1.nonce)).unwrap();
verify_transaction(&tx, genesis, &mut nonces).unwrap();
assert_eq!(nonces, HashMap::from([(tx.1.signer, tx.1.nonce.wrapping_add(1))]));
} }
#[test] #[test]
fn invalid_nonce() { fn invalid_nonce() {
let (genesis, tx) = random_signed_transaction(&mut OsRng); let (genesis, tx) = random_signed_transaction(&mut OsRng);
assert!(verify_transaction( assert!(verify_transaction(&tx, genesis, &mut |_, _| Some(tx.1.nonce.wrapping_add(1)),).is_err());
&tx,
genesis,
&mut HashMap::from([(tx.1.signer, tx.1.nonce.wrapping_add(1))]),
)
.is_err());
} }

View file

@ -1,5 +1,5 @@
use core::fmt::Debug; use core::fmt::Debug;
use std::{io, collections::HashMap}; use std::io;
use zeroize::Zeroize; use zeroize::Zeroize;
use thiserror::Error; use thiserror::Error;
@ -82,7 +82,7 @@ impl ReadWrite for Signed {
} }
impl Signed { impl Signed {
fn read_without_nonce<R: io::Read>(reader: &mut R, nonce: u32) -> io::Result<Self> { pub fn read_without_nonce<R: io::Read>(reader: &mut R, nonce: u32) -> io::Result<Self> {
let signer = Ristretto::read_G(reader)?; let signer = Ristretto::read_G(reader)?;
let mut signature = SchnorrSignature::<Ristretto>::read(reader)?; let mut signature = SchnorrSignature::<Ristretto>::read(reader)?;
@ -97,7 +97,7 @@ impl Signed {
Ok(Signed { signer, nonce, signature }) Ok(Signed { signer, nonce, signature })
} }
fn write_without_nonce<W: io::Write>(&self, writer: &mut W) -> io::Result<()> { pub fn write_without_nonce<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
// This is either an invalid signature or a private key leak // This is either an invalid signature or a private key leak
if self.signature.R.is_identity().into() { if self.signature.R.is_identity().into() {
Err(io::Error::other("signature nonce was identity"))?; Err(io::Error::other("signature nonce was identity"))?;
@ -132,7 +132,7 @@ pub enum TransactionKind<'a> {
Unsigned, Unsigned,
/// A signed transaction. /// A signed transaction.
Signed(&'a Signed), Signed(Vec<u8>, &'a Signed),
} }
// TODO: Should this be renamed TransactionTrait now that a literal Transaction exists? // TODO: Should this be renamed TransactionTrait now that a literal Transaction exists?
@ -156,13 +156,14 @@ pub trait Transaction: 'static + Send + Sync + Clone + Eq + Debug + ReadWrite {
/// Panics if called on non-signed transactions. /// Panics if called on non-signed transactions.
fn sig_hash(&self, genesis: [u8; 32]) -> <Ristretto as Ciphersuite>::F { fn sig_hash(&self, genesis: [u8; 32]) -> <Ristretto as Ciphersuite>::F {
match self.kind() { match self.kind() {
TransactionKind::Signed(Signed { signature, .. }) => { TransactionKind::Signed(order, Signed { signature, .. }) => {
<Ristretto as Ciphersuite>::F::from_bytes_mod_order_wide( <Ristretto as Ciphersuite>::F::from_bytes_mod_order_wide(
&Blake2b512::digest( &Blake2b512::digest(
[ [
b"Tributary Signed Transaction", b"Tributary Signed Transaction",
genesis.as_ref(), genesis.as_ref(),
&self.hash(), &self.hash(),
order.as_ref(),
signature.R.to_bytes().as_ref(), signature.R.to_bytes().as_ref(),
] ]
.concat(), .concat(),
@ -175,11 +176,14 @@ pub trait Transaction: 'static + Send + Sync + Clone + Eq + Debug + ReadWrite {
} }
} }
pub trait GAIN: FnMut(&<Ristretto as Ciphersuite>::G, &[u8]) -> Option<u32> {}
impl<F: FnMut(&<Ristretto as Ciphersuite>::G, &[u8]) -> Option<u32>> GAIN for F {}
// 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>( pub(crate) fn verify_transaction<F: GAIN, T: Transaction>(
tx: &T, tx: &T,
genesis: [u8; 32], genesis: [u8; 32],
next_nonces: &mut HashMap<<Ristretto as Ciphersuite>::G, u32>, get_and_increment_nonce: &mut F,
) -> Result<(), TransactionError> { ) -> Result<(), TransactionError> {
if tx.serialize().len() > TRANSACTION_SIZE_LIMIT { if tx.serialize().len() > TRANSACTION_SIZE_LIMIT {
Err(TransactionError::TooLargeTransaction)?; Err(TransactionError::TooLargeTransaction)?;
@ -190,9 +194,9 @@ pub(crate) fn verify_transaction<T: Transaction>(
match tx.kind() { match tx.kind() {
TransactionKind::Provided(_) => {} TransactionKind::Provided(_) => {}
TransactionKind::Unsigned => {} TransactionKind::Unsigned => {}
TransactionKind::Signed(Signed { signer, nonce, signature }) => { TransactionKind::Signed(order, Signed { signer, nonce, signature }) => {
if let Some(next_nonce) = next_nonces.get(signer) { if let Some(next_nonce) = get_and_increment_nonce(signer, &order) {
if nonce != next_nonce { if *nonce != next_nonce {
Err(TransactionError::InvalidNonce)?; Err(TransactionError::InvalidNonce)?;
} }
} else { } else {
@ -204,8 +208,6 @@ pub(crate) fn verify_transaction<T: Transaction>(
if !signature.verify(*signer, tx.sig_hash(genesis)) { if !signature.verify(*signer, tx.sig_hash(genesis)) {
Err(TransactionError::InvalidSignature)?; Err(TransactionError::InvalidSignature)?;
} }
next_nonces.insert(*signer, nonce + 1);
} }
} }