Skip to main content

miden_protocol/block/
header.rs

1use alloc::vec::Vec;
2
3use crate::block::{BlockNumber, BlockSignatures, SignatureVerificationError, ValidatorConfig};
4use crate::protocol_config::NextProtocolConfig;
5use crate::utils::serde::{
6    ByteReader,
7    ByteWriter,
8    Deserializable,
9    DeserializationError,
10    Serializable,
11};
12use crate::{Felt, Hasher, Word, ZERO};
13
14// BLOCK HEADER
15// ================================================================================================
16
17/// The header of a block. It contains metadata about the block, commitments to the current state of
18/// the chain and the hash of the proof that attests to the integrity of the chain.
19///
20/// A block header includes the following fields:
21///
22/// - `version` specifies the version of the block header itself. It changes when fields are added
23///   to or removed from the header, and when the scheme behind one of the header's commitments
24///   changes, for example the structure of the SMTs or of the MMR.
25/// - `timestamp` is the time when the block was created, in seconds since UNIX epoch. The u32 is
26///   sufficient to represent timestamps up to year 2106.
27/// - `block_num` is a unique sequential number of the current block.
28/// - `prev_block_commitment` is the hash of the previous block header.
29/// - `chain_commitment` is a commitment to an MMR of the entire chain where each block is a leaf.
30/// - `account_root` is a commitment to account database.
31/// - `nullifier_root` is a commitment to the nullifier database.
32/// - `note_root` is a commitment to all notes created in the current block.
33/// - `tx_commitment` is a commitment to the set of transaction IDs which affected accounts in the
34///   block.
35/// - `validator_config` is the set of validator public keys authorized to sign the *next* block,
36///   together with the quorum, see [`ValidatorConfig`] for more details.
37/// - `fee_parameters` are the parameters defining the base fees, see [`FeeParameters`] for more
38///   details.
39/// - `protocol_config_commitment` is the commitment to the chain's
40///   [`ProtocolConfig`](crate::protocol_config::ProtocolConfig).
41/// - `next_protocol_config` is the scheduled protocol upgrade, if any, see [`NextProtocolConfig`]
42///   for more details.
43/// - `sub_commitment` is a sequential hash of all fields except the note_root.
44/// - `commitment` is a 2-to-1 hash of the sub_commitment and the note_root.
45#[derive(Debug, Eq, PartialEq, Clone)]
46pub struct BlockHeader {
47    version: u8,
48    timestamp: u32,
49    block_num: BlockNumber,
50    prev_block_commitment: Word,
51    chain_commitment: Word,
52    account_root: Word,
53    nullifier_root: Word,
54    note_root: Word,
55    tx_commitment: Word,
56    validator_config: ValidatorConfig,
57    fee_parameters: FeeParameters,
58    protocol_config_commitment: Word,
59    next_protocol_config: Option<NextProtocolConfig>,
60    sub_commitment: Word,
61    commitment: Word,
62}
63
64impl BlockHeader {
65    // CONSTANTS
66    // --------------------------------------------------------------------------------------------
67
68    /// Version 1 of the block header.
69    ///
70    /// It is encoded using 8 bits.
71    const VERSION_1: u8 = 1;
72
73    /// The number of field elements in the preimage of [`BlockHeader::sub_commitment`].
74    const NUM_SUB_COMMITMENT_ELEMENTS: usize = 40;
75
76    // CONSTRUCTORS
77    // --------------------------------------------------------------------------------------------
78
79    /// Creates a new block header.
80    #[allow(clippy::too_many_arguments)]
81    pub fn new(
82        prev_block_commitment: Word,
83        block_num: BlockNumber,
84        chain_commitment: Word,
85        account_root: Word,
86        nullifier_root: Word,
87        note_root: Word,
88        tx_commitment: Word,
89        validator_config: ValidatorConfig,
90        fee_parameters: FeeParameters,
91        protocol_config_commitment: Word,
92        next_protocol_config: Option<NextProtocolConfig>,
93        timestamp: u32,
94    ) -> Self {
95        let version = Self::VERSION_1;
96
97        let sub_elements = Self::to_sub_elements(
98            version,
99            prev_block_commitment,
100            block_num,
101            chain_commitment,
102            account_root,
103            nullifier_root,
104            tx_commitment,
105            &validator_config,
106            &fee_parameters,
107            protocol_config_commitment,
108            next_protocol_config.as_ref(),
109            timestamp,
110        );
111        let sub_commitment = Hasher::hash_elements(&sub_elements);
112
113        // The sub commitment is merged with the note_root - hash(sub_commitment, note_root) to
114        // produce the final hash. This is done to make the note_root easily accessible
115        // without having to unhash the entire header. Having the note_root easily
116        // accessible is useful when authenticating notes.
117        let commitment = Hasher::merge(&[sub_commitment, note_root]);
118
119        Self {
120            version,
121            timestamp,
122            block_num,
123            prev_block_commitment,
124            chain_commitment,
125            account_root,
126            nullifier_root,
127            note_root,
128            tx_commitment,
129            validator_config,
130            fee_parameters,
131            protocol_config_commitment,
132            next_protocol_config,
133            sub_commitment,
134            commitment,
135        }
136    }
137
138    // ACCESSORS
139    // --------------------------------------------------------------------------------------------
140
141    /// Returns the block version.
142    pub fn version(&self) -> u8 {
143        self.version
144    }
145
146    /// Returns the commitment of the block header.
147    pub fn commitment(&self) -> Word {
148        self.commitment
149    }
150
151    /// Returns the sub commitment of the block header.
152    ///
153    /// The sub commitment is a sequential hash of all block header fields except the note root.
154    /// This is used in the block commitment computation which is a 2-to-1 hash of the sub
155    /// commitment and the note root [hash(sub_commitment, note_root)]. This procedure is used to
156    /// make the note root easily accessible without having to unhash the entire header.
157    pub fn sub_commitment(&self) -> Word {
158        self.sub_commitment
159    }
160
161    /// Returns the commitment to the previous block header.
162    pub fn prev_block_commitment(&self) -> Word {
163        self.prev_block_commitment
164    }
165
166    /// Returns the block number.
167    pub fn block_num(&self) -> BlockNumber {
168        self.block_num
169    }
170
171    /// Returns the epoch to which this block belongs.
172    ///
173    /// This is the block number shifted right by [`BlockNumber::EPOCH_LENGTH_EXPONENT`].
174    pub fn block_epoch(&self) -> u16 {
175        self.block_num.block_epoch()
176    }
177
178    /// Returns the chain commitment.
179    pub fn chain_commitment(&self) -> Word {
180        self.chain_commitment
181    }
182
183    /// Returns the account database root.
184    pub fn account_root(&self) -> Word {
185        self.account_root
186    }
187
188    /// Returns the nullifier database root.
189    pub fn nullifier_root(&self) -> Word {
190        self.nullifier_root
191    }
192
193    /// Returns the note root.
194    pub fn note_root(&self) -> Word {
195        self.note_root
196    }
197
198    /// Returns the validator configuration authorized to sign the *next* block.
199    ///
200    /// A block's signatures are verified against the `validator_config` committed to by its parent
201    /// block, not against this field. See the [`BlockHeader`] docs for details.
202    pub fn validator_config(&self) -> &ValidatorConfig {
203        &self.validator_config
204    }
205
206    /// Returns the commitment to all transactions in this block.
207    ///
208    /// The commitment is computed as sequential hash of (`transaction_id`, `account_id`) tuples.
209    /// This makes it possible for the verifier to link transaction IDs to the accounts which
210    /// they were executed against.
211    pub fn tx_commitment(&self) -> Word {
212        self.tx_commitment
213    }
214
215    /// Returns a reference to the [`FeeParameters`] in this header.
216    pub fn fee_parameters(&self) -> &FeeParameters {
217        &self.fee_parameters
218    }
219
220    /// Returns the commitment to the chain's
221    /// [`ProtocolConfig`](crate::protocol_config::ProtocolConfig).
222    pub fn protocol_config_commitment(&self) -> Word {
223        self.protocol_config_commitment
224    }
225
226    /// Returns the protocol upgrade scheduled by this block, if any.
227    pub fn next_protocol_config(&self) -> Option<&NextProtocolConfig> {
228        self.next_protocol_config.as_ref()
229    }
230
231    /// Returns the commitment to the scheduled protocol upgrade, or [`Word::empty`] if no upgrade
232    /// is scheduled.
233    pub fn next_protocol_config_commitment(&self) -> Word {
234        Self::compute_next_protocol_config_commitment(self.next_protocol_config.as_ref())
235    }
236
237    /// Returns the timestamp at which the block was created, in seconds since UNIX epoch.
238    pub fn timestamp(&self) -> u32 {
239        self.timestamp
240    }
241
242    /// Returns the block number of the epoch block to which this block belongs.
243    pub fn epoch_block_num(&self) -> BlockNumber {
244        BlockNumber::from_epoch(self.block_epoch())
245    }
246
247    // ELEMENT ENCODING
248    // --------------------------------------------------------------------------------------------
249
250    /// Returns this header as a sequence of field elements.
251    ///
252    /// The element layout is:
253    ///
254    /// ```text
255    /// [
256    ///     [version, block_num, timestamp, 0],
257    ///     PREV_BLOCK_COMMITMENT,
258    ///     CHAIN_COMMITMENT,
259    ///     ACCOUNT_ROOT,
260    ///     NULLIFIER_ROOT,
261    ///     TX_COMMITMENT,
262    ///     PROTOCOL_CONFIG_COMMITMENT,
263    ///     VALIDATOR_CONFIG_COMMITMENT,
264    ///     NEXT_PROTOCOL_CONFIG_COMMITMENT,
265    ///     [verification_base_fee, 0, 0, 0],
266    ///     NOTE_ROOT,
267    /// ]
268    /// ```
269    ///
270    /// This is the canonical encoding of a header. It is the layout of the kernel's block data
271    /// memory section and the order in which the header is provided to the kernel, so keep it in
272    /// sync with the kernel's `process_block_data` procedure.
273    ///
274    /// Note that [`BlockHeader::commitment`] is *not* the sequential hash of these elements: the
275    /// note root is hashed separately so that it stays accessible without unhashing the whole
276    /// header. All elements but the trailing note root form the preimage of
277    /// [`BlockHeader::sub_commitment`].
278    pub fn to_elements(&self) -> Vec<Felt> {
279        let mut elements = Self::to_sub_elements(
280            self.version,
281            self.prev_block_commitment,
282            self.block_num,
283            self.chain_commitment,
284            self.account_root,
285            self.nullifier_root,
286            self.tx_commitment,
287            &self.validator_config,
288            &self.fee_parameters,
289            self.protocol_config_commitment,
290            self.next_protocol_config.as_ref(),
291            self.timestamp,
292        );
293        elements.extend_from_slice(self.note_root.as_elements());
294        elements
295    }
296
297    // VALIDATION
298    // --------------------------------------------------------------------------------------------
299
300    /// Validates that `parent` precedes and authorizes this block.
301    ///
302    /// The `signatures` are positional with respect to the validator set committed to by `parent`
303    /// (see [`ValidatorConfig`]): the signature at index `i` must verify against the parent's
304    /// validator key at index `i`. Every validator in the parent's set must have signed.
305    ///
306    /// # Errors
307    ///
308    /// Returns an error if the block is the genesis block (no parent), the parent's number or
309    /// commitment do not match, the number of signatures does not match the parent's validator
310    /// count, or a signature does not verify against its validator key.
311    pub(crate) fn validate_against_parent(
312        &self,
313        parent: &BlockHeader,
314        signatures: &BlockSignatures,
315    ) -> Result<(), ParentValidationError> {
316        // Block 0 does not have a parent.
317        let Some(expected_parent_num) = self.block_num().checked_sub(1) else {
318            return Err(ParentValidationError::GenesisBlockHasNoParent {
319                parent: parent.block_num(),
320            });
321        };
322
323        // Check block numbers.
324        if expected_parent_num != parent.block_num() {
325            return Err(ParentValidationError::ParentNumberMismatch {
326                expected: expected_parent_num,
327                parent: parent.block_num(),
328            });
329        }
330
331        // Check commitments.
332        let expected_prev_commitment = self.prev_block_commitment();
333        if expected_prev_commitment != parent.commitment() {
334            return Err(ParentValidationError::ParentCommitmentMismatch {
335                expected: expected_prev_commitment,
336                parent: parent.commitment(),
337            });
338        }
339
340        // Verify the signatures positionally against the parent's validator set using the shared,
341        // canonical verifier, which also enforces that every validator signed.
342        signatures
343            .verify_against(self.commitment(), parent.validator_config())
344            .map_err(|err| match err {
345                SignatureVerificationError::SignatureCountMismatch { expected, actual } => {
346                    ParentValidationError::SignatureCountMismatch { expected, actual }
347                },
348                SignatureVerificationError::InvalidSignatureAtPosition { position } => {
349                    ParentValidationError::InvalidSignatureAtPosition { position }
350                },
351            })?;
352
353        Ok(())
354    }
355
356    // HELPERS
357    // --------------------------------------------------------------------------------------------
358
359    /// Returns the preimage of the block's sub commitment as a sequence of field elements.
360    ///
361    /// This covers every header field except the note root. See [`BlockHeader::to_elements`] for
362    /// the element layout.
363    #[allow(clippy::too_many_arguments)]
364    fn to_sub_elements(
365        version: u8,
366        prev_block_commitment: Word,
367        block_num: BlockNumber,
368        chain_commitment: Word,
369        account_root: Word,
370        nullifier_root: Word,
371        tx_commitment: Word,
372        validator_config: &ValidatorConfig,
373        fee_parameters: &FeeParameters,
374        protocol_config_commitment: Word,
375        next_protocol_config: Option<&NextProtocolConfig>,
376        timestamp: u32,
377    ) -> Vec<Felt> {
378        let mut elements: Vec<Felt> = Vec::with_capacity(Self::NUM_SUB_COMMITMENT_ELEMENTS);
379        elements.extend([Felt::from(version), block_num.into(), Felt::from(timestamp), ZERO]);
380        elements.extend_from_slice(prev_block_commitment.as_elements());
381        elements.extend_from_slice(chain_commitment.as_elements());
382        elements.extend_from_slice(account_root.as_elements());
383        elements.extend_from_slice(nullifier_root.as_elements());
384        elements.extend_from_slice(tx_commitment.as_elements());
385        elements.extend_from_slice(protocol_config_commitment.as_elements());
386        elements.extend(validator_config.to_commitment());
387        elements.extend(Self::compute_next_protocol_config_commitment(next_protocol_config));
388        elements.extend([Felt::from(fee_parameters.verification_base_fee()), ZERO, ZERO, ZERO]);
389        elements
390    }
391
392    /// Returns the commitment to `next_protocol_config`, or [`Word::empty`] if no upgrade is
393    /// scheduled.
394    fn compute_next_protocol_config_commitment(
395        next_protocol_config: Option<&NextProtocolConfig>,
396    ) -> Word {
397        next_protocol_config.map_or(Word::empty(), NextProtocolConfig::to_commitment)
398    }
399
400    // TEST HELPERS
401    // --------------------------------------------------------------------------------------------
402
403    /// Builds a minimal block header with a controllable block number, previous-block commitment,
404    /// and validator key set.
405    ///
406    /// The remaining roots are zeroed except the note root and transaction commitment, which match
407    /// the empty [`BlockBody`](super::BlockBody) the block tests pair this header with, so the
408    /// self-consistency checks in `SignedBlock::validate` and  `ProvenBlock::validate` pass.
409    #[cfg(test)]
410    pub(crate) fn new_dummy(
411        block_num: u32,
412        prev_block_commitment: Word,
413        validator_config: ValidatorConfig,
414    ) -> Self {
415        use crate::block::{BlockBody, FeeParameters};
416        use crate::transaction::OrderedTransactionHeaders;
417
418        let body = BlockBody::new_unchecked(
419            Vec::new(),
420            Vec::new(),
421            Vec::new(),
422            OrderedTransactionHeaders::new_unchecked(Vec::new()),
423        );
424        let note_root = body.compute_block_note_tree().root();
425        let tx_commitment = body.transactions().commitment();
426
427        BlockHeader::new(
428            prev_block_commitment,
429            BlockNumber::from(block_num),
430            Word::empty(),
431            Word::empty(),
432            Word::empty(),
433            note_root,
434            tx_commitment,
435            validator_config,
436            FeeParameters::new(500),
437            Word::empty(),
438            None,
439            0,
440        )
441    }
442}
443
444// SERIALIZATION
445// ================================================================================================
446
447impl Serializable for BlockHeader {
448    fn write_into<W: ByteWriter>(&self, target: &mut W) {
449        let Self {
450            version,
451            prev_block_commitment,
452            block_num,
453            chain_commitment,
454            account_root,
455            nullifier_root,
456            note_root,
457            tx_commitment,
458            validator_config,
459            fee_parameters,
460            protocol_config_commitment,
461            next_protocol_config,
462            timestamp,
463            // Don't serialize sub commitment and commitment as they can be derived.
464            sub_commitment: _,
465            commitment: _,
466        } = self;
467
468        version.write_into(target);
469        prev_block_commitment.write_into(target);
470        block_num.write_into(target);
471        chain_commitment.write_into(target);
472        account_root.write_into(target);
473        nullifier_root.write_into(target);
474        note_root.write_into(target);
475        tx_commitment.write_into(target);
476        validator_config.write_into(target);
477        fee_parameters.write_into(target);
478        protocol_config_commitment.write_into(target);
479        next_protocol_config.write_into(target);
480        timestamp.write_into(target);
481    }
482}
483
484impl Deserializable for BlockHeader {
485    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
486        let version = u8::read_from(source)?;
487
488        if version != Self::VERSION_1 {
489            return Err(DeserializationError::InvalidValue(format!(
490                "block version is {} but only version {} is supported",
491                version,
492                Self::VERSION_1,
493            )));
494        }
495
496        let prev_block_commitment = source.read()?;
497        let block_num = source.read()?;
498        let chain_commitment = source.read()?;
499        let account_root = source.read()?;
500        let nullifier_root = source.read()?;
501        let note_root = source.read()?;
502        let tx_commitment = source.read()?;
503        let validator_config = source.read()?;
504        let fee_parameters = source.read()?;
505        let protocol_config_commitment = source.read()?;
506        let next_protocol_config = <Option<NextProtocolConfig>>::read_from(source)?;
507        let timestamp = source.read()?;
508
509        Ok(Self::new(
510            prev_block_commitment,
511            block_num,
512            chain_commitment,
513            account_root,
514            nullifier_root,
515            note_root,
516            tx_commitment,
517            validator_config,
518            fee_parameters,
519            protocol_config_commitment,
520            next_protocol_config,
521            timestamp,
522        ))
523    }
524}
525
526// FEE PARAMETERS
527// ================================================================================================
528
529/// The fee-related parameters of a block.
530///
531/// This defines how to compute the fees of a transaction. Which asset fees are paid in is defined
532/// by [`ProtocolConfig::fee_asset_id`](crate::protocol_config::ProtocolConfig::fee_asset_id).
533#[derive(Debug, Clone, PartialEq, Eq)]
534pub struct FeeParameters {
535    /// The base fee (in base units) capturing the cost for the verification of a transaction.
536    verification_base_fee: u32,
537}
538
539impl FeeParameters {
540    // CONSTRUCTORS
541    // --------------------------------------------------------------------------------------------
542
543    /// Creates [`FeeParameters`] from the provided inputs.
544    pub fn new(verification_base_fee: u32) -> Self {
545        Self { verification_base_fee }
546    }
547
548    // PUBLIC ACCESSORS
549    // --------------------------------------------------------------------------------------------
550
551    /// Returns the base fee capturing the cost for the verification of a transaction.
552    pub fn verification_base_fee(&self) -> u32 {
553        self.verification_base_fee
554    }
555}
556
557impl Serializable for FeeParameters {
558    fn write_into<W: ByteWriter>(&self, target: &mut W) {
559        let Self { verification_base_fee } = self;
560
561        verification_base_fee.write_into(target);
562    }
563}
564
565impl Deserializable for FeeParameters {
566    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
567        let verification_base_fee = source.read()?;
568
569        Ok(Self::new(verification_base_fee))
570    }
571}
572
573// PARENT VALIDATION ERROR
574// ================================================================================================
575
576/// Error returned when a block fails validation against its parent block.
577///
578/// This is the shared, block-type-agnostic error produced by
579/// [`BlockHeader::validate_against_parent`]. Each block type maps it into its own error enum via
580/// `From`, which preserves that type's specific error messages.
581#[derive(Debug)]
582pub(crate) enum ParentValidationError {
583    SignatureCountMismatch {
584        expected: usize,
585        actual: usize,
586    },
587    InvalidSignatureAtPosition {
588        position: usize,
589    },
590    ParentNumberMismatch {
591        expected: BlockNumber,
592        parent: BlockNumber,
593    },
594    ParentCommitmentMismatch {
595        expected: Word,
596        parent: Word,
597    },
598    GenesisBlockHasNoParent {
599        parent: BlockNumber,
600    },
601}
602
603// TESTS
604// ================================================================================================
605
606#[cfg(test)]
607mod tests {
608    use assert_matches::assert_matches;
609    use miden_core::Word;
610    use miden_crypto::rand::test_utils::rand_value;
611
612    use super::*;
613
614    #[test]
615    fn block_header_deserialization_rejects_unsupported_version() {
616        let error = BlockHeader::read_from_bytes(&[0]).unwrap_err();
617
618        assert_matches!(error, DeserializationError::InvalidValue(message) => {
619            assert!(message.contains("block version is 0"));
620        });
621    }
622
623    #[test]
624    fn test_serde() {
625        let chain_commitment = rand_value::<Word>();
626        let note_root = rand_value::<Word>();
627        let header = BlockHeader::mock(0, Some(chain_commitment), Some(note_root), &[]);
628        let serialized = header.to_bytes();
629        let deserialized = BlockHeader::read_from_bytes(&serialized).unwrap();
630
631        assert_eq!(deserialized, header);
632    }
633
634    /// Returns `header` with a protocol upgrade scheduled for block 42.
635    fn with_scheduled_upgrade(header: &BlockHeader) -> BlockHeader {
636        let next_protocol_config =
637            NextProtocolConfig::new(BlockNumber::from(42u32), rand_value::<Word>()).unwrap();
638
639        BlockHeader::new(
640            header.prev_block_commitment(),
641            header.block_num(),
642            header.chain_commitment(),
643            header.account_root(),
644            header.nullifier_root(),
645            header.note_root(),
646            header.tx_commitment(),
647            header.validator_config().clone(),
648            header.fee_parameters().clone(),
649            header.protocol_config_commitment(),
650            Some(next_protocol_config),
651            header.timestamp(),
652        )
653    }
654
655    #[test]
656    fn scheduled_upgrade_round_trips_and_changes_the_commitment() {
657        let without_upgrade = BlockHeader::mock(0, None, None, &[]);
658        let with_upgrade = with_scheduled_upgrade(&without_upgrade);
659
660        assert_eq!(without_upgrade.next_protocol_config_commitment(), Word::empty());
661        assert_ne!(with_upgrade.commitment(), without_upgrade.commitment());
662
663        let deserialized = BlockHeader::read_from_bytes(&with_upgrade.to_bytes()).unwrap();
664        assert_eq!(deserialized, with_upgrade);
665    }
666
667    /// The header's element encoding is the single definition of the block data layout, used by
668    /// both the commitment and the kernel's advice inputs. This checks the two stay in agreement.
669    #[test]
670    fn to_elements_is_the_sub_commitment_preimage_plus_the_note_root() {
671        let header = BlockHeader::mock(0, None, None, &[]);
672        let elements = header.to_elements();
673
674        let (sub_elements, note_root) = elements.split_at(BlockHeader::NUM_SUB_COMMITMENT_ELEMENTS);
675        assert_eq!(Hasher::hash_elements(sub_elements), header.sub_commitment());
676        assert_eq!(note_root, header.note_root().as_elements());
677    }
678
679    /// Builds a child of `parent` committing a fresh validator set of `next_count` validators as
680    /// the signer of the *next* block.
681    fn child_of(parent: &BlockHeader, child_num: u32, next_count: usize) -> BlockHeader {
682        let (_, next_keys) = ValidatorConfig::random_with_signers(next_count);
683        BlockHeader::new_dummy(child_num, parent.commitment(), next_keys)
684    }
685
686    #[test]
687    fn validate_against_parent_accepts_all_signatures() {
688        let (signers, keys) = ValidatorConfig::random_with_signers(5);
689        let parent = BlockHeader::new_dummy(0, Word::empty(), keys.clone());
690        let child = child_of(&parent, 1, 5);
691        let signatures = keys.sign_all(&signers, child.commitment());
692
693        child.validate_against_parent(&parent, &signatures).unwrap();
694    }
695
696    #[test]
697    fn validate_against_parent_accepts_single_validator() {
698        let (signers, keys) = ValidatorConfig::random_with_signers(1);
699        let parent = BlockHeader::new_dummy(0, Word::empty(), keys.clone());
700        let child = child_of(&parent, 1, 1);
701        let signatures = keys.sign_all(&signers, child.commitment());
702
703        child.validate_against_parent(&parent, &signatures).unwrap();
704    }
705
706    #[test]
707    fn validate_against_parent_rejects_incomplete_signatures() {
708        let (signers, keys) = ValidatorConfig::random_with_signers(3);
709        let parent = BlockHeader::new_dummy(0, Word::empty(), keys.clone());
710        let child = child_of(&parent, 1, 3);
711        // Only one of three validators signs, so the resulting set is too short to align
712        // positionally with the parent's validator keys. `BlockSignatures::new` does not check
713        // this -- only `verify_against` (called by `validate_against_parent`) does.
714        let signatures =
715            BlockSignatures::new(alloc::vec![signers[0].sign(child.commitment())]).unwrap();
716
717        let result = child.validate_against_parent(&parent, &signatures);
718        assert!(matches!(
719            result,
720            Err(ParentValidationError::SignatureCountMismatch { expected: 3, actual: 1 })
721        ));
722    }
723
724    #[test]
725    fn validate_against_parent_rejects_signature_count_mismatch() {
726        let (_, keys) = ValidatorConfig::random_with_signers(3);
727        let parent = BlockHeader::new_dummy(0, Word::empty(), keys.clone());
728        let child = child_of(&parent, 1, 3);
729
730        // A block signed by a validator set of a different size cannot align positionally with the
731        // parent's committed set. Deserialization does not check this, so build it directly.
732        let (other_signers, other_keys) = ValidatorConfig::random_with_signers(4);
733        let signatures = other_keys.sign_all(&other_signers, child.commitment());
734        let bytes = signatures.to_bytes();
735        let deserialized = BlockSignatures::read_from_bytes(&bytes).unwrap();
736
737        let result = child.validate_against_parent(&parent, &deserialized);
738        assert!(matches!(
739            result,
740            Err(ParentValidationError::SignatureCountMismatch { expected: 3, actual: 4 })
741        ));
742    }
743
744    #[test]
745    fn validate_against_parent_rejects_uncommitted_signatures() {
746        let (_, keys) = ValidatorConfig::random_with_signers(3);
747        let parent = BlockHeader::new_dummy(0, Word::empty(), keys.clone());
748        let child = child_of(&parent, 1, 3);
749
750        // The child is signed by a full, valid validator set of the same size the parent never
751        // committed, so the signatures do not verify against the parent's validator keys.
752        let (impostor_signers, impostor_keys) = ValidatorConfig::random_with_signers(3);
753        let signatures = impostor_keys.sign_all(&impostor_signers, child.commitment());
754
755        let result = child.validate_against_parent(&parent, &signatures);
756        assert!(matches!(result, Err(ParentValidationError::InvalidSignatureAtPosition { .. })));
757    }
758
759    #[test]
760    fn validate_against_parent_rejects_genesis() {
761        let (signers, keys) = ValidatorConfig::random_with_signers(3);
762        let parent = BlockHeader::new_dummy(0, Word::empty(), keys.clone());
763        // Block 0 has no parent to anchor against.
764        let child = BlockHeader::new_dummy(
765            0,
766            parent.commitment(),
767            ValidatorConfig::random_with_signers(3).1,
768        );
769        let signatures = keys.sign_all(&signers, child.commitment());
770
771        let result = child.validate_against_parent(&parent, &signatures);
772        assert!(matches!(result, Err(ParentValidationError::GenesisBlockHasNoParent { .. })));
773    }
774
775    #[test]
776    fn validate_against_parent_rejects_wrong_parent_number() {
777        let (signers, keys) = ValidatorConfig::random_with_signers(3);
778        let parent = BlockHeader::new_dummy(0, Word::empty(), keys.clone());
779        // Child claims to be block 2, but the parent is block 0.
780        let child = child_of(&parent, 2, 3);
781        let signatures = keys.sign_all(&signers, child.commitment());
782
783        let result = child.validate_against_parent(&parent, &signatures);
784        assert!(matches!(result, Err(ParentValidationError::ParentNumberMismatch { .. })));
785    }
786
787    #[test]
788    fn validate_against_parent_rejects_wrong_parent_commitment() {
789        let (signers, keys) = ValidatorConfig::random_with_signers(3);
790        let parent = BlockHeader::new_dummy(0, Word::empty(), keys.clone());
791        // Child does not link to the parent's commitment.
792        let child =
793            BlockHeader::new_dummy(1, Word::empty(), ValidatorConfig::random_with_signers(3).1);
794        let signatures = keys.sign_all(&signers, child.commitment());
795
796        let result = child.validate_against_parent(&parent, &signatures);
797        assert!(matches!(result, Err(ParentValidationError::ParentCommitmentMismatch { .. })));
798    }
799}