diff --git a/Cargo.lock b/Cargo.lock index bdb69e84..0589c728 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1313,7 +1313,12 @@ dependencies = [ name = "coordinator" version = "0.1.0" dependencies = [ + "blake2", + "modular-frost", + "processor-messages", + "rand_core 0.6.4", "tokio", + "tributary-chain", ] [[package]] @@ -10673,7 +10678,12 @@ dependencies = [ name = "tributary-chain" version = "0.1.0" dependencies = [ + "blake2", + "ciphersuite", + "rand_core 0.6.4", + "schnorr-signatures", "tendermint-machine", + "thiserror", ] [[package]] diff --git a/coordinator/Cargo.toml b/coordinator/Cargo.toml index 968e8831..067fb515 100644 --- a/coordinator/Cargo.toml +++ b/coordinator/Cargo.toml @@ -14,4 +14,16 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] +blake2 = "0.10" + +frost = { package = "modular-frost", path = "../crypto/frost" } + +processor-messages = { package = "processor-messages", path = "../processor/messages" } +tributary = { package = "tributary-chain", path = "./tributary" } + tokio = { version = "1", features = ["full"] } + +[dev-dependencies] +rand_core = "0.6" + +tributary = { package = "tributary-chain", path = "./tributary", features = ["tests"] } diff --git a/coordinator/src/main.rs b/coordinator/src/main.rs index 7f755fb7..60e9da44 100644 --- a/coordinator/src/main.rs +++ b/coordinator/src/main.rs @@ -1,2 +1,7 @@ +mod transaction; + +#[cfg(test)] +mod tests; + #[tokio::main] async fn main() {} diff --git a/coordinator/src/tests/mod.rs b/coordinator/src/tests/mod.rs new file mode 100644 index 00000000..8d760e2a --- /dev/null +++ b/coordinator/src/tests/mod.rs @@ -0,0 +1 @@ +mod transaction; diff --git a/coordinator/src/tests/transaction.rs b/coordinator/src/tests/transaction.rs new file mode 100644 index 00000000..1c1a064a --- /dev/null +++ b/coordinator/src/tests/transaction.rs @@ -0,0 +1,81 @@ +use core::fmt::Debug; +use std::collections::HashMap; + +use rand_core::{RngCore, OsRng}; + +use frost::Participant; + +use tributary::{ReadWrite, tests::random_signed}; + +use crate::transaction::{SignData, Transaction}; + +fn random_u32(rng: &mut R) -> u32 { + u32::try_from(rng.next_u64() >> 32).unwrap() +} + +fn random_vec(rng: &mut R, limit: usize) -> Vec { + let len = usize::try_from(rng.next_u64() % u64::try_from(limit).unwrap()).unwrap(); + let mut res = vec![0; len]; + rng.fill_bytes(&mut res); + res +} + +fn random_sign_data(rng: &mut R) -> SignData { + let mut plan = [0; 32]; + rng.fill_bytes(&mut plan); + + SignData { + plan, + attempt: random_u32(&mut OsRng), + + data: random_vec(&mut OsRng, 512), + + signed: random_signed(&mut OsRng), + } +} + +fn test_read_write(value: RW) { + assert_eq!(value, RW::read::<&[u8]>(&mut value.serialize().as_ref()).unwrap()); +} + +#[test] +fn serialize_sign_data() { + test_read_write(random_sign_data(&mut OsRng)); +} + +#[test] +fn serialize_transaction() { + test_read_write(Transaction::DkgCommitments( + random_u32(&mut OsRng), + random_vec(&mut OsRng, 512), + random_signed(&mut OsRng), + )); + + { + // 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(); + // Create a valid map of shares + let mut shares = HashMap::new(); + // Create up to 500 participants + for i in 0 .. (OsRng.next_u64() % 500) { + let mut share = vec![0; share_len]; + OsRng.fill_bytes(&mut share); + shares.insert(Participant::new(u16::try_from(i + 1).unwrap()).unwrap(), share); + } + + test_read_write(Transaction::DkgShares( + random_u32(&mut OsRng), + shares, + random_signed(&mut OsRng), + )); + } + + test_read_write(Transaction::SignPreprocess(random_sign_data(&mut OsRng))); + test_read_write(Transaction::SignShare(random_sign_data(&mut OsRng))); + + test_read_write(Transaction::FinalizedBlock(OsRng.next_u64())); + + test_read_write(Transaction::BatchPreprocess(random_sign_data(&mut OsRng))); + test_read_write(Transaction::BatchShare(random_sign_data(&mut OsRng))); +} diff --git a/coordinator/src/transaction.rs b/coordinator/src/transaction.rs new file mode 100644 index 00000000..607dd70b --- /dev/null +++ b/coordinator/src/transaction.rs @@ -0,0 +1,223 @@ +use std::{io, collections::HashMap}; + +use blake2::{Digest, Blake2s256}; + +use frost::Participant; + +#[rustfmt::skip] +use tributary::{ + ReadWrite, Signed, TransactionError, TransactionKind, Transaction as TransactionTrait +}; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct SignData { + pub plan: [u8; 32], + pub attempt: u32, + + pub data: Vec, + + pub signed: Signed, +} + +impl ReadWrite for SignData { + fn read(reader: &mut R) -> io::Result { + let mut plan = [0; 32]; + reader.read_exact(&mut plan)?; + + let mut attempt = [0; 4]; + reader.read_exact(&mut attempt)?; + let attempt = u32::from_le_bytes(attempt); + + let data = { + let mut data_len = [0; 2]; + reader.read_exact(&mut data_len)?; + let mut data = vec![0; usize::from(u16::from_le_bytes(data_len))]; + reader.read_exact(&mut data)?; + data + }; + + let signed = Signed::read(reader)?; + + Ok(SignData { plan, attempt, data, signed }) + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(&self.plan)?; + writer.write_all(&self.attempt.to_le_bytes())?; + + writer.write_all(&u16::try_from(self.data.len()).unwrap().to_le_bytes())?; + writer.write_all(&self.data)?; + + self.signed.write(writer) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum Transaction { + // Once this completes successfully, no more instances should be created. + DkgCommitments(u32, Vec, Signed), + DkgShares(u32, HashMap>, Signed), + + SignPreprocess(SignData), + SignShare(SignData), + + FinalizedBlock(u64), + + BatchPreprocess(SignData), + BatchShare(SignData), +} + +impl ReadWrite for Transaction { + fn read(reader: &mut R) -> io::Result { + let mut kind = [0]; + reader.read_exact(&mut kind)?; + + match kind[0] { + 0 => { + let mut attempt = [0; 4]; + reader.read_exact(&mut attempt)?; + let attempt = u32::from_le_bytes(attempt); + + let commitments = { + let mut commitments_len = [0; 2]; + reader.read_exact(&mut commitments_len)?; + let mut commitments = vec![0; usize::from(u16::from_le_bytes(commitments_len))]; + reader.read_exact(&mut commitments)?; + commitments + }; + + let signed = Signed::read(reader)?; + + Ok(Transaction::DkgCommitments(attempt, commitments, signed)) + } + + 1 => { + let mut attempt = [0; 4]; + reader.read_exact(&mut attempt)?; + let attempt = u32::from_le_bytes(attempt); + + let shares = { + let mut share_quantity = [0; 2]; + reader.read_exact(&mut share_quantity)?; + + let mut share_len = [0; 1]; + reader.read_exact(&mut share_len)?; + let share_len = usize::from(share_len[0]); + + let mut shares = HashMap::new(); + for i in 0 .. u16::from_le_bytes(share_quantity) { + let participant = Participant::new(i + 1).unwrap(); + let mut share = vec![0; share_len]; + reader.read_exact(&mut share)?; + shares.insert(participant, share); + } + shares + }; + + let signed = Signed::read(reader)?; + + Ok(Transaction::DkgShares(attempt, shares, signed)) + } + + 2 => SignData::read(reader).map(Transaction::SignPreprocess), + 3 => SignData::read(reader).map(Transaction::SignShare), + + 4 => { + let mut block = [0; 8]; + reader.read_exact(&mut block)?; + Ok(Transaction::FinalizedBlock(u64::from_le_bytes(block))) + } + + 5 => SignData::read(reader).map(Transaction::BatchPreprocess), + 6 => SignData::read(reader).map(Transaction::BatchShare), + _ => Err(io::Error::new(io::ErrorKind::Other, "invalid transaction type")), + } + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + match self { + Transaction::DkgCommitments(attempt, commitments, signed) => { + writer.write_all(&[0])?; + writer.write_all(&attempt.to_le_bytes())?; + writer.write_all(&u16::try_from(commitments.len()).unwrap().to_le_bytes())?; + writer.write_all(commitments)?; + signed.write(writer) + } + + Transaction::DkgShares(attempt, shares, signed) => { + writer.write_all(&[1])?; + writer.write_all(&attempt.to_le_bytes())?; + writer.write_all(&u16::try_from(shares.len()).unwrap().to_le_bytes())?; + let mut share_len = None; + for participant in 0 .. shares.len() { + let share = &shares[&Participant::new(u16::try_from(participant + 1).unwrap()).unwrap()]; + if let Some(share_len) = share_len { + if share.len() != share_len { + panic!("variable length shares"); + } + } else { + writer.write_all(&[u8::try_from(share.len()).unwrap()])?; + share_len = Some(share.len()); + } + + writer.write_all(share)?; + } + signed.write(writer) + } + + Transaction::SignPreprocess(data) => { + writer.write_all(&[2])?; + data.write(writer) + } + Transaction::SignShare(data) => { + writer.write_all(&[3])?; + data.write(writer) + } + + Transaction::FinalizedBlock(block) => { + writer.write_all(&[4])?; + writer.write_all(&block.to_le_bytes()) + } + + Transaction::BatchPreprocess(data) => { + writer.write_all(&[5])?; + data.write(writer) + } + Transaction::BatchShare(data) => { + writer.write_all(&[6])?; + data.write(writer) + } + } + } +} + +impl TransactionTrait for Transaction { + fn kind(&self) -> TransactionKind { + match self { + Transaction::DkgCommitments(_, _, signed) => TransactionKind::Signed(signed.clone()), + Transaction::DkgShares(_, _, signed) => TransactionKind::Signed(signed.clone()), + + Transaction::SignPreprocess(data) => TransactionKind::Signed(data.signed.clone()), + Transaction::SignShare(data) => TransactionKind::Signed(data.signed.clone()), + + Transaction::FinalizedBlock(_) => TransactionKind::Provided, + + Transaction::BatchPreprocess(data) => TransactionKind::Signed(data.signed.clone()), + Transaction::BatchShare(data) => TransactionKind::Signed(data.signed.clone()), + } + } + + fn hash(&self) -> [u8; 32] { + let mut tx = self.serialize(); + if let TransactionKind::Signed(signed) = self.kind() { + assert_eq!(&tx[(tx.len() - 64) ..], &signed.signature.serialize()); + tx.truncate(tx.len() - 64); + } + Blake2s256::digest(tx).into() + } + + fn verify(&self) -> Result<(), TransactionError> { + // TODO: Augment with checks that the Vecs can be deser'd and are for recognized IDs + Ok(()) + } +} diff --git a/coordinator/tributary/Cargo.toml b/coordinator/tributary/Cargo.toml index 66083e6c..4814e1f6 100644 --- a/coordinator/tributary/Cargo.toml +++ b/coordinator/tributary/Cargo.toml @@ -10,9 +10,17 @@ edition = "2021" [dependencies] thiserror = "1" +rand_core = { version = "0.6", optional = true } + blake2 = "0.10" ciphersuite = { package = "ciphersuite", path = "../../crypto/ciphersuite", features = ["ristretto"] } schnorr = { package = "schnorr-signatures", path = "../../crypto/schnorr" } tendermint = { package = "tendermint-machine", path = "./tendermint" } + +[dev-dependencies] +rand_core = "0.6" + +[features] +tests = ["rand_core"] diff --git a/coordinator/tributary/src/lib.rs b/coordinator/tributary/src/lib.rs index 0f24b9f9..c06c443a 100644 --- a/coordinator/tributary/src/lib.rs +++ b/coordinator/tributary/src/lib.rs @@ -9,6 +9,9 @@ pub use transaction::*; mod block; pub use block::*; +#[cfg(any(test, feature = "tests"))] +pub mod tests; + /// An item which can be read and written. pub trait ReadWrite: Sized { fn read(reader: &mut R) -> io::Result; diff --git a/coordinator/tributary/src/tests/mod.rs b/coordinator/tributary/src/tests/mod.rs new file mode 100644 index 00000000..402b89e5 --- /dev/null +++ b/coordinator/tributary/src/tests/mod.rs @@ -0,0 +1,2 @@ +mod transaction; +pub use transaction::*; diff --git a/coordinator/tributary/src/tests/transaction.rs b/coordinator/tributary/src/tests/transaction.rs new file mode 100644 index 00000000..4855b0cb --- /dev/null +++ b/coordinator/tributary/src/tests/transaction.rs @@ -0,0 +1,27 @@ +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 = signed(&mut rand_core::OsRng); + assert_eq!(Signed::read::<&[u8]>(&mut signed.serialize().as_ref()).unwrap(), signed); +}