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}