use core::fmt::Debug; use std::{ io, collections::{HashSet, HashMap}, }; use thiserror::Error; use blake2::{Digest, Blake2b512}; use ciphersuite::{group::GroupEncoding, 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, } /// Data for a signed transaction. #[derive(Clone, PartialEq, Eq, Debug)] pub struct Signed { pub signer: ::G, pub nonce: u32, pub signature: SchnorrSignature, } impl ReadWrite for Signed { fn read(reader: &mut R) -> io::Result { let signer = Ristretto::read_G(reader)?; let mut nonce = [0; 4]; reader.read_exact(&mut nonce)?; let nonce = u32::from_le_bytes(nonce); if nonce >= (u32::MAX - 1) { Err(io::Error::new(io::ErrorKind::Other, "nonce exceeded limit"))?; } let signature = SchnorrSignature::::read(reader)?; Ok(Signed { signer, nonce, signature }) } fn write(&self, writer: &mut W) -> io::Result<()> { writer.write_all(&self.signer.to_bytes())?; writer.write_all(&self.nonce.to_le_bytes())?; self.signature.write(writer) } } #[allow(clippy::large_enum_variant)] #[derive(Clone, PartialEq, Eq, Debug)] pub enum TransactionKind<'a> { /// 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(&'a Signed), } pub trait Transaction: Send + Sync + Clone + Eq + Debug + ReadWrite { /// Return what type of transaction this is. fn kind(&self) -> TransactionKind<'_>; /// Return the hash of this transaction. /// /// The hash must NOT commit to the signature. fn hash(&self) -> [u8; 32]; /// Perform transaction-specific verification. fn verify(&self) -> Result<(), TransactionError>; /// Obtain the challenge for this transaction's signature. /// /// Do not override this unless you know what you're doing. fn sig_hash(&self, genesis: [u8; 32]) -> ::F { ::F::from_bytes_mod_order_wide( &Blake2b512::digest([genesis, self.hash()].concat()).into(), ) } } // This will only cause mutations when the transaction is valid. pub(crate) fn verify_transaction( tx: &T, genesis: [u8; 32], locally_provided: &mut HashSet<[u8; 32]>, next_nonces: &mut HashMap<::G, u32>, ) -> Result<(), TransactionError> { tx.verify()?; match tx.kind() { TransactionKind::Provided => { if !locally_provided.remove(&tx.hash()) { Err(TransactionError::Temporal)?; } } 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)?; } // TODO: Use Schnorr half-aggregation and a batch verification here if !signature.verify(*signer, tx.sig_hash(genesis)) { Err(TransactionError::Fatal)?; } next_nonces.insert(*signer, nonce + 1); } } Ok(()) }