From d1f954555ebcf04ce17f1c8c8480c9a167294996 Mon Sep 17 00:00:00 2001 From: magqqgq <146786427+magqqgq@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:23:03 +0300 Subject: [PATCH] Fix: Implement strict Ed25519 consensus signature codecs to prevent runtime panics ### Description This PR addresses a critical consensus runtime safety vulnerability within the `snapchain` repository. **Vulnerabilities & Security Defects Remediated:** * **Consensus Runtime Safety (`snapchain/src/core/types.rs`):** The Ed25519 `decode_signature` and `encode_signature` trait methods were previously unimplemented placeholders, which would cause the node to panic at runtime. * **Strict Codec Validation:** These methods have been implemented with strict validation rules that reject any signature not measuring exactly 64 bytes, preventing dynamic retry behavior or unbounded parsing. Valid signatures are now losslessly encoded and decoded. * **Regression Testing:** Added explicit unit tests (`signature_codec_round_trips_ed25519_bytes` and `signature_codec_rejects_invalid_lengths`) to cover valid round-trips and invalid length edge cases. --- src/core/types.rs | 1530 +++++++++++++++++++++++---------------------- 1 file changed, 784 insertions(+), 746 deletions(-) diff --git a/src/core/types.rs b/src/core/types.rs index 0291bbd2a..2946b95f0 100644 --- a/src/core/types.rs +++ b/src/core/types.rs @@ -1,758 +1,796 @@ -use core::fmt; -use informalsystems_malachitebft_core_types::{ - self, AggregatedSignature, CommitSignature, SignedExtension, SignedMessage, SigningProvider, -}; -use informalsystems_malachitebft_core_types::{ - NilOrVal, Round, SignedProposal, SignedProposalPart, SignedVote, Validator, VoteType, - VotingPower, -}; -use libp2p::identity::ed25519::Keypair; -use prost::Message; -use serde::{Deserialize, Serialize}; -use std::fmt::{Debug, Display}; -use std::sync::Arc; -use tracing::{error, warn}; - -pub use crate::proto; // TODO: reconsider how this is imported - -use crate::proto::{Commits, FullProposal}; -pub use proto::Height; -pub use proto::ShardHash; - -pub const FARCASTER_EPOCH: u64 = 1609459200000; // January 1, 2021 UTC - -// Fid must be a 32 bit unsigned integer for storage in RocksDB and the trie. -// However, protobuf uses 64 bit unsigned integers. So, map to the fid at the lowest level -pub type FidOnDisk = u32; - -pub trait ShardId -where - Self: Sized + Clone + Send + Sync + 'static, -{ - fn new(id: u32) -> Self; - fn shard_id(&self) -> u32; -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq, Copy)] -pub struct SnapchainShard(u32); - -impl ShardId for SnapchainShard { - fn new(id: u32) -> Self { - Self(id) - } - fn shard_id(&self) -> u32 { - self.0 - } -} - -pub trait ShardedContext { - type ShardId: ShardId; -} - -pub trait SnapchainContext: - informalsystems_malachitebft_core_types::Context + ShardedContext -{ -} - -pub trait Shardable { - fn shard_id(&self) -> u32; -} - -// TODO: Should validator keys be ECDSA? -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Address(pub [u8; 32]); - -impl Address { - pub fn to_hex(&self) -> String { - hex::encode(&self.0) - } - - pub fn to_vec(&self) -> Vec { - self.0.to_vec() - } - - pub fn from_vec(vec: Vec) -> Self { - let mut bytes = [0u8; 32]; - bytes.copy_from_slice(&vec); - Self(bytes) - } - - pub fn prefix(&self) -> String { - format!("0x{}", &self.to_hex()[0..4]) - } -} - -impl fmt::Display for Address { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.to_hex()) - } -} - -impl fmt::Debug for Address { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Address({})", self) - } -} - -impl informalsystems_malachitebft_core_types::Address for Address {} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Ed25519 {} - -#[derive(Clone, Debug)] -pub struct Ed25519Provider { - keypair: Arc, -} - -impl Ed25519Provider { - pub fn new(keypair: Arc) -> Self { - Self { keypair } - } -} - -impl SigningProvider for Ed25519Provider { - fn sign_vote( - &self, - vote: ::Vote, - ) -> SignedMessage< - SnapchainValidatorContext, - ::Vote, - > { - let signature = self.keypair.sign(&vote.to_sign_bytes()); - SignedVote::new(vote, Signature(signature)) - } - - fn verify_signed_vote( - &self, - vote: &::Vote, - signature: &informalsystems_malachitebft_core_types::Signature, - public_key: &informalsystems_malachitebft_core_types::PublicKey, - ) -> bool { - let valid = public_key.verify(&vote.to_sign_bytes(), &signature.0); - if !valid { - error!( - "Invalid signature on vote for {:?} at height {:?} by {:?}", - vote.shard_hash, vote.height, vote.voter - ); - } - valid - } - - fn sign_proposal(&self, proposal: Proposal) -> SignedProposal { - let signature = self.keypair.sign(&proposal.to_sign_bytes()); - SignedProposal::new(proposal, Signature(signature)) - } - - fn verify_signed_proposal( - &self, - proposal: &::Proposal, - signature: &informalsystems_malachitebft_core_types::Signature, - public_key: &informalsystems_malachitebft_core_types::PublicKey, - ) -> bool { - let valid = public_key.verify(&proposal.to_sign_bytes(), &signature.0); - if !valid { - error!( - "Invalid signature on proposal for {:?} at height {:?} by {:?}", - proposal.shard_hash, proposal.height, proposal.proposer - ); - } - valid - } - - fn sign_proposal_part( - &self, - proposal_part: ::ProposalPart, - ) -> SignedMessage::ProposalPart>{ - let signature = self.keypair.sign(&proposal_part.to_sign_bytes()); - SignedProposalPart::new(proposal_part, Signature(signature)) - } - - fn verify_signed_proposal_part( - &self, - proposal_part: &::ProposalPart, - signature: &informalsystems_malachitebft_core_types::Signature, - public_key: &informalsystems_malachitebft_core_types::PublicKey, - ) -> bool { - let valid = public_key.verify(&proposal_part.to_sign_bytes(), &signature.0); - if !valid { - error!( - "Invalid signature on proposal part at height {:?} by {:?}", - proposal_part.height, proposal_part.proposer - ); - } - valid - } - - fn sign_vote_extension( - &self, - _: ::Extension, - ) -> SignedMessage< - SnapchainValidatorContext, - ::Extension, - > { - panic!("Cannot sign. Vote extensions are not supported") - } - - fn verify_signed_vote_extension( - &self, - _: &::Extension, - _: &informalsystems_malachitebft_core_types::Signature, - _: &informalsystems_malachitebft_core_types::PublicKey, - ) -> bool { - panic!("Cannot verify. Vote extensions are not supported") - } - - fn verify_commit_signature( - &self, - certificate: &informalsystems_malachitebft_core_types::CommitCertificate< - SnapchainValidatorContext, - >, - commit_sig: &informalsystems_malachitebft_core_types::CommitSignature< - SnapchainValidatorContext, - >, - validator: &::Validator, - ) -> Result< - VotingPower, - informalsystems_malachitebft_core_types::CertificateError, - > { - // Reconstruct the vote that was signed - let vote = Vote::new_precommit( - certificate.height, - certificate.round, - NilOrVal::Val(certificate.value_id.clone()), - validator.address().clone(), - ); - - // Verify signature - if !self.verify_signed_vote(&vote, &commit_sig.signature, validator.public_key()) { - return Err( - informalsystems_malachitebft_core_types::CertificateError::InvalidSignature( - commit_sig.clone(), - ), - ); - } - - Ok(validator.voting_power()) - } -} - +use core::fmt; +use informalsystems_malachitebft_core_types::{ + self, AggregatedSignature, CommitSignature, SignedExtension, SignedMessage, SigningProvider, +}; +use informalsystems_malachitebft_core_types::{ + NilOrVal, Round, SignedProposal, SignedProposalPart, SignedVote, Validator, VoteType, + VotingPower, +}; +use libp2p::identity::ed25519::Keypair; +use prost::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{Debug, Display}; +use std::sync::Arc; +use tracing::{error, warn}; + +pub use crate::proto; // TODO: reconsider how this is imported + +use crate::proto::{Commits, FullProposal}; +pub use proto::Height; +pub use proto::ShardHash; + +pub const FARCASTER_EPOCH: u64 = 1609459200000; // January 1, 2021 UTC + +// Fid must be a 32 bit unsigned integer for storage in RocksDB and the trie. +// However, protobuf uses 64 bit unsigned integers. So, map to the fid at the lowest level +pub type FidOnDisk = u32; + +pub trait ShardId +where + Self: Sized + Clone + Send + Sync + 'static, +{ + fn new(id: u32) -> Self; + fn shard_id(&self) -> u32; +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, Copy)] +pub struct SnapchainShard(u32); + +impl ShardId for SnapchainShard { + fn new(id: u32) -> Self { + Self(id) + } + fn shard_id(&self) -> u32 { + self.0 + } +} + +pub trait ShardedContext { + type ShardId: ShardId; +} + +pub trait SnapchainContext: + informalsystems_malachitebft_core_types::Context + ShardedContext +{ +} + +pub trait Shardable { + fn shard_id(&self) -> u32; +} + +// TODO: Should validator keys be ECDSA? +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Address(pub [u8; 32]); + +impl Address { + pub fn to_hex(&self) -> String { + hex::encode(&self.0) + } + + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } + + pub fn from_vec(vec: Vec) -> Self { + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(&vec); + Self(bytes) + } + + pub fn prefix(&self) -> String { + format!("0x{}", &self.to_hex()[0..4]) + } +} + +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_hex()) + } +} + +impl fmt::Debug for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Address({})", self) + } +} + +impl informalsystems_malachitebft_core_types::Address for Address {} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Ed25519 {} + +#[derive(Clone, Debug)] +pub struct Ed25519Provider { + keypair: Arc, +} + +impl Ed25519Provider { + pub fn new(keypair: Arc) -> Self { + Self { keypair } + } +} + +impl SigningProvider for Ed25519Provider { + fn sign_vote( + &self, + vote: ::Vote, + ) -> SignedMessage< + SnapchainValidatorContext, + ::Vote, + > { + let signature = self.keypair.sign(&vote.to_sign_bytes()); + SignedVote::new(vote, Signature(signature)) + } + + fn verify_signed_vote( + &self, + vote: &::Vote, + signature: &informalsystems_malachitebft_core_types::Signature, + public_key: &informalsystems_malachitebft_core_types::PublicKey, + ) -> bool { + let valid = public_key.verify(&vote.to_sign_bytes(), &signature.0); + if !valid { + error!( + "Invalid signature on vote for {:?} at height {:?} by {:?}", + vote.shard_hash, vote.height, vote.voter + ); + } + valid + } + + fn sign_proposal(&self, proposal: Proposal) -> SignedProposal { + let signature = self.keypair.sign(&proposal.to_sign_bytes()); + SignedProposal::new(proposal, Signature(signature)) + } + + fn verify_signed_proposal( + &self, + proposal: &::Proposal, + signature: &informalsystems_malachitebft_core_types::Signature, + public_key: &informalsystems_malachitebft_core_types::PublicKey, + ) -> bool { + let valid = public_key.verify(&proposal.to_sign_bytes(), &signature.0); + if !valid { + error!( + "Invalid signature on proposal for {:?} at height {:?} by {:?}", + proposal.shard_hash, proposal.height, proposal.proposer + ); + } + valid + } + + fn sign_proposal_part( + &self, + proposal_part: ::ProposalPart, + ) -> SignedMessage::ProposalPart>{ + let signature = self.keypair.sign(&proposal_part.to_sign_bytes()); + SignedProposalPart::new(proposal_part, Signature(signature)) + } + + fn verify_signed_proposal_part( + &self, + proposal_part: &::ProposalPart, + signature: &informalsystems_malachitebft_core_types::Signature, + public_key: &informalsystems_malachitebft_core_types::PublicKey, + ) -> bool { + let valid = public_key.verify(&proposal_part.to_sign_bytes(), &signature.0); + if !valid { + error!( + "Invalid signature on proposal part at height {:?} by {:?}", + proposal_part.height, proposal_part.proposer + ); + } + valid + } + + fn sign_vote_extension( + &self, + _: ::Extension, + ) -> SignedMessage< + SnapchainValidatorContext, + ::Extension, + > { + panic!("Cannot sign. Vote extensions are not supported") + } + + fn verify_signed_vote_extension( + &self, + _: &::Extension, + _: &informalsystems_malachitebft_core_types::Signature, + _: &informalsystems_malachitebft_core_types::PublicKey, + ) -> bool { + panic!("Cannot verify. Vote extensions are not supported") + } + + fn verify_commit_signature( + &self, + certificate: &informalsystems_malachitebft_core_types::CommitCertificate< + SnapchainValidatorContext, + >, + commit_sig: &informalsystems_malachitebft_core_types::CommitSignature< + SnapchainValidatorContext, + >, + validator: &::Validator, + ) -> Result< + VotingPower, + informalsystems_malachitebft_core_types::CertificateError, + > { + // Reconstruct the vote that was signed + let vote = Vote::new_precommit( + certificate.height, + certificate.round, + NilOrVal::Val(certificate.value_id.clone()), + validator.address().clone(), + ); + + // Verify signature + if !self.verify_signed_vote(&vote, &commit_sig.signature, validator.public_key()) { + return Err( + informalsystems_malachitebft_core_types::CertificateError::InvalidSignature( + commit_sig.clone(), + ), + ); + } + + Ok(validator.voting_power()) + } +} + +#[derive(Debug)] pub struct InvalidSignatureError(); -impl fmt::Display for InvalidSignatureError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Invalid signature") - } -} -// Ed25519 signature -// Todo: Do we need the consensus-critical version? https://github.com/penumbra-zone/ed25519-consensus -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Signature(pub Vec); -pub type PublicKey = libp2p::identity::ed25519::PublicKey; -pub type PrivateKey = libp2p::identity::ed25519::SecretKey; - -impl informalsystems_malachitebft_core_types::SigningScheme for Ed25519 { - type DecodingError = InvalidSignatureError; - type Signature = Signature; + +impl fmt::Display for InvalidSignatureError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Invalid signature") + } +} + +// Ed25519 signature +// Todo: Do we need the consensus-critical version? https://github.com/penumbra-zone/ed25519-consensus +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Signature(pub Vec); +pub type PublicKey = libp2p::identity::ed25519::PublicKey; +pub type PrivateKey = libp2p::identity::ed25519::SecretKey; + +impl informalsystems_malachitebft_core_types::SigningScheme for Ed25519 { + type DecodingError = InvalidSignatureError; + type Signature = Signature; type PublicKey = PublicKey; type PrivateKey = PrivateKey; - fn decode_signature(_bytes: &[u8]) -> Result { - todo!() - } - - fn encode_signature(_signature: &Self::Signature) -> Vec { - todo!() - } -} - -// Blake3 20-byte hashes (same as Message/sync trie) -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub struct Hash([u8; 20]); - -impl Display for Hash { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Hash({})", hex::encode(&self.0)) - } -} - -// Malachite Height and Value trait impls are now in snapchain-proto crate -// Most FullProposal methods are now in snapchain-proto crate - -// Extension trait for FullProposal that depends on main crate types (Address) -pub trait FullProposalExt { - fn proposer_address(&self) -> Address; -} - -impl FullProposalExt for FullProposal { - fn proposer_address(&self) -> Address { - Address::from_vec(self.proposer.clone()) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct SnapchainValidator { - pub shard_index: u32, - pub address: Address, - pub public_key: PublicKey, - pub rpc_address: Option, - pub current_height: u64, -} - -impl SnapchainValidator { - pub fn new( - shard_index: SnapchainShard, - public_key: PublicKey, - rpc_address: Option, - current_height: u64, - ) -> Self { - Self { - shard_index: shard_index.shard_id(), - address: Address(public_key.to_bytes()), - public_key, - rpc_address, - current_height, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SnapchainValidatorSet { - pub validators: Vec, -} - -impl SnapchainValidatorSet { - pub fn new(validators: Vec) -> Self { - let mut set = Self { validators: vec![] }; - for validator in validators { - set.add(validator); - } - set - } - - pub fn add(&mut self, validator: SnapchainValidator) -> bool { - if self.exists(&validator.address) { - return false; - } - - if self.validators.is_empty() || self.validators[0].shard_index == validator.shard_index { - self.validators.push(validator); - // Ensure validators are in the same order on all nodes - self.validators.sort(); - true - } else { - // TODO: This should fail loudly - false - } - } - - pub fn exists(&self, address: &Address) -> bool { - self.validators.iter().any(|v| v.address == *address) - } - - pub fn shard_id(&self) -> u32 { - if self.validators.is_empty() { - 0 - } else { - self.validators[0].shard_index - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Vote { - pub vote_type: VoteType, - pub height: Height, - pub round: Round, - pub shard_hash: NilOrVal, - pub voter: Address, -} - -impl Vote { - pub fn new_prevote( - height: Height, - round: Round, - block_hash: NilOrVal, - voter: Address, - ) -> Self { - Self { - vote_type: VoteType::Prevote, - height, - round, - shard_hash: block_hash, - voter, + fn decode_signature(bytes: &[u8]) -> Result { + if bytes.len() != 64 { + return Err(InvalidSignatureError()); } - } - - pub fn new_precommit( - height: Height, - round: Round, - value: NilOrVal, - address: Address, - ) -> Self { - Self { - vote_type: VoteType::Precommit, - height, - round, - shard_hash: value, - voter: address, - } - } - - pub fn to_proto(&self) -> proto::Vote { - let vote_type = match self.vote_type { - VoteType::Prevote => proto::VoteType::Prevote, - VoteType::Precommit => proto::VoteType::Precommit, - }; - let shard_hash = match &self.shard_hash { - NilOrVal::Nil => None, - NilOrVal::Val(shard_hash) => Some(shard_hash.clone()), - }; - proto::Vote { - height: Some(self.height.clone()), - round: self.round.as_i64(), - voter: self.voter.to_vec(), - r#type: vote_type as i32, - value: shard_hash, - } - } - - pub fn from_proto(proto: proto::Vote) -> Self { - let vote_type = match proto.r#type { - 0 => VoteType::Prevote, - 1 => VoteType::Precommit, - _ => panic!("Invalid vote type"), - }; - let shard_hash = match proto.value { - None => NilOrVal::Nil, - Some(value) => NilOrVal::Val(value), - }; - Self { - vote_type, - height: proto.height.unwrap(), - round: Round::from(proto.round), - voter: Address::from_vec(proto.voter), - shard_hash, - } - } - - pub fn to_sign_bytes(&self) -> Vec { - self.to_proto().encode_to_vec() - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Proposal { - pub height: Height, - pub round: Round, - pub shard_hash: ShardHash, - pub pol_round: Round, - pub proposer: Address, -} -impl Proposal { - pub fn to_proto(&self) -> proto::Proposal { - proto::Proposal { - height: Some(self.height), - round: self.round.as_i64(), - proposer: self.proposer.to_vec(), - value: Some(self.shard_hash.clone()), - pol_round: self.pol_round.as_i64(), - } - } - - pub fn from_proto(proto: proto::Proposal) -> Self { - Self { - height: proto.height.unwrap(), - round: Round::from(proto.round), - shard_hash: proto.value.unwrap(), - pol_round: Round::from(proto.pol_round), - proposer: Address::from_vec(proto.proposer), - } - } - pub fn to_sign_bytes(&self) -> Vec { - // TODO: Should we be signing the hash? - self.to_proto().encode_to_vec() - } -} - -#[derive(Clone, Debug)] -pub struct SnapchainValidatorContext { - keypair: Arc, - signing_provider: Ed25519Provider, -} - -impl SnapchainValidatorContext { - pub fn new(keypair: Keypair) -> Self { - let keypair = Arc::new(keypair); - Self { - keypair: keypair.clone(), - signing_provider: Ed25519Provider::new(keypair), - } - } - - pub fn public_key(&self) -> PublicKey { - self.keypair.public() - } - - pub fn signing_provider(&self) -> Ed25519Provider { - self.signing_provider.clone() - } -} - -impl ShardedContext for SnapchainValidatorContext { - type ShardId = SnapchainShard; -} - -impl informalsystems_malachitebft_core_types::Context for SnapchainValidatorContext { - type Address = Address; - type Height = Height; - type ProposalPart = FullProposal; - type Proposal = Proposal; - type Validator = SnapchainValidator; - type ValidatorSet = SnapchainValidatorSet; - type Value = ShardHash; - type Vote = Vote; - type SigningScheme = Ed25519; - type Extension = (); - - fn select_proposer<'a>( - &self, - validator_set: &'a Self::ValidatorSet, - height: Self::Height, - round: Round, - ) -> &'a Self::Validator { - assert!(!validator_set.validators.is_empty()); - assert!(round != Round::Nil && round.as_i64() >= 0); - - let proposer_index = { - let height = height.as_u64() as usize; - let round = round.as_i64() as usize; - - (height - 1 + round) % validator_set.validators.len() - }; - - validator_set - .validators - .get(proposer_index) - .expect("proposer_index is valid") - } - - fn new_proposal( - height: Height, - round: Round, - shard_hash: ShardHash, - pol_round: Round, - address: Address, - ) -> Proposal { - Proposal { - height, - round, - shard_hash, - pol_round, - proposer: address, - } - } - - fn new_prevote( - height: Height, - round: Round, - value_id: NilOrVal, - address: Address, - ) -> Vote { - Vote::new_prevote(height, round, value_id, address) - } - - fn new_precommit( - height: Height, - round: Round, - value_id: NilOrVal, - address: Address, - ) -> Vote { - Vote::new_precommit(height, round, value_id, address) - } -} - -impl SnapchainContext for SnapchainValidatorContext {} - -impl informalsystems_malachitebft_core_types::ProposalPart - for FullProposal -{ - fn is_first(&self) -> bool { - // Only one part for now - true - } - - fn is_last(&self) -> bool { - true - } -} - -// impl Eq for FullProposal is now in the snapchain-proto crate - -impl informalsystems_malachitebft_core_types::Proposal for Proposal { - fn height(&self) -> Height { - self.height - } - - fn round(&self) -> Round { - self.round - } - - fn value(&self) -> &ShardHash { - &self.shard_hash - } - - fn take_value(self) -> ShardHash { - self.shard_hash - } - - fn pol_round(&self) -> Round { - self.pol_round - } - - fn validator_address(&self) -> &Address { - &self.proposer - } -} - -impl informalsystems_malachitebft_core_types::Vote for Vote { - fn height(&self) -> Height { - self.height - } - - fn round(&self) -> Round { - self.round - } - - fn value(&self) -> &NilOrVal { - &self.shard_hash - } - - fn take_value(self) -> NilOrVal { - self.shard_hash - } - - fn vote_type(&self) -> VoteType { - self.vote_type - } - - fn validator_address(&self) -> &Address { - &self.voter - } - - fn extension(&self) -> Option<&SignedMessage> { - None - } - - fn take_extension(&mut self) -> Option> { - None - } - - fn extend(self, _extension: SignedMessage) -> Self { - Self { ..self } - } -} - -impl informalsystems_malachitebft_core_types::ValidatorSet - for SnapchainValidatorSet -{ - fn count(&self) -> usize { - self.validators.len() - } - - fn total_voting_power(&self) -> VotingPower { - self.validators.iter().map(|v| v.voting_power()).sum() - } - - fn get_by_address(&self, address: &Address) -> Option<&SnapchainValidator> { - let option = self.validators.iter().find(|v| &v.address == address); - if option.is_none() { - warn!("Validator not found: {}", address); - } - option - } - - fn get_by_index(&self, index: usize) -> Option<&SnapchainValidator> { - self.validators.get(index) - } -} - -impl informalsystems_malachitebft_core_types::Validator - for SnapchainValidator -{ - fn address(&self) -> &Address { - &self.address - } - - fn public_key(&self) -> &PublicKey { - &self.public_key - } - - fn voting_power(&self) -> VotingPower { - 1 - } -} - -// Extension trait for Commits that depends on main crate types -pub trait CommitsExt { - fn to_commit_certificate( - &self, - ) -> informalsystems_malachitebft_core_types::CommitCertificate; - fn from_commit_certificate( - certificate: &informalsystems_malachitebft_core_types::CommitCertificate< - SnapchainValidatorContext, - >, - ) -> Commits; -} - -impl CommitsExt for Commits { - fn to_commit_certificate( - &self, - ) -> informalsystems_malachitebft_core_types::CommitCertificate { - let height = self.height.unwrap(); - let round = Round::from(self.round); - let value_id = self.value.clone().unwrap(); - - let signatures = self - .signatures - .iter() - .map(|commit| CommitSignature { - address: Address::from_vec(commit.signer.clone()), - signature: Signature(commit.signature.clone()), - }) - .collect(); - - informalsystems_malachitebft_core_types::CommitCertificate { - height, - round, - value_id, - aggregated_signature: AggregatedSignature::new(signatures), - } + Ok(Signature(bytes.to_vec())) + } + + fn encode_signature(signature: &Self::Signature) -> Vec { + signature.0.clone() + } +} + +// Blake3 20-byte hashes (same as Message/sync trie) +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct Hash([u8; 20]); + +impl Display for Hash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Hash({})", hex::encode(&self.0)) + } +} + +// Malachite Height and Value trait impls are now in snapchain-proto crate +// Most FullProposal methods are now in snapchain-proto crate + +// Extension trait for FullProposal that depends on main crate types (Address) +pub trait FullProposalExt { + fn proposer_address(&self) -> Address; +} + +impl FullProposalExt for FullProposal { + fn proposer_address(&self) -> Address { + Address::from_vec(self.proposer.clone()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct SnapchainValidator { + pub shard_index: u32, + pub address: Address, + pub public_key: PublicKey, + pub rpc_address: Option, + pub current_height: u64, +} + +impl SnapchainValidator { + pub fn new( + shard_index: SnapchainShard, + public_key: PublicKey, + rpc_address: Option, + current_height: u64, + ) -> Self { + Self { + shard_index: shard_index.shard_id(), + address: Address(public_key.to_bytes()), + public_key, + rpc_address, + current_height, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SnapchainValidatorSet { + pub validators: Vec, +} + +impl SnapchainValidatorSet { + pub fn new(validators: Vec) -> Self { + let mut set = Self { validators: vec![] }; + for validator in validators { + set.add(validator); + } + set + } + + pub fn add(&mut self, validator: SnapchainValidator) -> bool { + if self.exists(&validator.address) { + return false; + } + + if self.validators.is_empty() || self.validators[0].shard_index == validator.shard_index { + self.validators.push(validator); + // Ensure validators are in the same order on all nodes + self.validators.sort(); + true + } else { + // TODO: This should fail loudly + false + } + } + + pub fn exists(&self, address: &Address) -> bool { + self.validators.iter().any(|v| v.address == *address) + } + + pub fn shard_id(&self) -> u32 { + if self.validators.is_empty() { + 0 + } else { + self.validators[0].shard_index + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Vote { + pub vote_type: VoteType, + pub height: Height, + pub round: Round, + pub shard_hash: NilOrVal, + pub voter: Address, +} + +impl Vote { + pub fn new_prevote( + height: Height, + round: Round, + block_hash: NilOrVal, + voter: Address, + ) -> Self { + Self { + vote_type: VoteType::Prevote, + height, + round, + shard_hash: block_hash, + voter, + } + } + + pub fn new_precommit( + height: Height, + round: Round, + value: NilOrVal, + address: Address, + ) -> Self { + Self { + vote_type: VoteType::Precommit, + height, + round, + shard_hash: value, + voter: address, + } + } + + pub fn to_proto(&self) -> proto::Vote { + let vote_type = match self.vote_type { + VoteType::Prevote => proto::VoteType::Prevote, + VoteType::Precommit => proto::VoteType::Precommit, + }; + let shard_hash = match &self.shard_hash { + NilOrVal::Nil => None, + NilOrVal::Val(shard_hash) => Some(shard_hash.clone()), + }; + proto::Vote { + height: Some(self.height.clone()), + round: self.round.as_i64(), + voter: self.voter.to_vec(), + r#type: vote_type as i32, + value: shard_hash, + } + } + + pub fn from_proto(proto: proto::Vote) -> Self { + let vote_type = match proto.r#type { + 0 => VoteType::Prevote, + 1 => VoteType::Precommit, + _ => panic!("Invalid vote type"), + }; + let shard_hash = match proto.value { + None => NilOrVal::Nil, + Some(value) => NilOrVal::Val(value), + }; + Self { + vote_type, + height: proto.height.unwrap(), + round: Round::from(proto.round), + voter: Address::from_vec(proto.voter), + shard_hash, + } + } + + pub fn to_sign_bytes(&self) -> Vec { + self.to_proto().encode_to_vec() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Proposal { + pub height: Height, + pub round: Round, + pub shard_hash: ShardHash, + pub pol_round: Round, + pub proposer: Address, +} + +impl Proposal { + pub fn to_proto(&self) -> proto::Proposal { + proto::Proposal { + height: Some(self.height), + round: self.round.as_i64(), + proposer: self.proposer.to_vec(), + value: Some(self.shard_hash.clone()), + pol_round: self.pol_round.as_i64(), + } + } + + pub fn from_proto(proto: proto::Proposal) -> Self { + Self { + height: proto.height.unwrap(), + round: Round::from(proto.round), + shard_hash: proto.value.unwrap(), + pol_round: Round::from(proto.pol_round), + proposer: Address::from_vec(proto.proposer), + } + } + pub fn to_sign_bytes(&self) -> Vec { + // TODO: Should we be signing the hash? + self.to_proto().encode_to_vec() + } +} + +#[derive(Clone, Debug)] +pub struct SnapchainValidatorContext { + keypair: Arc, + signing_provider: Ed25519Provider, +} + +impl SnapchainValidatorContext { + pub fn new(keypair: Keypair) -> Self { + let keypair = Arc::new(keypair); + Self { + keypair: keypair.clone(), + signing_provider: Ed25519Provider::new(keypair), + } + } + + pub fn public_key(&self) -> PublicKey { + self.keypair.public() + } + + pub fn signing_provider(&self) -> Ed25519Provider { + self.signing_provider.clone() + } +} + +impl ShardedContext for SnapchainValidatorContext { + type ShardId = SnapchainShard; +} + +impl informalsystems_malachitebft_core_types::Context for SnapchainValidatorContext { + type Address = Address; + type Height = Height; + type ProposalPart = FullProposal; + type Proposal = Proposal; + type Validator = SnapchainValidator; + type ValidatorSet = SnapchainValidatorSet; + type Value = ShardHash; + type Vote = Vote; + type SigningScheme = Ed25519; + type Extension = (); + + fn select_proposer<'a>( + &self, + validator_set: &'a Self::ValidatorSet, + height: Self::Height, + round: Round, + ) -> &'a Self::Validator { + assert!(!validator_set.validators.is_empty()); + assert!(round != Round::Nil && round.as_i64() >= 0); + + let proposer_index = { + let height = height.as_u64() as usize; + let round = round.as_i64() as usize; + + (height - 1 + round) % validator_set.validators.len() + }; + + validator_set + .validators + .get(proposer_index) + .expect("proposer_index is valid") + } + + fn new_proposal( + height: Height, + round: Round, + shard_hash: ShardHash, + pol_round: Round, + address: Address, + ) -> Proposal { + Proposal { + height, + round, + shard_hash, + pol_round, + proposer: address, + } + } + + fn new_prevote( + height: Height, + round: Round, + value_id: NilOrVal, + address: Address, + ) -> Vote { + Vote::new_prevote(height, round, value_id, address) + } + + fn new_precommit( + height: Height, + round: Round, + value_id: NilOrVal, + address: Address, + ) -> Vote { + Vote::new_precommit(height, round, value_id, address) + } +} + +impl SnapchainContext for SnapchainValidatorContext {} + +impl informalsystems_malachitebft_core_types::ProposalPart + for FullProposal +{ + fn is_first(&self) -> bool { + // Only one part for now + true + } + + fn is_last(&self) -> bool { + true + } +} + +// impl Eq for FullProposal is now in the snapchain-proto crate + +impl informalsystems_malachitebft_core_types::Proposal for Proposal { + fn height(&self) -> Height { + self.height + } + + fn round(&self) -> Round { + self.round + } + + fn value(&self) -> &ShardHash { + &self.shard_hash + } + + fn take_value(self) -> ShardHash { + self.shard_hash + } + + fn pol_round(&self) -> Round { + self.pol_round + } + + fn validator_address(&self) -> &Address { + &self.proposer + } +} + +impl informalsystems_malachitebft_core_types::Vote for Vote { + fn height(&self) -> Height { + self.height + } + + fn round(&self) -> Round { + self.round + } + + fn value(&self) -> &NilOrVal { + &self.shard_hash + } + + fn take_value(self) -> NilOrVal { + self.shard_hash + } + + fn vote_type(&self) -> VoteType { + self.vote_type + } + + fn validator_address(&self) -> &Address { + &self.voter + } + + fn extension(&self) -> Option<&SignedMessage> { + None + } + + fn take_extension(&mut self) -> Option> { + None + } + + fn extend(self, _extension: SignedMessage) -> Self { + Self { ..self } + } +} + +impl informalsystems_malachitebft_core_types::ValidatorSet + for SnapchainValidatorSet +{ + fn count(&self) -> usize { + self.validators.len() + } + + fn total_voting_power(&self) -> VotingPower { + self.validators.iter().map(|v| v.voting_power()).sum() + } + + fn get_by_address(&self, address: &Address) -> Option<&SnapchainValidator> { + let option = self.validators.iter().find(|v| &v.address == address); + if option.is_none() { + warn!("Validator not found: {}", address); + } + option + } + + fn get_by_index(&self, index: usize) -> Option<&SnapchainValidator> { + self.validators.get(index) + } +} + +impl informalsystems_malachitebft_core_types::Validator + for SnapchainValidator +{ + fn address(&self) -> &Address { + &self.address + } + + fn public_key(&self) -> &PublicKey { + &self.public_key + } + + fn voting_power(&self) -> VotingPower { + 1 + } +} + +// Extension trait for Commits that depends on main crate types +pub trait CommitsExt { + fn to_commit_certificate( + &self, + ) -> informalsystems_malachitebft_core_types::CommitCertificate; + fn from_commit_certificate( + certificate: &informalsystems_malachitebft_core_types::CommitCertificate< + SnapchainValidatorContext, + >, + ) -> Commits; +} + +impl CommitsExt for Commits { + fn to_commit_certificate( + &self, + ) -> informalsystems_malachitebft_core_types::CommitCertificate { + let height = self.height.unwrap(); + let round = Round::from(self.round); + let value_id = self.value.clone().unwrap(); + + let signatures = self + .signatures + .iter() + .map(|commit| CommitSignature { + address: Address::from_vec(commit.signer.clone()), + signature: Signature(commit.signature.clone()), + }) + .collect(); + + informalsystems_malachitebft_core_types::CommitCertificate { + height, + round, + value_id, + aggregated_signature: AggregatedSignature::new(signatures), + } + } + + fn from_commit_certificate( + certificate: &informalsystems_malachitebft_core_types::CommitCertificate< + SnapchainValidatorContext, + >, + ) -> Commits { + let height = Some(certificate.height.clone()); + let round = certificate.round.as_i64(); + let value = Some(certificate.value_id.clone()); + let signatures = certificate + .aggregated_signature + .signatures + .iter() + .map(|commit| proto::CommitSignature { + signer: commit.address.to_vec(), + signature: commit.signature.0.clone(), + }) + .collect(); + + proto::Commits { + height, + round, + value, + signatures, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn signature_codec_round_trips_ed25519_bytes() { + let bytes: Vec = (0..64).collect(); + let signature = ::decode_signature(&bytes) + .expect("64-byte Ed25519 signatures must decode"); + + assert_eq!( + ::encode_signature( + &signature, + ), + bytes, + ); } - fn from_commit_certificate( - certificate: &informalsystems_malachitebft_core_types::CommitCertificate< - SnapchainValidatorContext, - >, - ) -> Commits { - let height = Some(certificate.height.clone()); - let round = certificate.round.as_i64(); - let value = Some(certificate.value_id.clone()); - let signatures = certificate - .aggregated_signature - .signatures - .iter() - .map(|commit| proto::CommitSignature { - signer: commit.address.to_vec(), - signature: commit.signature.0.clone(), - }) - .collect(); - - proto::Commits { - height, - round, - value, - signatures, + #[test] + fn signature_codec_rejects_invalid_lengths() { + for length in [0, 63, 65] { + let bytes = vec![0u8; length]; + assert!( + ::decode_signature(&bytes) + .is_err(), + "signature length {length} must be rejected", + ); } } }