diff --git a/Cargo.lock b/Cargo.lock index 0589c728..8ecdc741 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10684,6 +10684,7 @@ dependencies = [ "schnorr-signatures", "tendermint-machine", "thiserror", + "zeroize", ] [[package]] diff --git a/coordinator/tributary/Cargo.toml b/coordinator/tributary/Cargo.toml index 4814e1f6..88d551e0 100644 --- a/coordinator/tributary/Cargo.toml +++ b/coordinator/tributary/Cargo.toml @@ -10,6 +10,7 @@ edition = "2021" [dependencies] thiserror = "1" +zeroize = { version = "^1.5", optional = true } rand_core = { version = "0.6", optional = true } blake2 = "0.10" @@ -20,7 +21,8 @@ schnorr = { package = "schnorr-signatures", path = "../../crypto/schnorr" } tendermint = { package = "tendermint-machine", path = "./tendermint" } [dev-dependencies] +zeroize = "^1.5" rand_core = "0.6" [features] -tests = ["rand_core"] +tests = ["zeroize", "rand_core"] diff --git a/coordinator/tributary/src/tests/transaction.rs b/coordinator/tributary/src/tests/transaction.rs deleted file mode 100644 index 961a20a4..00000000 --- a/coordinator/tributary/src/tests/transaction.rs +++ /dev/null @@ -1,27 +0,0 @@ -use rand_core::RngCore; - -use ciphersuite::{ - group::{ff::Field, Group}, - Ciphersuite, Ristretto, -}; -use schnorr::SchnorrSignature; - -use crate::Signed; - -pub fn random_signed(rng: &mut R) -> Signed { - Signed { - signer: ::G::random(&mut *rng), - nonce: u32::try_from(rng.next_u64() >> 32).unwrap(), - signature: SchnorrSignature:: { - R: ::G::random(&mut *rng), - s: ::F::random(rng), - }, - } -} - -#[test] -fn serialize_signed() { - use crate::ReadWrite; - 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/tests/transaction/mod.rs b/coordinator/tributary/src/tests/transaction/mod.rs new file mode 100644 index 00000000..cf4e799e --- /dev/null +++ b/coordinator/tributary/src/tests/transaction/mod.rs @@ -0,0 +1,137 @@ +use std::{ + io, + collections::{HashSet, HashMap}, +}; + +use rand_core::{RngCore, CryptoRng}; + +use blake2::{Digest, Blake2s256}; + +use ciphersuite::{ + group::{ff::Field, Group}, + Ciphersuite, Ristretto, +}; +use schnorr::SchnorrSignature; + +use crate::{ReadWrite, Signed, TransactionError, TransactionKind, Transaction, verify_transaction}; + +#[cfg(test)] +mod signed; + +#[cfg(test)] +mod provided; + +pub fn random_signed(rng: &mut R) -> Signed { + Signed { + signer: ::G::random(&mut *rng), + nonce: u32::try_from(rng.next_u64() >> 32).unwrap(), + signature: SchnorrSignature:: { + R: ::G::random(&mut *rng), + s: ::F::random(rng), + }, + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct ProvidedTransaction(pub Vec); + +impl ReadWrite for ProvidedTransaction { + fn read(reader: &mut R) -> io::Result { + let mut len = [0; 4]; + reader.read_exact(&mut len)?; + let mut data = vec![0; usize::try_from(u32::from_le_bytes(len)).unwrap()]; + reader.read_exact(&mut data)?; + Ok(ProvidedTransaction(data)) + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(&u32::try_from(self.0.len()).unwrap().to_le_bytes())?; + writer.write_all(&self.0) + } +} + +impl Transaction for ProvidedTransaction { + fn kind(&self) -> TransactionKind { + TransactionKind::Provided + } + + fn hash(&self) -> [u8; 32] { + Blake2s256::digest(self.serialize()).into() + } + + fn verify(&self) -> Result<(), TransactionError> { + Ok(()) + } +} + +pub fn random_provided_transaction(rng: &mut R) -> ProvidedTransaction { + let mut data = vec![0; 512]; + rng.fill_bytes(&mut data); + ProvidedTransaction(data) +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct SignedTransaction(pub Vec, pub Signed); + +impl ReadWrite for SignedTransaction { + fn read(reader: &mut R) -> io::Result { + let mut len = [0; 4]; + reader.read_exact(&mut len)?; + let mut data = vec![0; usize::try_from(u32::from_le_bytes(len)).unwrap()]; + reader.read_exact(&mut data)?; + + Ok(SignedTransaction(data, Signed::read(reader)?)) + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(&u32::try_from(self.0.len()).unwrap().to_le_bytes())?; + writer.write_all(&self.0)?; + self.1.write(writer) + } +} + +impl Transaction for SignedTransaction { + fn kind(&self) -> TransactionKind { + TransactionKind::Signed(self.1.clone()) + } + + fn hash(&self) -> [u8; 32] { + let serialized = self.serialize(); + Blake2s256::digest(&serialized[.. (serialized.len() - 64)]).into() + } + + fn verify(&self) -> Result<(), TransactionError> { + Ok(()) + } +} + +pub fn random_signed_transaction( + rng: &mut R, +) -> ([u8; 32], SignedTransaction) { + use zeroize::Zeroizing; + + let mut data = vec![0; 512]; + rng.fill_bytes(&mut data); + + let key = ::F::random(&mut *rng); + let signer = ::generator() * key; + // Shift over an additional bit to ensure it won't overflow when incremented + let nonce = u32::try_from(rng.next_u64() >> 32 >> 1).unwrap(); + + let mut tx = + SignedTransaction(data, Signed { signer, nonce, signature: random_signed(rng).signature }); + + let mut genesis = [0; 32]; + rng.fill_bytes(&mut genesis); + tx.1.signature = SchnorrSignature::sign( + &Zeroizing::new(key), + Zeroizing::new(::F::random(rng)), + tx.sig_hash(genesis), + ); + + let mut nonces = HashMap::from([(tx.1.signer, tx.1.nonce)]); + verify_transaction(&tx, genesis, &mut HashSet::new(), &mut nonces).unwrap(); + assert_eq!(nonces, HashMap::from([(tx.1.signer, tx.1.nonce.wrapping_add(1))])); + + (genesis, tx) +} diff --git a/coordinator/tributary/src/tests/transaction/provided.rs b/coordinator/tributary/src/tests/transaction/provided.rs new file mode 100644 index 00000000..15538206 --- /dev/null +++ b/coordinator/tributary/src/tests/transaction/provided.rs @@ -0,0 +1,18 @@ +use std::collections::{HashSet, HashMap}; + +use rand_core::OsRng; + +use crate::{Transaction, verify_transaction, tests::random_provided_transaction}; + +#[test] +fn provided_transaction() { + let tx = random_provided_transaction(&mut OsRng); + + // Make sure this works when provided + let mut provided = HashSet::from([tx.hash()]); + verify_transaction(&tx, [0x88; 32], &mut provided, &mut HashMap::new()).unwrap(); + assert_eq!(provided.len(), 0); + + // Make sure this fails when not provided + assert!(verify_transaction(&tx, [0x88; 32], &mut HashSet::new(), &mut HashMap::new()).is_err()); +} diff --git a/coordinator/tributary/src/tests/transaction/signed.rs b/coordinator/tributary/src/tests/transaction/signed.rs new file mode 100644 index 00000000..a6f301a6 --- /dev/null +++ b/coordinator/tributary/src/tests/transaction/signed.rs @@ -0,0 +1,126 @@ +use std::collections::{HashSet, HashMap}; + +use rand_core::OsRng; + +use blake2::{Digest, Blake2s256}; + +use ciphersuite::{group::ff::Field, Ciphersuite, Ristretto}; + +use crate::{ + ReadWrite, Signed, Transaction, verify_transaction, + tests::{random_signed, random_signed_transaction}, +}; + +#[test] +fn serialize_signed() { + let signed = random_signed(&mut rand_core::OsRng); + assert_eq!(Signed::read::<&[u8]>(&mut signed.serialize().as_ref()).unwrap(), signed); +} + +#[test] +fn sig_hash() { + let (genesis, tx1) = random_signed_transaction(&mut OsRng); + assert!(tx1.sig_hash(genesis) != tx1.sig_hash(Blake2s256::digest(genesis).into())); + + let (_, tx2) = random_signed_transaction(&mut OsRng); + assert!(tx1.hash() != tx2.hash()); + assert!(tx1.sig_hash(genesis) != tx2.sig_hash(genesis)); +} + +#[test] +fn signed_transaction() { + let (genesis, tx) = random_signed_transaction(&mut OsRng); + + // Mutate various properties and verify it no longer works + + // Different genesis + assert!(verify_transaction( + &tx, + Blake2s256::digest(genesis).into(), + &mut HashSet::new(), + &mut HashMap::from([(tx.1.signer, 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 HashSet::new(), + &mut HashMap::from([(tx.1.signer, tx.1.nonce)]), + ) + .is_err()); + } + + // Different signer + { + let mut tx = tx.clone(); + tx.1.signer += Ristretto::generator(); + assert!(verify_transaction( + &tx, + genesis, + &mut HashSet::new(), + &mut HashMap::from([(tx.1.signer, tx.1.nonce)]), + ) + .is_err()); + } + + // Different nonce + { + #[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 HashSet::new(), + &mut HashMap::from([(tx.1.signer, tx.1.nonce)]), + ) + .is_err()); + } + + // Different signature + { + let mut tx = tx.clone(); + tx.1.signature.R += Ristretto::generator(); + assert!(verify_transaction( + &tx, + genesis, + &mut HashSet::new(), + &mut HashMap::from([(tx.1.signer, tx.1.nonce)]), + ) + .is_err()); + } + { + let mut tx = tx.clone(); + tx.1.signature.s += ::F::ONE; + assert!(verify_transaction( + &tx, + genesis, + &mut HashSet::new(), + &mut HashMap::from([(tx.1.signer, 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 HashSet::new(), &mut nonces).unwrap(); + assert_eq!(nonces, HashMap::from([(tx.1.signer, tx.1.nonce.wrapping_add(1))])); +} + +#[test] +fn invalid_nonce() { + let (genesis, tx) = random_signed_transaction(&mut OsRng); + + assert!(verify_transaction( + &tx, + genesis, + &mut HashSet::new(), + &mut HashMap::from([(tx.1.signer, tx.1.nonce.wrapping_add(1))]), + ) + .is_err()); +} diff --git a/coordinator/tributary/src/transaction.rs b/coordinator/tributary/src/transaction.rs index 680345db..50562c3d 100644 --- a/coordinator/tributary/src/transaction.rs +++ b/coordinator/tributary/src/transaction.rs @@ -96,6 +96,7 @@ pub(crate) fn verify_transaction( } TransactionKind::Unsigned => {} TransactionKind::Signed(Signed { signer, nonce, signature }) => { + // TODO: Use presence as a whitelist, erroring on lack of if next_nonces.get(&signer).cloned().unwrap_or(0) != nonce { Err(TransactionError::Temporal)?; }