dig_block/builder/block_builder.rs
1//! [`BlockBuilder`] — incremental construction of signed [`crate::L2Block`] instances.
2//!
3//! ## Requirements trace
4//!
5//! - **[BLD-001](docs/requirements/domains/block_production/specs/BLD-001.md)** — struct fields and `new()` constructor.
6//! - **[BLD-002](docs/requirements/domains/block_production/specs/BLD-002.md)** — `add_spend_bundle()` with cost/size budget enforcement, `remaining_cost()`, `spend_bundle_count()`.
7//! - **[BLD-003](docs/requirements/domains/block_production/specs/BLD-003.md)** — `add_slash_proposal()` with count/size limits.
8//! - **[BLD-004](docs/requirements/domains/block_production/specs/BLD-004.md)** — `set_l1_proofs()`, `set_dfsp_roots()`, `set_extension_data()`.
9//! - **[BLD-005](docs/requirements/domains/block_production/specs/BLD-005.md)** — `build()` pipeline: compute all derived fields, sign header.
10//! - **[BLD-006](docs/requirements/domains/block_production/specs/BLD-006.md)** — [`crate::BlockSigner`] integration in `build()` (see `tests/test_bld_006_block_signer_integration.rs`).
11//! - **[BLD-007](docs/requirements/domains/block_production/specs/BLD-007.md)** — structural validity guarantee: output always passes `validate_structure()`.
12//! - **[NORMATIVE](docs/requirements/domains/block_production/NORMATIVE.md)** — full block production domain.
13//! - **[SPEC §6](docs/resources/SPEC.md)** — block production lifecycle.
14//!
15//! ## Build pipeline overview (BLD-005)
16//!
17//! ```text
18//! 1. compute spends_root ← MerkleTree(sha256(bundle)) per HSH-003
19//! 2. compute additions_root ← compute_merkle_set_root(grouped by puzzle_hash) per HSH-004
20//! 3. compute removals_root ← compute_merkle_set_root(coin IDs) per HSH-005
21//! 4. compute filter_hash ← SHA-256(BIP158 compact filter) per HSH-006
22//! 5. compute slash_proposals_root ← MerkleTree(sha256(payload)) per BLK-004
23//! 6. count all items ← spend_bundle_count, additions_count, removals_count, slash_proposal_count
24//! 7. auto-detect version ← protocol_version_for_height(height) per BLK-007
25//! 8. set timestamp ← current wall-clock time
26//! 9. compute block_size ← two-pass: assemble with size=0, measure, update
27//! 10. sign header ← signer.sign_block(&header_hash) per BLD-006
28//! ```
29//!
30//! ## Design decisions
31//!
32//! - **Consuming `build(self)`:** Takes ownership so the builder cannot be reused after producing a block,
33//! preventing accidental double-build or stale state.
34//! - **Budget enforcement on add, not build:** `add_spend_bundle()` rejects bundles that would exceed
35//! `MAX_COST_PER_BLOCK` or `MAX_BLOCK_SIZE` _before_ mutating state (BLD-002). This means a rejected
36//! bundle leaves the builder unchanged — callers can try a smaller bundle.
37//! - **State root and receipts_root are parameters to `build()`:** The builder doesn't maintain coin state,
38//! so the caller (proposer layer) must compute these externally and pass them in.
39//!
40//! ## Status
41//!
42//! **BLD-001**–**BLD-007** are implemented: successful [`BlockBuilder::build`] / [`BlockBuilder::build_with_dfsp_activation`]
43//! outputs pass [`crate::L2Block::validate_structure`]
44//! ([BLD-007](docs/requirements/domains/block_production/specs/BLD-007.md),
45//! `tests/test_bld_007_builder_validity_guarantee.rs`).
46
47use std::time::{SystemTime, UNIX_EPOCH};
48
49use bincode;
50use chia_protocol::{Coin, SpendBundle};
51
52use crate::error::BuilderError;
53use crate::merkle_util::empty_on_additions_err;
54use crate::primitives::{Bytes32, Cost, Signature};
55use crate::traits::BlockSigner;
56use crate::types::block::L2Block;
57use crate::types::header::L2BlockHeader;
58use crate::{
59 compute_additions_root, compute_filter_hash, compute_removals_root, compute_spends_root,
60 DFSP_ACTIVATION_HEIGHT, EMPTY_ROOT, MAX_BLOCK_SIZE, MAX_COST_PER_BLOCK,
61 MAX_SLASH_PROPOSALS_PER_BLOCK, MAX_SLASH_PROPOSAL_PAYLOAD_BYTES, VERSION_V2, ZERO_HASH,
62};
63
64/// Incremental accumulator for a single L2 block body and header metadata ([SPEC §6.1–6.2](docs/resources/SPEC.md),
65/// [BLD-001](docs/requirements/domains/block_production/specs/BLD-001.md)).
66///
67/// **Usage:** Construct with [`Self::new`], optionally configure L1 proof anchors / DFSP roots / [`L2BlockHeader::extension_data`]
68/// via [`Self::set_l1_proofs`], [`Self::set_dfsp_roots`], [`Self::set_extension_data`] (BLD-004), add spend bundles via
69/// [`Self::add_spend_bundle`] (BLD-002) and slash payloads via [`Self::add_slash_proposal`] (BLD-003), then call
70/// `build(...)` (BLD-005) to obtain a signed [`crate::L2Block`].
71/// The struct exposes **public fields** so
72/// advanced callers or tests can inspect partial state without accessor boilerplate; treat them as read-mostly except
73/// through official builder methods once those exist.
74///
75/// **Rationale:** Public fields match the BLD-001 specification prose and mirror the SPEC §6.1 layout (caller context,
76/// accumulated body, running totals). [`Coin`] and [`SpendBundle`] stay on **`chia-protocol`** types per project rules
77/// ([`docs/prompt/start.md`](docs/prompt/start.md) — Chia ecosystem first).
78///
79/// **Removals type:** NORMATIVE names this `Vec<CoinId>`; `chia-protocol` does not export a separate `CoinId` newtype in
80/// the versions we pin — coin IDs are the same 32-byte values as [`Bytes32`] (see BLK-004 / [`crate::L2Block::all_removals`]).
81pub struct BlockBuilder {
82 /// Block height this builder is assembling (immutable for the lifetime of the builder).
83 pub height: u64,
84 /// Epoch index ([`crate::L2BlockHeader::epoch`] semantics).
85 pub epoch: u64,
86 /// Parent L2 block header hash (chain link).
87 pub parent_hash: Bytes32,
88 /// Anchoring L1 block height for light-client / bridge logic.
89 pub l1_height: u32,
90 /// Anchoring L1 block hash.
91 pub l1_hash: Bytes32,
92 /// Proposer slot index in the validator set for this block.
93 pub proposer_index: u32,
94 /// Spend bundles accumulated in insertion order (body).
95 pub spend_bundles: Vec<SpendBundle>,
96 /// Raw slash-proposal payloads (opaque bytes per protocol).
97 pub slash_proposal_payloads: Vec<Vec<u8>>,
98 /// Running sum of CLVM costs for bundles accepted so far ([`Cost`] / BLK-006).
99 pub total_cost: Cost,
100 /// Running sum of fees from accepted bundles.
101 pub total_fees: u64,
102 /// Flattened [`Coin`] outputs extracted from spends ([`Self::add_spend_bundle`] / BLD-002).
103 pub additions: Vec<Coin>,
104 /// Spent coin IDs (same bytes as `coin.coin_id()` / NORMATIVE `CoinId`).
105 pub removals: Vec<Bytes32>,
106
107 // --- BLD-004: optional / deferred header fields (copied into [`L2BlockHeader`] in BLD-005) ---
108 /// L1 collateral proof anchor ([`L2BlockHeader::l1_collateral_coin_id`]); `None` until [`Self::set_l1_proofs`].
109 pub l1_collateral_coin_id: Option<Bytes32>,
110 /// Network validator collateral set anchor ([`L2BlockHeader::l1_reserve_coin_id`]).
111 pub l1_reserve_coin_id: Option<Bytes32>,
112 /// Previous-epoch finalizer proof coin ([`L2BlockHeader::l1_prev_epoch_finalizer_coin_id`]).
113 pub l1_prev_epoch_finalizer_coin_id: Option<Bytes32>,
114 /// Current-epoch finalizer proof coin ([`L2BlockHeader::l1_curr_epoch_finalizer_coin_id`]).
115 pub l1_curr_epoch_finalizer_coin_id: Option<Bytes32>,
116 /// Network singleton proof coin ([`L2BlockHeader::l1_network_coin_id`]).
117 pub l1_network_coin_id: Option<Bytes32>,
118
119 /// DFSP collateral registry root ([`L2BlockHeader::collateral_registry_root`]); defaults [`EMPTY_ROOT`] per SVL-002.
120 pub collateral_registry_root: Bytes32,
121 /// DFSP CID state root ([`L2BlockHeader::cid_state_root`]).
122 pub cid_state_root: Bytes32,
123 /// DFSP node registry root ([`L2BlockHeader::node_registry_root`]).
124 pub node_registry_root: Bytes32,
125 /// Namespace update delta root ([`L2BlockHeader::namespace_update_root`]).
126 pub namespace_update_root: Bytes32,
127 /// DFSP finalize commitment root ([`L2BlockHeader::dfsp_finalize_commitment_root`]).
128 pub dfsp_finalize_commitment_root: Bytes32,
129
130 /// Header extension slot ([`L2BlockHeader::extension_data`]); default [`ZERO_HASH`] matches [`L2BlockHeader::new`].
131 pub extension_data: Bytes32,
132}
133
134impl BlockBuilder {
135 /// Create an empty builder anchored at the given chain / L1 context ([BLD-001](docs/requirements/domains/block_production/specs/BLD-001.md)).
136 ///
137 /// **Contract:** All accumulation fields start empty or zero; identity arguments are copied into the struct so the
138 /// caller may reuse their locals afterward without aliasing the builder’s internal state.
139 #[must_use]
140 pub fn new(
141 height: u64,
142 epoch: u64,
143 parent_hash: Bytes32,
144 l1_height: u32,
145 l1_hash: Bytes32,
146 proposer_index: u32,
147 ) -> Self {
148 Self {
149 height,
150 epoch,
151 parent_hash,
152 l1_height,
153 l1_hash,
154 proposer_index,
155 spend_bundles: Vec::new(),
156 slash_proposal_payloads: Vec::new(),
157 total_cost: 0,
158 total_fees: 0,
159 additions: Vec::new(),
160 removals: Vec::new(),
161 l1_collateral_coin_id: None,
162 l1_reserve_coin_id: None,
163 l1_prev_epoch_finalizer_coin_id: None,
164 l1_curr_epoch_finalizer_coin_id: None,
165 l1_network_coin_id: None,
166 collateral_registry_root: EMPTY_ROOT,
167 cid_state_root: EMPTY_ROOT,
168 node_registry_root: EMPTY_ROOT,
169 namespace_update_root: EMPTY_ROOT,
170 dfsp_finalize_commitment_root: EMPTY_ROOT,
171 extension_data: ZERO_HASH,
172 }
173 }
174
175 /// Build a probe [`L2BlockHeader`] sharing this builder’s identity, L1 anchor, **optional L1 proofs**, **DFSP roots**,
176 /// and **extension** fields ([BLD-004](docs/requirements/domains/block_production/specs/BLD-004.md)).
177 ///
178 /// **Rationale:** `bincode` encodes `Option<Bytes32>` with a discriminant — `Some(_)` rows are larger than `None`.
179 /// After BLD-004, [`Self::serialized_l2_block_probe_len`] must fold these fields in so [`Self::add_spend_bundle`]
180 /// (BLD-002) cannot underestimate wire size when proofs are present.
181 fn probe_header_stub(&self) -> L2BlockHeader {
182 let mut h = L2BlockHeader::new(
183 self.height,
184 self.epoch,
185 self.parent_hash,
186 EMPTY_ROOT,
187 EMPTY_ROOT,
188 EMPTY_ROOT,
189 EMPTY_ROOT,
190 EMPTY_ROOT,
191 self.l1_height,
192 self.l1_hash,
193 self.proposer_index,
194 0,
195 0,
196 0,
197 0,
198 0,
199 0,
200 EMPTY_ROOT,
201 );
202 h.l1_collateral_coin_id = self.l1_collateral_coin_id;
203 h.l1_reserve_coin_id = self.l1_reserve_coin_id;
204 h.l1_prev_epoch_finalizer_coin_id = self.l1_prev_epoch_finalizer_coin_id;
205 h.l1_curr_epoch_finalizer_coin_id = self.l1_curr_epoch_finalizer_coin_id;
206 h.l1_network_coin_id = self.l1_network_coin_id;
207 h.collateral_registry_root = self.collateral_registry_root;
208 h.cid_state_root = self.cid_state_root;
209 h.node_registry_root = self.node_registry_root;
210 h.namespace_update_root = self.namespace_update_root;
211 h.dfsp_finalize_commitment_root = self.dfsp_finalize_commitment_root;
212 h.extension_data = self.extension_data;
213 h
214 }
215
216 /// Serialized [`L2Block`] byte length if the body were `spend_bundles` plus this builder’s slash payloads.
217 ///
218 /// **Rationale (BLD-002):** [`crate::L2Block::validate_structure`] and SVL-003 compare **full** `bincode(L2Block)`
219 /// against [`MAX_BLOCK_SIZE`](crate::MAX_BLOCK_SIZE). The builder does not yet have a final header (BLD-005), so we
220 /// synthesize a probe header via [`Self::probe_header_stub`] (counts/ Merkle fields still placeholders), while the
221 /// variable body (`Vec<SpendBundle>`, `Vec<Vec<u8>>` slash payloads) matches this builder — so the estimate tracks
222 /// growth in spend + slash bytes **and** optional header encodings from BLD-004.
223 ///
224 /// **Related:** [`L2Block::compute_size`](crate::L2Block::compute_size) (BLK-004) uses the same `bincode` schema.
225 fn serialized_l2_block_probe_len(&self, spend_bundles: &[SpendBundle]) -> usize {
226 let header = self.probe_header_stub();
227 let block = L2Block::new(
228 header,
229 spend_bundles.to_vec(),
230 self.slash_proposal_payloads.clone(),
231 Signature::default(),
232 );
233 bincode::serialize(&block)
234 .map(|bytes| bytes.len())
235 .unwrap_or(usize::MAX)
236 }
237
238 /// Append a [`SpendBundle`] after validating CLVM cost and serialized block-size budgets ([BLD-002](docs/requirements/domains/block_production/specs/BLD-002.md)).
239 ///
240 /// **Parameters:** `cost` / `fee` are **caller-supplied** aggregates for this bundle (typically from
241 /// `dig_clvm::validate_spend_bundle` / execution preview). The builder trusts these numbers for budgeting only;
242 /// [`L2BlockHeader::total_cost`] consistency is enforced later at execution tier (EXE-007) and in `build()` (BLD-005).
243 ///
244 /// **Budget order:** cost is checked first (cheap, no cloning). Size uses a bincode probe that temporarily
245 /// [`clone`]s the candidate bundle — if the probe fails, `bundle` is still owned by the caller on `Err`.
246 ///
247 /// **Mutation contract:** On `Err`, **no** field of `self` changes. On `Ok`, additions/removals/totals/spend_bundles
248 /// advance together so partial state is impossible.
249 ///
250 /// **Additions / removals:** Mirrors [`crate::L2Block::all_additions`] / [`crate::L2Block::all_removals`] —
251 /// [`SpendBundle::additions`] (CLVM-simulated `CREATE_COIN`s) plus one removal [`Bytes32`] per [`CoinSpend`].
252 pub fn add_spend_bundle(
253 &mut self,
254 bundle: SpendBundle,
255 cost: Cost,
256 fee: u64,
257 ) -> Result<(), BuilderError> {
258 let next_cost = self.total_cost.saturating_add(cost);
259 if next_cost > MAX_COST_PER_BLOCK {
260 return Err(BuilderError::CostBudgetExceeded {
261 current: self.total_cost,
262 addition: cost,
263 max: MAX_COST_PER_BLOCK,
264 });
265 }
266
267 let base_bytes = self.serialized_l2_block_probe_len(&self.spend_bundles);
268 let mut probe_bundles = self.spend_bundles.clone();
269 probe_bundles.push(bundle.clone());
270 let with_bytes = self.serialized_l2_block_probe_len(&probe_bundles);
271
272 if with_bytes > MAX_BLOCK_SIZE as usize {
273 return Err(BuilderError::SizeBudgetExceeded {
274 current: u32::try_from(base_bytes).unwrap_or(u32::MAX),
275 addition: u32::try_from(with_bytes.saturating_sub(base_bytes)).unwrap_or(u32::MAX),
276 max: MAX_BLOCK_SIZE,
277 });
278 }
279
280 self.additions
281 .extend(empty_on_additions_err(bundle.additions()));
282 for cs in &bundle.coin_spends {
283 self.removals.push(cs.coin.coin_id());
284 }
285 self.total_cost += cost;
286 self.total_fees += fee;
287 self.spend_bundles.push(bundle);
288 Ok(())
289 }
290
291 /// Remaining CLVM cost budget before hitting [`MAX_COST_PER_BLOCK`](crate::MAX_COST_PER_BLOCK) ([BLD-002](docs/requirements/domains/block_production/specs/BLD-002.md)).
292 ///
293 /// **Usage:** Proposers can gate bundle selection without duplicating protocol constants. Saturates at zero if
294 /// `total_cost` ever overshoots (should not happen if only [`Self::add_spend_bundle`] mutates cost).
295 #[must_use]
296 pub fn remaining_cost(&self) -> Cost {
297 MAX_COST_PER_BLOCK.saturating_sub(self.total_cost)
298 }
299
300 /// Number of spend bundles accepted so far (same as `spend_bundles.len()`).
301 #[must_use]
302 pub fn spend_bundle_count(&self) -> usize {
303 self.spend_bundles.len()
304 }
305
306 /// Append an opaque slash-proposal payload after enforcing protocol caps ([BLD-003](docs/requirements/domains/block_production/specs/BLD-003.md)).
307 ///
308 /// **Count:** Rejects when [`Self::slash_proposal_payloads`] already holds [`MAX_SLASH_PROPOSALS_PER_BLOCK`](crate::MAX_SLASH_PROPOSALS_PER_BLOCK)
309 /// rows — the guard uses `>=` so the **next** push would exceed the cap ([`BuilderError::TooManySlashProposals`]).
310 ///
311 /// **Size:** Rejects when `payload.len() > `[`MAX_SLASH_PROPOSAL_PAYLOAD_BYTES`](crate::MAX_SLASH_PROPOSAL_PAYLOAD_BYTES)
312 /// ([`BuilderError::SlashProposalTooLarge`]). A payload whose length **equals** the limit is accepted (strict `>`).
313 ///
314 /// **Check order (spec):** Count is validated **before** size so that a builder already at the count cap surfaces
315 /// [`BuilderError::TooManySlashProposals`] even if the candidate payload is also oversized — callers get a stable
316 /// primary failure mode ([BLD-003 implementation notes](docs/requirements/domains/block_production/specs/BLD-003.md#implementation-notes)).
317 ///
318 /// **Mutation contract:** On `Err`, `slash_proposal_payloads` and all other fields are unchanged. On `Ok`, only
319 /// `slash_proposal_payloads` grows; spend-bundle state is untouched (slash bytes still participate in the BLD-002
320 /// `bincode(L2Block)` size probe on later [`Self::add_spend_bundle`] calls).
321 pub fn add_slash_proposal(&mut self, payload: Vec<u8>) -> Result<(), BuilderError> {
322 if self.slash_proposal_payloads.len() >= MAX_SLASH_PROPOSALS_PER_BLOCK as usize {
323 return Err(BuilderError::TooManySlashProposals {
324 max: MAX_SLASH_PROPOSALS_PER_BLOCK,
325 });
326 }
327 let len = payload.len();
328 if len > MAX_SLASH_PROPOSAL_PAYLOAD_BYTES as usize {
329 return Err(BuilderError::SlashProposalTooLarge {
330 size: u32::try_from(len).unwrap_or(u32::MAX),
331 max: MAX_SLASH_PROPOSAL_PAYLOAD_BYTES,
332 });
333 }
334 self.slash_proposal_payloads.push(payload);
335 Ok(())
336 }
337
338 /// Set all five L1 proof anchor coin IDs at once ([BLD-004](docs/requirements/domains/block_production/specs/BLD-004.md)).
339 ///
340 /// **Semantics:** Each argument becomes `Some(hash)` on the builder, matching [`L2BlockHeader`]’s optional L1 proof
341 /// fields ([`L2BlockHeader::l1_collateral_coin_id`] … [`L2BlockHeader::l1_network_coin_id`]). Callers omitting L1
342 /// proofs should leave these as `None` (default from [`Self::new`]).
343 ///
344 /// **Overwrite:** Later calls replace the entire quintuple — there is no partial merge.
345 pub fn set_l1_proofs(
346 &mut self,
347 collateral: Bytes32,
348 reserve: Bytes32,
349 prev_finalizer: Bytes32,
350 curr_finalizer: Bytes32,
351 network_coin: Bytes32,
352 ) {
353 self.l1_collateral_coin_id = Some(collateral);
354 self.l1_reserve_coin_id = Some(reserve);
355 self.l1_prev_epoch_finalizer_coin_id = Some(prev_finalizer);
356 self.l1_curr_epoch_finalizer_coin_id = Some(curr_finalizer);
357 self.l1_network_coin_id = Some(network_coin);
358 }
359
360 /// Set all five DFSP data-layer Merkle roots ([BLD-004](docs/requirements/domains/block_production/specs/BLD-004.md)).
361 ///
362 /// **Semantics:** Mirrors [`L2BlockHeader`]’s DFSP root block ([`L2BlockHeader::collateral_registry_root`] …
363 /// [`L2BlockHeader::dfsp_finalize_commitment_root`]). Pre-activation callers typically keep [`EMPTY_ROOT`] values
364 /// (SVL-002); post-activation, pass real roots before `build()` (BLD-005 validates against version).
365 pub fn set_dfsp_roots(
366 &mut self,
367 collateral_registry_root: Bytes32,
368 cid_state_root: Bytes32,
369 node_registry_root: Bytes32,
370 namespace_update_root: Bytes32,
371 dfsp_finalize_commitment_root: Bytes32,
372 ) {
373 self.collateral_registry_root = collateral_registry_root;
374 self.cid_state_root = cid_state_root;
375 self.node_registry_root = node_registry_root;
376 self.namespace_update_root = namespace_update_root;
377 self.dfsp_finalize_commitment_root = dfsp_finalize_commitment_root;
378 }
379
380 /// Set the header extension hash ([BLD-004](docs/requirements/domains/block_production/specs/BLD-004.md)).
381 ///
382 /// **Semantics:** Stored as [`L2BlockHeader::extension_data`]; [`Self::new`] initializes to [`ZERO_HASH`] like
383 /// [`L2BlockHeader::new`].
384 pub fn set_extension_data(&mut self, extension_data: Bytes32) {
385 self.extension_data = extension_data;
386 }
387
388 /// Finalize this builder into a signed [`L2Block`] ([BLD-005](docs/requirements/domains/block_production/specs/BLD-005.md)).
389 ///
390 /// **Parameters:** `state_root` / `receipts_root` come from the execution + receipt pipeline outside this crate
391 /// (see module-level **Rationale**). `signer` produces the BLS attestation over [`L2BlockHeader::hash`] (HSH-001).
392 ///
393 /// **Pipeline:** Computes Merkle roots and counts using the same public functions as [`L2Block::validate_structure`]
394 /// (`compute_spends_root`, `compute_additions_root`, `compute_removals_root`, [`L2Block::slash_proposals_root_from`],
395 /// [`compute_filter_hash`]), sets wall-clock [`L2BlockHeader::timestamp`], runs the two-pass `block_size` fill
396 /// (assemble with `block_size == 0`, measure [`L2Block::compute_size`], write), then signs.
397 ///
398 /// **Errors:** [`BuilderError::EmptyBlock`] if no spend bundles ([ERR-004](docs/requirements/domains/error_types/specs/ERR-004.md));
399 /// [`BuilderError::MissingDfspRoots`] when [`VERSION_V2`](crate::VERSION_V2) applies but all DFSP roots are still
400 /// [`EMPTY_ROOT`]; [`BuilderError::SigningFailed`] wraps [`crate::traits::SignerError`].
401 ///
402 /// **DFSP activation:** Uses [`crate::DFSP_ACTIVATION_HEIGHT`]. For tests or fork simulation with a different
403 /// activation height, call [`Self::build_with_dfsp_activation`] instead.
404 pub fn build(
405 self,
406 state_root: Bytes32,
407 receipts_root: Bytes32,
408 signer: &dyn BlockSigner,
409 ) -> Result<L2Block, BuilderError> {
410 self.build_with_dfsp_activation(state_root, receipts_root, signer, DFSP_ACTIVATION_HEIGHT)
411 }
412
413 /// Like [`Self::build`], but supplies an explicit `dfsp_activation_height` for BLK-007 / SVL-001 version selection and
414 /// the BLD-005 DFSP-root precondition ([`BuilderError::MissingDfspRoots`]).
415 ///
416 /// **Rationale:** Crate tests keep [`DFSP_ACTIVATION_HEIGHT`] at `u64::MAX` (DFSP off) so normal `build()` always
417 /// selects [`crate::VERSION_V1`]. Passing a finite `dfsp_activation_height` **≤** [`Self::height`] forces V2 in
418 /// integration tests without recompiling constants.
419 pub fn build_with_dfsp_activation(
420 self,
421 state_root: Bytes32,
422 receipts_root: Bytes32,
423 signer: &dyn BlockSigner,
424 dfsp_activation_height: u64,
425 ) -> Result<L2Block, BuilderError> {
426 if self.spend_bundles.is_empty() {
427 return Err(BuilderError::EmptyBlock);
428 }
429
430 let spends_root = compute_spends_root(&self.spend_bundles);
431 let additions_root = compute_additions_root(&self.additions);
432 let removals_root = compute_removals_root(&self.removals);
433 let slash_proposals_root =
434 L2Block::slash_proposals_root_from(&self.slash_proposal_payloads);
435 let filter_hash = compute_filter_hash(self.parent_hash, &self.additions, &self.removals);
436
437 let version = L2BlockHeader::protocol_version_for_height_with_activation(
438 self.height,
439 dfsp_activation_height,
440 );
441 if version == VERSION_V2 {
442 let dfsp = [
443 self.collateral_registry_root,
444 self.cid_state_root,
445 self.node_registry_root,
446 self.namespace_update_root,
447 self.dfsp_finalize_commitment_root,
448 ];
449 if dfsp.iter().all(|r| *r == EMPTY_ROOT) {
450 return Err(BuilderError::MissingDfspRoots);
451 }
452 }
453
454 let spend_bundle_count = usize_to_u32_count(self.spend_bundles.len());
455 let additions_count = usize_to_u32_count(self.additions.len());
456 let removals_rows: usize = self
457 .spend_bundles
458 .iter()
459 .map(|sb| sb.coin_spends.len())
460 .sum();
461 let removals_count = usize_to_u32_count(removals_rows);
462 let slash_proposal_count = usize_to_u32_count(self.slash_proposal_payloads.len());
463
464 let timestamp = SystemTime::now()
465 .duration_since(UNIX_EPOCH)
466 .map(|d| d.as_secs())
467 .unwrap_or(0);
468
469 let mut header = L2BlockHeader::new(
470 self.height,
471 self.epoch,
472 self.parent_hash,
473 state_root,
474 spends_root,
475 additions_root,
476 removals_root,
477 receipts_root,
478 self.l1_height,
479 self.l1_hash,
480 self.proposer_index,
481 spend_bundle_count,
482 self.total_cost,
483 self.total_fees,
484 additions_count,
485 removals_count,
486 0,
487 filter_hash,
488 );
489 header.version = version;
490 header.timestamp = timestamp;
491 header.slash_proposal_count = slash_proposal_count;
492 header.slash_proposals_root = slash_proposals_root;
493 header.extension_data = self.extension_data;
494 header.l1_collateral_coin_id = self.l1_collateral_coin_id;
495 header.l1_reserve_coin_id = self.l1_reserve_coin_id;
496 header.l1_prev_epoch_finalizer_coin_id = self.l1_prev_epoch_finalizer_coin_id;
497 header.l1_curr_epoch_finalizer_coin_id = self.l1_curr_epoch_finalizer_coin_id;
498 header.l1_network_coin_id = self.l1_network_coin_id;
499 header.collateral_registry_root = self.collateral_registry_root;
500 header.cid_state_root = self.cid_state_root;
501 header.node_registry_root = self.node_registry_root;
502 header.namespace_update_root = self.namespace_update_root;
503 header.dfsp_finalize_commitment_root = self.dfsp_finalize_commitment_root;
504
505 let spend_bundles = self.spend_bundles;
506 let slash_proposal_payloads = self.slash_proposal_payloads;
507
508 let mut block = L2Block::new(
509 header,
510 spend_bundles,
511 slash_proposal_payloads,
512 Signature::default(),
513 );
514
515 // Two-pass `block_size` (BLD-005): measure with placeholder zero, then store the full `bincode(L2Block)` length.
516 let measured = block.compute_size();
517 block.header.block_size = usize_to_u32_count(measured);
518
519 // BLD-006: sign the **final** header digest (includes two-pass `block_size`; HSH-001 preimage omits BLS sig —
520 // see BLK-003 `L2Block::proposer_signature`). ERR-004 stores `SignerError` as `String` via `Display` so
521 // [`crate::BuilderError`] stays `Clone` without wrapping a nested `thiserror` source type.
522 let header_hash = block.header.hash();
523 let sig = signer
524 .sign_block(&header_hash)
525 .map_err(|e| BuilderError::SigningFailed(e.to_string()))?;
526 block.proposer_signature = sig;
527
528 Ok(block)
529 }
530}
531
532/// Header/body count fields are `u32` on wire; saturate when `usize` exceeds `u32::MAX` (same helper pattern as BLK-004).
533#[inline]
534fn usize_to_u32_count(n: usize) -> u32 {
535 u32::try_from(n).unwrap_or(u32::MAX)
536}