mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-28 22:49:42 +00:00
ad0ecc5185
* complete various todos * fix pr comments * Document bounds on unique hashes in TransactionKind --------- Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
218 lines
7.4 KiB
Rust
218 lines
7.4 KiB
Rust
use core::fmt::Debug;
|
|
use std::io;
|
|
|
|
use zeroize::Zeroize;
|
|
use thiserror::Error;
|
|
|
|
use blake2::{Digest, Blake2b512};
|
|
|
|
use ciphersuite::{
|
|
group::{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 is too large")]
|
|
TooLargeTransaction,
|
|
/// Transaction's signer isn't a participant.
|
|
#[error("invalid signer")]
|
|
InvalidSigner,
|
|
/// Transaction's nonce isn't the prior nonce plus one.
|
|
#[error("invalid nonce")]
|
|
InvalidNonce,
|
|
/// Transaction's signature is invalid.
|
|
#[error("invalid signature")]
|
|
InvalidSignature,
|
|
/// Transaction's content is invalid.
|
|
#[error("transaction content is invalid")]
|
|
InvalidContent,
|
|
/// Transaction's signer has too many transactions in the mempool.
|
|
#[error("signer has too many transactions in the mempool")]
|
|
TooManyInMempool,
|
|
/// Provided Transaction added to mempool.
|
|
#[error("provided transaction added to mempool")]
|
|
ProvidedAddedToMempool,
|
|
}
|
|
|
|
/// Data for a signed transaction.
|
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
pub struct Signed {
|
|
pub signer: <Ristretto as Ciphersuite>::G,
|
|
pub nonce: u32,
|
|
pub signature: SchnorrSignature<Ristretto>,
|
|
}
|
|
|
|
impl ReadWrite for Signed {
|
|
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
|
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::other("nonce exceeded limit"))?;
|
|
}
|
|
|
|
let mut signature = SchnorrSignature::<Ristretto>::read(reader)?;
|
|
if signature.R.is_identity().into() {
|
|
// Anyone malicious could remove this and try to find zero signatures
|
|
// We should never produce zero signatures though meaning this should never come up
|
|
// If it does somehow come up, this is a decent courtesy
|
|
signature.zeroize();
|
|
Err(io::Error::other("signature nonce was identity"))?;
|
|
}
|
|
|
|
Ok(Signed { signer, nonce, signature })
|
|
}
|
|
|
|
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
// This is either an invalid signature or a private key leak
|
|
if self.signature.R.is_identity().into() {
|
|
Err(io::Error::other("signature nonce was identity"))?;
|
|
}
|
|
writer.write_all(&self.signer.to_bytes())?;
|
|
writer.write_all(&self.nonce.to_le_bytes())?;
|
|
self.signature.write(writer)
|
|
}
|
|
}
|
|
|
|
impl Signed {
|
|
pub fn read_without_nonce<R: io::Read>(reader: &mut R, nonce: u32) -> io::Result<Self> {
|
|
let signer = Ristretto::read_G(reader)?;
|
|
|
|
let mut signature = SchnorrSignature::<Ristretto>::read(reader)?;
|
|
if signature.R.is_identity().into() {
|
|
// Anyone malicious could remove this and try to find zero signatures
|
|
// We should never produce zero signatures though meaning this should never come up
|
|
// If it does somehow come up, this is a decent courtesy
|
|
signature.zeroize();
|
|
Err(io::Error::other("signature nonce was identity"))?;
|
|
}
|
|
|
|
Ok(Signed { signer, nonce, signature })
|
|
}
|
|
|
|
pub fn write_without_nonce<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
// This is either an invalid signature or a private key leak
|
|
if self.signature.R.is_identity().into() {
|
|
Err(io::Error::other("signature nonce was identity"))?;
|
|
}
|
|
writer.write_all(&self.signer.to_bytes())?;
|
|
self.signature.write(writer)
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::large_enum_variant)]
|
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
pub enum TransactionKind<'a> {
|
|
/// This transaction should be provided by every validator, in an exact order.
|
|
///
|
|
/// The contained static string names the orderer to use. This allows two distinct provided
|
|
/// transaction kinds, without a synchronized order, to be ordered within their own kind without
|
|
/// requiring ordering with each other.
|
|
///
|
|
/// 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 produce a commit for a block with a provided transaction
|
|
/// which isn't locally held, the block will be added to the local chain. When the transaction is
|
|
/// locally provided, it will be compared for correctness to the on-chain version
|
|
///
|
|
/// In order to ensure TXs aren't accidentally provided multiple times, all provided transactions
|
|
/// must have a unique hash which is also unique to all Unsigned transactions.
|
|
Provided(&'static str),
|
|
|
|
/// An unsigned transaction, only able to be included by the block producer.
|
|
///
|
|
/// Once an Unsigned transaction is included on-chain, it may not be included again. In order to
|
|
/// have multiple Unsigned transactions with the same values included on-chain, some distinct
|
|
/// nonce must be included in order to cause a distinct hash.
|
|
///
|
|
/// The hash must also be unique with all Provided transactions.
|
|
Unsigned,
|
|
|
|
/// A signed transaction.
|
|
Signed(Vec<u8>, &'a Signed),
|
|
}
|
|
|
|
// TODO: Should this be renamed TransactionTrait now that a literal Transaction exists?
|
|
// Or should the literal Transaction be renamed to Event?
|
|
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.
|
|
///
|
|
/// Panics if called on non-signed transactions.
|
|
fn sig_hash(&self, genesis: [u8; 32]) -> <Ristretto as Ciphersuite>::F {
|
|
match self.kind() {
|
|
TransactionKind::Signed(order, Signed { signature, .. }) => {
|
|
<Ristretto as Ciphersuite>::F::from_bytes_mod_order_wide(
|
|
&Blake2b512::digest(
|
|
[
|
|
b"Tributary Signed Transaction",
|
|
genesis.as_ref(),
|
|
&self.hash(),
|
|
order.as_ref(),
|
|
signature.R.to_bytes().as_ref(),
|
|
]
|
|
.concat(),
|
|
)
|
|
.into(),
|
|
)
|
|
}
|
|
_ => panic!("sig_hash called on non-signed transaction"),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub trait GAIN: FnMut(&<Ristretto as Ciphersuite>::G, &[u8]) -> Option<u32> {}
|
|
impl<F: FnMut(&<Ristretto as Ciphersuite>::G, &[u8]) -> Option<u32>> GAIN for F {}
|
|
|
|
pub(crate) fn verify_transaction<F: GAIN, T: Transaction>(
|
|
tx: &T,
|
|
genesis: [u8; 32],
|
|
get_and_increment_nonce: &mut F,
|
|
) -> Result<(), TransactionError> {
|
|
if tx.serialize().len() > TRANSACTION_SIZE_LIMIT {
|
|
Err(TransactionError::TooLargeTransaction)?;
|
|
}
|
|
|
|
tx.verify()?;
|
|
|
|
match tx.kind() {
|
|
TransactionKind::Provided(_) | TransactionKind::Unsigned => {}
|
|
TransactionKind::Signed(order, Signed { signer, nonce, signature }) => {
|
|
if let Some(next_nonce) = get_and_increment_nonce(signer, &order) {
|
|
if *nonce != next_nonce {
|
|
Err(TransactionError::InvalidNonce)?;
|
|
}
|
|
} else {
|
|
// Not a participant
|
|
Err(TransactionError::InvalidSigner)?;
|
|
}
|
|
|
|
// TODO: Use a batch verification here
|
|
if !signature.verify(*signer, tx.sig_hash(genesis)) {
|
|
Err(TransactionError::InvalidSignature)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|