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 ciphersuite::{Ciphersuite, Ristretto};
use tendermint::ext::{Network, Commit};
use crate::{
transaction::{
TransactionError, Signed, TransactionKind, Transaction as TransactionTrait, verify_transaction,
TransactionError, Signed, TransactionKind, Transaction as TransactionTrait, GAIN,
verify_transaction,
},
BLOCK_SIZE_LIMIT, ReadWrite, merkle, Transaction,
tendermint::tx::verify_tendermint_tx,
@ -122,7 +121,7 @@ impl<T: TransactionTrait> Block<T> {
let mut unsigned = vec![];
for tx in mempool {
match tx.kind() {
TransactionKind::Signed(_) => signed.push(tx),
TransactionKind::Signed(_, _) => signed.push(tx),
TransactionKind::Unsigned => unsigned.push(tx),
TransactionKind::Provided(_) => panic!("provided transaction entered mempool"),
}
@ -135,7 +134,7 @@ impl<T: TransactionTrait> Block<T> {
// Check TXs are sorted by nonce.
let nonce = |tx: &Transaction<T>| {
if let TransactionKind::Signed(Signed { nonce, .. }) = tx.kind() {
if let TransactionKind::Signed(_, Signed { nonce, .. }) = tx.kind() {
*nonce
} else {
0
@ -169,12 +168,12 @@ impl<T: TransactionTrait> Block<T> {
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn verify<N: Network>(
pub(crate) fn verify<N: Network, G: GAIN>(
&self,
genesis: [u8; 32],
last_block: [u8; 32],
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,
commit: impl Fn(u32) -> Option<Commit<N::SignatureScheme>>,
unsigned_in_chain: impl Fn([u8; 32]) -> bool,
@ -259,10 +258,12 @@ impl<T: TransactionTrait> Block<T> {
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(()) => {}
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 serai_db::{DbTxn, Db};
use serai_db::{Get, DbTxn, Db};
use scale::Decode;
@ -20,7 +20,7 @@ pub(crate) struct Blockchain<D: Db, T: TransactionTrait> {
block_number: u32,
tip: [u8; 32],
next_nonces: HashMap<<Ristretto as Ciphersuite>::G, u32>,
participants: HashSet<<Ristretto as Ciphersuite>::G>,
provided: ProvidedTransactions<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> {
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(
b"tributary_blockchain",
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],
participants: &[<Ristretto as Ciphersuite>::G],
) -> Self {
let mut next_nonces = HashMap::new();
for participant in participants {
next_nonces.insert(*participant, 0);
}
let mut res = Self {
db: Some(db.clone()),
genesis,
participants: participants.iter().cloned().collect(),
block_number: 0,
tip: genesis,
next_nonces,
provided: ProvidedTransactions::new(db.clone(), genesis),
mempool: Mempool::new(db, genesis),
@ -93,12 +92,6 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
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
}
@ -179,27 +172,58 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
let unsigned_in_chain =
|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> {
self.provided.provide(tx)
}
/// Returns the next nonce for signing, or None if they aren't a participant.
pub(crate) fn next_nonce(&self, key: <Ristretto as Ciphersuite>::G) -> Option<u32> {
Some(self.next_nonces.get(&key).cloned()?.max(self.mempool.next_nonce(&key).unwrap_or(0)))
pub(crate) fn next_nonce(
&self,
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> {
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(
self.tip,
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
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
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.tip,
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,
&commit,
unsigned_in_chain,
provided_in_chain,
allow_non_local_provided,
)
);
drop(txn);
res
}
/// Add a block.
@ -285,18 +326,9 @@ impl<D: Db, T: TransactionTrait> Blockchain<D, T> {
// remove from the mempool
self.mempool.remove(&hash);
}
TransactionKind::Signed(Signed { signer, nonce, .. }) => {
TransactionKind::Signed(order, Signed { signer, nonce, .. }) => {
let next_nonce = nonce + 1;
let prev = self
.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());
txn.put(Self::next_nonce_key(&self.genesis, signer, &order), next_nonce.to_le_bytes());
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)
}
pub async fn next_nonce(&self, signer: <Ristretto as Ciphersuite>::G) -> Option<u32> {
self.network.blockchain.read().await.next_nonce(signer)
pub async fn next_nonce(
&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.

View file

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

View file

@ -89,7 +89,7 @@ impl<D: Db, T: Transaction> ProvidedTransactions<D, T> {
pub(crate) fn provide(&mut self, tx: T) -> Result<(), ProvidedError> {
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(()) => {}
Err(e) => Err(ProvidedError::InvalidProvided(e))?,
}

View file

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

View file

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

View file

@ -36,16 +36,15 @@ async fn mempool_addition() {
let first_tx = signed_transaction(&mut OsRng, genesis, &key, 0);
let signer = first_tx.1.signer;
assert_eq!(mempool.next_nonce(&signer), None);
assert_eq!(mempool.next_nonce_in_mempool(&signer, vec![]), None);
// validators
let validators = Arc::new(Validators::new(genesis, vec![(signer, 1)]).unwrap());
// Add TX 0
let mut blockchain_next_nonces = HashMap::from([(signer, 0)]);
assert!(mempool
.add::<N>(
&blockchain_next_nonces,
.add::<N, _>(
&|_, _| Some(0),
true,
Transaction::Application(first_tx.clone()),
validators.clone(),
@ -53,15 +52,15 @@ async fn mempool_addition() {
commit,
)
.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
let evidence_tx =
random_evidence_tx::<N>(Signer::new(genesis, key.clone()).into(), TendermintBlock(vec![]))
.await;
assert!(mempool
.add::<N>(
&blockchain_next_nonces,
.add::<N, _>(
&|_, _| None,
true,
Transaction::Tendermint(evidence_tx.clone()),
validators.clone(),
@ -75,8 +74,8 @@ async fn mempool_addition() {
// Adding them again should fail
assert_eq!(
mempool.add::<N>(
&blockchain_next_nonces,
mempool.add::<N, _>(
&|_, _| Some(0),
true,
Transaction::Application(first_tx.clone()),
validators.clone(),
@ -86,8 +85,8 @@ async fn mempool_addition() {
Err(TransactionError::InvalidNonce)
);
assert_eq!(
mempool.add::<N>(
&blockchain_next_nonces,
mempool.add::<N, _>(
&|_, _| None,
true,
Transaction::Tendermint(evidence_tx.clone()),
validators.clone(),
@ -100,8 +99,8 @@ async fn mempool_addition() {
// Do the same with the next nonce
let second_tx = signed_transaction(&mut OsRng, genesis, &key, 1);
assert_eq!(
mempool.add::<N>(
&blockchain_next_nonces,
mempool.add::<N, _>(
&|_, _| Some(0),
true,
Transaction::Application(second_tx.clone()),
validators.clone(),
@ -110,10 +109,10 @@ async fn mempool_addition() {
),
Ok(true)
);
assert_eq!(mempool.next_nonce(&signer), Some(2));
assert_eq!(mempool.next_nonce_in_mempool(&signer, vec![]), Some(2));
assert_eq!(
mempool.add::<N>(
&blockchain_next_nonces,
mempool.add::<N, _>(
&|_, _| Some(0),
true,
Transaction::Application(second_tx.clone()),
validators.clone(),
@ -128,11 +127,10 @@ async fn mempool_addition() {
let second_key = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(&mut OsRng));
let tx = signed_transaction(&mut OsRng, genesis, &second_key, 2);
let second_signer = tx.1.signer;
assert_eq!(mempool.next_nonce(&second_signer), None);
blockchain_next_nonces.insert(second_signer, 2);
assert_eq!(mempool.next_nonce_in_mempool(&second_signer, vec![]), None);
assert!(mempool
.add::<N>(
&blockchain_next_nonces,
.add::<N, _>(
&|_, _| Some(2),
true,
Transaction::Application(tx.clone()),
validators.clone(),
@ -140,24 +138,18 @@ async fn mempool_addition() {
commit
)
.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
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
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
// Removing should successfully prune
mempool.remove(&tx.hash());
assert_eq!(
mempool.txs(),
&HashMap::from([
(first_tx.hash(), Transaction::Application(first_tx)),
(second_tx.hash(), Transaction::Application(second_tx)),
(evidence_tx.hash(), Transaction::Tendermint(evidence_tx))
])
@ -173,13 +165,12 @@ fn too_many_mempool() {
};
let unsigned_in_chain = |_: [u8; 32]| false;
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
for i in 0 .. ACCOUNT_MEMPOOL_LIMIT {
assert!(mempool
.add::<N>(
&HashMap::from([(signer, 0)]),
.add::<N, _>(
&|_, _| Some(0),
false,
Transaction::Application(signed_transaction(&mut OsRng, genesis, &key, i)),
validators.clone(),
@ -190,8 +181,8 @@ fn too_many_mempool() {
}
// Yet adding more should fail
assert_eq!(
mempool.add::<N>(
&HashMap::from([(signer, 0)]),
mempool.add::<N, _>(
&|_, _| Some(0),
false,
Transaction::Application(signed_transaction(
&mut OsRng,

View file

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

View file

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

View file

@ -1,5 +1,5 @@
use core::fmt::Debug;
use std::{io, collections::HashMap};
use std::io;
use zeroize::Zeroize;
use thiserror::Error;
@ -82,7 +82,7 @@ impl ReadWrite for 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 mut signature = SchnorrSignature::<Ristretto>::read(reader)?;
@ -97,7 +97,7 @@ impl Signed {
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
if self.signature.R.is_identity().into() {
Err(io::Error::other("signature nonce was identity"))?;
@ -132,7 +132,7 @@ pub enum TransactionKind<'a> {
Unsigned,
/// A signed transaction.
Signed(&'a Signed),
Signed(Vec<u8>, &'a Signed),
}
// 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.
fn sig_hash(&self, genesis: [u8; 32]) -> <Ristretto as Ciphersuite>::F {
match self.kind() {
TransactionKind::Signed(Signed { signature, .. }) => {
TransactionKind::Signed(order, Signed { signature, .. }) => {
<Ristretto as Ciphersuite>::F::from_bytes_mod_order_wide(
&Blake2b512::digest(
[
b"Tributary Signed Transaction",
genesis.as_ref(),
&self.hash(),
order.as_ref(),
signature.R.to_bytes().as_ref(),
]
.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
pub(crate) fn verify_transaction<T: Transaction>(
pub(crate) fn verify_transaction<F: GAIN, T: Transaction>(
tx: &T,
genesis: [u8; 32],
next_nonces: &mut HashMap<<Ristretto as Ciphersuite>::G, u32>,
get_and_increment_nonce: &mut F,
) -> Result<(), TransactionError> {
if tx.serialize().len() > TRANSACTION_SIZE_LIMIT {
Err(TransactionError::TooLargeTransaction)?;
@ -190,9 +194,9 @@ pub(crate) fn verify_transaction<T: Transaction>(
match tx.kind() {
TransactionKind::Provided(_) => {}
TransactionKind::Unsigned => {}
TransactionKind::Signed(Signed { signer, nonce, signature }) => {
if let Some(next_nonce) = next_nonces.get(signer) {
if nonce != next_nonce {
TransactionKind::Signed(order, Signed { signer, nonce, signature }) => {
if let Some(next_nonce) = get_and_increment_nonce(signer, &order) {
if *nonce != next_nonce {
Err(TransactionError::InvalidNonce)?;
}
} else {
@ -204,8 +208,6 @@ pub(crate) fn verify_transaction<T: Transaction>(
if !signature.verify(*signer, tx.sig_hash(genesis)) {
Err(TransactionError::InvalidSignature)?;
}
next_nonces.insert(*signer, nonce + 1);
}
}