Skip to main content

dig_block/types/
header.rs

1//! `L2BlockHeader` — independently hashable L2 block metadata and commitments.
2//!
3//! **Requirements:**
4//! - [BLK-001](docs/requirements/domains/block_types/specs/BLK-001.md) — field groups /
5//!   [NORMATIVE § BLK-001](docs/requirements/domains/block_types/NORMATIVE.md#blk-001-l2blockheader-struct)
6//! - [BLK-002](docs/requirements/domains/block_types/specs/BLK-002.md) — constructors /
7//!   [NORMATIVE § BLK-002](docs/requirements/domains/block_types/NORMATIVE.md#blk-002-l2blockheader-constructors)
8//! - [BLK-007](docs/requirements/domains/block_types/specs/BLK-007.md) — version auto-detection /
9//!   [NORMATIVE](docs/requirements/domains/block_types/NORMATIVE.md) (BLK-007)
10//! - [SVL-001](docs/requirements/domains/structural_validation/specs/SVL-001.md) — header `version` vs height / DFSP activation ([`L2BlockHeader::validate`])
11//! - [SVL-002](docs/requirements/domains/structural_validation/specs/SVL-002.md) — DFSP roots must be [`EMPTY_ROOT`] before activation ([`L2BlockHeader::validate_with_dfsp_activation`])
12//! - [SVL-003](docs/requirements/domains/structural_validation/specs/SVL-003.md) — declared [`L2BlockHeader::total_cost`] / [`L2BlockHeader::block_size`] vs protocol caps ([`crate::MAX_COST_PER_BLOCK`], [`crate::MAX_BLOCK_SIZE`])
13//! - [SVL-004](docs/requirements/domains/structural_validation/specs/SVL-004.md) — [`L2BlockHeader::timestamp`] vs wall clock + [`crate::MAX_FUTURE_TIMESTAMP_SECONDS`]; production [`validate`]/[`L2BlockHeader::validate_with_dfsp_activation`], tests [`L2BlockHeader::validate_with_dfsp_activation_at_unix`]
14//! - [HSH-001](docs/requirements/domains/hashing/specs/HSH-001.md) — header `hash()` (SPEC §3.1 field order;
15//!   preimage length [`L2BlockHeader::HASH_PREIMAGE_LEN`])
16//! - [SER-002](docs/requirements/domains/serialization/specs/SER-002.md) — [`L2BlockHeader::to_bytes`] / [`L2BlockHeader::from_bytes`] (SPEC §8.2; bincode + [`BlockError::InvalidData`](crate::BlockError::InvalidData) on decode)
17//! - [SER-003](docs/requirements/domains/serialization/specs/SER-003.md) — [`L2BlockHeader::genesis`] deterministic bootstrap (SPEC §8.3; see NORMATIVE § SER-003)
18//! - [SPEC §2.2](docs/resources/SPEC.md), [SPEC §8.3 Genesis](docs/resources/SPEC.md#83-genesis-block)
19//!
20//! ## Usage
21//!
22//! Prefer [`L2BlockHeader::new`], [`L2BlockHeader::new_with_collateral`], [`L2BlockHeader::new_with_l1_proofs`],
23//! or [`L2BlockHeader::genesis`] so **`version` is never caller-supplied** (auto-detected from `height`;
24//! shared rules in [`L2BlockHeader::protocol_version_for_height`] and
25//! [`L2BlockHeader::protocol_version_for_height_with_activation`] (BLK-007). Production code
26//! that needs wall-clock timestamps should set `timestamp` after `new()` or use [`crate::builder::BlockBuilder`]
27//! (BLD-005): [`L2BlockHeader::new`] leaves `timestamp` at **0** per SPEC’s derived-`new()` parameter list.
28//!
29//! Field order matches SPEC §2.2 so **bincode** layout stays deterministic (SER-001, HSH-001). Canonical
30//! encode/decode helpers live on [`L2BlockHeader::to_bytes`] / [`L2BlockHeader::from_bytes`] (SER-002).
31//!
32//! ## Rationale
33//!
34//! Splitting header from body ([`super::block::L2Block`], BLK-003) mirrors an Ethereum-style header/body
35//! split: attestations and light clients can process headers without deserializing `SpendBundle` payloads.
36//!
37//! ## Decisions
38//!
39//! - **`Bytes32`** and **`Cost`** come from [`crate::primitives`] so this crate has one type identity for
40//!   hashes and CLVM cost (BLK-006).
41//! - **L1 proof anchors** are `Option<Bytes32>`; omitted proofs serialize as `None` (default) per SPEC.
42//! - **DFSP roots** are mandatory `Bytes32` fields; pre-activation they are set to [`crate::EMPTY_ROOT`]
43//!   by constructors / validation (SVL-002), not by the type itself.
44
45use std::time::{SystemTime, UNIX_EPOCH};
46
47use chia_sha2::Sha256;
48use chia_streamable_macro::Streamable;
49use serde::{Deserialize, Serialize};
50
51use crate::constants::{
52    DFSP_ACTIVATION_HEIGHT, EMPTY_ROOT, MAX_BLOCK_SIZE, MAX_COST_PER_BLOCK,
53    MAX_FUTURE_TIMESTAMP_SECONDS, ZERO_HASH,
54};
55use crate::error::BlockError;
56use crate::primitives::{Bytes32, Cost, VERSION_V1, VERSION_V2};
57
58/// DIG L2 block header: identity, Merkle commitments, L1 anchor, metadata, optional L1 proofs, slash
59/// proposal commitments, and DFSP data-layer roots.
60///
61/// Field layout and semantics follow SPEC §2.2 table **Field groups**; keep this definition in sync with
62/// that section when the wire format evolves.
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Streamable)]
64pub struct L2BlockHeader {
65    // ── Core identity ──
66    /// Protocol version (`VERSION_V1` / `VERSION_V2`; BLK-007 selects from height).
67    pub version: u16,
68    /// Block height (genesis = 0).
69    pub height: u64,
70    /// Epoch number.
71    pub epoch: u64,
72    /// Hash of the parent block header.
73    pub parent_hash: Bytes32,
74
75    // ── State commitments ──
76    /// CoinSet / state Merkle root after this block.
77    pub state_root: Bytes32,
78    /// Merkle root of spend-bundle hashes.
79    pub spends_root: Bytes32,
80    /// Merkle root of additions (Chia-style grouping by `puzzle_hash`).
81    pub additions_root: Bytes32,
82    /// Merkle root of removed coin IDs.
83    pub removals_root: Bytes32,
84    /// Merkle root of execution receipts.
85    pub receipts_root: Bytes32,
86
87    // ── L1 anchor ──
88    /// Chia L1 block height this L2 block references.
89    pub l1_height: u32,
90    /// Chia L1 block hash.
91    pub l1_hash: Bytes32,
92
93    // ── Block metadata ──
94    /// Unix timestamp (seconds).
95    pub timestamp: u64,
96    /// Proposer validator index.
97    pub proposer_index: u32,
98    /// Number of spend bundles in the block body.
99    pub spend_bundle_count: u32,
100    /// Aggregate CLVM cost of all spends in the block.
101    pub total_cost: Cost,
102    /// Total fees (value in − value out).
103    pub total_fees: u64,
104    /// Number of coin additions.
105    pub additions_count: u32,
106    /// Number of coin removals.
107    pub removals_count: u32,
108    /// Serialized full block size in bytes (header + body).
109    pub block_size: u32,
110    /// BIP158-style compact block filter hash.
111    pub filter_hash: Bytes32,
112    /// Reserved extension field (SPEC: default `ZERO_HASH` in constructors).
113    pub extension_data: Bytes32,
114
115    // ── L1 proof anchors ──
116    /// Proposer L1 collateral proof coin id.
117    #[serde(default)]
118    pub l1_collateral_coin_id: Option<Bytes32>,
119    /// Network validator collateral set anchor.
120    #[serde(default)]
121    pub l1_reserve_coin_id: Option<Bytes32>,
122    /// Previous epoch finalization proof.
123    #[serde(default)]
124    pub l1_prev_epoch_finalizer_coin_id: Option<Bytes32>,
125    /// Current epoch finalizer state.
126    #[serde(default)]
127    pub l1_curr_epoch_finalizer_coin_id: Option<Bytes32>,
128    /// Network singleton existence proof.
129    #[serde(default)]
130    pub l1_network_coin_id: Option<Bytes32>,
131
132    // ── Slash proposals ──
133    /// Number of slash proposal payloads in the body.
134    pub slash_proposal_count: u32,
135    /// Merkle root over per-proposal hashes.
136    pub slash_proposals_root: Bytes32,
137
138    // ── DFSP data layer roots ──
139    /// Collateral registry sparse Merkle root.
140    pub collateral_registry_root: Bytes32,
141    /// CID lifecycle state machine root.
142    pub cid_state_root: Bytes32,
143    /// Node registry sparse Merkle root.
144    pub node_registry_root: Bytes32,
145    /// Namespace update delta root for this block.
146    pub namespace_update_root: Bytes32,
147    /// DFSP epoch-boundary commitment digest.
148    pub dfsp_finalize_commitment_root: Bytes32,
149}
150
151impl L2BlockHeader {
152    /// Protocol version for `height` given an explicit DFSP activation height (BLK-007).
153    ///
154    /// **Rules:** If `dfsp_activation_height == u64::MAX` (DFSP disabled sentinel), returns [`VERSION_V1`].
155    /// Otherwise returns [`VERSION_V2`] when `height >= dfsp_activation_height`, else [`VERSION_V1`].
156    ///
157    /// **Rationale:** Parameterizing activation height lets tests cover pre/post-fork behavior without
158    /// recompiling [`DFSP_ACTIVATION_HEIGHT`](crate::constants::DFSP_ACTIVATION_HEIGHT). Production code
159    /// should call [`Self::protocol_version_for_height`] instead.
160    #[inline]
161    pub fn protocol_version_for_height_with_activation(
162        height: u64,
163        dfsp_activation_height: u64,
164    ) -> u16 {
165        if dfsp_activation_height == u64::MAX {
166            VERSION_V1
167        } else if height >= dfsp_activation_height {
168            VERSION_V2
169        } else {
170            VERSION_V1
171        }
172    }
173
174    /// Protocol version for `height` using the crate’s [`DFSP_ACTIVATION_HEIGHT`] constant.
175    #[inline]
176    pub fn protocol_version_for_height(height: u64) -> u16 {
177        Self::protocol_version_for_height_with_activation(height, DFSP_ACTIVATION_HEIGHT)
178    }
179
180    /// Sample `SystemTime::now()` as whole Unix seconds for [`Self::validate_with_dfsp_activation`].
181    ///
182    /// **Rationale:** SVL-004 needs a monotonic-ish wall clock; if the host clock is before 1970, validation cannot
183    /// define “future” sensibly — we surface [`BlockError::InvalidData`] rather than panicking.
184    fn unix_secs_wall_clock() -> Result<u64, BlockError> {
185        SystemTime::now()
186            .duration_since(UNIX_EPOCH)
187            .map(|d| d.as_secs())
188            .map_err(|_| {
189                BlockError::InvalidData(
190                    "system clock before UNIX epoch; cannot validate header timestamp".into(),
191                )
192            })
193    }
194
195    /// Tier-1 header checks **SVL-001 through SVL-004** with an explicit DFSP activation height **and** a fixed
196    /// `now_secs` reference for the timestamp bound ([SVL-004](docs/requirements/domains/structural_validation/specs/SVL-004.md)).
197    ///
198    /// **SVL-001 / SPEC §5.1 Step 1:** [`L2BlockHeader::version`] must match [`Self::protocol_version_for_height_with_activation`]
199    /// for [`L2BlockHeader::height`] and `dfsp_activation_height`; else [`BlockError::InvalidVersion`].
200    ///
201    /// **SVL-002 / SPEC §5.1 Step 2:** when `height < dfsp_activation_height`, all five DFSP roots must equal [`EMPTY_ROOT`];
202    /// else [`BlockError::InvalidData`] (fixed message per [SVL-002 spec](docs/requirements/domains/structural_validation/specs/SVL-002.md)).
203    ///
204    /// **SVL-003 / SPEC §5.1 Steps 3–4:** `total_cost > `[`MAX_COST_PER_BLOCK`] ⇒ [`BlockError::CostExceeded`]; then
205    /// `block_size > `[`MAX_BLOCK_SIZE`] ⇒ [`BlockError::TooLarge`] ([SVL-003 spec](docs/requirements/domains/structural_validation/specs/SVL-003.md)).
206    ///
207    /// **SVL-004 / SPEC §5.1 Step 5:** let `max_allowed = now_secs + `[`MAX_FUTURE_TIMESTAMP_SECONDS`]. If
208    /// `timestamp > max_allowed`, reject with [`BlockError::TimestampTooFarInFuture`] (Chia `block_header_validation.py`
209    /// Check 26a analogue). **Strict `>`:** `timestamp == max_allowed` is accepted.
210    ///
211    /// **Usage:** Production should call [`Self::validate`] or [`Self::validate_with_dfsp_activation`] (wall-clock `now`).
212    /// Integration tests call **this** method with a synthetic `now_secs` so boundary arithmetic is deterministic
213    /// ([SVL-004 spec — Implementation Notes](docs/requirements/domains/structural_validation/specs/SVL-004.md)).
214    pub fn validate_with_dfsp_activation_at_unix(
215        &self,
216        dfsp_activation_height: u64,
217        now_secs: u64,
218    ) -> Result<(), BlockError> {
219        let expected =
220            Self::protocol_version_for_height_with_activation(self.height, dfsp_activation_height);
221        if self.version != expected {
222            return Err(BlockError::InvalidVersion {
223                expected,
224                actual: self.version,
225            });
226        }
227        if self.height < dfsp_activation_height {
228            let dfsp_roots = [
229                self.collateral_registry_root,
230                self.cid_state_root,
231                self.node_registry_root,
232                self.namespace_update_root,
233                self.dfsp_finalize_commitment_root,
234            ];
235            for root in &dfsp_roots {
236                if *root != EMPTY_ROOT {
237                    return Err(BlockError::InvalidData(
238                        "DFSP root must be EMPTY_ROOT before activation".into(),
239                    ));
240                }
241            }
242        }
243        // SVL-003: strict `>` so values exactly at the limit pass (spec acceptance + ERR-001 semantics).
244        if self.total_cost > MAX_COST_PER_BLOCK {
245            return Err(BlockError::CostExceeded {
246                cost: self.total_cost,
247                max: MAX_COST_PER_BLOCK,
248            });
249        }
250        if self.block_size > MAX_BLOCK_SIZE {
251            return Err(BlockError::TooLarge {
252                size: self.block_size,
253                max: MAX_BLOCK_SIZE,
254            });
255        }
256        let max_allowed = now_secs.saturating_add(MAX_FUTURE_TIMESTAMP_SECONDS);
257        if self.timestamp > max_allowed {
258            return Err(BlockError::TimestampTooFarInFuture {
259                timestamp: self.timestamp,
260                max_allowed,
261            });
262        }
263        Ok(())
264    }
265
266    /// Same as [`Self::validate_with_dfsp_activation_at_unix`] after sampling the host wall clock ([`Self::unix_secs_wall_clock`]).
267    ///
268    /// **Rationale:** Parameterizing `dfsp_activation_height` mirrors SVL-001 so integration tests can inject a finite
269    /// fork height; production uses [`Self::validate`] → [`DFSP_ACTIVATION_HEIGHT`](crate::constants::DFSP_ACTIVATION_HEIGHT).
270    /// With the BLK-005 sentinel `u64::MAX`, every finite `height` satisfies `height < u64::MAX`, so DFSP payloads cannot
271    /// appear on-chain until governance lowers the constant.
272    ///
273    /// **SVL-004:** Uses real `SystemTime` for `now_secs`. For deterministic timestamp tests, call
274    /// [`Self::validate_with_dfsp_activation_at_unix`] directly.
275    pub fn validate_with_dfsp_activation(
276        &self,
277        dfsp_activation_height: u64,
278    ) -> Result<(), BlockError> {
279        let now_secs = Self::unix_secs_wall_clock()?;
280        self.validate_with_dfsp_activation_at_unix(dfsp_activation_height, now_secs)
281    }
282
283    /// Tier 1 header structural validation using crate-wide constants ([SVL-*](docs/requirements/domains/structural_validation/NORMATIVE.md)).
284    ///
285    /// **Current steps:** [SVL-001](docs/requirements/domains/structural_validation/specs/SVL-001.md) (version),
286    /// [SVL-002](docs/requirements/domains/structural_validation/specs/SVL-002.md) (DFSP roots before activation),
287    /// [SVL-003](docs/requirements/domains/structural_validation/specs/SVL-003.md) (cost/size caps on declared header fields),
288    /// [SVL-004](docs/requirements/domains/structural_validation/specs/SVL-004.md) (timestamp vs wall clock + [`MAX_FUTURE_TIMESTAMP_SECONDS`]).
289    pub fn validate(&self) -> Result<(), BlockError> {
290        self.validate_with_dfsp_activation(DFSP_ACTIVATION_HEIGHT)?;
291        Ok(())
292    }
293
294    /// Byte length of the fixed preimage fed to [`Self::hash`] (all 33 rows of [SPEC §3.1](docs/resources/SPEC.md)).
295    ///
296    /// **Accounting:** 20×[`Bytes32`] fields + `u16` + 6×`u64` + 7×`u32` = 640 + 70 = **710** bytes.
297    /// The SPEC prose once said “626 bytes”; summing the §3.1 table yields **710** — this constant is authoritative for code.
298    pub const HASH_PREIMAGE_LEN: usize = 710;
299
300    /// Serialize the exact **710-byte** preimage for [HSH-001](docs/requirements/domains/hashing/specs/HSH-001.md) /
301    /// [SPEC §3.1](docs/resources/SPEC.md) (same order as [`Self::hash`]).
302    ///
303    /// **Usage:** Tests and debug tooling can diff preimages without re-deriving field order; [`Self::hash`] is
304    /// `SHA-256(self.hash_preimage_bytes())`.
305    ///
306    /// **Optionals:** Each `Option<Bytes32>` occupies 32 bytes: [`ZERO_HASH`] when `None`, raw bytes when `Some`.
307    pub fn hash_preimage_bytes(&self) -> [u8; Self::HASH_PREIMAGE_LEN] {
308        fn put(buf: &mut [u8; L2BlockHeader::HASH_PREIMAGE_LEN], i: &mut usize, bytes: &[u8]) {
309            buf[*i..*i + bytes.len()].copy_from_slice(bytes);
310            *i += bytes.len();
311        }
312        fn put_opt(
313            buf: &mut [u8; L2BlockHeader::HASH_PREIMAGE_LEN],
314            i: &mut usize,
315            o: &Option<Bytes32>,
316        ) {
317            let slice = match o {
318                Some(b) => b.as_ref(),
319                None => ZERO_HASH.as_ref(),
320            };
321            buf[*i..*i + 32].copy_from_slice(slice);
322            *i += 32;
323        }
324        let mut buf = [0u8; Self::HASH_PREIMAGE_LEN];
325        let mut i = 0usize;
326        put(&mut buf, &mut i, &self.version.to_le_bytes());
327        put(&mut buf, &mut i, &self.height.to_le_bytes());
328        put(&mut buf, &mut i, &self.epoch.to_le_bytes());
329        put(&mut buf, &mut i, self.parent_hash.as_ref());
330        put(&mut buf, &mut i, self.state_root.as_ref());
331        put(&mut buf, &mut i, self.spends_root.as_ref());
332        put(&mut buf, &mut i, self.additions_root.as_ref());
333        put(&mut buf, &mut i, self.removals_root.as_ref());
334        put(&mut buf, &mut i, self.receipts_root.as_ref());
335        put(&mut buf, &mut i, &self.l1_height.to_le_bytes());
336        put(&mut buf, &mut i, self.l1_hash.as_ref());
337        put(&mut buf, &mut i, &self.timestamp.to_le_bytes());
338        put(&mut buf, &mut i, &self.proposer_index.to_le_bytes());
339        put(&mut buf, &mut i, &self.spend_bundle_count.to_le_bytes());
340        put(&mut buf, &mut i, &self.total_cost.to_le_bytes());
341        put(&mut buf, &mut i, &self.total_fees.to_le_bytes());
342        put(&mut buf, &mut i, &self.additions_count.to_le_bytes());
343        put(&mut buf, &mut i, &self.removals_count.to_le_bytes());
344        put(&mut buf, &mut i, &self.block_size.to_le_bytes());
345        put(&mut buf, &mut i, self.filter_hash.as_ref());
346        put(&mut buf, &mut i, self.extension_data.as_ref());
347        put_opt(&mut buf, &mut i, &self.l1_collateral_coin_id);
348        put_opt(&mut buf, &mut i, &self.l1_reserve_coin_id);
349        put_opt(&mut buf, &mut i, &self.l1_prev_epoch_finalizer_coin_id);
350        put_opt(&mut buf, &mut i, &self.l1_curr_epoch_finalizer_coin_id);
351        put_opt(&mut buf, &mut i, &self.l1_network_coin_id);
352        put(&mut buf, &mut i, &self.slash_proposal_count.to_le_bytes());
353        put(&mut buf, &mut i, self.slash_proposals_root.as_ref());
354        put(&mut buf, &mut i, self.collateral_registry_root.as_ref());
355        put(&mut buf, &mut i, self.cid_state_root.as_ref());
356        put(&mut buf, &mut i, self.node_registry_root.as_ref());
357        put(&mut buf, &mut i, self.namespace_update_root.as_ref());
358        put(
359            &mut buf,
360            &mut i,
361            self.dfsp_finalize_commitment_root.as_ref(),
362        );
363        debug_assert_eq!(i, Self::HASH_PREIMAGE_LEN);
364        buf
365    }
366
367    /// Canonical block identity: SHA-256 over [`Self::hash_preimage_bytes`] ([HSH-001](docs/requirements/domains/hashing/specs/HSH-001.md)).
368    ///
369    /// **Requirement:** [SPEC §3.1](docs/resources/SPEC.md). Numeric fields are little-endian; each optional L1 anchor
370    /// contributes 32 bytes of raw [`Bytes32`] or [`ZERO_HASH`] when `None` (malleability-safe encoding).
371    ///
372    /// **Primitive:** [`chia_sha2::Sha256`] only ([`crate::primitives`] / project crypto rules).
373    pub fn hash(&self) -> Bytes32 {
374        let mut hasher = Sha256::new();
375        hasher.update(self.hash_preimage_bytes());
376        Bytes32::new(hasher.finalize())
377    }
378
379    /// Serialize this header to **bincode** bytes for wire / storage ([SER-002](docs/requirements/domains/serialization/specs/SER-002.md),
380    /// [NORMATIVE § SER-002](docs/requirements/domains/serialization/NORMATIVE.md#ser-002-to_bytes-and-from_bytes-conventions), SPEC §8.2).
381    ///
382    /// **Infallible:** Uses [`Result::expect`] because an in-memory, well-formed [`L2BlockHeader`] should always serialize
383    /// with the crate’s serde schema; a panic indicates programmer error or schema drift, not recoverable I/O.
384    #[must_use]
385    pub fn to_bytes(&self) -> Vec<u8> {
386        bincode::serialize(self).expect("L2BlockHeader serialization should never fail")
387    }
388
389    /// Deserialize a header from **bincode** bytes ([SER-002](docs/requirements/domains/serialization/specs/SER-002.md)).
390    ///
391    /// **Errors:** Any `bincode` failure maps to [`BlockError::InvalidData`] (message includes the decoder error) —
392    /// covers empty input, truncated payloads, corrupted bytes, and schema mismatches.
393    pub fn from_bytes(bytes: &[u8]) -> Result<Self, BlockError> {
394        bincode::deserialize(bytes).map_err(|e| BlockError::InvalidData(e.to_string()))
395    }
396
397    /// Standard header constructor (SPEC §2.2 **Derived methods** / `new()`).
398    ///
399    /// Sets `version` via [`Self::protocol_version_for_height`]; `timestamp` to **0** (SPEC omits it from
400    /// the `new` parameter list—set explicitly or use [`Self::genesis`] / block builder for wall clock);
401    /// L1 proof anchors to `None`; slash summary to empty; DFSP roots to [`EMPTY_ROOT`]; `extension_data`
402    /// to [`ZERO_HASH`].
403    #[allow(clippy::too_many_arguments)]
404    pub fn new(
405        height: u64,
406        epoch: u64,
407        parent_hash: Bytes32,
408        state_root: Bytes32,
409        spends_root: Bytes32,
410        additions_root: Bytes32,
411        removals_root: Bytes32,
412        receipts_root: Bytes32,
413        l1_height: u32,
414        l1_hash: Bytes32,
415        proposer_index: u32,
416        spend_bundle_count: u32,
417        total_cost: Cost,
418        total_fees: u64,
419        additions_count: u32,
420        removals_count: u32,
421        block_size: u32,
422        filter_hash: Bytes32,
423    ) -> Self {
424        Self::with_l1_anchors(
425            height,
426            epoch,
427            parent_hash,
428            state_root,
429            spends_root,
430            additions_root,
431            removals_root,
432            receipts_root,
433            l1_height,
434            l1_hash,
435            0,
436            proposer_index,
437            spend_bundle_count,
438            total_cost,
439            total_fees,
440            additions_count,
441            removals_count,
442            block_size,
443            filter_hash,
444            ZERO_HASH,
445            None,
446            None,
447            None,
448            None,
449            None,
450            0,
451            EMPTY_ROOT,
452            EMPTY_ROOT,
453            EMPTY_ROOT,
454            EMPTY_ROOT,
455            EMPTY_ROOT,
456            EMPTY_ROOT,
457        )
458    }
459
460    /// Like [`Self::new`] but sets [`L2BlockHeader::l1_collateral_coin_id`] to the given proof coin id.
461    #[allow(clippy::too_many_arguments)]
462    pub fn new_with_collateral(
463        height: u64,
464        epoch: u64,
465        parent_hash: Bytes32,
466        state_root: Bytes32,
467        spends_root: Bytes32,
468        additions_root: Bytes32,
469        removals_root: Bytes32,
470        receipts_root: Bytes32,
471        l1_height: u32,
472        l1_hash: Bytes32,
473        proposer_index: u32,
474        spend_bundle_count: u32,
475        total_cost: Cost,
476        total_fees: u64,
477        additions_count: u32,
478        removals_count: u32,
479        block_size: u32,
480        filter_hash: Bytes32,
481        l1_collateral_coin_id: Bytes32,
482    ) -> Self {
483        Self::with_l1_anchors(
484            height,
485            epoch,
486            parent_hash,
487            state_root,
488            spends_root,
489            additions_root,
490            removals_root,
491            receipts_root,
492            l1_height,
493            l1_hash,
494            0,
495            proposer_index,
496            spend_bundle_count,
497            total_cost,
498            total_fees,
499            additions_count,
500            removals_count,
501            block_size,
502            filter_hash,
503            ZERO_HASH,
504            Some(l1_collateral_coin_id),
505            None,
506            None,
507            None,
508            None,
509            0,
510            EMPTY_ROOT,
511            EMPTY_ROOT,
512            EMPTY_ROOT,
513            EMPTY_ROOT,
514            EMPTY_ROOT,
515            EMPTY_ROOT,
516        )
517    }
518
519    /// Full L1 proof anchor set (SPEC field order: collateral, reserve, prev/curr finalizer, network coin).
520    #[allow(clippy::too_many_arguments)]
521    pub fn new_with_l1_proofs(
522        height: u64,
523        epoch: u64,
524        parent_hash: Bytes32,
525        state_root: Bytes32,
526        spends_root: Bytes32,
527        additions_root: Bytes32,
528        removals_root: Bytes32,
529        receipts_root: Bytes32,
530        l1_height: u32,
531        l1_hash: Bytes32,
532        proposer_index: u32,
533        spend_bundle_count: u32,
534        total_cost: Cost,
535        total_fees: u64,
536        additions_count: u32,
537        removals_count: u32,
538        block_size: u32,
539        filter_hash: Bytes32,
540        l1_collateral_coin_id: Bytes32,
541        l1_reserve_coin_id: Bytes32,
542        l1_prev_epoch_finalizer_coin_id: Bytes32,
543        l1_curr_epoch_finalizer_coin_id: Bytes32,
544        l1_network_coin_id: Bytes32,
545    ) -> Self {
546        Self::with_l1_anchors(
547            height,
548            epoch,
549            parent_hash,
550            state_root,
551            spends_root,
552            additions_root,
553            removals_root,
554            receipts_root,
555            l1_height,
556            l1_hash,
557            0,
558            proposer_index,
559            spend_bundle_count,
560            total_cost,
561            total_fees,
562            additions_count,
563            removals_count,
564            block_size,
565            filter_hash,
566            ZERO_HASH,
567            Some(l1_collateral_coin_id),
568            Some(l1_reserve_coin_id),
569            Some(l1_prev_epoch_finalizer_coin_id),
570            Some(l1_curr_epoch_finalizer_coin_id),
571            Some(l1_network_coin_id),
572            0,
573            EMPTY_ROOT,
574            EMPTY_ROOT,
575            EMPTY_ROOT,
576            EMPTY_ROOT,
577            EMPTY_ROOT,
578            EMPTY_ROOT,
579        )
580    }
581
582    /// Genesis header ([SER-003](docs/requirements/domains/serialization/specs/SER-003.md), [NORMATIVE § SER-003](docs/requirements/domains/serialization/NORMATIVE.md#ser-003-genesis-block-construction), SPEC §8.3).
583    ///
584    /// ## Field obligations
585    ///
586    /// - **`height` / `epoch`:** `0` — chain bootstrap position.
587    /// - **`parent_hash`:** `network_id` — there is no prior L2 block; binding the parent slot to the network identity
588    ///   blocks cross-network replay of height-0 material ([SER-003](docs/requirements/domains/serialization/specs/SER-003.md) summary).
589    /// - **Merkle / commitment roots:** [`EMPTY_ROOT`](crate::EMPTY_ROOT) for state, spends, additions, removals, receipts,
590    ///   filter, slash proposals, and all DFSP layer roots; [`ZERO_HASH`](crate::ZERO_HASH) for `extension_data` (opaque
591    ///   extension slot starts empty).
592    /// - **Counts / costs / size:** all zero; **L1 anchor options:** all [`None`]; **slash count:** `0`.
593    /// - **`l1_height` / `l1_hash`:** caller-supplied L1 observation the genesis L2 header is anchored to.
594    /// - **`version`:** [`Self::protocol_version_for_height`](Self::protocol_version_for_height)(`0`) — same BLK-007
595    ///   auto-detection as every other constructor (not `CARGO_PKG_VERSION`; see SER-003 spec errata vs older pseudocode).
596    ///
597    /// **`timestamp`:** wall-clock Unix seconds from [`SystemTime::now`]. Host clocks before 1970 cannot be represented;
598    /// we **panic** with a clear message (same contract as “real” wall time for genesis in SPEC §8.3).
599    pub fn genesis(network_id: Bytes32, l1_height: u32, l1_hash: Bytes32) -> Self {
600        let timestamp = SystemTime::now()
601            .duration_since(UNIX_EPOCH)
602            .expect("system clock before UNIX epoch; genesis header requires wall-clock time")
603            .as_secs();
604        let height = 0u64;
605        Self::with_l1_anchors(
606            height, 0, network_id, EMPTY_ROOT, EMPTY_ROOT, EMPTY_ROOT, EMPTY_ROOT, EMPTY_ROOT,
607            l1_height, l1_hash, timestamp, 0, 0, 0, 0, 0, 0, 0, EMPTY_ROOT, ZERO_HASH, None, None,
608            None, None, None, 0, EMPTY_ROOT, EMPTY_ROOT, EMPTY_ROOT, EMPTY_ROOT, EMPTY_ROOT,
609            EMPTY_ROOT,
610        )
611    }
612
613    #[allow(clippy::too_many_arguments)]
614    fn with_l1_anchors(
615        height: u64,
616        epoch: u64,
617        parent_hash: Bytes32,
618        state_root: Bytes32,
619        spends_root: Bytes32,
620        additions_root: Bytes32,
621        removals_root: Bytes32,
622        receipts_root: Bytes32,
623        l1_height: u32,
624        l1_hash: Bytes32,
625        timestamp: u64,
626        proposer_index: u32,
627        spend_bundle_count: u32,
628        total_cost: Cost,
629        total_fees: u64,
630        additions_count: u32,
631        removals_count: u32,
632        block_size: u32,
633        filter_hash: Bytes32,
634        extension_data: Bytes32,
635        l1_collateral_coin_id: Option<Bytes32>,
636        l1_reserve_coin_id: Option<Bytes32>,
637        l1_prev_epoch_finalizer_coin_id: Option<Bytes32>,
638        l1_curr_epoch_finalizer_coin_id: Option<Bytes32>,
639        l1_network_coin_id: Option<Bytes32>,
640        slash_proposal_count: u32,
641        slash_proposals_root: Bytes32,
642        collateral_registry_root: Bytes32,
643        cid_state_root: Bytes32,
644        node_registry_root: Bytes32,
645        namespace_update_root: Bytes32,
646        dfsp_finalize_commitment_root: Bytes32,
647    ) -> Self {
648        Self {
649            version: Self::protocol_version_for_height(height),
650            height,
651            epoch,
652            parent_hash,
653            state_root,
654            spends_root,
655            additions_root,
656            removals_root,
657            receipts_root,
658            l1_height,
659            l1_hash,
660            timestamp,
661            proposer_index,
662            spend_bundle_count,
663            total_cost,
664            total_fees,
665            additions_count,
666            removals_count,
667            block_size,
668            filter_hash,
669            extension_data,
670            l1_collateral_coin_id,
671            l1_reserve_coin_id,
672            l1_prev_epoch_finalizer_coin_id,
673            l1_curr_epoch_finalizer_coin_id,
674            l1_network_coin_id,
675            slash_proposal_count,
676            slash_proposals_root,
677            collateral_registry_root,
678            cid_state_root,
679            node_registry_root,
680            namespace_update_root,
681            dfsp_finalize_commitment_root,
682        }
683    }
684}