From 7488d23e0d4c8edee2607ff88e5c8a9814ba63c6 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Tue, 11 Apr 2023 13:42:18 -0400 Subject: [PATCH] Add basic transaction/block code to Tributary --- coordinator/tributary/Cargo.toml | 7 ++ coordinator/tributary/src/block.rs | 116 +++++++++++++++++++++++ coordinator/tributary/src/lib.rs | 22 +++++ coordinator/tributary/src/merkle.rs | 41 ++++++++ coordinator/tributary/src/transaction.rs | 74 +++++++++++++++ 5 files changed, 260 insertions(+) create mode 100644 coordinator/tributary/src/block.rs create mode 100644 coordinator/tributary/src/merkle.rs create mode 100644 coordinator/tributary/src/transaction.rs diff --git a/coordinator/tributary/Cargo.toml b/coordinator/tributary/Cargo.toml index a126aca0..66083e6c 100644 --- a/coordinator/tributary/Cargo.toml +++ b/coordinator/tributary/Cargo.toml @@ -8,4 +8,11 @@ authors = ["Luke Parker "] edition = "2021" [dependencies] +thiserror = "1" + +blake2 = "0.10" + +ciphersuite = { package = "ciphersuite", path = "../../crypto/ciphersuite", features = ["ristretto"] } +schnorr = { package = "schnorr-signatures", path = "../../crypto/schnorr" } + tendermint = { package = "tendermint-machine", path = "./tendermint" } diff --git a/coordinator/tributary/src/block.rs b/coordinator/tributary/src/block.rs new file mode 100644 index 00000000..b7d44e44 --- /dev/null +++ b/coordinator/tributary/src/block.rs @@ -0,0 +1,116 @@ +use std::{ + io, + collections::{HashSet, HashMap}, +}; + +use thiserror::Error; + +use blake2::{Digest, Blake2s256}; + +use ciphersuite::{Ciphersuite, Ristretto}; + +#[derive(Clone, PartialEq, Eq, Debug, Error)] +pub enum BlockError { + /// Header specified a parent which wasn't the chain tip. + #[error("header doesn't build off the chain tip")] + InvalidParent, + /// Header specified an invalid transactions merkle tree hash. + #[error("header transactions hash is incorrect")] + InvalidTransactions, + /// An included transaction was invalid. + #[error("included transaction had an error")] + TransactionError(TransactionError), +} + +use crate::{ReadWrite, TransactionError, Transaction, merkle, verify_transaction}; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct BlockHeader { + parent: [u8; 32], + transactions: [u8; 32], +} + +impl ReadWrite for BlockHeader { + fn read(reader: &mut R) -> io::Result { + let mut header = BlockHeader { parent: [0; 32], transactions: [0; 32] }; + reader.read_exact(&mut header.parent)?; + reader.read_exact(&mut header.transactions)?; + Ok(header) + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(&self.parent)?; + writer.write_all(&self.transactions) + } +} + +impl BlockHeader { + fn hash(&self) -> [u8; 32] { + Blake2s256::digest([b"tributary_block".as_ref(), &self.serialize()].concat()).into() + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Block { + header: BlockHeader, + transactions: Vec, +} + +impl ReadWrite for Block { + fn read(reader: &mut R) -> io::Result { + let header = BlockHeader::read(reader)?; + + let mut txs = [0; 4]; + reader.read_exact(&mut txs)?; + let txs = u32::from_le_bytes(txs); + + let mut transactions = Vec::with_capacity(usize::try_from(txs).unwrap()); + for _ in 0 .. txs { + transactions.push(T::read(reader)?); + } + + Ok(Block { header, transactions }) + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + self.header.write(writer)?; + writer.write_all(&u32::try_from(self.transactions.len()).unwrap().to_le_bytes())?; + for tx in &self.transactions { + tx.write(writer)?; + } + Ok(()) + } +} + +impl Block { + pub fn hash(&self) -> [u8; 32] { + self.header.hash() + } + + pub fn verify( + &self, + last_block: [u8; 32], + locally_provided: &mut HashSet<[u8; 32]>, + next_nonces: &mut HashMap<::G, u32>, + ) -> Result<(), BlockError> { + if self.header.parent != last_block { + Err(BlockError::InvalidParent)?; + } + + let mut txs = Vec::with_capacity(self.transactions.len()); + for tx in &self.transactions { + match verify_transaction(tx, locally_provided, next_nonces) { + Ok(()) => {} + Err(e) => Err(BlockError::TransactionError(e))?, + } + + txs.push(tx.hash()); + } + + if merkle(&txs) != self.header.transactions { + Err(BlockError::InvalidTransactions)?; + } + + Ok(()) + } +} diff --git a/coordinator/tributary/src/lib.rs b/coordinator/tributary/src/lib.rs index 8b137891..0f24b9f9 100644 --- a/coordinator/tributary/src/lib.rs +++ b/coordinator/tributary/src/lib.rs @@ -1 +1,23 @@ +use std::io; +mod merkle; +pub(crate) use merkle::*; + +mod transaction; +pub use transaction::*; + +mod block; +pub use block::*; + +/// An item which can be read and written. +pub trait ReadWrite: Sized { + fn read(reader: &mut R) -> io::Result; + fn write(&self, writer: &mut W) -> io::Result<()>; + + fn serialize(&self) -> Vec { + // BlockHeader is 64 bytes and likely the smallest item in this system + let mut buf = Vec::with_capacity(64); + self.write(&mut buf).unwrap(); + buf + } +} diff --git a/coordinator/tributary/src/merkle.rs b/coordinator/tributary/src/merkle.rs new file mode 100644 index 00000000..9c3aabae --- /dev/null +++ b/coordinator/tributary/src/merkle.rs @@ -0,0 +1,41 @@ +use blake2::{Digest, Blake2s256}; + +pub(crate) fn merkle(hash_args: &[[u8; 32]]) -> [u8; 32] { + let mut hashes = Vec::with_capacity(hash_args.len()); + for hash in hash_args { + hashes.push(Blake2s256::digest([b"leaf_hash".as_ref(), hash].concat())); + } + + let zero = [0; 32]; + let mut interim; + while hashes.len() > 1 { + interim = Vec::with_capacity(hashes.len() / 2); + + let mut i = 0; + while i < hashes.len() { + interim.push(Blake2s256::digest( + [ + b"branch_hash".as_ref(), + hashes[i].as_ref(), + hashes + .get(i + i) + .map(|hash| { + let res: &[u8] = hash.as_ref(); + res + }) + .unwrap_or(zero.as_ref()), + ] + .concat(), + )); + i += 2; + } + + hashes = interim; + } + + let mut res = zero; + if let Some(hash) = hashes.get(0) { + res.copy_from_slice(hash.as_ref()); + } + res +} diff --git a/coordinator/tributary/src/transaction.rs b/coordinator/tributary/src/transaction.rs new file mode 100644 index 00000000..be2105c5 --- /dev/null +++ b/coordinator/tributary/src/transaction.rs @@ -0,0 +1,74 @@ +use std::collections::{HashSet, HashMap}; + +use thiserror::Error; + +use ciphersuite::{Ciphersuite, Ristretto}; +use schnorr::SchnorrSignature; + +use crate::ReadWrite; + +#[derive(Clone, PartialEq, Eq, Debug, Error)] +pub enum TransactionError { + /// This transaction was perceived as invalid against the current state. + #[error("transaction temporally invalid")] + Temporal, + /// This transaction is definitively invalid. + #[error("transaction definitively invalid")] + Fatal, +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum TransactionKind { + /// This tranaction should be provided by every validator, solely ordered by the block producer. + /// + /// This transaction is only valid if a supermajority of validators provided it. + Provided, + + /// An unsigned transaction, only able to be included by the block producer. + Unsigned, + + /// A signed transaction. + Signed { + signer: ::G, + nonce: u32, + signature: SchnorrSignature, + }, +} + +pub trait Transaction: Send + Sync + Clone + Eq + ReadWrite { + fn kind(&self) -> TransactionKind; + fn hash(&self) -> [u8; 32]; + + fn verify(&self) -> Result<(), TransactionError>; +} + +pub(crate) fn verify_transaction( + tx: &T, + locally_provided: &mut HashSet<[u8; 32]>, + next_nonces: &mut HashMap<::G, u32>, +) -> Result<(), TransactionError> { + match tx.kind() { + TransactionKind::Provided => { + if !locally_provided.remove(&tx.hash()) { + Err(TransactionError::Temporal)?; + } + } + TransactionKind::Unsigned => {} + TransactionKind::Signed { signer, nonce, signature } => { + if next_nonces.get(&signer).cloned().unwrap_or(0) != nonce { + Err(TransactionError::Temporal)?; + } + next_nonces.insert(signer, nonce + 1); + + // TODO: Use Schnorr half-aggregation and a batch verification here + let mut wide = [0; 64]; + wide[.. 32].copy_from_slice(&tx.hash()); + if !signature.verify(signer, ::F::from_bytes_mod_order_wide(&wide)) + { + Err(TransactionError::Fatal)?; + } + } + } + + tx.verify() +}