//! Types used in [crate::ordered_broadcast]. use super::scheme; use crate::types::Epoch; use bytes::{Buf, BufMut, Bytes, BytesMut}; use commonware_codec::{ varint::UInt, Encode, EncodeSize, Error as CodecError, Read, ReadExt, Write, }; use commonware_cryptography::{ certificate::{self, Attestation, Provider, Scheme}, Digest, PublicKey, Signer, }; use commonware_utils::{ordered::Set, union}; use futures::channel::oneshot; use rand::{CryptoRng, Rng}; use std::{ hash::{Hash, Hasher}, sync::Arc, }; /// Error that may be encountered when interacting with `ordered-broadcast`. /// /// These errors are categorized into several groups: /// - Parser errors (missing parent, etc.) /// - Application verification errors /// - P2P errors /// - Broadcast errors (certificate-related issues) /// - Epoch errors (unknown validators or sequencers) /// - Peer errors /// - Signature errors /// - Ignorable message errors (outside epoch/height bounds) /// - Attributable faults (conflicting chunks) #[derive(Debug, thiserror::Error)] pub enum Error { // Parser Errors /// The parent is missing for a non-genesis chunk #[error("Missing parent")] ParentMissing, /// The parent was provided for a genesis chunk (height 0) #[error("Parent on genesis chunk")] ParentOnGenesis, /// Verification failed because no public key was provided #[error("Public key required")] PublicKeyRequired, // Application Verification Errors /// The verification was canceled by the application #[error("Application verify error: {0}")] AppVerifyCanceled(oneshot::Canceled), /// The application tried to verify a chunk but no tip was found #[error("Application verified no tip")] AppVerifiedNoTip, /// The application verified a chunk but the height doesn't match the tip #[error("Application verified height mismatch")] AppVerifiedHeightMismatch, /// The application verified a chunk but the payload doesn't match the tip #[error("Application verified payload mismatch")] AppVerifiedPayloadMismatch, // P2P Errors /// Unable to send a message over the P2P network #[error("Unable to send message")] UnableToSendMessage, // Broadcast errors /// The chunk already has a certificate #[error("Already certified")] AlreadyCertified, /// I am not a sequencer in the specified epoch #[error("I am not a sequencer in epoch {0}")] IAmNotASequencer(Epoch), /// Nothing to rebroadcast #[error("Nothing to rebroadcast")] NothingToRebroadcast, /// The broadcast failed #[error("Broadcast failed")] BroadcastFailed, /// A certificate is missing #[error("Missing certificate")] MissingCertificate, /// The sequencer in the context doesn't match the expected sequencer #[error("Invalid context sequencer")] ContextSequencer, /// The height in the context is invalid #[error("Invalid context height")] ContextHeight, // Epoch Errors /// No signing scheme is known for the specified epoch #[error("Unknown signing scheme at epoch {0}")] UnknownScheme(Epoch), /// The specified sequencer is not a participant in the epoch #[error("Epoch {0} has no sequencer {1}")] UnknownSequencer(Epoch, String), /// The specified validator is not a participant in the epoch #[error("Epoch {0} has no validator {1}")] UnknownValidator(Epoch, String), /// The local validator is not a signer in the scheme for the specified epoch. #[error("Not a signer at epoch {0}")] NotSigner(Epoch), // Peer Errors /// The sender's public key doesn't match the expected key #[error("Peer mismatch")] PeerMismatch, // Signature Errors /// The sequencer's signature is invalid #[error("Invalid sequencer signature")] InvalidSequencerSignature, /// The certificate is invalid #[error("Invalid certificate")] InvalidCertificate, /// The node signature is invalid #[error("Invalid node signature")] InvalidNodeSignature, /// The acknowledgment signature is invalid #[error("Invalid ack signature")] InvalidAckSignature, // Ignorable Message Errors /// The acknowledgment's epoch is outside the accepted bounds #[error("Invalid ack epoch {0} outside bounds {1} - {2}")] AckEpochOutsideBounds(Epoch, Epoch, Epoch), /// The acknowledgment's height is outside the accepted bounds #[error("Invalid ack height {0} outside bounds {1} - {2}")] AckHeightOutsideBounds(u64, u64, u64), /// The chunk's height is lower than the current tip height #[error("Chunk height {0} lower than tip height {1}")] ChunkHeightTooLow(u64, u64), // Attributable Faults /// The chunk conflicts with an existing chunk at the same height #[error("Chunk payload mismatch from sequencer {0} at height {1}")] ChunkMismatch(String, u64), } /// Interface responsible for providing the set of sequencers active at a given epoch. pub trait SequencersProvider: Clone + Send + Sync + 'static { /// Public key used to identify sequencers. type PublicKey: PublicKey; /// Get the **sorted** sequencers for the given epoch. /// Returns `None` if the epoch is not known. fn sequencers(&self, epoch: Epoch) -> Option>>; } /// Suffix used to identify a chunk namespace for domain separation. /// Used when signing and verifying chunks to prevent signature reuse across different message types. pub const CHUNK_SUFFIX: &[u8] = b"_CHUNK"; /// Suffix used to identify an acknowledgment (ack) namespace for domain separation. /// Used when signing and verifying acks to prevent signature reuse across different message types. pub const ACK_SUFFIX: &[u8] = b"_ACK"; /// Returns a suffixed namespace for signing a chunk. /// /// This provides domain separation for signatures, preventing cross-protocol attacks /// by ensuring signatures for chunks cannot be reused for other message types. #[inline] pub fn chunk_namespace(namespace: &[u8]) -> Vec { union(namespace, CHUNK_SUFFIX) } /// Returns a suffixed namespace for signing an ack. /// /// This provides domain separation for signatures, preventing cross-protocol attacks /// by ensuring signatures for acks cannot be reused for other message types. #[inline] pub fn ack_namespace(namespace: &[u8]) -> Vec { union(namespace, ACK_SUFFIX) } /// Used as the [crate::Automaton::Context] type. /// /// Carries the necessary context for the automaton to verify a payload, including /// the sequencer's public key and its sequencer-specific height. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct Context { /// Sequencer's public key. pub sequencer: P, /// Sequencer-specific sequential height. Zero-indexed. pub height: u64, } /// Chunk is a message generated by a sequencer that is broadcasted to all validators. /// /// A chunk represents a unit of data in the ordered broadcast system. Each sequencer /// maintains its own chain of chunks with monotonically increasing heights. Validators /// acknowledge chunks with a vote, which are then aggregated into a certificate to prove /// reliable broadcast. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Chunk { /// Sequencer's public key. pub sequencer: P, /// Sequencer-specific sequential height. Zero-indexed. pub height: u64, /// Digest of the payload. pub payload: D, } impl Chunk { /// Create a new chunk with the given sequencer, height, and payload. /// /// This is the basic unit of data in the ordered broadcast system. pub const fn new(sequencer: P, height: u64, payload: D) -> Self { Self { sequencer, height, payload, } } } impl Write for Chunk { fn write(&self, writer: &mut impl BufMut) { self.sequencer.write(writer); UInt(self.height).write(writer); self.payload.write(writer); } } impl Read for Chunk { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let sequencer = P::read(reader)?; let height = UInt::read(reader)?.into(); let payload = D::read(reader)?; Ok(Self { sequencer, height, payload, }) } } impl EncodeSize for Chunk { fn encode_size(&self) -> usize { self.sequencer.encode_size() + UInt(self.height).encode_size() + self.payload.encode_size() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Chunk where P: for<'a> arbitrary::Arbitrary<'a>, D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let sequencer = P::arbitrary(u)?; let height = u.arbitrary::()?; let payload = D::arbitrary(u)?; Ok(Self { sequencer, height, payload, }) } } /// Context for signing/verifying validator acknowledgments. /// /// This is used as the context type for `Scheme` implementations for validators. /// It contains the chunk being acknowledged and the epoch of the validator set. #[derive(Debug, Clone)] pub struct AckSubject<'a, P: PublicKey, D: Digest> { /// The chunk being acknowledged. pub chunk: &'a Chunk, /// The epoch of the validator set. pub epoch: Epoch, } impl<'a, P: PublicKey, D: Digest> certificate::Subject for AckSubject<'a, P, D> { fn namespace_and_message(&self, namespace: &[u8]) -> (Bytes, Bytes) { let mut message = BytesMut::with_capacity(self.chunk.encode_size() + self.epoch.encode_size()); self.chunk.write(&mut message); self.epoch.write(&mut message); (ack_namespace(namespace).into(), message.freeze()) } } /// Parent is a message that contains information about the parent (previous height) of a Chunk. /// /// The sequencer and height are not provided as they are implied by the sequencer and height of the /// current chunk. The parent includes a certificate which proves that a quorum of validators have /// seen and acknowledged the parent chunk, making it an essential part of the chain linking /// mechanism. #[derive(Clone, Debug)] pub struct Parent { /// Digest of the parent chunk. pub digest: D, /// Epoch of the validator set that signed the parent. pub epoch: Epoch, /// Certificate over the parent, proving that a quorum of validators /// in the specified epoch have acknowledged the parent chunk. pub certificate: S::Certificate, } impl Parent { /// Create a new parent with the given digest, epoch, and signature. /// /// The parent links a chunk to its predecessor in the chain and provides /// the certificate that proves the predecessor was reliably broadcast. pub const fn new(digest: D, epoch: Epoch, certificate: S::Certificate) -> Self { Self { digest, epoch, certificate, } } } impl PartialEq for Parent where S::Certificate: PartialEq, { fn eq(&self, other: &Self) -> bool { self.digest == other.digest && self.epoch == other.epoch && self.certificate == other.certificate } } impl Eq for Parent where S::Certificate: Eq {} impl Hash for Parent where S::Certificate: Hash, { fn hash(&self, state: &mut H) { self.digest.hash(state); self.epoch.hash(state); self.certificate.hash(state); } } impl Write for Parent { fn write(&self, writer: &mut impl BufMut) { self.digest.write(writer); self.epoch.write(writer); self.certificate.write(writer); } } impl Read for Parent { type Cfg = ::Cfg; fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result { let digest = D::read(reader)?; let epoch = Epoch::read(reader)?; let certificate = S::Certificate::read_cfg(reader, cfg)?; Ok(Self { digest, epoch, certificate, }) } } impl EncodeSize for Parent { fn encode_size(&self) -> usize { self.digest.encode_size() + self.epoch.encode_size() + self.certificate.encode_size() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Parent where D: for<'a> arbitrary::Arbitrary<'a>, S::Certificate: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let digest = u.arbitrary::()?; let epoch = u.arbitrary::()?; let certificate = u.arbitrary::()?; Ok(Self { digest, epoch, certificate, }) } } /// Node is a message from a sequencer that contains a Chunk and a proof that the parent was correctly broadcasted. /// /// It represents a newly-proposed tip of the chain for the given sequencer. The node includes: /// 1. The chunk itself (sequencer, height, payload) /// 2. The sequencer's signature over the chunk /// 3. For non-genesis nodes (height > 0), proof that the previous chunk was acknowledged by a quorum of validators /// /// Nodes form a linked chain from each sequencer, ensuring that new chunks can only be added /// after their predecessors have been properly acknowledged by the validator set. #[derive(Clone, Debug)] pub struct Node { /// Chunk of the node. pub chunk: Chunk, /// Signature of the sequencer over the chunk. pub signature: P::Signature, /// Information about the parent chunk (previous height) /// /// This part is not signed over, but it is used to verif that the previous chunk /// in the chain was correctly broadcast. It contains the certificate that proves /// a quorum of validators acknowledged the parent. /// /// For genesis nodes (height = 0), this is None. pub parent: Option>, } impl Node { /// Create a new node with the given chunk, signature, and parent. /// /// For genesis nodes (height = 0), parent should be None. /// For all other nodes, parent must be provided. pub const fn new( chunk: Chunk, signature: P::Signature, parent: Option>, ) -> Self { Self { chunk, signature, parent, } } /// Verify the Node (and its parent). /// /// This ensures: /// 1. The sequencer's signature over the chunk is valid /// 2. For non-genesis nodes, the parent's certificate is valid /// /// If verification is successful, returns: /// - None for genesis nodes /// - Some(parent_chunk) for non-genesis nodes /// /// If verification fails, returns an appropriate error. pub fn verify( &self, rng: &mut R, namespace: &[u8], provider: &impl Provider, ) -> Result>, Error> where R: Rng + CryptoRng, S: scheme::Scheme, { // Verify chunk let chunk_namespace = chunk_namespace(namespace); let message = self.chunk.encode(); if !self .chunk .sequencer .verify(chunk_namespace.as_ref(), &message, &self.signature) { return Err(Error::InvalidSequencerSignature); } let Some(parent) = &self.parent else { return Ok(None); }; // Verify parent (if present) let parent_chunk = Chunk::new( self.chunk.sequencer.clone(), self.chunk .height .checked_sub(1) .ok_or(Error::ParentMissing)?, parent.digest, ); // Verify parent certificate using the scheme for the parent's epoch let parent_scheme = provider .scoped(parent.epoch) .ok_or(Error::UnknownScheme(parent.epoch))?; let ack_namespace = ack_namespace(namespace); let ack_ctx = AckSubject { chunk: &parent_chunk, epoch: parent.epoch, }; if !parent_scheme.verify_certificate::( rng, &ack_namespace, ack_ctx, &parent.certificate, ) { return Err(Error::InvalidCertificate); } Ok(Some(parent_chunk)) } /// Generate a new node with the given chunk, signature, (and parent). /// /// This is used by sequencers to create and sign new nodes for broadcast. /// For non-genesis nodes (height > 0), a parent with a certificate must be provided. pub fn sign( namespace: &[u8], signer: &mut C, height: u64, payload: D, parent: Option>, ) -> Self where C: Signer, { let chunk_namespace = chunk_namespace(namespace); let pub_key = signer.public_key(); let chunk = Chunk::new(pub_key, height, payload); let signature = signer.sign(chunk_namespace.as_ref(), &chunk.encode()); Self::new(chunk, signature, parent) } /// Decode a Node from network bytes with epoch-aware certificate decoding. /// /// This method performs staged decoding: /// 1. Decodes the chunk and signature /// 2. Checks if a parent exists /// 3. If present, decodes parent fields including epoch /// 4. Fetches the appropriate scheme for that epoch from the provider /// 5. Decodes the certificate using the epoch-specific bounded codec config pub fn read_staged( reader: &mut impl Buf, provider: &impl Provider, ) -> Result { // Decode chunk and signature let chunk = Chunk::read(reader)?; let signature = P::Signature::read(reader)?; // Decode `Option<()>` to check if parent exists // This consumes the bool prefix and positions us correctly let parent = if Option::<()>::read(reader)?.is_some() { // The bool prefix has been consumed, now read parent fields let digest = D::read(reader)?; let epoch = Epoch::read(reader)?; // Get scheme for parent's epoch let scheme = provider.scoped(epoch).ok_or_else(|| { CodecError::Wrapped( "consensus::ordered_broadcast::Node::read_staged", Box::new(Error::UnknownScheme(epoch)), ) })?; // Decode certificate with epoch-specific bounded config let certificate = S::Certificate::read_cfg(reader, &scheme.certificate_codec_config())?; Some(Parent { digest, epoch, certificate, }) } else { None }; // Validate height/parent consistency if chunk.height == 0 && parent.is_some() { return Err(CodecError::Wrapped( "consensus::ordered_broadcast::Node::read_staged", Box::new(Error::ParentOnGenesis), )); } else if chunk.height > 0 && parent.is_none() { return Err(CodecError::Wrapped( "consensus::ordered_broadcast::Node::read_staged", Box::new(Error::ParentMissing), )); } Ok(Self { chunk, signature, parent, }) } } impl Hash for Node { fn hash(&self, state: &mut H) { self.chunk.hash(state); self.signature.hash(state); self.parent.hash(state); } } impl PartialEq for Node { fn eq(&self, other: &Self) -> bool { self.chunk == other.chunk && self.signature == other.signature && self.parent == other.parent } } impl Eq for Node {} impl Write for Node { fn write(&self, writer: &mut impl BufMut) { self.chunk.write(writer); self.signature.write(writer); self.parent.write(writer); } } impl Read for Node { type Cfg = ::Cfg; fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result { let chunk = Chunk::read(reader)?; let signature = P::Signature::read(reader)?; let parent = >>::read_cfg(reader, cfg)?; if chunk.height == 0 && parent.is_some() { return Err(CodecError::Wrapped( "consensus::ordered_broadcast::Node", Box::new(Error::ParentOnGenesis), )); } else if chunk.height > 0 && parent.is_none() { return Err(CodecError::Wrapped( "consensus::ordered_broadcast::Node", Box::new(Error::ParentMissing), )); } Ok(Self { chunk, signature, parent, }) } } impl EncodeSize for Node { fn encode_size(&self) -> usize { self.chunk.encode_size() + self.signature.encode_size() + self.parent.encode_size() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Node where C: for<'a> arbitrary::Arbitrary<'a>, C::Signature: for<'a> arbitrary::Arbitrary<'a>, S::Certificate: for<'a> arbitrary::Arbitrary<'a>, D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let chunk = Chunk::::arbitrary(u)?; let signature = C::Signature::arbitrary(u)?; let parent = if chunk.height == 0 { None } else { Some(Parent::::arbitrary(u)?) }; Ok(Self { chunk, signature, parent, }) } } /// Ack is a message sent by a validator to acknowledge the receipt of a Chunk. /// /// When a validator receives and validates a chunk, it sends an Ack containing: /// 1. The chunk being acknowledged /// 2. The current epoch /// 3. An attestation over the chunk and epoch /// /// These attestations from validators can be aggregated to form a certificate /// once enough validators (a quorum) have acknowledged the chunk. This certificate /// serves as proof that the chunk was reliably broadcast. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Ack { /// Chunk that is being acknowledged. pub chunk: Chunk, /// Epoch of the validator set. pub epoch: Epoch, /// Attestation for this chunk. /// /// This is a cryptographic attestation that can be combined with other attestations /// to form a certificate once a quorum is reached. pub attestation: Attestation, } impl Ack { /// Create a new ack with the given chunk, epoch, and attestation. pub const fn new(chunk: Chunk, epoch: Epoch, attestation: Attestation) -> Self { Self { chunk, epoch, attestation, } } /// Verify the Ack. /// /// This ensures that the attestation is valid for the given chunk and epoch, /// using the provided scheme. /// /// Returns true if the attestation is valid, false otherwise. pub fn verify(&self, namespace: &[u8], scheme: &S) -> bool where S: scheme::Scheme, { let ack_namespace = ack_namespace(namespace); let ctx = AckSubject { chunk: &self.chunk, epoch: self.epoch, }; scheme.verify_attestation::(&ack_namespace, ctx, &self.attestation) } /// Generate a new Ack by signing with the provided scheme. /// /// This is used by validators to create and sign new acknowledgments for chunks. /// Returns None if the scheme cannot sign. pub fn sign(namespace: &[u8], scheme: &S, chunk: Chunk, epoch: Epoch) -> Option where S: scheme::Scheme, { let ack_namespace = ack_namespace(namespace); let ctx = AckSubject { chunk: &chunk, epoch, }; let attestation = scheme.sign::(&ack_namespace, ctx)?; Some(Self::new(chunk, epoch, attestation)) } } impl Write for Ack { fn write(&self, writer: &mut impl BufMut) { self.chunk.write(writer); self.epoch.write(writer); self.attestation.write(writer); } } impl Read for Ack { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let chunk = Chunk::read(reader)?; let epoch = Epoch::read(reader)?; let attestation = Attestation::read(reader)?; Ok(Self { chunk, epoch, attestation, }) } } impl EncodeSize for Ack { fn encode_size(&self) -> usize { self.chunk.encode_size() + self.epoch.encode_size() + self.attestation.encode_size() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Ack where P: for<'a> arbitrary::Arbitrary<'a>, D: for<'a> arbitrary::Arbitrary<'a>, S::Signature: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let chunk = Chunk::::arbitrary(u)?; let epoch = u.arbitrary::()?; let attestation = Attestation::arbitrary(u)?; Ok(Self { chunk, epoch, attestation, }) } } /// Activity is the type associated with the [crate::Reporter] trait. /// /// This enum represents the two main types of activities that are reported: /// 1. Tips - when a new chunk at the latest tip is verified for some sequencer /// 2. Locks - when a certificate is formed for a chunk /// /// The Reporter is notified of these activities so it can track the state of the system /// and provide the appropriate information to other components. #[allow(clippy::large_enum_variant)] #[derive(Clone, Debug, PartialEq)] pub enum Activity { /// A new tip for a sequencer /// /// This activity is only emitted when the application has verified some peer proposal. Tip(Proposal), /// A certificate for a chunk, indicating it has been acknowledged by a quorum Lock(Lock), } impl Write for Activity { fn write(&self, writer: &mut impl BufMut) { match self { Self::Tip(proposal) => { 0u8.write(writer); proposal.write(writer); } Self::Lock(lock) => { 1u8.write(writer); lock.write(writer); } } } } impl Read for Activity { type Cfg = ::Cfg; fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result { match u8::read(reader)? { 0 => Ok(Self::Tip(Proposal::read(reader)?)), 1 => Ok(Self::Lock(Lock::read_cfg(reader, cfg)?)), _ => Err(CodecError::Invalid( "consensus::ordered_broadcast::Activity", "Invalid type", )), } } } impl EncodeSize for Activity { fn encode_size(&self) -> usize { 1 + match self { Self::Tip(proposal) => proposal.encode_size(), Self::Lock(lock) => lock.encode_size(), } } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Activity where Proposal: for<'a> arbitrary::Arbitrary<'a>, Lock: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let choice = u.int_in_range(0..=1)?; match choice { 0 => Ok(Self::Tip(Proposal::::arbitrary(u)?)), 1 => Ok(Self::Lock(Lock::::arbitrary(u)?)), _ => unreachable!(), } } } /// Proposal is a message that is generated by a sequencer when proposing a new chunk. /// /// This represents a new chunk that has been created by a sequencer and is being /// broadcast to validators for acknowledgment. It contains the chunk itself and the /// sequencer's signature over that chunk. #[derive(Clone, Debug)] pub struct Proposal { /// Chunk that is being proposed. pub chunk: Chunk, /// Signature over the chunk. /// This is the sequencer's signature proving authenticity of the chunk. pub signature: P::Signature, } impl Proposal { /// Create a new Proposal with the given chunk and signature. pub const fn new(chunk: Chunk, signature: P::Signature) -> Self { Self { chunk, signature } } /// Verify the Proposal. /// /// This ensures that the sequencer's signature over the chunk is valid. /// Returns true if the signature is valid, false otherwise. pub fn verify(&self, namespace: &[u8]) -> bool { // Verify chunk let chunk_namespace = chunk_namespace(namespace); let message = self.chunk.encode(); self.chunk .sequencer .verify(chunk_namespace.as_ref(), &message, &self.signature) } } impl Hash for Proposal { fn hash(&self, state: &mut H) { self.chunk.hash(state); self.signature.hash(state); } } impl PartialEq for Proposal { fn eq(&self, other: &Self) -> bool { self.chunk == other.chunk && self.signature == other.signature } } /// This is needed to implement `Eq` for `Proposal`. impl Eq for Proposal {} impl Write for Proposal { fn write(&self, writer: &mut impl BufMut) { self.chunk.write(writer); self.signature.write(writer); } } impl Read for Proposal { type Cfg = (); fn read_cfg(reader: &mut impl Buf, _: &()) -> Result { let chunk = Chunk::read(reader)?; let signature = P::Signature::read(reader)?; Ok(Self { chunk, signature }) } } impl EncodeSize for Proposal { fn encode_size(&self) -> usize { self.chunk.encode_size() + self.signature.encode_size() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Proposal where Chunk: for<'a> arbitrary::Arbitrary<'a>, C::Signature: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let chunk = Chunk::::arbitrary(u)?; let signature = C::Signature::arbitrary(u)?; Ok(Self { chunk, signature }) } } /// Lock is a message that can be generated once `2f + 1` acks are received for a Chunk. /// /// A Lock represents proof that a quorum of validators (at least 2f+1, where f is the /// maximum number of faulty validators) have acknowledged a chunk. This proof is in the /// form of a certificate that can be verified by anyone. /// /// The Lock is essential for: /// 1. Proving that a chunk has been reliably broadcast /// 2. Allowing sequencers to build chains of chunks /// 3. Preventing sequencers from creating forks in their chains #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Lock { /// Chunk that is being locked. pub chunk: Chunk, /// Epoch of the validator set. pub epoch: Epoch, /// Recovered certificate over the chunk. /// This is a cryptographic proof that proves a quorum of validators /// have acknowledged the chunk. pub certificate: S::Certificate, } impl Lock { /// Create a new Lock with the given chunk, epoch, and certificate. pub const fn new(chunk: Chunk, epoch: Epoch, certificate: S::Certificate) -> Self { Self { chunk, epoch, certificate, } } /// Verify the Lock. /// /// This ensures that the certificate is valid for the given chunk and epoch, /// using the provided scheme. /// /// Returns true if the signature is valid, false otherwise. pub fn verify(&self, rng: &mut R, namespace: &[u8], scheme: &S) -> bool where R: Rng + CryptoRng, S: scheme::Scheme, { let ack_namespace = ack_namespace(namespace); let ctx = AckSubject { chunk: &self.chunk, epoch: self.epoch, }; scheme.verify_certificate::(rng, &ack_namespace, ctx, &self.certificate) } } impl Write for Lock { fn write(&self, writer: &mut impl BufMut) { self.chunk.write(writer); self.epoch.write(writer); self.certificate.write(writer); } } impl Read for Lock { type Cfg = ::Cfg; fn read_cfg(reader: &mut impl Buf, cfg: &Self::Cfg) -> Result { let chunk = Chunk::read(reader)?; let epoch = Epoch::read(reader)?; let certificate = S::Certificate::read_cfg(reader, cfg)?; Ok(Self { chunk, epoch, certificate, }) } } impl EncodeSize for Lock { fn encode_size(&self) -> usize { self.chunk.encode_size() + self.epoch.encode_size() + self.certificate.encode_size() } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Lock where P: for<'a> arbitrary::Arbitrary<'a>, S::Certificate: for<'a> arbitrary::Arbitrary<'a>, D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { Ok(Self { chunk: u.arbitrary()?, epoch: u.arbitrary()?, certificate: u.arbitrary()?, }) } } #[cfg(test)] mod tests { use super::*; use crate::ordered_broadcast::{ mocks::Provider, scheme::{bls12381_multisig, bls12381_threshold, ed25519, Scheme}, }; use commonware_codec::{DecodeExt as _, Encode, Read}; use commonware_cryptography::{ bls12381::primitives::variant::{MinPk, MinSig}, certificate::{mocks::Fixture, ConstantProvider}, ed25519::{PrivateKey, PublicKey}, sha256::Digest as Sha256Digest, Signer, }; use commonware_utils::quorum; use rand::{rngs::StdRng, SeedableRng}; use std::panic::catch_unwind; const NAMESPACE: &[u8] = b"test"; // Helper function to create a sample digest fn sample_digest(v: u8) -> Sha256Digest { Sha256Digest::from([v; 32]) // Simple fixed digest for testing } // Helper function to create a sample Ed25519 private key fn sample_scheme(v: u64) -> PrivateKey { PrivateKey::from_seed(v) } /// Generate a fixture using the provided generator function. fn setup(n: u32, fixture: F) -> Fixture where F: FnOnce(&mut StdRng, u32) -> Fixture, { let mut rng = StdRng::seed_from_u64(0); fixture(&mut rng, n) } /// Generate a fixture using the provided generator function with a specific seed. fn setup_seeded(n: u32, seed: u64, fixture: F) -> Fixture where F: FnOnce(&mut StdRng, u32) -> Fixture, { let mut rng = StdRng::seed_from_u64(seed); fixture(&mut rng, n) } #[test] fn test_chunk_encode_decode() { let public_key = sample_scheme(0).public_key(); let chunk = Chunk::new(public_key, 42, sample_digest(1)); let encoded = chunk.encode(); let decoded = Chunk::::decode(encoded).unwrap(); assert_eq!(chunk, decoded); } // Tests migrated to use Scheme-based API fn parent_encode_decode(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); let chunk = Chunk::new(fixture.participants[0].clone(), 0, sample_digest(1)); let epoch = Epoch::new(5); let quorum = commonware_utils::quorum(fixture.schemes.len() as u32) as usize; // Generate acks from quorum validators let ctx = AckSubject { chunk: &chunk, epoch, }; let attestations: Vec<_> = fixture.schemes[..quorum] .iter() .map(|scheme| { scheme .sign::(&ack_namespace(NAMESPACE), ctx.clone()) .unwrap() }) .collect(); // Assemble certificate let certificate = fixture.schemes[0] .assemble(attestations) .expect("Should assemble certificate"); // Create and test parent let parent = Parent::::new(sample_digest(1), epoch, certificate); let encoded = parent.encode(); let cfg = fixture.schemes[0].certificate_codec_config(); let decoded = Parent::::read_cfg(&mut encoded.as_ref(), &cfg).unwrap(); assert_eq!(parent, decoded); } #[test] fn test_parent_encode_decode() { parent_encode_decode(ed25519::fixture); parent_encode_decode(bls12381_multisig::fixture::); parent_encode_decode(bls12381_multisig::fixture::); parent_encode_decode(bls12381_threshold::fixture::); parent_encode_decode(bls12381_threshold::fixture::); } fn node_encode_decode(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); let ed_scheme = sample_scheme(0); let public_key = ed_scheme.public_key(); let chunk_namespace = chunk_namespace(NAMESPACE); let quorum = commonware_utils::quorum(fixture.schemes.len() as u32) as usize; let cfg = fixture.schemes[0].certificate_codec_config(); // Test with no parent (genesis) let chunk = Chunk::new(public_key.clone(), 0, sample_digest(1)); let message = chunk.encode(); let signature = ed_scheme.sign(chunk_namespace.as_ref(), &message); let node = Node::::new(chunk, signature, None); let encoded = node.encode(); let decoded = Node::::read_cfg(&mut encoded.as_ref(), &cfg).unwrap(); assert_eq!(decoded.chunk, node.chunk); assert_eq!(decoded.signature, node.signature); assert_eq!(decoded.parent, node.parent); // Test with parent - generate a proper certificate let parent_chunk = Chunk::new(public_key.clone(), 0, sample_digest(0)); let parent_epoch = Epoch::new(5); // Generate parent certificate let parent_ctx = AckSubject { chunk: &parent_chunk, epoch: parent_epoch, }; let parent_attestations: Vec<_> = fixture.schemes[..quorum] .iter() .map(|scheme| { scheme .sign::(&ack_namespace(NAMESPACE), parent_ctx.clone()) .unwrap() }) .collect(); let parent_certificate = fixture.schemes[0] .assemble(parent_attestations) .expect("Should assemble certificate"); // Create proper parent with valid certificate let parent = Some(Parent::::new( parent_chunk.payload, parent_epoch, parent_certificate, )); // Create child node let chunk2 = Chunk::new(public_key, 1, sample_digest(2)); let message2 = chunk2.encode(); let signature2 = ed_scheme.sign(chunk_namespace.as_ref(), &message2); let node2 = Node::::new(chunk2, signature2, parent); // Test encode/decode let encoded2 = node2.encode(); let decoded2 = Node::::read_cfg(&mut encoded2.as_ref(), &cfg).unwrap(); assert_eq!(decoded2.chunk, node2.chunk); assert_eq!(decoded2.signature, node2.signature); assert_eq!(decoded2.parent, node2.parent); } #[test] fn test_node_encode_decode() { node_encode_decode(ed25519::fixture); node_encode_decode(bls12381_multisig::fixture::); node_encode_decode(bls12381_multisig::fixture::); node_encode_decode(bls12381_threshold::fixture::); node_encode_decode(bls12381_threshold::fixture::); } fn node_read_staged(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); // Create a provider that returns the verifier for any epoch. // This simulates the normal case where the scheme is available. let provider = ConstantProvider::new(fixture.verifier.clone()); // Create common test data: a sequencer public key and the chunk namespace. let public_key = fixture.participants[0].clone(); let chunk_namespace = chunk_namespace(NAMESPACE); // Genesis nodes have no parent certificate, so read_staged should succeed // without needing to look up a scheme for the parent's epoch. let genesis_chunk = Chunk::new(public_key.clone(), 0, sample_digest(1)); let genesis_message = genesis_chunk.encode(); let genesis_signature = fixture.private_keys[0].sign(chunk_namespace.as_ref(), &genesis_message); let genesis_node = Node::::new(genesis_chunk.clone(), genesis_signature, None); let encoded = genesis_node.encode(); let decoded = Node::::read_staged(&mut encoded.as_ref(), &provider) .expect("Should decode genesis node"); assert_eq!(decoded.chunk, genesis_node.chunk); assert_eq!(decoded.signature, genesis_node.signature); assert_eq!(decoded.parent, genesis_node.parent); // Non-genesis nodes have a parent with a certificate. read_staged must // look up the scheme for the parent's epoch to decode the certificate. let parent_epoch = Epoch::new(5); let parent_ctx = AckSubject { chunk: &genesis_chunk, epoch: parent_epoch, }; // Collect signatures from a quorum of validators to form the parent certificate. let parent_attestations: Vec<_> = fixture.schemes[..quorum(4) as usize] .iter() .map(|scheme| { scheme .sign::(&ack_namespace(NAMESPACE), parent_ctx.clone()) .unwrap() }) .collect(); let parent_certificate = fixture.schemes[0] .assemble(parent_attestations) .expect("Should assemble certificate"); let parent = Parent::::new(sample_digest(0), parent_epoch, parent_certificate); let chunk_height_1 = Chunk::new(public_key, 1, sample_digest(2)); let message_height_1 = chunk_height_1.encode(); let signature_height_1 = fixture.private_keys[0].sign(chunk_namespace.as_ref(), &message_height_1); let node_with_parent = Node::::new( chunk_height_1, signature_height_1, Some(parent), ); let encoded2 = node_with_parent.encode(); let decoded2 = Node::::read_staged(&mut encoded2.as_ref(), &provider) .expect("Should decode non-genesis node"); assert_eq!(decoded2.chunk, node_with_parent.chunk); assert_eq!(decoded2.signature, node_with_parent.signature); assert_eq!(decoded2.parent, node_with_parent.parent); // When the provider doesn't have a scheme registered for the parent's epoch, // read_staged should return an UnknownScheme error. let empty_provider = Provider::::new(); let result = Node::::read_staged( &mut encoded2.as_ref(), &empty_provider, ); assert!( result.is_err(), "Should fail when scheme is missing for parent's epoch" ); } #[test] fn test_node_read_staged() { node_read_staged(ed25519::fixture); node_read_staged(bls12381_multisig::fixture::); node_read_staged(bls12381_multisig::fixture::); node_read_staged(bls12381_threshold::fixture::); node_read_staged(bls12381_threshold::fixture::); } fn ack_encode_decode(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); let chunk = Chunk::new(fixture.participants[0].clone(), 42, sample_digest(1)); let epoch = Epoch::new(5); let ctx = AckSubject { chunk: &chunk, epoch, }; let attestation = fixture.schemes[0] .sign::(NAMESPACE, ctx) .expect("Should sign vote"); let ack = Ack:: { chunk, epoch, attestation, }; let encoded = ack.encode(); let decoded = Ack::::read_cfg(&mut encoded.as_ref(), &()).unwrap(); assert_eq!(decoded.chunk, ack.chunk); assert_eq!(decoded.epoch, ack.epoch); assert_eq!(decoded.attestation.signer, ack.attestation.signer); } #[test] fn test_ack_encode_decode() { ack_encode_decode(ed25519::fixture); ack_encode_decode(bls12381_multisig::fixture::); ack_encode_decode(bls12381_multisig::fixture::); ack_encode_decode(bls12381_threshold::fixture::); ack_encode_decode(bls12381_threshold::fixture::); } fn activity_encode_decode(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); let scheme = sample_scheme(0); let public_key = scheme.public_key(); let chunk_namespace = chunk_namespace(NAMESPACE); let quorum = commonware_utils::quorum(fixture.schemes.len() as u32) as usize; let cfg = fixture.schemes[0].certificate_codec_config(); // Test Proposal let chunk = Chunk::new(public_key, 42, sample_digest(1)); let message = chunk.encode(); let signature = scheme.sign(chunk_namespace.as_ref(), &message); let proposal = Proposal::::new(chunk.clone(), signature.clone()); let activity = Activity::::Tip(proposal); let encoded = activity.encode(); let decoded = Activity::::read_cfg(&mut encoded.as_ref(), &cfg).unwrap(); match decoded { Activity::Tip(p) => { assert_eq!(p.chunk, chunk); assert_eq!(p.signature, signature); } _ => panic!("Decoded activity has wrong type"), } // Test Lock with proper certificate let epoch = Epoch::new(5); // Generate votes from quorum validators let ctx = AckSubject { chunk: &chunk, epoch, }; let attestations: Vec<_> = fixture.schemes[..quorum] .iter() .map(|scheme| { scheme .sign::(&ack_namespace(NAMESPACE), ctx.clone()) .unwrap() }) .collect(); // Assemble certificate let certificate = fixture.schemes[0] .assemble(attestations) .expect("Should assemble certificate"); // Create lock let lock = Lock::::new(chunk.clone(), epoch, certificate); // Verify lock let mut rng = StdRng::seed_from_u64(0); assert!(lock.verify(&mut rng, NAMESPACE, &fixture.verifier)); // Test activity with the lock let activity = Activity::::Lock(lock.clone()); let encoded = activity.encode(); let decoded = Activity::::read_cfg(&mut encoded.as_ref(), &cfg).unwrap(); match decoded { Activity::Lock(l) => { assert_eq!(l.chunk, chunk); assert_eq!(l.epoch, epoch); assert!(l.verify(&mut rng, NAMESPACE, &fixture.verifier)); } _ => panic!("Decoded activity has wrong type"), } } #[test] fn test_activity_encode_decode() { activity_encode_decode(ed25519::fixture); activity_encode_decode(bls12381_multisig::fixture::); activity_encode_decode(bls12381_multisig::fixture::); activity_encode_decode(bls12381_threshold::fixture::); activity_encode_decode(bls12381_threshold::fixture::); } #[test] fn test_proposal_encode_decode() { let scheme = sample_scheme(0); let public_key = scheme.public_key(); let chunk = Chunk::new(public_key, 42, sample_digest(1)); // Create a properly signed proposal let chunk_namespace = chunk_namespace(NAMESPACE); let message = chunk.encode(); let signature = scheme.sign(chunk_namespace.as_ref(), &message); let proposal = Proposal::::new(chunk, signature); let encoded = proposal.encode(); let decoded = Proposal::::decode(encoded).unwrap(); assert_eq!(decoded.chunk, proposal.chunk); assert_eq!(decoded.signature, proposal.signature); // Verify the decoded proposal assert!(decoded.verify(NAMESPACE)); } fn lock_encode_decode(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); let public_key = sample_scheme(0).public_key(); let chunk = Chunk::new(public_key, 42, sample_digest(1)); let epoch = Epoch::new(5); let quorum = commonware_utils::quorum(fixture.schemes.len() as u32) as usize; // Generate votes from quorum validators let ctx = AckSubject { chunk: &chunk, epoch, }; let attestations: Vec<_> = fixture.schemes[..quorum] .iter() .map(|scheme| { scheme .sign::(&ack_namespace(NAMESPACE), ctx.clone()) .unwrap() }) .collect(); // Assemble certificate let certificate = fixture.schemes[0] .assemble(attestations) .expect("Should assemble certificate"); // Create lock, encode and decode let lock = Lock::::new(chunk, epoch, certificate); let encoded = lock.encode(); let cfg = fixture.schemes[0].certificate_codec_config(); let decoded = Lock::::read_cfg(&mut encoded.as_ref(), &cfg).unwrap(); assert_eq!(decoded.chunk, lock.chunk); assert_eq!(decoded.epoch, lock.epoch); // Verify the signature in the decoded lock let mut rng = StdRng::seed_from_u64(0); assert!(decoded.verify(&mut rng, NAMESPACE, &fixture.verifier)); } #[test] fn test_lock_encode_decode() { lock_encode_decode(ed25519::fixture); lock_encode_decode(bls12381_multisig::fixture::); lock_encode_decode(bls12381_multisig::fixture::); lock_encode_decode(bls12381_threshold::fixture::); lock_encode_decode(bls12381_threshold::fixture::); } fn node_sign_verify(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); let mut scheme = sample_scheme(0); let public_key = scheme.public_key(); let quorum = commonware_utils::quorum(fixture.schemes.len() as u32) as usize; // Test genesis node (no parent) let node = Node::::sign( NAMESPACE, &mut scheme, 0, sample_digest(1), None, ); let mut rng = StdRng::seed_from_u64(0); let provider = ConstantProvider::new(fixture.verifier.clone()); let result = node.verify(&mut rng, NAMESPACE, &provider); assert!(result.is_ok()); assert!(result.unwrap().is_none()); // Test node with parent let parent_chunk = Chunk::new(public_key, 0, sample_digest(1)); let parent_epoch = Epoch::new(5); // Create certificate for parent let parent_ctx = AckSubject { chunk: &parent_chunk, epoch: parent_epoch, }; let parent_attestations: Vec<_> = fixture.schemes[..quorum] .iter() .map(|scheme| { scheme .sign::(&ack_namespace(NAMESPACE), parent_ctx.clone()) .unwrap() }) .collect(); let parent_certificate = fixture.schemes[0] .assemble(parent_attestations) .expect("Should assemble certificate"); let parent = Some(Parent::::new( parent_chunk.payload, parent_epoch, parent_certificate, )); let node = Node::::sign( NAMESPACE, &mut scheme, 1, sample_digest(2), parent, ); let result = node.verify(&mut rng, NAMESPACE, &provider); assert!(result.is_ok()); assert!(result.unwrap().is_some()); } #[test] fn test_node_sign_verify() { node_sign_verify(ed25519::fixture); node_sign_verify(bls12381_multisig::fixture::); node_sign_verify(bls12381_multisig::fixture::); node_sign_verify(bls12381_threshold::fixture::); node_sign_verify(bls12381_threshold::fixture::); } fn ack_sign_verify(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); let public_key = sample_scheme(0).public_key(); let chunk = Chunk::new(public_key, 42, sample_digest(1)); let epoch = Epoch::new(5); let ack = Ack::sign(NAMESPACE, &fixture.schemes[0], chunk, epoch).expect("Should sign ack"); assert!(ack.verify(NAMESPACE, &fixture.verifier)); // Test that verification fails with wrong namespace assert!(!ack.verify(b"wrong", &fixture.verifier)); } #[test] fn test_ack_sign_verify() { ack_sign_verify(ed25519::fixture); ack_sign_verify(bls12381_multisig::fixture::); ack_sign_verify(bls12381_multisig::fixture::); ack_sign_verify(bls12381_threshold::fixture::); ack_sign_verify(bls12381_threshold::fixture::); } fn certificate_assembly(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); let public_key = sample_scheme(0).public_key(); let chunk = Chunk::new(public_key, 42, sample_digest(1)); let epoch = Epoch::new(5); let quorum = commonware_utils::quorum(fixture.schemes.len() as u32) as usize; // Create quorum votes let ctx = AckSubject { chunk: &chunk, epoch, }; let attestations: Vec<_> = fixture.schemes[..quorum] .iter() .map(|scheme| { scheme .sign::(&ack_namespace(NAMESPACE), ctx.clone()) .unwrap() }) .collect(); // Assemble certificate let certificate = fixture.schemes[0] .assemble(attestations) .expect("Should assemble certificate"); // Create lock with certificate let lock = Lock::::new(chunk, epoch, certificate); // Verify lock let mut rng = StdRng::seed_from_u64(0); assert!(lock.verify(&mut rng, NAMESPACE, &fixture.verifier)); } #[test] fn test_certificate_assembly() { certificate_assembly(ed25519::fixture); certificate_assembly(bls12381_multisig::fixture::); certificate_assembly(bls12381_multisig::fixture::); certificate_assembly(bls12381_threshold::fixture::); certificate_assembly(bls12381_threshold::fixture::); } fn lock_verify(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); let public_key = sample_scheme(0).public_key(); let chunk = Chunk::new(public_key, 42, sample_digest(1)); let epoch = Epoch::new(5); let quorum = commonware_utils::quorum(fixture.schemes.len() as u32) as usize; // Create certificate let ctx = AckSubject { chunk: &chunk, epoch, }; let attestations: Vec<_> = fixture.schemes[..quorum] .iter() .map(|scheme| { scheme .sign::(&ack_namespace(NAMESPACE), ctx.clone()) .unwrap() }) .collect(); let certificate = fixture.schemes[0] .assemble(attestations) .expect("Should assemble certificate"); // Create lock let lock = Lock::::new(chunk, epoch, certificate); // Verify lock let mut rng = StdRng::seed_from_u64(0); assert!(lock.verify(&mut rng, NAMESPACE, &fixture.verifier)); // Test that verification fails with wrong namespace assert!(!lock.verify(&mut rng, b"wrong", &fixture.verifier)); } #[test] fn test_lock_verify() { lock_verify(ed25519::fixture); lock_verify(bls12381_multisig::fixture::); lock_verify(bls12381_multisig::fixture::); lock_verify(bls12381_threshold::fixture::); lock_verify(bls12381_threshold::fixture::); } #[test] fn test_proposal_verify() { let scheme = sample_scheme(0); let chunk = Chunk::new(scheme.public_key(), 42, sample_digest(1)); // Sign and create proposal let chunk_namespace = chunk_namespace(NAMESPACE); let message = chunk.encode(); let signature = scheme.sign(chunk_namespace.as_ref(), &message); let proposal = Proposal::::new(chunk, signature); // Verify proposal assert!(proposal.verify(NAMESPACE)); // Test that verification fails with wrong namespace assert!(!proposal.verify(b"wrong")); } fn node_verify_invalid_signature(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); let scheme = sample_scheme(0); let public_key = scheme.public_key(); // Create a valid chunk let chunk = Chunk::new(public_key, 0, sample_digest(1)); // Create a valid signature let chunk_namespace = chunk_namespace(NAMESPACE); let message = chunk.encode(); let signature = scheme.sign(chunk_namespace.as_ref(), &message); // Create a node with valid signature let node = Node::::new(chunk.clone(), signature, None); // Verification should succeed let mut rng = StdRng::seed_from_u64(0); let provider = ConstantProvider::new(fixture.verifier); assert!(node.verify(&mut rng, NAMESPACE, &provider).is_ok()); // Now create a node with invalid signature let tampered_signature = scheme.sign(chunk_namespace.as_ref(), &node.encode()); let invalid_node = Node::::new(chunk, tampered_signature, None); // Verification should fail assert!(matches!( invalid_node.verify(&mut rng, NAMESPACE, &provider), Err(Error::InvalidSequencerSignature) )); } #[test] fn test_node_verify_invalid_signature() { node_verify_invalid_signature(ed25519::fixture); node_verify_invalid_signature(bls12381_multisig::fixture::); node_verify_invalid_signature(bls12381_multisig::fixture::); node_verify_invalid_signature(bls12381_threshold::fixture::); node_verify_invalid_signature(bls12381_threshold::fixture::); } fn node_verify_invalid_parent_signature(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); let scheme = sample_scheme(0); let public_key = scheme.public_key(); let quorum = commonware_utils::quorum(fixture.schemes.len() as u32) as usize; // Create parent and child chunks let parent_chunk = Chunk::new(public_key.clone(), 0, sample_digest(0)); let child_chunk = Chunk::new(public_key, 1, sample_digest(1)); let epoch = Epoch::new(5); // Generate a valid certificate for the parent let parent_ctx = AckSubject { chunk: &parent_chunk, epoch, }; let parent_attestations: Vec<_> = fixture.schemes[..quorum] .iter() .map(|scheme| { scheme .sign::(&ack_namespace(NAMESPACE), parent_ctx.clone()) .unwrap() }) .collect(); let certificate = fixture.schemes[0] .assemble(parent_attestations) .expect("Should assemble certificate"); // Create parent with valid certificate let parent = Parent::::new(parent_chunk.payload, epoch, certificate); // Create child node let chunk_namespace = chunk_namespace(NAMESPACE); let message = child_chunk.encode(); let node_signature = scheme.sign(chunk_namespace.as_ref(), &message); let node = Node::::new( child_chunk.clone(), node_signature.clone(), Some(parent), ); // Verification should succeed let mut rng = StdRng::seed_from_u64(0); let provider = ConstantProvider::new(fixture.verifier.clone()); assert!(node.verify(&mut rng, NAMESPACE, &provider).is_ok()); // Now create a parent with invalid certificate // Generate certificate with the wrong keys (sign with schemes[1..] but pretend it's from schemes[0..]) let wrong_ctx = AckSubject { chunk: &parent_chunk, epoch: Epoch::new(99), // Different epoch to get different signatures }; let wrong_attestations: Vec<_> = fixture.schemes[..quorum] .iter() .map(|scheme| { scheme .sign::(&ack_namespace(NAMESPACE), wrong_ctx.clone()) .unwrap() }) .collect(); let wrong_certificate = fixture.schemes[0] .assemble(wrong_attestations) .expect("Should assemble certificate"); // Create parent with certificate signed for wrong context (wrong epoch) let wrong_parent = Parent::::new(parent_chunk.payload, epoch, wrong_certificate); // Create child node with wrong parent let node = Node::::new( child_chunk, node_signature, Some(wrong_parent), ); // Verification should fail because the parent certificate was signed for different epoch assert!(matches!( node.verify(&mut rng, NAMESPACE, &provider), Err(Error::InvalidCertificate) )); } #[test] fn test_node_verify_invalid_parent_signature() { node_verify_invalid_parent_signature(ed25519::fixture); node_verify_invalid_parent_signature(bls12381_multisig::fixture::); node_verify_invalid_parent_signature(bls12381_multisig::fixture::); node_verify_invalid_parent_signature(bls12381_threshold::fixture::); node_verify_invalid_parent_signature(bls12381_threshold::fixture::); } fn ack_verify_invalid_signature(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); // Create a chunk and ack let public_key = sample_scheme(0).public_key(); let chunk = Chunk::new(public_key, 42, sample_digest(1)); let epoch = Epoch::new(5); // Create a valid ack let ack = Ack::::sign( NAMESPACE, &fixture.schemes[0], chunk.clone(), epoch, ) .expect("Should sign ack"); // Verification should succeed assert!(ack.verify(NAMESPACE, &fixture.verifier)); // Create an ack with tampered signature by signing with a different scheme let ctx = AckSubject { chunk: &chunk, epoch, }; let mut tampered_vote = fixture.schemes[1] .sign::(NAMESPACE, ctx) .expect("Should sign vote"); // Change the signer index to mismatch with the actual signature // The vote was signed by validator 1, but we claim it's from validator 0 tampered_vote.signer = 0; let invalid_ack = Ack::::new(chunk, epoch, tampered_vote); // Verification should fail because the signer index doesn't match the signature assert!(!invalid_ack.verify(NAMESPACE, &fixture.verifier)); } #[test] fn test_ack_verify_invalid_signature() { ack_verify_invalid_signature(ed25519::fixture); ack_verify_invalid_signature(bls12381_multisig::fixture::); ack_verify_invalid_signature(bls12381_multisig::fixture::); ack_verify_invalid_signature(bls12381_threshold::fixture::); ack_verify_invalid_signature(bls12381_threshold::fixture::); } fn ack_verify_wrong_validator(f: F) where S: Scheme, F: Fn(&mut StdRng, u32) -> Fixture, { let fixture = setup_seeded(4, 0, &f); let wrong_fixture = setup_seeded(4, 1, &f); // Create a chunk and ack let public_key = sample_scheme(0).public_key(); let chunk = Chunk::new(public_key, 42, sample_digest(1)); let epoch = Epoch::new(5); // Create a valid ack let ack = Ack::::sign(NAMESPACE, &fixture.schemes[0], chunk, epoch) .expect("Should sign ack"); // Verification should succeed with correct verifier assert!(ack.verify(NAMESPACE, &fixture.verifier)); // Verification should fail with wrong verifier assert!(!ack.verify(NAMESPACE, &wrong_fixture.verifier)); } #[test] fn test_ack_verify_wrong_validator() { ack_verify_wrong_validator(ed25519::fixture); ack_verify_wrong_validator(bls12381_multisig::fixture::); ack_verify_wrong_validator(bls12381_multisig::fixture::); ack_verify_wrong_validator(bls12381_threshold::fixture::); ack_verify_wrong_validator(bls12381_threshold::fixture::); } fn lock_verify_invalid_signature(f: F) where S: Scheme, F: Fn(&mut StdRng, u32) -> Fixture, { let fixture = setup_seeded(4, 0, &f); let wrong_fixture = setup_seeded(4, 1, &f); let public_key = sample_scheme(0).public_key(); let chunk = Chunk::new(public_key, 42, sample_digest(1)); let epoch = Epoch::new(5); let quorum_size = commonware_utils::quorum(fixture.schemes.len() as u32) as usize; // Generate certificate let ctx = AckSubject { chunk: &chunk, epoch, }; let attestations: Vec<_> = fixture.schemes[..quorum_size] .iter() .map(|scheme| { scheme .sign::(&ack_namespace(NAMESPACE), ctx.clone()) .unwrap() }) .collect(); let certificate = fixture.schemes[0] .assemble(attestations) .expect("Should assemble certificate"); // Create lock let lock = Lock::::new(chunk.clone(), epoch, certificate); // Verification should succeed let mut rng = StdRng::seed_from_u64(0); assert!(lock.verify(&mut rng, NAMESPACE, &fixture.verifier)); // Generate certificate with the wrong keys let wrong_attestations: Vec<_> = wrong_fixture.schemes[..quorum_size] .iter() .map(|scheme| { scheme .sign::(&ack_namespace(NAMESPACE), ctx.clone()) .unwrap() }) .collect(); let wrong_certificate = wrong_fixture.schemes[0] .assemble(wrong_attestations) .expect("Should assemble certificate"); // Create lock with wrong signature let wrong_lock = Lock::::new(chunk, epoch, wrong_certificate); // Verification should fail with the original public key assert!(!wrong_lock.verify(&mut rng, NAMESPACE, &fixture.verifier)); // But succeed with the matching wrong verifier assert!(wrong_lock.verify(&mut rng, NAMESPACE, &wrong_fixture.verifier)); } #[test] fn test_lock_verify_invalid_signature() { lock_verify_invalid_signature(ed25519::fixture); lock_verify_invalid_signature(bls12381_multisig::fixture::); lock_verify_invalid_signature(bls12381_multisig::fixture::); lock_verify_invalid_signature(bls12381_threshold::fixture::); lock_verify_invalid_signature(bls12381_threshold::fixture::); } #[test] fn test_proposal_verify_wrong_namespace() { let scheme = sample_scheme(0); let chunk = Chunk::new(scheme.public_key(), 42, sample_digest(1)); // Sign and create proposal let chunk_namespace = chunk_namespace(NAMESPACE); let message = chunk.encode(); let signature = scheme.sign(chunk_namespace.as_ref(), &message); let proposal = Proposal::::new(chunk, signature); // Verify with correct namespace - should pass assert!(proposal.verify(NAMESPACE)); // Verify with wrong namespace - should fail assert!(!proposal.verify(b"wrong_namespace")); } #[test] fn test_proposal_verify_wrong_sequencer() { let scheme1 = sample_scheme(0); let scheme2 = sample_scheme(1); // Different key // Create chunk with scheme1's public key let chunk = Chunk::new(scheme1.public_key(), 42, sample_digest(1)); // But sign it with scheme2 (wrong key) let chunk_namespace = chunk_namespace(NAMESPACE); let message = chunk.encode(); let signature = scheme2.sign(chunk_namespace.as_ref(), &message); let proposal = Proposal::::new(chunk, signature); // Verification should fail because the signature doesn't match the sequencer's public key assert!(!proposal.verify(NAMESPACE)); } fn node_genesis_with_parent_fails(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); // Try to create a node with height 0 and a parent let public_key = sample_scheme(0).public_key(); let chunk = Chunk::new(public_key.clone(), 0, sample_digest(1)); let chunk_namespace = chunk_namespace(NAMESPACE); let message = chunk.encode(); let signature = sample_scheme(0).sign(chunk_namespace.as_ref(), &message); let quorum_size = commonware_utils::quorum(fixture.schemes.len() as u32) as usize; // Create a parent with a dummy certificate (content doesn't matter for this test) let dummy_chunk = Chunk::new(public_key, 0, sample_digest(0)); let dummy_epoch = Epoch::new(5); let ctx = AckSubject { chunk: &dummy_chunk, epoch: dummy_epoch, }; let attestations: Vec<_> = fixture.schemes[..quorum_size] .iter() .map(|scheme| { scheme .sign::(&ack_namespace(NAMESPACE), ctx.clone()) .unwrap() }) .collect(); let certificate = fixture.schemes[0] .assemble(attestations) .expect("Should assemble certificate"); let parent = Parent::::new(sample_digest(0), Epoch::new(5), certificate); // Create the genesis node with a parent - should fail to decode let encoded = Node::::new(chunk, signature, Some(parent)).encode(); // This should error because genesis nodes can't have parents let cfg = fixture.schemes[0].certificate_codec_config(); let result = Node::::read_cfg(&mut encoded.as_ref(), &cfg); assert!(result.is_err()); } #[test] fn test_node_genesis_with_parent_fails() { node_genesis_with_parent_fails(ed25519::fixture); node_genesis_with_parent_fails(bls12381_multisig::fixture::); node_genesis_with_parent_fails(bls12381_multisig::fixture::); node_genesis_with_parent_fails(bls12381_threshold::fixture::); node_genesis_with_parent_fails(bls12381_threshold::fixture::); } fn node_non_genesis_without_parent_fails(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); // Try to create a non-genesis node without a parent let public_key = sample_scheme(0).public_key(); let chunk = Chunk::new(public_key, 1, sample_digest(1)); // Height > 0 let chunk_namespace = chunk_namespace(NAMESPACE); let message = chunk.encode(); let signature = sample_scheme(0).sign(chunk_namespace.as_ref(), &message); // Create the node without a parent - should fail to decode let encoded = Node::::new(chunk, signature, None).encode(); // This should error because non-genesis nodes must have parents let cfg = fixture.schemes[0].certificate_codec_config(); let result = Node::::read_cfg(&mut encoded.as_ref(), &cfg); assert!(result.is_err()); } #[test] fn test_node_non_genesis_without_parent_fails() { node_non_genesis_without_parent_fails(ed25519::fixture); node_non_genesis_without_parent_fails(bls12381_multisig::fixture::); node_non_genesis_without_parent_fails(bls12381_multisig::fixture::); node_non_genesis_without_parent_fails(bls12381_threshold::fixture::); node_non_genesis_without_parent_fails(bls12381_threshold::fixture::); } fn node_genesis_with_parent_panics(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); // Try to create a genesis node (height 0) with a parent - should panic in Node::new let public_key = sample_scheme(0).public_key(); let chunk = Chunk::new(public_key.clone(), 0, sample_digest(1)); let chunk_namespace = chunk_namespace(NAMESPACE); let message = chunk.encode(); let signature = sample_scheme(0).sign(chunk_namespace.as_ref(), &message); // Generate a valid parent certificate let parent_chunk = Chunk::new(public_key, 0, sample_digest(0)); let parent_epoch = Epoch::new(5); let parent_ctx = AckSubject { chunk: &parent_chunk, epoch: parent_epoch, }; let parent_attestations: Vec<_> = fixture.schemes[..quorum(4) as usize] .iter() .map(|scheme| { scheme .sign::(&ack_namespace(NAMESPACE), parent_ctx.clone()) .unwrap() }) .collect(); let parent_certificate = fixture.schemes[0] .assemble(parent_attestations) .expect("Should assemble certificate"); let parent = Parent::::new(sample_digest(0), parent_epoch, parent_certificate); // Create invalid node (genesis with parent), encode it, then decode - should panic on unwrap let encoded = Node::::new(chunk, signature, Some(parent)).encode(); let cfg = fixture.schemes[0].certificate_codec_config(); Node::::read_cfg(&mut encoded.as_ref(), &cfg).unwrap(); } #[test] fn test_node_genesis_with_parent_panics() { assert!(catch_unwind(|| node_genesis_with_parent_panics(ed25519::fixture)).is_err()); assert!(catch_unwind(|| node_genesis_with_parent_panics( bls12381_multisig::fixture:: )) .is_err()); assert!(catch_unwind(|| node_genesis_with_parent_panics( bls12381_multisig::fixture:: )) .is_err()); assert!(catch_unwind(|| node_genesis_with_parent_panics( bls12381_threshold::fixture:: )) .is_err()); assert!(catch_unwind(|| node_genesis_with_parent_panics( bls12381_threshold::fixture:: )) .is_err()); } fn node_non_genesis_without_parent_panics(fixture: F) where S: Scheme, F: FnOnce(&mut StdRng, u32) -> Fixture, { let fixture = setup(4, fixture); // Try to create a non-genesis node (height > 0) without a parent - should panic on decode let public_key = sample_scheme(0).public_key(); let chunk = Chunk::new(public_key, 1, sample_digest(1)); let chunk_namespace = chunk_namespace(NAMESPACE); let message = chunk.encode(); let signature = sample_scheme(0).sign(chunk_namespace.as_ref(), &message); // Create invalid node (non-genesis without parent), encode it, then decode - should panic on unwrap let encoded = Node::::new(chunk, signature, None).encode(); let cfg = fixture.schemes[0].certificate_codec_config(); Node::::read_cfg(&mut encoded.as_ref(), &cfg).unwrap(); } #[test] fn test_node_non_genesis_without_parent_panics() { assert!(catch_unwind(|| node_non_genesis_without_parent_panics(ed25519::fixture)).is_err()); assert!(catch_unwind(|| node_non_genesis_without_parent_panics( bls12381_multisig::fixture:: )) .is_err()); assert!(catch_unwind(|| node_non_genesis_without_parent_panics( bls12381_multisig::fixture:: )) .is_err()); assert!(catch_unwind(|| node_non_genesis_without_parent_panics( bls12381_threshold::fixture:: )) .is_err()); assert!(catch_unwind(|| node_non_genesis_without_parent_panics( bls12381_threshold::fixture:: )) .is_err()); } #[cfg(feature = "arbitrary")] mod conformance { use super::*; use crate::ordered_broadcast::scheme::bls12381_threshold; use commonware_codec::conformance::CodecConformance; type Scheme = bls12381_threshold::Scheme; commonware_conformance::conformance_tests! { CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance>, } } }