dig_block/merkle_util.rs
1//! Internal helpers for L2 block Merkle roots and BIP158 filters ([BLK-004](docs/requirements/domains/block_types/specs/BLK-004.md)).
2//!
3//! **Spec:** [SPEC §3.3–§3.6](docs/resources/SPEC.md). Uses `chia-consensus` Merkle sets, `chia-sdk-types`
4//! binary Merkle trees, `chia-sha2`, and `bitcoin::bip158::GcsFilterWriter` (BIP-158 parameters `M`, `P`
5//! matching Bitcoin Core / [`bitcoin::bip158`](https://docs.rs/bitcoin/latest/bitcoin/bip158/index.html)).
6//!
7//! **HSH-007 nuance:** `chia_sdk_types::MerkleTree` leaves/nodes use `0x01`/`0x02` tagging ([`crate::hash`]); `chia_consensus::merkle_set::compute_merkle_set_root` is a
8//! **different** radix-tree hash (see `chia_consensus::merkle_set`) for sorted coin-id sets — do not mix the two formulas.
9//!
10//! **HSH-003 ([`compute_spends_root`]):** spends roots use **binary** [`MerkleTree`] over per-bundle digests; each leaf is
11//! SHA-256 of the streamable [`SpendBundle`](chia_protocol::SpendBundle) bytes (same digest as [`SpendBundle::name`] /
12//! [`chia_traits::Streamable::hash`]) — see [HSH-003](docs/requirements/domains/hashing/specs/HSH-003.md).
13//!
14//! **HSH-004 ([`compute_additions_root`]):** additions roots use **`chia_consensus::merkle_set`** (radix Merkle **set**),
15//! not the tagged binary [`MerkleTree`] from HSH-007 — see [HSH-004](docs/requirements/domains/hashing/specs/HSH-004.md).
16//!
17//! **HSH-005 ([`compute_removals_root`]):** removals roots are a Merkle **set** over **raw removal coin IDs** (one leaf per
18//! ID) — see [HSH-005](docs/requirements/domains/hashing/specs/HSH-005.md).
19//!
20//! **HSH-006 ([`compute_filter_hash`]):** `filter_hash` is SHA-256 of BIP-158 GCS bytes over addition `puzzle_hash`s then
21//! removal `coin_id`s — see [HSH-006](docs/requirements/domains/hashing/specs/HSH-006.md).
22
23use bitcoin::bip158::GcsFilterWriter;
24use chia_consensus::merkle_set::compute_merkle_set_root;
25use chia_protocol::{Bytes32, Coin, SpendBundle};
26use chia_sdk_types::MerkleTree;
27use chia_sha2::Sha256;
28use chia_traits::Streamable;
29use clvmr::reduction::EvalErr;
30use indexmap::IndexMap;
31
32use crate::constants::EMPTY_ROOT;
33
34/// BIP-158 Golomb-Rice `P` and range `M` (same as Bitcoin / [`bitcoin::bip158`](https://docs.rs/bitcoin/latest/bitcoin/bip158/index.html)).
35const BIP158_M: u64 = 784_931;
36const BIP158_P: u8 = 19;
37
38/// Hash a list of coin IDs the way Chia L1 does for grouped additions ([`hash_coin_ids`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/types/blockchain_format/coin.py)).
39///
40/// **Rules:** one id → SHA-256(raw id); multiple → sort ids descending lexicographic, concatenate, SHA-256.
41pub fn hash_coin_ids(ids: &mut [Bytes32]) -> Bytes32 {
42 match ids.len() {
43 0 => EMPTY_ROOT,
44 1 => {
45 let mut h = Sha256::new();
46 h.update(ids[0].as_ref());
47 Bytes32::new(h.finalize())
48 }
49 _ => {
50 ids.sort_unstable_by(|a, b| b.as_ref().cmp(a.as_ref()));
51 let mut h = Sha256::new();
52 for id in ids.iter() {
53 h.update(id.as_ref());
54 }
55 Bytes32::new(h.finalize())
56 }
57 }
58}
59
60/// [`compute_merkle_set_root`] with DIG empty-set semantics: **empty → [`EMPTY_ROOT`]** (SPEC §3.3), not the
61/// internal all-zero sentinel used inside `chia-consensus`’s radix tree.
62pub fn merkle_set_root(leafs: &mut [[u8; 32]]) -> Bytes32 {
63 if leafs.is_empty() {
64 return EMPTY_ROOT;
65 }
66 Bytes32::new(compute_merkle_set_root(leafs))
67}
68
69/// Binary Merkle tree ([`MerkleTree`]) for spend-bundle hashes / slash leaves; empty → [`EMPTY_ROOT`].
70pub fn merkle_tree_root(leaves: &[Bytes32]) -> Bytes32 {
71 if leaves.is_empty() {
72 return EMPTY_ROOT;
73 }
74 MerkleTree::new(leaves).root()
75}
76
77/// **Spends root** (header `spends_root`, SPEC §3.3) — Merkle root over ordered spend-bundle leaf digests.
78///
79/// **Normative:** [HSH-003](docs/requirements/domains/hashing/specs/HSH-003.md).
80/// **Algorithm:** empty slice → [`EMPTY_ROOT`]; else a binary Merkle tree (`chia_sdk_types::MerkleTree::new`) over
81/// leaves `SHA-256(bundle.to_bytes())` in **slice order** (block order). Tagged hashing inside the tree follows
82/// HSH-007 using the same `chia-sdk-types` implementation.
83///
84/// **Equivalence:** For valid in-memory bundles, `SHA-256(to_bytes())` matches [`SpendBundle::name`] because Chia’s
85/// streamable `hash()` hashes the same serialized bytes ([`Streamable::hash`](chia_traits::Streamable::hash)).
86///
87/// **Callers:** [`crate::L2Block::compute_spends_root`](crate::L2Block::compute_spends_root) delegates here so block
88/// bodies and standalone bundle slices share one definition.
89#[must_use]
90pub fn compute_spends_root(spend_bundles: &[SpendBundle]) -> Bytes32 {
91 if spend_bundles.is_empty() {
92 return EMPTY_ROOT;
93 }
94 let leaves: Vec<Bytes32> = spend_bundles
95 .iter()
96 .map(|bundle| {
97 let bytes = bundle.to_bytes().unwrap_or_else(|e| {
98 panic!("SpendBundle::to_bytes failed for Merkle leaf (invariant): {e:?}")
99 });
100 let mut h = Sha256::new();
101 h.update(&bytes);
102 Bytes32::new(h.finalize())
103 })
104 .collect();
105 merkle_tree_root(&leaves)
106}
107
108/// **`additions_root`** (header field, SPEC §3.4) — Merkle-set root over created coins grouped by `puzzle_hash`.
109///
110/// **Normative:** [HSH-004](docs/requirements/domains/hashing/specs/HSH-004.md) and Chia
111/// [`block_body_validation`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/block_body_validation.py)
112/// (additions handling ~158–175).
113/// **Algorithm:** Walk `additions` in **slice order**; bucket coin IDs by [`Coin::puzzle_hash`]. Emit **two** 32-byte
114/// leaves per group, in **first-seen `puzzle_hash` order**: `[puzzle_hash, hash_coin_ids(ids…)]`, then
115/// `chia_consensus::merkle_set::compute_merkle_set_root` with DIG empty-set semantics ([`EMPTY_ROOT`] when there are
116/// no additions).
117///
118/// **Why [`IndexMap`] instead of `HashMap`:** HSH-004’s pseudocode uses a map for grouping; Chia’s Python uses dict
119/// **insertion order** when flattening groups. Rust’s `HashMap` iteration order is nondeterministic, which would make roots
120/// non-reproducible. [`IndexMap`] matches insertion-order semantics and the existing [`crate::L2Block::compute_additions_root`]
121/// behavior exercised by BLK-004 tests.
122///
123/// **`hash_coin_ids`:** sorts multiple IDs descending by bytes, concatenates, SHA-256 — see the crate-internal
124/// `hash_coin_ids` helper and Chia
125/// [`coin.py`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/types/blockchain_format/coin.py).
126///
127/// **Callers:** [`crate::L2Block::compute_additions_root`](crate::L2Block::compute_additions_root) delegates here after
128/// collecting [`SpendBundle::additions`]-derived [`Coin`]s so validation and tooling share one definition.
129#[must_use]
130pub fn compute_additions_root(additions: &[Coin]) -> Bytes32 {
131 if additions.is_empty() {
132 return EMPTY_ROOT;
133 }
134 let mut groups: IndexMap<Bytes32, Vec<Bytes32>> = IndexMap::new();
135 for coin in additions {
136 let id = coin.coin_id();
137 groups.entry(coin.puzzle_hash).or_default().push(id);
138 }
139 let mut leafs: Vec<[u8; 32]> = Vec::with_capacity(groups.len() * 2);
140 for (ph, mut ids) in groups {
141 leafs.push(ph.to_bytes());
142 leafs.push(hash_coin_ids(&mut ids).to_bytes());
143 }
144 merkle_set_root(&mut leafs)
145}
146
147/// **`removals_root`** (header field, SPEC §3.5) — Merkle-set root over all spent coin IDs in the block.
148///
149/// **Normative:** [HSH-005](docs/requirements/domains/hashing/specs/HSH-005.md) and Chia
150/// [`block_body_validation`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/block_body_validation.py)
151/// (~185, removal coin name set).
152/// **Algorithm:** Empty slice → [`EMPTY_ROOT`]. Otherwise each [`Bytes32`] removal ID is one **leaf** in the radix Merkle
153/// set (`chia_consensus::merkle_set::compute_merkle_set_root`). Unlike [`compute_additions_root`], there is **no** grouping:
154/// each spent coin ID is inserted directly.
155///
156/// **Order:** [`chia_consensus::merkle_set::compute_merkle_set_root`] defines a **set** hash: the same multiset of IDs
157/// yields the same root regardless of slice order (validated in `tests/test_hsh_005_removals_root.rs`).
158///
159/// **Callers:** [`crate::L2Block::compute_removals_root`](crate::L2Block::compute_removals_root) delegates here using
160/// [`crate::L2Block::all_removals`] order as the canonical body walk; permuting the input slice must not change the root
161/// when the set of IDs is unchanged.
162#[must_use]
163pub fn compute_removals_root(removals: &[Bytes32]) -> Bytes32 {
164 if removals.is_empty() {
165 return EMPTY_ROOT;
166 }
167 // UFCS / ownership: inherent [`Bytes32::to_bytes`] takes `self` (Copy); avoid resolving to
168 // [`Streamable::to_bytes`] (`Result`) while `chia_traits::Streamable` is in scope for HSH-003.
169 let mut leafs: Vec<[u8; 32]> = removals.iter().map(|b| (*b).to_bytes()).collect();
170 merkle_set_root(&mut leafs)
171}
172
173/// [`L2Block::slash_proposal_leaf_hash`](crate::types::block::L2Block::slash_proposal_leaf_hash) — SHA-256 over raw payload.
174#[inline]
175pub fn slash_leaf_hash(payload: &[u8]) -> Bytes32 {
176 let mut h = Sha256::new();
177 h.update(payload);
178 Bytes32::new(h.finalize())
179}
180
181/// BIP-158 **encoded filter bytes** (Golomb–Rice GCS) for block light-client filtering ([HSH-006](docs/requirements/domains/hashing/specs/HSH-006.md)).
182///
183/// **Element order:** each [`Coin::puzzle_hash`] for `additions` in slice order, then each removal [`Bytes32`] in
184/// `removals` slice order — matches [`crate::L2Block::all_additions`] / [`crate::L2Block::all_removals`] when those slices are built
185/// the same way ([SPEC §3.6](docs/resources/SPEC.md)).
186///
187/// **Wire:** [`compact_block_filter_encoded`] is a thin wrapper assembling the `[[u8;32]; n]` table and calling
188/// the crate-internal `bip158_filter_encoded` helper. **Commitment:** [`compute_filter_hash`] is **SHA-256** of the returned bytes (Chia
189/// `std_hash(encoded)` pattern).
190///
191/// **Downstream:** Light clients need these bytes (not only the hash) to run membership queries via
192/// [`bitcoin::bip158::BlockFilter`].
193pub fn compact_block_filter_encoded(
194 block_identity: Bytes32,
195 additions: &[Coin],
196 removals: &[Bytes32],
197) -> Result<Vec<u8>, std::io::Error> {
198 let mut elements: Vec<[u8; 32]> = Vec::with_capacity(additions.len() + removals.len());
199 for c in additions {
200 // UFCS: `Bytes32` also has [`Streamable::to_bytes`] (`Result`); use inherent `to_bytes(self) -> [u8; 32]`.
201 elements.push(Bytes32::to_bytes(c.puzzle_hash));
202 }
203 for id in removals {
204 elements.push(Bytes32::to_bytes(*id));
205 }
206 bip158_filter_encoded(block_identity, &elements)
207}
208
209/// **`filter_hash`** (header field, SPEC §3.6) — SHA-256 over BIP-158 compact filter bytes.
210///
211/// **Normative:** [HSH-006](docs/requirements/domains/hashing/specs/HSH-006.md).
212/// **Algorithm:** [`compact_block_filter_encoded`] then [`Sha256`] over the wire (empty IO error → hash of empty encoding,
213/// matching previous [`crate::L2Block::compute_filter_hash`] behavior via `unwrap_or_default()`).
214///
215/// **SipHash keys:** First 8 + next 8 bytes (LE `u64`) of `block_identity` — same as the crate-internal
216/// `bip158_filter_encoded` helper /
217/// Bitcoin [`GcsFilterWriter`](bitcoin::bip158::GcsFilterWriter) initialization used in this crate.
218///
219/// **Callers:** [`crate::L2Block::compute_filter_hash`](crate::L2Block::compute_filter_hash) passes
220/// [`crate::L2BlockHeader::parent_hash`](crate::types::header::L2BlockHeader::parent_hash) as `block_identity` (SPEC §6.4;
221/// avoids circular dependence on the committed `filter_hash` inside [`L2Block::hash`](crate::L2Block::hash)).
222#[must_use]
223pub fn compute_filter_hash(
224 block_identity: Bytes32,
225 additions: &[Coin],
226 removals: &[Bytes32],
227) -> Bytes32 {
228 let encoded =
229 compact_block_filter_encoded(block_identity, additions, removals).unwrap_or_default();
230 let mut h = Sha256::new();
231 h.update(&encoded);
232 Bytes32::new(h.finalize())
233}
234
235/// Encode BIP-158 compact filter bytes (GCS) over **32-byte elements** (puzzle hashes and coin IDs), keyed
236/// by the first 16 bytes of `block_identity` (same layout as Bitcoin’s block-filter construction).
237///
238/// **Chia parity:** [`block_creation.py`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/consensus/block_creation.py)
239/// builds `byte_array_tx` from puzzle hashes / coin names, then `filter_hash = std_hash(PyBIP158(...).GetEncoded())`.
240/// Elements should follow block semantics: addition puzzle hashes in [`crate::L2Block::all_additions`] order, then
241/// removal coin IDs in [`crate::L2Block::all_removals`] order (SPEC §3.6).
242pub fn bip158_filter_encoded(
243 block_identity: Bytes32,
244 elements: &[[u8; 32]],
245) -> Result<Vec<u8>, std::io::Error> {
246 let b = block_identity.as_ref();
247 let k0 = u64::from_le_bytes(b[0..8].try_into().expect("8 bytes"));
248 let k1 = u64::from_le_bytes(b[8..16].try_into().expect("8 bytes"));
249 let mut out = Vec::new();
250 {
251 let mut w = GcsFilterWriter::new(&mut out, k0, k1, BIP158_M, BIP158_P);
252 for e in elements {
253 w.add_element(e);
254 }
255 w.finish()?;
256 }
257 Ok(out)
258}
259
260/// Map failed CLVM addition parsing to an empty vec (malformed spends contribute no additions for helpers).
261#[inline]
262pub fn empty_on_additions_err<T>(r: Result<Vec<T>, EvalErr>) -> Vec<T> {
263 r.unwrap_or_default()
264}