use core::fmt::Debug; use std::{io, collections::HashMap}; use thiserror::Error; use blake2::{Digest, Blake2b512}; use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto}; use schnorr::SchnorrSignature; use crate::{TRANSACTION_SIZE_LIMIT, ReadWrite}; #[derive(Clone, PartialEq, Eq, Debug, Error)] pub enum TransactionError { /// Transaction exceeded the size limit. #[error("transaction was too large")] TooLargeTransaction, /// This transaction's signer isn't a participant. #[error("invalid signer")] InvalidSigner, /// This transaction's nonce isn't the prior nonce plus one. #[error("invalid nonce")] InvalidNonce, /// This transaction's signature is invalid. #[error("invalid signature")] InvalidSignature, } /// 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, in an exact order. /// /// The only malleability is in when this transaction appears on chain. The block producer will /// include it when they have it. Block verification will fail for validators without it. /// /// If a supermajority of validators still produce a commit for a block with a provided /// transaction which isn't locally held, the chain will sleep until it is locally provided. Provided, /// An unsigned transaction, only able to be included by the block producer. Unsigned, /// A signed transaction. Signed(&'a Signed), } pub trait Transaction: 'static + 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], next_nonces: &mut HashMap<::G, u32>, ) -> Result<(), TransactionError> { if tx.serialize().len() > TRANSACTION_SIZE_LIMIT { Err(TransactionError::TooLargeTransaction)?; } tx.verify()?; match tx.kind() { TransactionKind::Provided => {} TransactionKind::Unsigned => {} TransactionKind::Signed(Signed { signer, nonce, signature }) => { if let Some(next_nonce) = next_nonces.get(signer) { if nonce != next_nonce { Err(TransactionError::InvalidNonce)?; } } else { // Not a participant Err(TransactionError::InvalidSigner)?; } // TODO: Use Schnorr half-aggregation and a batch verification here if !signature.verify(*signer, tx.sig_hash(genesis)) { Err(TransactionError::InvalidSignature)?; } next_nonces.insert(*signer, nonce + 1); } } Ok(()) }