dig_block/validation/execution.rs
1//! Tier 2 **execution validation** (EXE-*): CLVM execution, condition parsing, signatures, conservation.
2//!
3//! ## Requirements trace
4//!
5//! - **[EXE-001](docs/requirements/domains/execution_validation/specs/EXE-001.md)** — `validate_execution()` API accepting `ValidationConfig` + `genesis_challenge`.
6//! - **[EXE-002](docs/requirements/domains/execution_validation/specs/EXE-002.md)** — puzzle hash verification via `clvm-utils::tree_hash()`.
7//! - **[EXE-003](docs/requirements/domains/execution_validation/specs/EXE-003.md)** — CLVM execution via `dig_clvm::validate_spend_bundle()` (not raw `chia-consensus`).
8//! - **[EXE-004](docs/requirements/domains/execution_validation/specs/EXE-004.md)** — two-pass condition validation: collect (Pass 1) then assert (Pass 2). Height/time/ephemeral assertions deferred to Tier 3.
9//! - **[EXE-005](docs/requirements/domains/execution_validation/specs/EXE-005.md)** — BLS aggregate signature verification (inside `dig-clvm`, not separate).
10//! - **[EXE-006](docs/requirements/domains/execution_validation/specs/EXE-006.md)** — coin conservation per-bundle (dig-clvm) + block-level fee consistency.
11//! - **[EXE-007](docs/requirements/domains/execution_validation/specs/EXE-007.md)** — cost consistency: `sum(SpendResult.conditions.cost) == header.total_cost`.
12//! - **[EXE-008](docs/requirements/domains/execution_validation/specs/EXE-008.md)** — [`ExecutionResult`] output struct carrying additions, removals, assertions, cost, fees, receipts.
13//! - **[EXE-009](docs/requirements/domains/execution_validation/specs/EXE-009.md)** — `PendingAssertion` type: `AssertionKind` enum (8 height/time variants) + `from_condition()` factory.
14//! - **[SER-001](docs/requirements/domains/serialization/specs/SER-001.md)** — [`Serialize`] / [`Deserialize`] on [`ExecutionResult`], [`AssertionKind`], [`PendingAssertion`] for bincode.
15//! - **[NORMATIVE](docs/requirements/domains/execution_validation/NORMATIVE.md)** — full execution validation domain.
16//! - **[SPEC §7.4](docs/resources/SPEC.md)** — Tier 2 execution validation pipeline.
17//!
18//! ## Pipeline (per SpendBundle, in block order)
19//!
20//! ```text
21//! for each SpendBundle in block.spend_bundles:
22//! 1. For each CoinSpend: tree_hash(puzzle_reveal) == coin.puzzle_hash [EXE-002]
23//! 2. dig_clvm::validate_spend_bundle(bundle, config, genesis_challenge) [EXE-003]
24//! ├── CLVM execution (clvmr)
25//! ├── condition parsing (chia-sdk-types::Condition) [EXE-004]
26//! ├── BLS aggregate signature verification (chia-bls::aggregate_verify) [EXE-005]
27//! └── per-bundle conservation check (total_input >= total_output) [EXE-006]
28//! 3. Accumulate SpendResult: additions, removals, conditions, cost, fee
29//! 4. Collect height/time/ephemeral assertions → PendingAssertion [EXE-004/009]
30//! after all bundles:
31//! 5. Check computed_total_fees == header.total_fees [EXE-006]
32//! 6. Check computed_total_cost == header.total_cost [EXE-007]
33//! 7. Build ExecutionResult [EXE-008]
34//! ```
35//!
36//! ## Chia parity
37//!
38//! - Puzzle hash: [`block_body_validation.py` Check 20](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/block_body_validation.py) (`WRONG_PUZZLE_HASH`).
39//! - Signatures: [`block_body_validation.py` Check 22](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/block_body_validation.py) (`BAD_AGGREGATE_SIGNATURE`).
40//! - Conservation: [`block_body_validation.py` Check 16](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/block_body_validation.py) (`MINTING_COIN`).
41//! - Fee consistency: [`block_body_validation.py` Check 19](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/block_body_validation.py) (`INVALID_BLOCK_FEE_AMOUNT`).
42//! - Cost consistency: [`block_body_validation.py` Check 9](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/block_body_validation.py) (`INVALID_BLOCK_COST`).
43//!
44//! ## Status
45//!
46//! Stub — [`ExecutionResult`] placeholder defined; full pipeline implementation in EXE-001 through EXE-009.
47//!
48//! ## Serialization ([SER-001](docs/requirements/domains/serialization/specs/SER-001.md))
49//!
50//! [`ExecutionResult`], [`AssertionKind`], and [`PendingAssertion`] derive [`serde::Serialize`] / [`Deserialize`] so
51//! Tier-2 outputs and deferred assertions use the same **bincode** wire discipline as block types ([SPEC §8.1](docs/resources/SPEC.md)).
52
53use chia_protocol::CoinSpend;
54use chia_sdk_types::Condition;
55use serde::{Deserialize, Serialize};
56
57use crate::primitives::Bytes32;
58
59/// Height / time assertion opcode mirrored as a stable, bincode-friendly enum ([EXE-009](docs/requirements/domains/execution_validation/specs/EXE-009.md)).
60///
61/// **Rationale:** `chia-sdk-types::Condition` is a large open enum with CLVM-specific payloads; Tier 3 only needs the
62/// eight height/time variants in a compact shape for STV-005. Values mirror the `u32` / `u64` fields on the underlying
63/// [`Condition`] variants (height conditions use `u32` on wire; we widen to `u64` in DIG for uniform handling).
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub enum AssertionKind {
66 /// `ASSERT_HEIGHT_ABSOLUTE` — chain height must be ≥ threshold.
67 HeightAbsolute(u64),
68 /// `ASSERT_HEIGHT_RELATIVE` — chain height must be ≥ coin confirmed height + threshold.
69 HeightRelative(u64),
70 /// `ASSERT_SECONDS_ABSOLUTE` — wall clock must be ≥ threshold.
71 SecondsAbsolute(u64),
72 /// `ASSERT_SECONDS_RELATIVE` — wall clock must be ≥ coin time + threshold.
73 SecondsRelative(u64),
74 /// `ASSERT_BEFORE_HEIGHT_ABSOLUTE` — chain height must be < threshold.
75 BeforeHeightAbsolute(u64),
76 /// `ASSERT_BEFORE_HEIGHT_RELATIVE` — chain height must be < confirmed height + threshold.
77 BeforeHeightRelative(u64),
78 /// `ASSERT_BEFORE_SECONDS_ABSOLUTE` — wall clock must be < threshold.
79 BeforeSecondsAbsolute(u64),
80 /// `ASSERT_BEFORE_SECONDS_RELATIVE` — wall clock must be < coin time + threshold.
81 BeforeSecondsRelative(u64),
82}
83
84/// Deferred height/time assertion collected in Tier 2 and evaluated in Tier 3 ([EXE-009](docs/requirements/domains/execution_validation/specs/EXE-009.md)).
85///
86/// **Usage:** Call [`Self::from_condition`] on each parsed [`Condition`] from a [`CoinSpend`]; `None` means the
87/// condition is not a height/time lock (or is `ASSERT_EPHEMERAL`, handled separately in EXE-004).
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct PendingAssertion {
90 /// Opcode + threshold payload.
91 pub kind: AssertionKind,
92 /// Coin that produced the assertion (`coin_spend.coin.coin_id()`); required for relative comparisons in STV-005.
93 pub coin_id: Bytes32,
94}
95
96impl PendingAssertion {
97 /// Map a parsed SDK [`Condition`] into a DIG [`PendingAssertion`] when it is one of the eight height/time variants.
98 ///
99 /// **Returns:** `None` for every other condition (including `CreateCoin`, aggregate signatures, announcements, …).
100 /// **Spec:** [EXE-009](docs/requirements/domains/execution_validation/specs/EXE-009.md) factory table.
101 pub fn from_condition<T>(condition: &Condition<T>, coin_spend: &CoinSpend) -> Option<Self> {
102 let coin_id = coin_spend.coin.coin_id();
103 let kind = match condition {
104 Condition::AssertHeightAbsolute(a) => {
105 AssertionKind::HeightAbsolute(u64::from(a.height))
106 }
107 Condition::AssertHeightRelative(a) => {
108 AssertionKind::HeightRelative(u64::from(a.height))
109 }
110 Condition::AssertSecondsAbsolute(a) => AssertionKind::SecondsAbsolute(a.seconds),
111 Condition::AssertSecondsRelative(a) => AssertionKind::SecondsRelative(a.seconds),
112 Condition::AssertBeforeHeightAbsolute(a) => {
113 AssertionKind::BeforeHeightAbsolute(u64::from(a.height))
114 }
115 Condition::AssertBeforeHeightRelative(a) => {
116 AssertionKind::BeforeHeightRelative(u64::from(a.height))
117 }
118 Condition::AssertBeforeSecondsAbsolute(a) => {
119 AssertionKind::BeforeSecondsAbsolute(a.seconds)
120 }
121 Condition::AssertBeforeSecondsRelative(a) => {
122 AssertionKind::BeforeSecondsRelative(a.seconds)
123 }
124 _ => return None,
125 };
126 Some(Self { kind, coin_id })
127 }
128}
129
130/// Validated output from Tier 2 execution, bridging to Tier 3 state validation
131/// ([EXE-008](docs/requirements/domains/execution_validation/specs/EXE-008.md), [SPEC §7.4.7](docs/resources/SPEC.md)).
132///
133/// ## Field semantics
134///
135/// - **`additions`** — Flat list of [`Coin`] outputs created by all `CREATE_COIN` conditions across
136/// every [`chia_protocol::SpendBundle`] in the block, in block order (SPEC §3.4 grouping applies to
137/// the Merkle root; this vector is raw). STV-004 checks non-existence against [`crate::CoinLookup`].
138/// - **`removals`** — Coin IDs of every spent coin in the block, in block order. STV-002 looks these
139/// up to verify existence and "unspent" status; STV-003 cross-checks the puzzle hash against
140/// [`chia_protocol::CoinState`].
141/// - **`pending_assertions`** — Height / time lock assertions deferred from Tier 2 to Tier 3
142/// (EXE-009; evaluated by STV-005). Includes the eight `ASSERT_HEIGHT_*` / `ASSERT_SECONDS_*`
143/// variants plus their `BEFORE_*` counterparts.
144/// - **`total_cost`** — Sum of `SpendResult.conditions.cost` across every bundle; EXE-007 asserts
145/// `== header.total_cost`.
146/// - **`total_fees`** — Sum of per-bundle fees (input value − output value); EXE-006 asserts
147/// `== header.total_fees`.
148/// - **`receipts`** — One [`Receipt`] per included bundle for logging / indexing (RCP-002).
149/// Length equals `header.spend_bundle_count` on success.
150///
151/// ## Usage
152///
153/// Produced by `L2Block::validate_execution` (EXE-001, [SPEC §7.4](docs/resources/SPEC.md)) and
154/// consumed by `L2Block::validate_state` (STV-001, [SPEC §7.5](docs/resources/SPEC.md)). The
155/// struct is freely cloneable / serializable (SER-001) so Tier-2 outputs can be cached or shipped
156/// between tiers separated by a process boundary.
157///
158/// ## Field shape rationale
159///
160/// - **`Vec<Coin>` vs `Vec<Bytes32>` asymmetry:** Additions need the full coin record (parent id,
161/// puzzle hash, amount) for STV-004 / state-root recompute in STV-007; removals only need the id
162/// because Tier 3 resolves the full record through [`crate::CoinLookup`]. This matches SPEC §3.4 / §3.5
163/// where additions_root groups by `puzzle_hash` and removals_root is a Merkle set of ids.
164/// - **Pending assertions separate from receipts:** Receipts are per-bundle summary; pending
165/// assertions are per-spend condition decisions. Keeping them in distinct vectors avoids forcing
166/// Tier-3 code to walk receipts for condition data (EXE-004 collector semantics).
167#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
168pub struct ExecutionResult {
169 /// Coins created by `CREATE_COIN` conditions across all bundles in block order
170 /// ([SPEC §2.3](docs/resources/SPEC.md), EXE-004).
171 pub additions: Vec<chia_protocol::Coin>,
172 /// Coin IDs consumed (spent) across all bundles in block order (STV-002 target).
173 pub removals: Vec<Bytes32>,
174 /// Height / time lock assertions collected in Tier 2, evaluated in Tier 3 (EXE-009 / STV-005).
175 pub pending_assertions: Vec<PendingAssertion>,
176 /// Aggregate CLVM cost across all bundles ([`crate::primitives::Cost`]; EXE-007 consistency check).
177 pub total_cost: crate::primitives::Cost,
178 /// Aggregate fees across all bundles (EXE-006 consistency check).
179 pub total_fees: u64,
180 /// Per-bundle receipts ([`crate::Receipt`] / RCP-002) in insertion order.
181 pub receipts: Vec<crate::types::receipt::Receipt>,
182}
183
184/// Collect the height / time / before-height / before-time assertions carried by a
185/// [`chia_consensus::owned_conditions::OwnedSpendBundleConditions`] into a flat
186/// [`Vec<PendingAssertion>`] for Tier-3 evaluation
187/// ([EXE-004](docs/requirements/domains/execution_validation/specs/EXE-004.md), [SPEC §7.4.4](docs/resources/SPEC.md)).
188///
189/// ## Ordering
190///
191/// 1. **Block-level absolutes** (in this order): `height_absolute`, `seconds_absolute`,
192/// `before_height_absolute`, `before_seconds_absolute`. These have no owning spend — `coin_id`
193/// is [`Bytes32::default`] (all zeros). A block-level scalar of `0` means "no constraint" and
194/// is **not** emitted (absence vs explicit-zero disambiguation).
195/// 2. **Per-spend relatives** (in spend order): for each
196/// [`chia_consensus::owned_conditions::OwnedSpendConditions`], its `height_relative` /
197/// `seconds_relative` / `before_height_relative` / `before_seconds_relative` Options (when
198/// `Some`) become [`PendingAssertion`]s tagged with the spend's `coin_id`.
199///
200/// ## Why two-pass condition processing lives in `dig-clvm`
201///
202/// NORMATIVE EXE-004 describes a two-pass walk (collect outputs → validate assertions), but
203/// Implementation Notes explicitly allow delegation: "This logic may be partially inside
204/// dig-clvm." Announcement / concurrent-spend / self-assertion checks (Pass 2) run inside
205/// `chia_consensus::run_spendbundle` (called via [`dig_clvm::validate_spend_bundle`]); failures
206/// surface as [`dig_clvm::ValidationError::Clvm`] → [`crate::BlockError::ClvmExecutionFailed`]
207/// per EXE-003 mapping. Height / time / ASSERT_BEFORE_* conditions are **deferred** to Tier 3
208/// (STV-005) because they require chain context; this helper is the bridge.
209///
210/// ## ASSERT_EPHEMERAL
211///
212/// Not emitted here — ASSERT_EPHEMERAL is carried in spend `flags` inside
213/// `OwnedSpendConditions` and handled by Tier 3 (STV-002) against
214/// [`crate::ExecutionResult::additions`]. There is no dedicated `Ephemeral` variant on
215/// [`AssertionKind`]; the EXE-009 enum is intentionally limited to the 8 height/time opcodes.
216///
217/// ## Chia parity
218///
219/// `OwnedSpendBundleConditions` is the parsed form Chia's `run_spendbundle` produces
220/// ([`chia-consensus/src/owned_conditions.rs`](https://github.com/Chia-Network/chia_rs)). This
221/// helper walks it without duplicating any parse / CLVM logic.
222pub fn collect_pending_assertions_from_conditions(
223 conditions: &chia_consensus::owned_conditions::OwnedSpendBundleConditions,
224) -> Vec<PendingAssertion> {
225 let mut out = Vec::new();
226
227 // Block-level absolute assertions. A `0` for `height_absolute` / `seconds_absolute` means
228 // "no constraint" (chia-consensus aggregates the most strict value; 0 is the identity).
229 if conditions.height_absolute != 0 {
230 out.push(PendingAssertion {
231 kind: AssertionKind::HeightAbsolute(u64::from(conditions.height_absolute)),
232 coin_id: Bytes32::default(),
233 });
234 }
235 if conditions.seconds_absolute != 0 {
236 out.push(PendingAssertion {
237 kind: AssertionKind::SecondsAbsolute(conditions.seconds_absolute),
238 coin_id: Bytes32::default(),
239 });
240 }
241 if let Some(h) = conditions.before_height_absolute {
242 out.push(PendingAssertion {
243 kind: AssertionKind::BeforeHeightAbsolute(u64::from(h)),
244 coin_id: Bytes32::default(),
245 });
246 }
247 if let Some(t) = conditions.before_seconds_absolute {
248 out.push(PendingAssertion {
249 kind: AssertionKind::BeforeSecondsAbsolute(t),
250 coin_id: Bytes32::default(),
251 });
252 }
253
254 // Per-spend relative assertions, in spend order.
255 for spend in &conditions.spends {
256 if let Some(h) = spend.height_relative {
257 out.push(PendingAssertion {
258 kind: AssertionKind::HeightRelative(u64::from(h)),
259 coin_id: spend.coin_id,
260 });
261 }
262 if let Some(t) = spend.seconds_relative {
263 out.push(PendingAssertion {
264 kind: AssertionKind::SecondsRelative(t),
265 coin_id: spend.coin_id,
266 });
267 }
268 if let Some(h) = spend.before_height_relative {
269 out.push(PendingAssertion {
270 kind: AssertionKind::BeforeHeightRelative(u64::from(h)),
271 coin_id: spend.coin_id,
272 });
273 }
274 if let Some(t) = spend.before_seconds_relative {
275 out.push(PendingAssertion {
276 kind: AssertionKind::BeforeSecondsRelative(t),
277 coin_id: spend.coin_id,
278 });
279 }
280 }
281
282 out
283}
284
285/// Compute the [`crate::EMPTY_ROOT`]-anchored **state-delta root** for a block's additions +
286/// removals ([STV-007](docs/requirements/domains/state_validation/specs/STV-007.md), [SPEC §7.5.6](docs/resources/SPEC.md)).
287///
288/// ## Formula
289///
290/// - If `additions` AND `removals` are both empty: returns [`crate::EMPTY_ROOT`].
291/// - Else: `SHA256(0x01 || sorted_addition_ids_concat || 0x02 || sorted_removal_ids_concat)`
292/// where:
293/// - `sorted_addition_ids` = `additions.iter().map(|c| c.coin_id()).sorted()`
294/// - `sorted_removal_ids` = `removals.iter().sorted()`
295/// - `0x01` and `0x02` are domain separators borrowed from [`crate::HASH_LEAF_PREFIX`] /
296/// [`crate::HASH_TREE_PREFIX`] (HSH-007) so this value cannot be confused with other Merkle
297/// digests.
298///
299/// Sort-before-hash ensures determinism across insertion orders — proposer and validator agree
300/// even if their aggregation sequences differ.
301///
302/// ## Interim vs full sparse-Merkle state root
303///
304/// NORMATIVE STV-007 envisions a sparse-Merkle / Patricia-trie state computation reading from a
305/// parent state commitment exposed via [`crate::CoinLookup`]. `dig_block` does not yet require
306/// callers to expose `get_state_tree()`; this function provides a deterministic delta hash that
307/// satisfies the STV-007 acceptance criteria (match / mismatch / empty / ordering) for blocks
308/// whose parent state root is committed in header fields, letting producers and validators
309/// converge on the same `header.state_root` value. Adopters running a full state tree can
310/// shadow this function with their own root computation and keep the same header semantics.
311///
312/// ## Why a single SHA-256, not a Merkle tree
313///
314/// The delta is unordered sets of coin ids, not ordered leaves with membership proofs. A flat
315/// SHA-256 over sorted concatenation is enough for determinism + tamper detection at
316/// block-validation time. The header's Merkle roots for additions / removals ([HSH-004](docs/requirements/domains/hashing/specs/HSH-004.md) /
317/// [HSH-005](docs/requirements/domains/hashing/specs/HSH-005.md)) already give light clients membership proofs — `state_root`
318/// is a separate commitment covering the net state transition.
319#[must_use]
320pub fn compute_state_root_from_delta(
321 additions: &[chia_protocol::Coin],
322 removals: &[Bytes32],
323) -> Bytes32 {
324 use chia_sha2::Sha256;
325
326 if additions.is_empty() && removals.is_empty() {
327 return crate::constants::EMPTY_ROOT;
328 }
329
330 let mut add_ids: Vec<Bytes32> = additions.iter().map(|c| c.coin_id()).collect();
331 add_ids.sort();
332 let mut rem_ids: Vec<Bytes32> = removals.to_vec();
333 rem_ids.sort();
334
335 let mut hasher = Sha256::new();
336 hasher.update([crate::constants::HASH_LEAF_PREFIX]);
337 for id in &add_ids {
338 hasher.update(id.as_ref());
339 }
340 hasher.update([crate::constants::HASH_TREE_PREFIX]);
341 for id in &rem_ids {
342 hasher.update(id.as_ref());
343 }
344 Bytes32::new(hasher.finalize())
345}
346
347/// Map a [`dig_clvm::ValidationError`] to the appropriate [`crate::BlockError`] variant
348/// ([EXE-003](docs/requirements/domains/execution_validation/specs/EXE-003.md),
349/// [SPEC §7.4.3](docs/resources/SPEC.md)).
350///
351/// ## Why this lives in dig-block
352///
353/// `dig-clvm` is a vendored CLVM consensus engine; it raises domain-specific errors (per-coin id,
354/// per-spend issues). dig-block exposes a single taxonomy ([`crate::BlockError`]) so downstream
355/// callers never see `dig_clvm` variants — matching NORMATIVE EXE-003 / ERR-002.
356///
357/// ## Mapping table
358///
359/// | `dig_clvm::ValidationError` | `dig_block::BlockError` |
360/// |---|---|
361/// | `PuzzleHashMismatch(coin_id)` | `PuzzleHashMismatch { coin_id, expected, computed }` (hashes echo coin id as a best-effort — caller may enrich) |
362/// | `SignatureFailed` | `SignatureFailed { bundle_index: 0 }` (caller with bundle loop context may rewrap with the correct index) |
363/// | `CostExceeded { limit, consumed }` | `ClvmCostExceeded { cost: consumed, remaining: limit, coin_id: default }` |
364/// | `ConservationViolation { input, output }` | `CoinMinting { removed: input, added: output }` |
365/// | `CoinNotFound(coin_id)` | `CoinNotFound { coin_id }` |
366/// | `AlreadySpent(coin_id)` | `CoinAlreadySpent { coin_id, spent_height: 0 }` |
367/// | `DoubleSpend(coin_id)` | `DoubleSpendInBlock { coin_id }` |
368/// | `Clvm(reason)` | `ClvmExecutionFailed { coin_id: default, reason }` |
369/// | `Driver(e)` | `InvalidData(e.to_string())` |
370///
371/// ## Rationale
372///
373/// `dig-clvm`'s error variants carry less context than dig-block's (e.g. no `expected` /
374/// `computed` hash split for puzzle-hash mismatches — that split is a dig-block ergonomic on top
375/// of `coin_id`). The `bundle_index` field on [`crate::BlockError::SignatureFailed`] is set to
376/// `0` here because `dig-clvm` operates on one bundle at a time; callers iterating bundles can
377/// rewrap with the correct index (see `L2Block::validate_execution_with_context`).
378///
379/// ## Chia parity
380///
381/// Aligns with [`block_body_validation.py` Checks 15–22](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/block_body_validation.py)
382/// error codes.
383pub fn map_clvm_validation_error(err: dig_clvm::ValidationError) -> crate::error::BlockError {
384 use crate::error::BlockError;
385 match err {
386 dig_clvm::ValidationError::PuzzleHashMismatch(coin_id) => BlockError::PuzzleHashMismatch {
387 coin_id,
388 expected: coin_id,
389 computed: coin_id,
390 },
391 dig_clvm::ValidationError::SignatureFailed => {
392 BlockError::SignatureFailed { bundle_index: 0 }
393 }
394 dig_clvm::ValidationError::CostExceeded { limit, consumed } => {
395 BlockError::ClvmCostExceeded {
396 coin_id: Bytes32::default(),
397 cost: consumed,
398 remaining: limit,
399 }
400 }
401 dig_clvm::ValidationError::ConservationViolation { input, output } => {
402 BlockError::CoinMinting {
403 removed: input,
404 added: output,
405 }
406 }
407 dig_clvm::ValidationError::CoinNotFound(coin_id) => BlockError::CoinNotFound { coin_id },
408 dig_clvm::ValidationError::AlreadySpent(coin_id) => BlockError::CoinAlreadySpent {
409 coin_id,
410 spent_height: 0,
411 },
412 dig_clvm::ValidationError::DoubleSpend(coin_id) => {
413 BlockError::DoubleSpendInBlock { coin_id }
414 }
415 dig_clvm::ValidationError::Clvm(reason) => BlockError::ClvmExecutionFailed {
416 coin_id: Bytes32::default(),
417 reason,
418 },
419 dig_clvm::ValidationError::Driver(e) => BlockError::InvalidData(e.to_string()),
420 }
421}
422
423/// Verify that `tree_hash(coin_spend.puzzle_reveal) == coin_spend.coin.puzzle_hash`
424/// ([EXE-002](docs/requirements/domains/execution_validation/specs/EXE-002.md), [SPEC §7.4.2](docs/resources/SPEC.md)).
425///
426/// ## Rationale
427///
428/// A coin's `puzzle_hash` is committed on creation as the SHA-256-based Merkle tree hash of the
429/// spending puzzle. When the coin is spent, the spender reveals the full puzzle program in
430/// `CoinSpend.puzzle_reveal`. This function enforces the fundamental consensus rule that the
431/// revealed puzzle **must** hash to the committed value — otherwise the spender is substituting a
432/// different program (potentially with different conditions).
433///
434/// ## Implementation
435///
436/// Uses [`clvm_utils::tree_hash_from_bytes`] directly on the serialized CLVM bytes from
437/// [`chia_protocol::Program::as_slice`]. No allocator roundtrip is needed because the puzzle is
438/// already in canonical serialized form. NORMATIVE EXE-002 forbids custom tree-hash code.
439///
440/// ## Errors
441///
442/// - [`BlockError::PuzzleHashMismatch`] — the computed hash differs from `coin.puzzle_hash`.
443/// Carries the offending `coin_id`, the `expected` (committed) hash, and the `computed` hash
444/// so the caller can log / diagnose.
445/// - [`BlockError::InvalidData`] — `puzzle_reveal` is not well-formed CLVM bytes (rare; indicates
446/// a malformed upstream payload). Wraps the `clvm-utils` error message.
447///
448/// ## Chia parity
449///
450/// Matches [`block_body_validation.py` Check 20 (`WRONG_PUZZLE_HASH`)](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/block_body_validation.py).
451/// `dig-clvm::validate_spend_bundle` also performs this check internally (EXE-003); this
452/// standalone helper exists so the Tier-2 entry point can short-circuit before invoking CLVM.
453pub fn verify_coin_spend_puzzle_hash(
454 coin_spend: &chia_protocol::CoinSpend,
455) -> Result<(), crate::error::BlockError> {
456 let bytes = coin_spend.puzzle_reveal.as_slice();
457 let computed: Bytes32 = clvm_utils::tree_hash_from_bytes(bytes)
458 .map_err(|e| crate::error::BlockError::InvalidData(format!("tree_hash_from_bytes: {e}")))?
459 .into();
460 if computed != coin_spend.coin.puzzle_hash {
461 return Err(crate::error::BlockError::PuzzleHashMismatch {
462 coin_id: coin_spend.coin.coin_id(),
463 expected: coin_spend.coin.puzzle_hash,
464 computed,
465 });
466 }
467 Ok(())
468}