Sign the genesis when signing transactions

Prevents replaying across tributaries, which is a risk for BTC/ETH (regarding key gen).
This commit is contained in:
Luke Parker 2023-04-11 19:03:36 -04:00
parent 7488d23e0d
commit 4d17b922fe
No known key found for this signature in database
2 changed files with 48 additions and 13 deletions

View file

@ -89,6 +89,7 @@ impl<T: Transaction> Block<T> {
pub fn verify( pub fn verify(
&self, &self,
genesis: [u8; 32],
last_block: [u8; 32], last_block: [u8; 32],
locally_provided: &mut HashSet<[u8; 32]>, locally_provided: &mut HashSet<[u8; 32]>,
next_nonces: &mut HashMap<<Ristretto as Ciphersuite>::G, u32>, next_nonces: &mut HashMap<<Ristretto as Ciphersuite>::G, u32>,
@ -99,7 +100,7 @@ impl<T: Transaction> Block<T> {
let mut txs = Vec::with_capacity(self.transactions.len()); let mut txs = Vec::with_capacity(self.transactions.len());
for tx in &self.transactions { for tx in &self.transactions {
match verify_transaction(tx, locally_provided, next_nonces) { match verify_transaction(tx, genesis, locally_provided, next_nonces) {
Ok(()) => {} Ok(()) => {}
Err(e) => Err(BlockError::TransactionError(e))?, Err(e) => Err(BlockError::TransactionError(e))?,
} }

View file

@ -1,8 +1,13 @@
use std::collections::{HashSet, HashMap}; use std::{
io,
collections::{HashSet, HashMap},
};
use thiserror::Error; use thiserror::Error;
use ciphersuite::{Ciphersuite, Ristretto}; use blake2::{Digest, Blake2b512};
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
use schnorr::SchnorrSignature; use schnorr::SchnorrSignature;
use crate::ReadWrite; use crate::ReadWrite;
@ -17,6 +22,35 @@ pub enum TransactionError {
Fatal, Fatal,
} }
/// 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);
let signature = SchnorrSignature::<Ristretto>::read(reader)?;
Ok(Signed { signer, nonce, signature })
}
fn write<W: io::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)] #[derive(Clone, PartialEq, Eq, Debug)]
pub enum TransactionKind { pub enum TransactionKind {
/// This tranaction should be provided by every validator, solely ordered by the block producer. /// This tranaction should be provided by every validator, solely ordered by the block producer.
@ -28,11 +62,7 @@ pub enum TransactionKind {
Unsigned, Unsigned,
/// A signed transaction. /// A signed transaction.
Signed { Signed(Signed),
signer: <Ristretto as Ciphersuite>::G,
nonce: u32,
signature: SchnorrSignature<Ristretto>,
},
} }
pub trait Transaction: Send + Sync + Clone + Eq + ReadWrite { pub trait Transaction: Send + Sync + Clone + Eq + ReadWrite {
@ -40,10 +70,17 @@ pub trait Transaction: Send + Sync + Clone + Eq + ReadWrite {
fn hash(&self) -> [u8; 32]; fn hash(&self) -> [u8; 32];
fn verify(&self) -> Result<(), TransactionError>; fn verify(&self) -> Result<(), TransactionError>;
fn sig_hash(&self, genesis: [u8; 32]) -> <Ristretto as Ciphersuite>::F {
<Ristretto as Ciphersuite>::F::from_bytes_mod_order_wide(
&Blake2b512::digest([genesis, self.hash()].concat()).into(),
)
}
} }
pub(crate) fn verify_transaction<T: Transaction>( pub(crate) fn verify_transaction<T: Transaction>(
tx: &T, tx: &T,
genesis: [u8; 32],
locally_provided: &mut HashSet<[u8; 32]>, locally_provided: &mut HashSet<[u8; 32]>,
next_nonces: &mut HashMap<<Ristretto as Ciphersuite>::G, u32>, next_nonces: &mut HashMap<<Ristretto as Ciphersuite>::G, u32>,
) -> Result<(), TransactionError> { ) -> Result<(), TransactionError> {
@ -54,17 +91,14 @@ pub(crate) fn verify_transaction<T: Transaction>(
} }
} }
TransactionKind::Unsigned => {} TransactionKind::Unsigned => {}
TransactionKind::Signed { signer, nonce, signature } => { TransactionKind::Signed(Signed { signer, nonce, signature }) => {
if next_nonces.get(&signer).cloned().unwrap_or(0) != nonce { if next_nonces.get(&signer).cloned().unwrap_or(0) != nonce {
Err(TransactionError::Temporal)?; Err(TransactionError::Temporal)?;
} }
next_nonces.insert(signer, nonce + 1); next_nonces.insert(signer, nonce + 1);
// TODO: Use Schnorr half-aggregation and a batch verification here // TODO: Use Schnorr half-aggregation and a batch verification here
let mut wide = [0; 64]; if !signature.verify(signer, tx.sig_hash(genesis)) {
wide[.. 32].copy_from_slice(&tx.hash());
if !signature.verify(signer, <Ristretto as Ciphersuite>::F::from_bytes_mod_order_wide(&wide))
{
Err(TransactionError::Fatal)?; Err(TransactionError::Fatal)?;
} }
} }