diff --git a/coordinator/src/tributary/mod.rs b/coordinator/src/tributary/mod.rs index 8478ce989..6e2f26610 100644 --- a/coordinator/src/tributary/mod.rs +++ b/coordinator/src/tributary/mod.rs @@ -1,37 +1,18 @@ -use core::{ - ops::{Deref, Range}, - fmt::Debug, -}; -use std::io; - -use zeroize::Zeroizing; -use rand_core::{RngCore, CryptoRng}; - -use blake2::{Digest, Blake2s256}; -use transcript::{Transcript, RecommendedTranscript}; - -use ciphersuite::{ - group::{ff::Field, GroupEncoding}, - Ciphersuite, Ristretto, -}; -use schnorr::SchnorrSignature; -use frost::Participant; - -use scale::{Encode, Decode}; -use borsh::{BorshSerialize, BorshDeserialize}; -use processor_messages::coordinator::SubstrateSignableId; - -use serai_client::{primitives::PublicKey, validator_sets::primitives::ValidatorSet}; - use tributary::{ - TRANSACTION_SIZE_LIMIT, ReadWrite, - transaction::{Signed, TransactionError, TransactionKind, Transaction as TransactionTrait}, + ReadWrite, + transaction::{TransactionError, TransactionKind, Transaction as TransactionTrait}, Tributary, }; mod db; pub use db::*; +mod spec; +pub use spec::TributarySpec; + +mod transaction; +pub use transaction::{Label, SignData, Transaction}; + mod signing_protocol; mod handle; @@ -39,780 +20,6 @@ pub use handle::*; pub mod scanner; -fn borsh_serialize_validators( - validators: &Vec<(::G, u16)>, - writer: &mut W, -) -> Result<(), io::Error> { - let len = u16::try_from(validators.len()).unwrap(); - BorshSerialize::serialize(&len, writer)?; - for validator in validators { - BorshSerialize::serialize(&validator.0.to_bytes(), writer)?; - BorshSerialize::serialize(&validator.1, writer)?; - } - Ok(()) -} - -fn borsh_deserialize_validators( - reader: &mut R, -) -> Result::G, u16)>, io::Error> { - let len: u16 = BorshDeserialize::deserialize_reader(reader)?; - let mut res = vec![]; - for _ in 0 .. len { - let compressed: [u8; 32] = BorshDeserialize::deserialize_reader(reader)?; - let point = Option::from(::G::from_bytes(&compressed)) - .ok_or_else(|| io::Error::other("invalid point for validator"))?; - let weight: u16 = BorshDeserialize::deserialize_reader(reader)?; - res.push((point, weight)); - } - Ok(res) -} - -#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] -pub struct TributarySpec { - serai_block: [u8; 32], - start_time: u64, - set: ValidatorSet, - #[borsh( - serialize_with = "borsh_serialize_validators", - deserialize_with = "borsh_deserialize_validators" - )] - validators: Vec<(::G, u16)>, -} - -impl TributarySpec { - pub fn new( - serai_block: [u8; 32], - start_time: u64, - set: ValidatorSet, - set_participants: Vec<(PublicKey, u16)>, - ) -> TributarySpec { - let mut validators = vec![]; - for (participant, shares) in set_participants { - let participant = ::read_G::<&[u8]>(&mut participant.0.as_ref()) - .expect("invalid key registered as participant"); - validators.push((participant, shares)); - } - - Self { serai_block, start_time, set, validators } - } - - pub fn set(&self) -> ValidatorSet { - self.set - } - - pub fn genesis(&self) -> [u8; 32] { - // Calculate the genesis for this Tributary - let mut genesis = RecommendedTranscript::new(b"Serai Tributary Genesis"); - // This locks it to a specific Serai chain - genesis.append_message(b"serai_block", self.serai_block); - genesis.append_message(b"session", self.set.session.0.to_le_bytes()); - genesis.append_message(b"network", self.set.network.encode()); - let genesis = genesis.challenge(b"genesis"); - let genesis_ref: &[u8] = genesis.as_ref(); - genesis_ref[.. 32].try_into().unwrap() - } - - pub fn start_time(&self) -> u64 { - self.start_time - } - - pub fn n(&self) -> u16 { - self.validators.iter().map(|(_, weight)| weight).sum() - } - - pub fn t(&self) -> u16 { - ((2 * self.n()) / 3) + 1 - } - - pub fn i(&self, key: ::G) -> Option> { - let mut i = 1; - for (validator, weight) in &self.validators { - if validator == &key { - return Some(Range { - start: Participant::new(i).unwrap(), - end: Participant::new(i + weight).unwrap(), - }); - } - i += weight; - } - None - } - - pub fn validators(&self) -> Vec<(::G, u64)> { - self.validators.iter().map(|(validator, weight)| (*validator, u64::from(*weight))).collect() - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode)] -pub enum Label { - Preprocess, - Share, -} - -impl Label { - // TODO: Should nonces be u8 thanks to our use of topics? - pub fn nonce(&self) -> u32 { - match self { - Label::Preprocess => 0, - Label::Share => 1, - } - } -} - -#[derive(Clone, PartialEq, Eq)] -pub struct SignData { - pub plan: Id, - pub attempt: u32, - pub label: Label, - - pub data: Vec>, - - pub signed: Signed, -} - -impl Debug for SignData { - fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { - fmt - .debug_struct("SignData") - .field("id", &hex::encode(self.plan.encode())) - .field("attempt", &self.attempt) - .field("label", &self.label) - .field("signer", &hex::encode(self.signed.signer.to_bytes())) - .finish_non_exhaustive() - } -} - -impl SignData { - pub(crate) fn read(reader: &mut R) -> io::Result { - let plan = Id::decode(&mut scale::IoReader(&mut *reader)) - .map_err(|_| io::Error::other("invalid plan in SignData"))?; - - let mut attempt = [0; 4]; - reader.read_exact(&mut attempt)?; - let attempt = u32::from_le_bytes(attempt); - - let mut label = [0; 1]; - reader.read_exact(&mut label)?; - let label = match label[0] { - 0 => Label::Preprocess, - 1 => Label::Share, - _ => Err(io::Error::other("invalid label in SignData"))?, - }; - - let data = { - let mut data_pieces = [0]; - reader.read_exact(&mut data_pieces)?; - if data_pieces[0] == 0 { - Err(io::Error::other("zero pieces of data in SignData"))?; - } - let mut all_data = vec![]; - for _ in 0 .. data_pieces[0] { - let mut data_len = [0; 2]; - reader.read_exact(&mut data_len)?; - let mut data = vec![0; usize::from(u16::from_le_bytes(data_len))]; - reader.read_exact(&mut data)?; - all_data.push(data); - } - all_data - }; - - let signed = Signed::read_without_nonce(reader, label.nonce())?; - - Ok(SignData { plan, attempt, label, data, signed }) - } - - pub(crate) fn write(&self, writer: &mut W) -> io::Result<()> { - writer.write_all(&self.plan.encode())?; - writer.write_all(&self.attempt.to_le_bytes())?; - writer.write_all(&[match self.label { - Label::Preprocess => 0, - Label::Share => 1, - }])?; - - writer.write_all(&[u8::try_from(self.data.len()).unwrap()])?; - for data in &self.data { - if data.len() > u16::MAX.into() { - // Currently, the largest individual preprocess is a Monero transaction - // It provides 4 commitments per input (128 bytes), a 64-byte proof for them, along with a - // key image and proof (96 bytes) - // Even with all of that, we could support 227 inputs in a single TX - // Monero is limited to ~120 inputs per TX - // - // Bitcoin has a much higher input count of 520, yet it only uses 64 bytes per preprocess - Err(io::Error::other("signing data exceeded 65535 bytes"))?; - } - writer.write_all(&u16::try_from(data.len()).unwrap().to_le_bytes())?; - writer.write_all(data)?; - } - - self.signed.write_without_nonce(writer) - } -} - -#[derive(Clone, PartialEq, Eq)] -pub enum Transaction { - RemoveParticipant(Participant), - - // Once this completes successfully, no more instances should be created. - DkgCommitments { - attempt: u32, - commitments: Vec>, - signed: Signed, - }, - DkgShares { - attempt: u32, - // Sending Participant, Receiving Participant, Share - shares: Vec>>, - confirmation_nonces: [u8; 64], - signed: Signed, - }, - InvalidDkgShare { - attempt: u32, - accuser: Participant, - faulty: Participant, - blame: Option>, - signed: Signed, - }, - DkgConfirmed { - attempt: u32, - confirmation_share: [u8; 32], - signed: Signed, - }, - - DkgRemoval(SignData<[u8; 32]>), - - // Co-sign a Substrate block. - CosignSubstrateBlock([u8; 32]), - - // When we have synchrony on a batch, we can allow signing it - // TODO (never?): This is less efficient compared to an ExternalBlock provided transaction, - // which would be binding over the block hash and automatically achieve synchrony on all - // relevant batches. ExternalBlock was removed for this due to complexity around the pipeline - // with the current processor, yet it would still be an improvement. - Batch { - block: [u8; 32], - batch: [u8; 5], - }, - // When a Serai block is finalized, with the contained batches, we can allow the associated plan - // IDs - SubstrateBlock(u64), - - SubstrateSign(SignData), - Sign(SignData<[u8; 32]>), - // This is defined as an Unsigned transaction in order to de-duplicate SignCompleted amongst - // reporters (who should all report the same thing) - // We do still track the signer in order to prevent a single signer from publishing arbitrarily - // many TXs without penalty - // Here, they're denoted as the first_signer, as only the signer of the first TX to be included - // with this pairing will be remembered on-chain - SignCompleted { - plan: [u8; 32], - tx_hash: Vec, - first_signer: ::G, - signature: SchnorrSignature, - }, -} - -impl Debug for Transaction { - fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { - match self { - Transaction::RemoveParticipant(participant) => fmt - .debug_struct("Transaction::RemoveParticipant") - .field("participant", participant) - .finish(), - Transaction::DkgCommitments { attempt, commitments: _, signed } => fmt - .debug_struct("Transaction::DkgCommitments") - .field("attempt", attempt) - .field("signer", &hex::encode(signed.signer.to_bytes())) - .finish_non_exhaustive(), - Transaction::DkgShares { attempt, signed, .. } => fmt - .debug_struct("Transaction::DkgShares") - .field("attempt", attempt) - .field("signer", &hex::encode(signed.signer.to_bytes())) - .finish_non_exhaustive(), - Transaction::InvalidDkgShare { attempt, accuser, faulty, .. } => fmt - .debug_struct("Transaction::InvalidDkgShare") - .field("attempt", attempt) - .field("accuser", accuser) - .field("faulty", faulty) - .finish_non_exhaustive(), - Transaction::DkgConfirmed { attempt, confirmation_share: _, signed } => fmt - .debug_struct("Transaction::DkgConfirmed") - .field("attempt", attempt) - .field("signer", &hex::encode(signed.signer.to_bytes())) - .finish_non_exhaustive(), - Transaction::DkgRemoval(sign_data) => { - fmt.debug_struct("Transaction::DkgRemoval").field("sign_data", sign_data).finish() - } - Transaction::CosignSubstrateBlock(block) => fmt - .debug_struct("Transaction::CosignSubstrateBlock") - .field("block", &hex::encode(block)) - .finish(), - Transaction::Batch { block, batch } => fmt - .debug_struct("Transaction::Batch") - .field("block", &hex::encode(block)) - .field("batch", &hex::encode(batch)) - .finish(), - Transaction::SubstrateBlock(block) => { - fmt.debug_struct("Transaction::SubstrateBlock").field("block", block).finish() - } - Transaction::SubstrateSign(sign_data) => { - fmt.debug_struct("Transaction::Substrate").field("sign_data", sign_data).finish() - } - Transaction::Sign(sign_data) => { - fmt.debug_struct("Transaction::Sign").field("sign_data", sign_data).finish() - } - Transaction::SignCompleted { plan, tx_hash, .. } => fmt - .debug_struct("Transaction::SignCompleted") - .field("plan", &hex::encode(plan)) - .field("tx_hash", &hex::encode(tx_hash)) - .finish_non_exhaustive(), - } - } -} - -impl ReadWrite for Transaction { - fn read(reader: &mut R) -> io::Result { - let mut kind = [0]; - reader.read_exact(&mut kind)?; - - match kind[0] { - 0 => Ok(Transaction::RemoveParticipant({ - let mut participant = [0; 2]; - reader.read_exact(&mut participant)?; - Participant::new(u16::from_le_bytes(participant)) - .ok_or_else(|| io::Error::other("invalid participant in RemoveParticipant"))? - })), - - 1 => { - let mut attempt = [0; 4]; - reader.read_exact(&mut attempt)?; - let attempt = u32::from_le_bytes(attempt); - - let commitments = { - let mut commitments_len = [0; 1]; - reader.read_exact(&mut commitments_len)?; - let commitments_len = usize::from(commitments_len[0]); - if commitments_len == 0 { - Err(io::Error::other("zero commitments in DkgCommitments"))?; - } - - let mut each_commitments_len = [0; 2]; - reader.read_exact(&mut each_commitments_len)?; - let each_commitments_len = usize::from(u16::from_le_bytes(each_commitments_len)); - if (commitments_len * each_commitments_len) > TRANSACTION_SIZE_LIMIT { - Err(io::Error::other( - "commitments present in transaction exceeded transaction size limit", - ))?; - } - let mut commitments = vec![vec![]; commitments_len]; - for commitments in &mut commitments { - *commitments = vec![0; each_commitments_len]; - reader.read_exact(commitments)?; - } - commitments - }; - - let signed = Signed::read_without_nonce(reader, 0)?; - - Ok(Transaction::DkgCommitments { attempt, commitments, signed }) - } - - 2 => { - let mut attempt = [0; 4]; - reader.read_exact(&mut attempt)?; - let attempt = u32::from_le_bytes(attempt); - - let shares = { - let mut share_quantity = [0; 1]; - reader.read_exact(&mut share_quantity)?; - - let mut key_share_quantity = [0; 1]; - reader.read_exact(&mut key_share_quantity)?; - - let mut share_len = [0; 2]; - reader.read_exact(&mut share_len)?; - let share_len = usize::from(u16::from_le_bytes(share_len)); - - let mut all_shares = vec![]; - for _ in 0 .. share_quantity[0] { - let mut shares = vec![]; - for _ in 0 .. key_share_quantity[0] { - let mut share = vec![0; share_len]; - reader.read_exact(&mut share)?; - shares.push(share); - } - all_shares.push(shares); - } - all_shares - }; - - let mut confirmation_nonces = [0; 64]; - reader.read_exact(&mut confirmation_nonces)?; - - let signed = Signed::read_without_nonce(reader, 1)?; - - Ok(Transaction::DkgShares { attempt, shares, confirmation_nonces, signed }) - } - - 3 => { - let mut attempt = [0; 4]; - reader.read_exact(&mut attempt)?; - let attempt = u32::from_le_bytes(attempt); - - let mut accuser = [0; 2]; - reader.read_exact(&mut accuser)?; - let accuser = Participant::new(u16::from_le_bytes(accuser)) - .ok_or_else(|| io::Error::other("invalid participant in InvalidDkgShare"))?; - - let mut faulty = [0; 2]; - reader.read_exact(&mut faulty)?; - let faulty = Participant::new(u16::from_le_bytes(faulty)) - .ok_or_else(|| io::Error::other("invalid participant in InvalidDkgShare"))?; - - let mut blame_len = [0; 2]; - reader.read_exact(&mut blame_len)?; - let mut blame = vec![0; u16::from_le_bytes(blame_len).into()]; - reader.read_exact(&mut blame)?; - - // This shares a nonce with DkgConfirmed as only one is expected - let signed = Signed::read_without_nonce(reader, 2)?; - - Ok(Transaction::InvalidDkgShare { - attempt, - accuser, - faulty, - blame: Some(blame).filter(|blame| !blame.is_empty()), - signed, - }) - } - - 4 => { - let mut attempt = [0; 4]; - reader.read_exact(&mut attempt)?; - let attempt = u32::from_le_bytes(attempt); - - let mut confirmation_share = [0; 32]; - reader.read_exact(&mut confirmation_share)?; - - let signed = Signed::read_without_nonce(reader, 2)?; - - Ok(Transaction::DkgConfirmed { attempt, confirmation_share, signed }) - } - - 5 => SignData::read(reader).map(Transaction::DkgRemoval), - - 6 => { - let mut block = [0; 32]; - reader.read_exact(&mut block)?; - Ok(Transaction::CosignSubstrateBlock(block)) - } - - 7 => { - let mut block = [0; 32]; - reader.read_exact(&mut block)?; - let mut batch = [0; 5]; - reader.read_exact(&mut batch)?; - Ok(Transaction::Batch { block, batch }) - } - - 8 => { - let mut block = [0; 8]; - reader.read_exact(&mut block)?; - Ok(Transaction::SubstrateBlock(u64::from_le_bytes(block))) - } - - 9 => SignData::read(reader).map(Transaction::SubstrateSign), - 10 => SignData::read(reader).map(Transaction::Sign), - - 11 => { - let mut plan = [0; 32]; - reader.read_exact(&mut plan)?; - - let mut tx_hash_len = [0]; - reader.read_exact(&mut tx_hash_len)?; - let mut tx_hash = vec![0; usize::from(tx_hash_len[0])]; - reader.read_exact(&mut tx_hash)?; - - let first_signer = Ristretto::read_G(reader)?; - let signature = SchnorrSignature::::read(reader)?; - - Ok(Transaction::SignCompleted { plan, tx_hash, first_signer, signature }) - } - - _ => Err(io::Error::other("invalid transaction type")), - } - } - - fn write(&self, writer: &mut W) -> io::Result<()> { - match self { - Transaction::RemoveParticipant(i) => { - writer.write_all(&[0])?; - writer.write_all(&u16::from(*i).to_le_bytes()) - } - - Transaction::DkgCommitments { attempt, commitments, signed } => { - writer.write_all(&[1])?; - writer.write_all(&attempt.to_le_bytes())?; - if commitments.is_empty() { - Err(io::Error::other("zero commitments in DkgCommitments"))? - } - writer.write_all(&[u8::try_from(commitments.len()).unwrap()])?; - for commitments_i in commitments { - if commitments_i.len() != commitments[0].len() { - Err(io::Error::other("commitments of differing sizes in DkgCommitments"))? - } - } - writer.write_all(&u16::try_from(commitments[0].len()).unwrap().to_le_bytes())?; - for commitments in commitments { - writer.write_all(commitments)?; - } - signed.write_without_nonce(writer) - } - - Transaction::DkgShares { attempt, shares, confirmation_nonces, signed } => { - writer.write_all(&[2])?; - writer.write_all(&attempt.to_le_bytes())?; - - // `shares` is a Vec which is supposed to map to a HashMap>. Since we - // bound participants to 150, this conversion is safe if a valid in-memory transaction. - writer.write_all(&[u8::try_from(shares.len()).unwrap()])?; - // This assumes at least one share is being sent to another party - writer.write_all(&[u8::try_from(shares[0].len()).unwrap()])?; - let share_len = shares[0][0].len(); - // For BLS12-381 G2, this would be: - // - A 32-byte share - // - A 96-byte ephemeral key - // - A 128-byte signature - // Hence why this has to be u16 - writer.write_all(&u16::try_from(share_len).unwrap().to_le_bytes())?; - - for these_shares in shares { - assert_eq!(these_shares.len(), shares[0].len(), "amount of sent shares was variable"); - for share in these_shares { - assert_eq!(share.len(), share_len, "sent shares were of variable length"); - writer.write_all(share)?; - } - } - - writer.write_all(confirmation_nonces)?; - signed.write_without_nonce(writer) - } - - Transaction::InvalidDkgShare { attempt, accuser, faulty, blame, signed } => { - writer.write_all(&[3])?; - writer.write_all(&attempt.to_le_bytes())?; - writer.write_all(&u16::from(*accuser).to_le_bytes())?; - writer.write_all(&u16::from(*faulty).to_le_bytes())?; - - // Flattens Some(vec![]) to None on the expectation no actual blame will be 0-length - assert!(blame.as_ref().map(|blame| blame.len()).unwrap_or(1) != 0); - let blame_len = - u16::try_from(blame.as_ref().unwrap_or(&vec![]).len()).expect("blame exceeded 64 KB"); - writer.write_all(&blame_len.to_le_bytes())?; - writer.write_all(blame.as_ref().unwrap_or(&vec![]))?; - - signed.write_without_nonce(writer) - } - - Transaction::DkgConfirmed { attempt, confirmation_share, signed } => { - writer.write_all(&[4])?; - writer.write_all(&attempt.to_le_bytes())?; - writer.write_all(confirmation_share)?; - signed.write_without_nonce(writer) - } - - Transaction::DkgRemoval(data) => { - writer.write_all(&[5])?; - data.write(writer) - } - - Transaction::CosignSubstrateBlock(block) => { - writer.write_all(&[6])?; - writer.write_all(block) - } - - Transaction::Batch { block, batch } => { - writer.write_all(&[7])?; - writer.write_all(block)?; - writer.write_all(batch) - } - - Transaction::SubstrateBlock(block) => { - writer.write_all(&[8])?; - writer.write_all(&block.to_le_bytes()) - } - - Transaction::SubstrateSign(data) => { - writer.write_all(&[9])?; - data.write(writer) - } - Transaction::Sign(data) => { - writer.write_all(&[10])?; - data.write(writer) - } - Transaction::SignCompleted { plan, tx_hash, first_signer, signature } => { - writer.write_all(&[11])?; - writer.write_all(plan)?; - writer - .write_all(&[u8::try_from(tx_hash.len()).expect("tx hash length exceed 255 bytes")])?; - writer.write_all(tx_hash)?; - writer.write_all(&first_signer.to_bytes())?; - signature.write(writer) - } - } - } -} - -impl TransactionTrait for Transaction { - fn kind(&self) -> TransactionKind<'_> { - match self { - Transaction::RemoveParticipant(_) => TransactionKind::Provided("remove"), - - Transaction::DkgCommitments { attempt, commitments: _, signed } => { - TransactionKind::Signed((b"dkg", attempt).encode(), signed) - } - Transaction::DkgShares { attempt, signed, .. } => { - TransactionKind::Signed((b"dkg", attempt).encode(), signed) - } - Transaction::InvalidDkgShare { attempt, signed, .. } => { - TransactionKind::Signed((b"dkg", attempt).encode(), signed) - } - Transaction::DkgConfirmed { attempt, signed, .. } => { - TransactionKind::Signed((b"dkg", attempt).encode(), signed) - } - - Transaction::DkgRemoval(data) => { - TransactionKind::Signed((b"dkg_removal", data.plan, data.attempt).encode(), &data.signed) - } - - Transaction::CosignSubstrateBlock(_) => TransactionKind::Provided("cosign"), - - Transaction::Batch { .. } => TransactionKind::Provided("batch"), - Transaction::SubstrateBlock(_) => TransactionKind::Provided("serai"), - - Transaction::SubstrateSign(data) => { - TransactionKind::Signed((b"substrate", data.plan, data.attempt).encode(), &data.signed) - } - Transaction::Sign(data) => { - TransactionKind::Signed((b"sign", data.plan, data.attempt).encode(), &data.signed) - } - Transaction::SignCompleted { .. } => TransactionKind::Unsigned, - } - } - - fn hash(&self) -> [u8; 32] { - let mut tx = self.serialize(); - if let TransactionKind::Signed(_, signed) = self.kind() { - // Make sure the part we're cutting off is the signature - assert_eq!(tx.drain((tx.len() - 64) ..).collect::>(), signed.signature.serialize()); - } - Blake2s256::digest([b"Coordinator Tributary Transaction".as_slice(), &tx].concat()).into() - } - - fn verify(&self) -> Result<(), TransactionError> { - // TODO: Check DkgRemoval and SubstrateSign's lengths here - - if let Transaction::SignCompleted { first_signer, signature, .. } = self { - if !signature.verify(*first_signer, self.sign_completed_challenge()) { - Err(TransactionError::InvalidContent)?; - } - } - - Ok(()) - } -} - -impl Transaction { - // Used to initially construct transactions so we can then get sig hashes and perform signing - pub fn empty_signed() -> Signed { - Signed { - signer: Ristretto::generator(), - nonce: 0, - signature: SchnorrSignature:: { - R: Ristretto::generator(), - s: ::F::ZERO, - }, - } - } - - // Sign a transaction - pub fn sign( - &mut self, - rng: &mut R, - genesis: [u8; 32], - key: &Zeroizing<::F>, - ) { - fn signed(tx: &mut Transaction) -> (u32, &mut Signed) { - let nonce = match tx { - Transaction::RemoveParticipant(_) => panic!("signing RemoveParticipant"), - - Transaction::DkgCommitments { .. } => 0, - Transaction::DkgShares { .. } => 1, - Transaction::InvalidDkgShare { .. } => 2, - Transaction::DkgConfirmed { .. } => 2, - - Transaction::DkgRemoval(data) => data.label.nonce(), - - Transaction::CosignSubstrateBlock(_) => panic!("signing CosignSubstrateBlock"), - - Transaction::Batch { .. } => panic!("signing Batch"), - Transaction::SubstrateBlock(_) => panic!("signing SubstrateBlock"), - - Transaction::SubstrateSign(data) => data.label.nonce(), - Transaction::Sign(data) => data.label.nonce(), - Transaction::SignCompleted { .. } => panic!("signing SignCompleted"), - }; - - ( - nonce, - match tx { - Transaction::RemoveParticipant(_) => panic!("signing RemoveParticipant"), - - Transaction::DkgCommitments { ref mut signed, .. } => signed, - Transaction::DkgShares { ref mut signed, .. } => signed, - Transaction::InvalidDkgShare { ref mut signed, .. } => signed, - Transaction::DkgConfirmed { ref mut signed, .. } => signed, - - Transaction::DkgRemoval(ref mut data) => &mut data.signed, - - Transaction::CosignSubstrateBlock(_) => panic!("signing CosignSubstrateBlock"), - - Transaction::Batch { .. } => panic!("signing Batch"), - Transaction::SubstrateBlock(_) => panic!("signing SubstrateBlock"), - - Transaction::SubstrateSign(ref mut data) => &mut data.signed, - Transaction::Sign(ref mut data) => &mut data.signed, - Transaction::SignCompleted { .. } => panic!("signing SignCompleted"), - }, - ) - } - - let (nonce, signed_ref) = signed(self); - signed_ref.signer = Ristretto::generator() * key.deref(); - signed_ref.nonce = nonce; - - let sig_nonce = Zeroizing::new(::F::random(rng)); - signed(self).1.signature.R = ::generator() * sig_nonce.deref(); - let sig_hash = self.sig_hash(genesis); - signed(self).1.signature = SchnorrSignature::::sign(key, sig_nonce, sig_hash); - } - - pub fn sign_completed_challenge(&self) -> ::F { - if let Transaction::SignCompleted { plan, tx_hash, first_signer, signature } = self { - let mut transcript = - RecommendedTranscript::new(b"Coordinator Tributary Transaction SignCompleted"); - transcript.append_message(b"plan", plan); - transcript.append_message(b"tx_hash", tx_hash); - transcript.append_message(b"signer", first_signer.to_bytes()); - transcript.append_message(b"nonce", signature.R.to_bytes()); - Ristretto::hash_to_F(b"SignCompleted signature", &transcript.challenge(b"challenge")) - } else { - panic!("sign_completed_challenge called on transaction which wasn't SignCompleted") - } - } -} - pub async fn publish_signed_transaction( txn: &mut D::Transaction<'_>, tributary: &Tributary, diff --git a/coordinator/src/tributary/spec.rs b/coordinator/src/tributary/spec.rs new file mode 100644 index 000000000..95f9e595b --- /dev/null +++ b/coordinator/src/tributary/spec.rs @@ -0,0 +1,116 @@ +use core::{ops::Range, fmt::Debug}; +use std::io; + +use transcript::{Transcript, RecommendedTranscript}; + +use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto}; +use frost::Participant; + +use scale::Encode; +use borsh::{BorshSerialize, BorshDeserialize}; + +use serai_client::{primitives::PublicKey, validator_sets::primitives::ValidatorSet}; + +fn borsh_serialize_validators( + validators: &Vec<(::G, u16)>, + writer: &mut W, +) -> Result<(), io::Error> { + let len = u16::try_from(validators.len()).unwrap(); + BorshSerialize::serialize(&len, writer)?; + for validator in validators { + BorshSerialize::serialize(&validator.0.to_bytes(), writer)?; + BorshSerialize::serialize(&validator.1, writer)?; + } + Ok(()) +} + +fn borsh_deserialize_validators( + reader: &mut R, +) -> Result::G, u16)>, io::Error> { + let len: u16 = BorshDeserialize::deserialize_reader(reader)?; + let mut res = vec![]; + for _ in 0 .. len { + let compressed: [u8; 32] = BorshDeserialize::deserialize_reader(reader)?; + let point = Option::from(::G::from_bytes(&compressed)) + .ok_or_else(|| io::Error::other("invalid point for validator"))?; + let weight: u16 = BorshDeserialize::deserialize_reader(reader)?; + res.push((point, weight)); + } + Ok(res) +} + +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +pub struct TributarySpec { + serai_block: [u8; 32], + start_time: u64, + set: ValidatorSet, + #[borsh( + serialize_with = "borsh_serialize_validators", + deserialize_with = "borsh_deserialize_validators" + )] + validators: Vec<(::G, u16)>, +} + +impl TributarySpec { + pub fn new( + serai_block: [u8; 32], + start_time: u64, + set: ValidatorSet, + set_participants: Vec<(PublicKey, u16)>, + ) -> TributarySpec { + let mut validators = vec![]; + for (participant, shares) in set_participants { + let participant = ::read_G::<&[u8]>(&mut participant.0.as_ref()) + .expect("invalid key registered as participant"); + validators.push((participant, shares)); + } + + Self { serai_block, start_time, set, validators } + } + + pub fn set(&self) -> ValidatorSet { + self.set + } + + pub fn genesis(&self) -> [u8; 32] { + // Calculate the genesis for this Tributary + let mut genesis = RecommendedTranscript::new(b"Serai Tributary Genesis"); + // This locks it to a specific Serai chain + genesis.append_message(b"serai_block", self.serai_block); + genesis.append_message(b"session", self.set.session.0.to_le_bytes()); + genesis.append_message(b"network", self.set.network.encode()); + let genesis = genesis.challenge(b"genesis"); + let genesis_ref: &[u8] = genesis.as_ref(); + genesis_ref[.. 32].try_into().unwrap() + } + + pub fn start_time(&self) -> u64 { + self.start_time + } + + pub fn n(&self) -> u16 { + self.validators.iter().map(|(_, weight)| weight).sum() + } + + pub fn t(&self) -> u16 { + ((2 * self.n()) / 3) + 1 + } + + pub fn i(&self, key: ::G) -> Option> { + let mut i = 1; + for (validator, weight) in &self.validators { + if validator == &key { + return Some(Range { + start: Participant::new(i).unwrap(), + end: Participant::new(i + weight).unwrap(), + }); + } + i += weight; + } + None + } + + pub fn validators(&self) -> Vec<(::G, u64)> { + self.validators.iter().map(|(validator, weight)| (*validator, u64::from(*weight))).collect() + } +} diff --git a/coordinator/src/tributary/transaction.rs b/coordinator/src/tributary/transaction.rs new file mode 100644 index 000000000..4a2748d52 --- /dev/null +++ b/coordinator/src/tributary/transaction.rs @@ -0,0 +1,693 @@ +use core::{ops::Deref, fmt::Debug}; +use std::io; + +use zeroize::Zeroizing; +use rand_core::{RngCore, CryptoRng}; + +use blake2::{Digest, Blake2s256}; +use transcript::{Transcript, RecommendedTranscript}; + +use ciphersuite::{ + group::{ff::Field, GroupEncoding}, + Ciphersuite, Ristretto, +}; +use schnorr::SchnorrSignature; +use frost::Participant; + +use scale::{Encode, Decode}; +use processor_messages::coordinator::SubstrateSignableId; + +use tributary::{ + TRANSACTION_SIZE_LIMIT, ReadWrite, + transaction::{Signed, TransactionError, TransactionKind, Transaction as TransactionTrait}, +}; + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode)] +pub enum Label { + Preprocess, + Share, +} + +impl Label { + // TODO: Should nonces be u8 thanks to our use of topics? + pub fn nonce(&self) -> u32 { + match self { + Label::Preprocess => 0, + Label::Share => 1, + } + } +} + +#[derive(Clone, PartialEq, Eq)] +pub struct SignData { + pub plan: Id, + pub attempt: u32, + pub label: Label, + + pub data: Vec>, + + pub signed: Signed, +} + +impl Debug for SignData { + fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + fmt + .debug_struct("SignData") + .field("id", &hex::encode(self.plan.encode())) + .field("attempt", &self.attempt) + .field("label", &self.label) + .field("signer", &hex::encode(self.signed.signer.to_bytes())) + .finish_non_exhaustive() + } +} + +impl SignData { + pub(crate) fn read(reader: &mut R) -> io::Result { + let plan = Id::decode(&mut scale::IoReader(&mut *reader)) + .map_err(|_| io::Error::other("invalid plan in SignData"))?; + + let mut attempt = [0; 4]; + reader.read_exact(&mut attempt)?; + let attempt = u32::from_le_bytes(attempt); + + let mut label = [0; 1]; + reader.read_exact(&mut label)?; + let label = match label[0] { + 0 => Label::Preprocess, + 1 => Label::Share, + _ => Err(io::Error::other("invalid label in SignData"))?, + }; + + let data = { + let mut data_pieces = [0]; + reader.read_exact(&mut data_pieces)?; + if data_pieces[0] == 0 { + Err(io::Error::other("zero pieces of data in SignData"))?; + } + let mut all_data = vec![]; + for _ in 0 .. data_pieces[0] { + let mut data_len = [0; 2]; + reader.read_exact(&mut data_len)?; + let mut data = vec![0; usize::from(u16::from_le_bytes(data_len))]; + reader.read_exact(&mut data)?; + all_data.push(data); + } + all_data + }; + + let signed = Signed::read_without_nonce(reader, label.nonce())?; + + Ok(SignData { plan, attempt, label, data, signed }) + } + + pub(crate) fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(&self.plan.encode())?; + writer.write_all(&self.attempt.to_le_bytes())?; + writer.write_all(&[match self.label { + Label::Preprocess => 0, + Label::Share => 1, + }])?; + + writer.write_all(&[u8::try_from(self.data.len()).unwrap()])?; + for data in &self.data { + if data.len() > u16::MAX.into() { + // Currently, the largest individual preprocess is a Monero transaction + // It provides 4 commitments per input (128 bytes), a 64-byte proof for them, along with a + // key image and proof (96 bytes) + // Even with all of that, we could support 227 inputs in a single TX + // Monero is limited to ~120 inputs per TX + // + // Bitcoin has a much higher input count of 520, yet it only uses 64 bytes per preprocess + Err(io::Error::other("signing data exceeded 65535 bytes"))?; + } + writer.write_all(&u16::try_from(data.len()).unwrap().to_le_bytes())?; + writer.write_all(data)?; + } + + self.signed.write_without_nonce(writer) + } +} + +#[derive(Clone, PartialEq, Eq)] +pub enum Transaction { + RemoveParticipant(Participant), + + // Once this completes successfully, no more instances should be created. + DkgCommitments { + attempt: u32, + commitments: Vec>, + signed: Signed, + }, + DkgShares { + attempt: u32, + // Sending Participant, Receiving Participant, Share + shares: Vec>>, + confirmation_nonces: [u8; 64], + signed: Signed, + }, + InvalidDkgShare { + attempt: u32, + accuser: Participant, + faulty: Participant, + blame: Option>, + signed: Signed, + }, + DkgConfirmed { + attempt: u32, + confirmation_share: [u8; 32], + signed: Signed, + }, + + DkgRemoval(SignData<[u8; 32]>), + + // Co-sign a Substrate block. + CosignSubstrateBlock([u8; 32]), + + // When we have synchrony on a batch, we can allow signing it + // TODO (never?): This is less efficient compared to an ExternalBlock provided transaction, + // which would be binding over the block hash and automatically achieve synchrony on all + // relevant batches. ExternalBlock was removed for this due to complexity around the pipeline + // with the current processor, yet it would still be an improvement. + Batch { + block: [u8; 32], + batch: [u8; 5], + }, + // When a Serai block is finalized, with the contained batches, we can allow the associated plan + // IDs + SubstrateBlock(u64), + + SubstrateSign(SignData), + Sign(SignData<[u8; 32]>), + // This is defined as an Unsigned transaction in order to de-duplicate SignCompleted amongst + // reporters (who should all report the same thing) + // We do still track the signer in order to prevent a single signer from publishing arbitrarily + // many TXs without penalty + // Here, they're denoted as the first_signer, as only the signer of the first TX to be included + // with this pairing will be remembered on-chain + SignCompleted { + plan: [u8; 32], + tx_hash: Vec, + first_signer: ::G, + signature: SchnorrSignature, + }, +} + +impl Debug for Transaction { + fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + match self { + Transaction::RemoveParticipant(participant) => fmt + .debug_struct("Transaction::RemoveParticipant") + .field("participant", participant) + .finish(), + Transaction::DkgCommitments { attempt, commitments: _, signed } => fmt + .debug_struct("Transaction::DkgCommitments") + .field("attempt", attempt) + .field("signer", &hex::encode(signed.signer.to_bytes())) + .finish_non_exhaustive(), + Transaction::DkgShares { attempt, signed, .. } => fmt + .debug_struct("Transaction::DkgShares") + .field("attempt", attempt) + .field("signer", &hex::encode(signed.signer.to_bytes())) + .finish_non_exhaustive(), + Transaction::InvalidDkgShare { attempt, accuser, faulty, .. } => fmt + .debug_struct("Transaction::InvalidDkgShare") + .field("attempt", attempt) + .field("accuser", accuser) + .field("faulty", faulty) + .finish_non_exhaustive(), + Transaction::DkgConfirmed { attempt, confirmation_share: _, signed } => fmt + .debug_struct("Transaction::DkgConfirmed") + .field("attempt", attempt) + .field("signer", &hex::encode(signed.signer.to_bytes())) + .finish_non_exhaustive(), + Transaction::DkgRemoval(sign_data) => { + fmt.debug_struct("Transaction::DkgRemoval").field("sign_data", sign_data).finish() + } + Transaction::CosignSubstrateBlock(block) => fmt + .debug_struct("Transaction::CosignSubstrateBlock") + .field("block", &hex::encode(block)) + .finish(), + Transaction::Batch { block, batch } => fmt + .debug_struct("Transaction::Batch") + .field("block", &hex::encode(block)) + .field("batch", &hex::encode(batch)) + .finish(), + Transaction::SubstrateBlock(block) => { + fmt.debug_struct("Transaction::SubstrateBlock").field("block", block).finish() + } + Transaction::SubstrateSign(sign_data) => { + fmt.debug_struct("Transaction::Substrate").field("sign_data", sign_data).finish() + } + Transaction::Sign(sign_data) => { + fmt.debug_struct("Transaction::Sign").field("sign_data", sign_data).finish() + } + Transaction::SignCompleted { plan, tx_hash, .. } => fmt + .debug_struct("Transaction::SignCompleted") + .field("plan", &hex::encode(plan)) + .field("tx_hash", &hex::encode(tx_hash)) + .finish_non_exhaustive(), + } + } +} + +impl ReadWrite for Transaction { + fn read(reader: &mut R) -> io::Result { + let mut kind = [0]; + reader.read_exact(&mut kind)?; + + match kind[0] { + 0 => Ok(Transaction::RemoveParticipant({ + let mut participant = [0; 2]; + reader.read_exact(&mut participant)?; + Participant::new(u16::from_le_bytes(participant)) + .ok_or_else(|| io::Error::other("invalid participant in RemoveParticipant"))? + })), + + 1 => { + let mut attempt = [0; 4]; + reader.read_exact(&mut attempt)?; + let attempt = u32::from_le_bytes(attempt); + + let commitments = { + let mut commitments_len = [0; 1]; + reader.read_exact(&mut commitments_len)?; + let commitments_len = usize::from(commitments_len[0]); + if commitments_len == 0 { + Err(io::Error::other("zero commitments in DkgCommitments"))?; + } + + let mut each_commitments_len = [0; 2]; + reader.read_exact(&mut each_commitments_len)?; + let each_commitments_len = usize::from(u16::from_le_bytes(each_commitments_len)); + if (commitments_len * each_commitments_len) > TRANSACTION_SIZE_LIMIT { + Err(io::Error::other( + "commitments present in transaction exceeded transaction size limit", + ))?; + } + let mut commitments = vec![vec![]; commitments_len]; + for commitments in &mut commitments { + *commitments = vec![0; each_commitments_len]; + reader.read_exact(commitments)?; + } + commitments + }; + + let signed = Signed::read_without_nonce(reader, 0)?; + + Ok(Transaction::DkgCommitments { attempt, commitments, signed }) + } + + 2 => { + let mut attempt = [0; 4]; + reader.read_exact(&mut attempt)?; + let attempt = u32::from_le_bytes(attempt); + + let shares = { + let mut share_quantity = [0; 1]; + reader.read_exact(&mut share_quantity)?; + + let mut key_share_quantity = [0; 1]; + reader.read_exact(&mut key_share_quantity)?; + + let mut share_len = [0; 2]; + reader.read_exact(&mut share_len)?; + let share_len = usize::from(u16::from_le_bytes(share_len)); + + let mut all_shares = vec![]; + for _ in 0 .. share_quantity[0] { + let mut shares = vec![]; + for _ in 0 .. key_share_quantity[0] { + let mut share = vec![0; share_len]; + reader.read_exact(&mut share)?; + shares.push(share); + } + all_shares.push(shares); + } + all_shares + }; + + let mut confirmation_nonces = [0; 64]; + reader.read_exact(&mut confirmation_nonces)?; + + let signed = Signed::read_without_nonce(reader, 1)?; + + Ok(Transaction::DkgShares { attempt, shares, confirmation_nonces, signed }) + } + + 3 => { + let mut attempt = [0; 4]; + reader.read_exact(&mut attempt)?; + let attempt = u32::from_le_bytes(attempt); + + let mut accuser = [0; 2]; + reader.read_exact(&mut accuser)?; + let accuser = Participant::new(u16::from_le_bytes(accuser)) + .ok_or_else(|| io::Error::other("invalid participant in InvalidDkgShare"))?; + + let mut faulty = [0; 2]; + reader.read_exact(&mut faulty)?; + let faulty = Participant::new(u16::from_le_bytes(faulty)) + .ok_or_else(|| io::Error::other("invalid participant in InvalidDkgShare"))?; + + let mut blame_len = [0; 2]; + reader.read_exact(&mut blame_len)?; + let mut blame = vec![0; u16::from_le_bytes(blame_len).into()]; + reader.read_exact(&mut blame)?; + + // This shares a nonce with DkgConfirmed as only one is expected + let signed = Signed::read_without_nonce(reader, 2)?; + + Ok(Transaction::InvalidDkgShare { + attempt, + accuser, + faulty, + blame: Some(blame).filter(|blame| !blame.is_empty()), + signed, + }) + } + + 4 => { + let mut attempt = [0; 4]; + reader.read_exact(&mut attempt)?; + let attempt = u32::from_le_bytes(attempt); + + let mut confirmation_share = [0; 32]; + reader.read_exact(&mut confirmation_share)?; + + let signed = Signed::read_without_nonce(reader, 2)?; + + Ok(Transaction::DkgConfirmed { attempt, confirmation_share, signed }) + } + + 5 => SignData::read(reader).map(Transaction::DkgRemoval), + + 6 => { + let mut block = [0; 32]; + reader.read_exact(&mut block)?; + Ok(Transaction::CosignSubstrateBlock(block)) + } + + 7 => { + let mut block = [0; 32]; + reader.read_exact(&mut block)?; + let mut batch = [0; 5]; + reader.read_exact(&mut batch)?; + Ok(Transaction::Batch { block, batch }) + } + + 8 => { + let mut block = [0; 8]; + reader.read_exact(&mut block)?; + Ok(Transaction::SubstrateBlock(u64::from_le_bytes(block))) + } + + 9 => SignData::read(reader).map(Transaction::SubstrateSign), + 10 => SignData::read(reader).map(Transaction::Sign), + + 11 => { + let mut plan = [0; 32]; + reader.read_exact(&mut plan)?; + + let mut tx_hash_len = [0]; + reader.read_exact(&mut tx_hash_len)?; + let mut tx_hash = vec![0; usize::from(tx_hash_len[0])]; + reader.read_exact(&mut tx_hash)?; + + let first_signer = Ristretto::read_G(reader)?; + let signature = SchnorrSignature::::read(reader)?; + + Ok(Transaction::SignCompleted { plan, tx_hash, first_signer, signature }) + } + + _ => Err(io::Error::other("invalid transaction type")), + } + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + match self { + Transaction::RemoveParticipant(i) => { + writer.write_all(&[0])?; + writer.write_all(&u16::from(*i).to_le_bytes()) + } + + Transaction::DkgCommitments { attempt, commitments, signed } => { + writer.write_all(&[1])?; + writer.write_all(&attempt.to_le_bytes())?; + if commitments.is_empty() { + Err(io::Error::other("zero commitments in DkgCommitments"))? + } + writer.write_all(&[u8::try_from(commitments.len()).unwrap()])?; + for commitments_i in commitments { + if commitments_i.len() != commitments[0].len() { + Err(io::Error::other("commitments of differing sizes in DkgCommitments"))? + } + } + writer.write_all(&u16::try_from(commitments[0].len()).unwrap().to_le_bytes())?; + for commitments in commitments { + writer.write_all(commitments)?; + } + signed.write_without_nonce(writer) + } + + Transaction::DkgShares { attempt, shares, confirmation_nonces, signed } => { + writer.write_all(&[2])?; + writer.write_all(&attempt.to_le_bytes())?; + + // `shares` is a Vec which is supposed to map to a HashMap>. Since we + // bound participants to 150, this conversion is safe if a valid in-memory transaction. + writer.write_all(&[u8::try_from(shares.len()).unwrap()])?; + // This assumes at least one share is being sent to another party + writer.write_all(&[u8::try_from(shares[0].len()).unwrap()])?; + let share_len = shares[0][0].len(); + // For BLS12-381 G2, this would be: + // - A 32-byte share + // - A 96-byte ephemeral key + // - A 128-byte signature + // Hence why this has to be u16 + writer.write_all(&u16::try_from(share_len).unwrap().to_le_bytes())?; + + for these_shares in shares { + assert_eq!(these_shares.len(), shares[0].len(), "amount of sent shares was variable"); + for share in these_shares { + assert_eq!(share.len(), share_len, "sent shares were of variable length"); + writer.write_all(share)?; + } + } + + writer.write_all(confirmation_nonces)?; + signed.write_without_nonce(writer) + } + + Transaction::InvalidDkgShare { attempt, accuser, faulty, blame, signed } => { + writer.write_all(&[3])?; + writer.write_all(&attempt.to_le_bytes())?; + writer.write_all(&u16::from(*accuser).to_le_bytes())?; + writer.write_all(&u16::from(*faulty).to_le_bytes())?; + + // Flattens Some(vec![]) to None on the expectation no actual blame will be 0-length + assert!(blame.as_ref().map(|blame| blame.len()).unwrap_or(1) != 0); + let blame_len = + u16::try_from(blame.as_ref().unwrap_or(&vec![]).len()).expect("blame exceeded 64 KB"); + writer.write_all(&blame_len.to_le_bytes())?; + writer.write_all(blame.as_ref().unwrap_or(&vec![]))?; + + signed.write_without_nonce(writer) + } + + Transaction::DkgConfirmed { attempt, confirmation_share, signed } => { + writer.write_all(&[4])?; + writer.write_all(&attempt.to_le_bytes())?; + writer.write_all(confirmation_share)?; + signed.write_without_nonce(writer) + } + + Transaction::DkgRemoval(data) => { + writer.write_all(&[5])?; + data.write(writer) + } + + Transaction::CosignSubstrateBlock(block) => { + writer.write_all(&[6])?; + writer.write_all(block) + } + + Transaction::Batch { block, batch } => { + writer.write_all(&[7])?; + writer.write_all(block)?; + writer.write_all(batch) + } + + Transaction::SubstrateBlock(block) => { + writer.write_all(&[8])?; + writer.write_all(&block.to_le_bytes()) + } + + Transaction::SubstrateSign(data) => { + writer.write_all(&[9])?; + data.write(writer) + } + Transaction::Sign(data) => { + writer.write_all(&[10])?; + data.write(writer) + } + Transaction::SignCompleted { plan, tx_hash, first_signer, signature } => { + writer.write_all(&[11])?; + writer.write_all(plan)?; + writer + .write_all(&[u8::try_from(tx_hash.len()).expect("tx hash length exceed 255 bytes")])?; + writer.write_all(tx_hash)?; + writer.write_all(&first_signer.to_bytes())?; + signature.write(writer) + } + } + } +} + +impl TransactionTrait for Transaction { + fn kind(&self) -> TransactionKind<'_> { + match self { + Transaction::RemoveParticipant(_) => TransactionKind::Provided("remove"), + + Transaction::DkgCommitments { attempt, commitments: _, signed } => { + TransactionKind::Signed((b"dkg", attempt).encode(), signed) + } + Transaction::DkgShares { attempt, signed, .. } => { + TransactionKind::Signed((b"dkg", attempt).encode(), signed) + } + Transaction::InvalidDkgShare { attempt, signed, .. } => { + TransactionKind::Signed((b"dkg", attempt).encode(), signed) + } + Transaction::DkgConfirmed { attempt, signed, .. } => { + TransactionKind::Signed((b"dkg", attempt).encode(), signed) + } + + Transaction::DkgRemoval(data) => { + TransactionKind::Signed((b"dkg_removal", data.plan, data.attempt).encode(), &data.signed) + } + + Transaction::CosignSubstrateBlock(_) => TransactionKind::Provided("cosign"), + + Transaction::Batch { .. } => TransactionKind::Provided("batch"), + Transaction::SubstrateBlock(_) => TransactionKind::Provided("serai"), + + Transaction::SubstrateSign(data) => { + TransactionKind::Signed((b"substrate", data.plan, data.attempt).encode(), &data.signed) + } + Transaction::Sign(data) => { + TransactionKind::Signed((b"sign", data.plan, data.attempt).encode(), &data.signed) + } + Transaction::SignCompleted { .. } => TransactionKind::Unsigned, + } + } + + fn hash(&self) -> [u8; 32] { + let mut tx = self.serialize(); + if let TransactionKind::Signed(_, signed) = self.kind() { + // Make sure the part we're cutting off is the signature + assert_eq!(tx.drain((tx.len() - 64) ..).collect::>(), signed.signature.serialize()); + } + Blake2s256::digest([b"Coordinator Tributary Transaction".as_slice(), &tx].concat()).into() + } + + fn verify(&self) -> Result<(), TransactionError> { + // TODO: Check DkgRemoval and SubstrateSign's lengths here + + if let Transaction::SignCompleted { first_signer, signature, .. } = self { + if !signature.verify(*first_signer, self.sign_completed_challenge()) { + Err(TransactionError::InvalidContent)?; + } + } + + Ok(()) + } +} + +impl Transaction { + // Used to initially construct transactions so we can then get sig hashes and perform signing + pub fn empty_signed() -> Signed { + Signed { + signer: Ristretto::generator(), + nonce: 0, + signature: SchnorrSignature:: { + R: Ristretto::generator(), + s: ::F::ZERO, + }, + } + } + + // Sign a transaction + pub fn sign( + &mut self, + rng: &mut R, + genesis: [u8; 32], + key: &Zeroizing<::F>, + ) { + fn signed(tx: &mut Transaction) -> (u32, &mut Signed) { + let nonce = match tx { + Transaction::RemoveParticipant(_) => panic!("signing RemoveParticipant"), + + Transaction::DkgCommitments { .. } => 0, + Transaction::DkgShares { .. } => 1, + Transaction::InvalidDkgShare { .. } => 2, + Transaction::DkgConfirmed { .. } => 2, + + Transaction::DkgRemoval(data) => data.label.nonce(), + + Transaction::CosignSubstrateBlock(_) => panic!("signing CosignSubstrateBlock"), + + Transaction::Batch { .. } => panic!("signing Batch"), + Transaction::SubstrateBlock(_) => panic!("signing SubstrateBlock"), + + Transaction::SubstrateSign(data) => data.label.nonce(), + Transaction::Sign(data) => data.label.nonce(), + Transaction::SignCompleted { .. } => panic!("signing SignCompleted"), + }; + + ( + nonce, + match tx { + Transaction::RemoveParticipant(_) => panic!("signing RemoveParticipant"), + + Transaction::DkgCommitments { ref mut signed, .. } => signed, + Transaction::DkgShares { ref mut signed, .. } => signed, + Transaction::InvalidDkgShare { ref mut signed, .. } => signed, + Transaction::DkgConfirmed { ref mut signed, .. } => signed, + + Transaction::DkgRemoval(ref mut data) => &mut data.signed, + + Transaction::CosignSubstrateBlock(_) => panic!("signing CosignSubstrateBlock"), + + Transaction::Batch { .. } => panic!("signing Batch"), + Transaction::SubstrateBlock(_) => panic!("signing SubstrateBlock"), + + Transaction::SubstrateSign(ref mut data) => &mut data.signed, + Transaction::Sign(ref mut data) => &mut data.signed, + Transaction::SignCompleted { .. } => panic!("signing SignCompleted"), + }, + ) + } + + let (nonce, signed_ref) = signed(self); + signed_ref.signer = Ristretto::generator() * key.deref(); + signed_ref.nonce = nonce; + + let sig_nonce = Zeroizing::new(::F::random(rng)); + signed(self).1.signature.R = ::generator() * sig_nonce.deref(); + let sig_hash = self.sig_hash(genesis); + signed(self).1.signature = SchnorrSignature::::sign(key, sig_nonce, sig_hash); + } + + pub fn sign_completed_challenge(&self) -> ::F { + if let Transaction::SignCompleted { plan, tx_hash, first_signer, signature } = self { + let mut transcript = + RecommendedTranscript::new(b"Coordinator Tributary Transaction SignCompleted"); + transcript.append_message(b"plan", plan); + transcript.append_message(b"tx_hash", tx_hash); + transcript.append_message(b"signer", first_signer.to_bytes()); + transcript.append_message(b"nonce", signature.R.to_bytes()); + Ristretto::hash_to_F(b"SignCompleted signature", &transcript.challenge(b"challenge")) + } else { + panic!("sign_completed_challenge called on transaction which wasn't SignCompleted") + } + } +}