From 402a7be9664ef815b1d265b77ebfd25fd0952e1f Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Tue, 11 Apr 2023 20:24:27 -0400 Subject: [PATCH] Block contructor and tests --- coordinator/src/tests/transaction.rs | 6 +- coordinator/tributary/src/block.rs | 46 ++++++- coordinator/tributary/src/lib.rs | 3 + coordinator/tributary/src/provided.rs | 33 +++++ coordinator/tributary/src/tests/block.rs | 123 ++++++++++++++++++ coordinator/tributary/src/tests/mod.rs | 5 + .../tributary/src/tests/transaction.rs | 2 +- coordinator/tributary/src/transaction.rs | 6 +- 8 files changed, 218 insertions(+), 6 deletions(-) create mode 100644 coordinator/tributary/src/provided.rs create mode 100644 coordinator/tributary/src/tests/block.rs diff --git a/coordinator/src/tests/transaction.rs b/coordinator/src/tests/transaction.rs index 1c1a064a..aa6882ae 100644 --- a/coordinator/src/tests/transaction.rs +++ b/coordinator/src/tests/transaction.rs @@ -54,11 +54,11 @@ fn serialize_transaction() { { // This supports a variable share length, yet share length is expected to be constant among // shares - let share_len = usize::try_from(96 + (OsRng.next_u64() % 32)).unwrap(); + let share_len = usize::try_from(OsRng.next_u64() % 512).unwrap(); // Create a valid map of shares let mut shares = HashMap::new(); - // Create up to 500 participants - for i in 0 .. (OsRng.next_u64() % 500) { + // Create up to 512 participants + for i in 0 .. (OsRng.next_u64() % 512) { let mut share = vec![0; share_len]; OsRng.fill_bytes(&mut share); shares.insert(Participant::new(u16::try_from(i + 1).unwrap()).unwrap(), share); diff --git a/coordinator/tributary/src/block.rs b/coordinator/tributary/src/block.rs index acab2c08..a3e70788 100644 --- a/coordinator/tributary/src/block.rs +++ b/coordinator/tributary/src/block.rs @@ -22,7 +22,10 @@ pub enum BlockError { TransactionError(TransactionError), } -use crate::{ReadWrite, TransactionError, Transaction, merkle, verify_transaction}; +use crate::{ + ReadWrite, TransactionError, Signed, TransactionKind, Transaction, ProvidedTransactions, merkle, + verify_transaction, +}; #[derive(Clone, PartialEq, Eq, Debug)] pub struct BlockHeader { @@ -83,6 +86,47 @@ impl ReadWrite for Block { } impl Block { + /// Create a new block. + /// + /// mempool is expected to only have valid, non-conflicting transactions. + pub fn new( + parent: [u8; 32], + provided: &ProvidedTransactions, + mempool: HashMap<[u8; 32], T>, + ) -> Self { + let mut txs = vec![]; + for tx in provided.transactions.values().cloned() { + txs.push(tx); + } + for tx in mempool.values().cloned() { + assert!(tx.kind() != TransactionKind::Provided, "provided transaction entered mempool"); + txs.push(tx); + } + + // Sort txs by nonces. + let nonce = |tx: &T| { + if let TransactionKind::Signed(Signed { nonce, .. }) = tx.kind() { + nonce + } else { + 0 + } + }; + txs.sort_by(|a, b| nonce(a).partial_cmp(&nonce(b)).unwrap()); + + // Check the sort. + let mut last = 0; + for tx in &txs { + let nonce = nonce(tx); + if nonce < last { + panic!("failed to sort txs by nonce"); + } + last = nonce; + } + + let hashes = txs.iter().map(Transaction::hash).collect::>(); + Block { header: BlockHeader { parent, transactions: merkle(&hashes) }, transactions: txs } + } + pub fn hash(&self) -> [u8; 32] { self.header.hash() } diff --git a/coordinator/tributary/src/lib.rs b/coordinator/tributary/src/lib.rs index c06c443a..6400b51a 100644 --- a/coordinator/tributary/src/lib.rs +++ b/coordinator/tributary/src/lib.rs @@ -6,6 +6,9 @@ pub(crate) use merkle::*; mod transaction; pub use transaction::*; +mod provided; +pub use provided::*; + mod block; pub use block::*; diff --git a/coordinator/tributary/src/provided.rs b/coordinator/tributary/src/provided.rs new file mode 100644 index 00000000..32d3c68d --- /dev/null +++ b/coordinator/tributary/src/provided.rs @@ -0,0 +1,33 @@ +use std::collections::HashMap; + +use crate::{TransactionKind, Transaction}; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct ProvidedTransactions { + pub(crate) transactions: HashMap<[u8; 32], T>, +} + +impl Default for ProvidedTransactions { + fn default() -> Self { + ProvidedTransactions { transactions: HashMap::new() } + } +} + +impl ProvidedTransactions { + pub fn new() -> Self { + ProvidedTransactions::default() + } + + /// Provide a transaction for inclusion in a block. + pub fn provide(&mut self, tx: T) { + assert_eq!(tx.kind(), TransactionKind::Provided, "provided a non-provided transaction"); + self.transactions.insert(tx.hash(), tx); + } + + /// Withdraw a transaction, no longer proposing it or voting for its validity. + /// + /// Returns true if the transaction was withdrawn and false otherwise. + pub fn withdraw(&mut self, tx: [u8; 32]) -> bool { + self.transactions.remove(&tx).is_some() + } +} diff --git a/coordinator/tributary/src/tests/block.rs b/coordinator/tributary/src/tests/block.rs new file mode 100644 index 00000000..ac91904b --- /dev/null +++ b/coordinator/tributary/src/tests/block.rs @@ -0,0 +1,123 @@ +use std::{ + io, + collections::{HashSet, HashMap}, +}; + +use rand_core::{RngCore, OsRng}; + +use blake2::{Digest, Blake2s256}; + +use ciphersuite::{ + group::{ff::Field, Group}, + Ciphersuite, Ristretto, +}; +use schnorr::SchnorrSignature; + +use crate::{ + ReadWrite, TransactionError, Signed, TransactionKind, Transaction, ProvidedTransactions, Block, +}; +// A transaction solely defined by its nonce and a distinguisher (to allow creating distinct TXs +// sharing a nonce). +#[derive(Clone, PartialEq, Eq, Debug)] +struct NonceTransaction(u32, u8); + +impl ReadWrite for NonceTransaction { + fn read(reader: &mut R) -> io::Result { + let mut nonce = [0; 4]; + reader.read_exact(&mut nonce)?; + let mut distinguisher = [0]; + reader.read_exact(&mut distinguisher)?; + Ok(Self(u32::from_le_bytes(nonce), distinguisher[0])) + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(&self.0.to_le_bytes())?; + writer.write_all(&[self.1]) + } +} + +impl Transaction for NonceTransaction { + fn kind(&self) -> TransactionKind { + TransactionKind::Signed(Signed { + signer: ::G::identity(), + nonce: self.0, + signature: SchnorrSignature:: { + R: ::G::identity(), + s: ::F::ZERO, + }, + }) + } + + fn hash(&self) -> [u8; 32] { + Blake2s256::digest([self.0.to_le_bytes().as_ref(), &[self.1]].concat()).into() + } + + fn verify(&self) -> Result<(), TransactionError> { + Ok(()) + } +} + +#[test] +fn empty_block() { + const GENESIS: [u8; 32] = [0xff; 32]; + const LAST: [u8; 32] = [0x01; 32]; + Block::new(LAST, &ProvidedTransactions::::new(), HashMap::new()) + .verify(GENESIS, LAST, &mut HashSet::new(), &mut HashMap::new()) + .unwrap(); +} + +#[test] +fn duplicate_nonces() { + const GENESIS: [u8; 32] = [0xff; 32]; + const LAST: [u8; 32] = [0x01; 32]; + + // Run once without duplicating a nonce, and once with, so that's confirmed to be the faulty + // component + for i in [1, 0] { + let mut mempool = HashMap::new(); + let mut insert = |tx: NonceTransaction| mempool.insert(tx.hash(), tx); + insert(NonceTransaction(0, 0)); + insert(NonceTransaction(i, 1)); + + let mut nonces = HashMap::new(); + let res = Block::new(LAST, &ProvidedTransactions::new(), mempool).verify( + GENESIS, + LAST, + &mut HashSet::new(), + &mut nonces, + ); + if i == 1 { + res.unwrap(); + assert_eq!(nonces[&::G::identity()], 2); + } else { + assert!(res.is_err()); + } + } +} + +#[test] +fn unsorted_nonces() { + let mut mempool = HashMap::new(); + // Create a large amount of nonces so the retrieval from the HashMapis effectively guaranteed to + // be out of order + let mut nonces = (0 .. 64).collect::>(); + // Insert in a random order + while !nonces.is_empty() { + let nonce = nonces.swap_remove( + usize::try_from(OsRng.next_u64() % u64::try_from(nonces.len()).unwrap()).unwrap(), + ); + let tx = NonceTransaction(nonce, 0); + mempool.insert(tx.hash(), tx); + } + + // Create and verify the block + const GENESIS: [u8; 32] = [0xff; 32]; + const LAST: [u8; 32] = [0x01; 32]; + let mut nonces = HashMap::new(); + Block::new(LAST, &ProvidedTransactions::new(), mempool) + .verify(GENESIS, LAST, &mut HashSet::new(), &mut nonces) + .unwrap(); + + // Make sure the nonce was properly set + assert_eq!(nonces[&::G::identity()], 64); +} diff --git a/coordinator/tributary/src/tests/mod.rs b/coordinator/tributary/src/tests/mod.rs index 402b89e5..9867de21 100644 --- a/coordinator/tributary/src/tests/mod.rs +++ b/coordinator/tributary/src/tests/mod.rs @@ -1,2 +1,7 @@ mod transaction; pub use transaction::*; + +#[cfg(test)] +mod block; +#[cfg(test)] +pub use block::*; diff --git a/coordinator/tributary/src/tests/transaction.rs b/coordinator/tributary/src/tests/transaction.rs index 4855b0cb..961a20a4 100644 --- a/coordinator/tributary/src/tests/transaction.rs +++ b/coordinator/tributary/src/tests/transaction.rs @@ -22,6 +22,6 @@ pub fn random_signed(rng: &mut R) -> Signed { #[test] fn serialize_signed() { use crate::ReadWrite; - let signed = signed(&mut rand_core::OsRng); + let signed = random_signed(&mut rand_core::OsRng); assert_eq!(Signed::read::<&[u8]>(&mut signed.serialize().as_ref()).unwrap(), signed); } diff --git a/coordinator/tributary/src/transaction.rs b/coordinator/tributary/src/transaction.rs index f21bc9f3..680345db 100644 --- a/coordinator/tributary/src/transaction.rs +++ b/coordinator/tributary/src/transaction.rs @@ -1,3 +1,4 @@ +use core::fmt::Debug; use std::{ io, collections::{HashSet, HashMap}, @@ -65,8 +66,11 @@ pub enum TransactionKind { Signed(Signed), } -pub trait Transaction: Send + Sync + Clone + Eq + ReadWrite { +pub trait Transaction: Send + Sync + Clone + Eq + Debug + ReadWrite { fn kind(&self) -> TransactionKind; + /// Return the hash of this transaction. + /// + /// The hash must NOT commit to the signature. fn hash(&self) -> [u8; 32]; fn verify(&self) -> Result<(), TransactionError>;