dig_block/types/receipt.rs
1//! Receipt domain types: [`ReceiptStatus`], [`Receipt`], [`ReceiptList`].
2//!
3//! ## Requirements trace
4//!
5//! - **[RCP-001](docs/requirements/domains/receipt/specs/RCP-001.md)** — [`ReceiptStatus`] discriminants `0..=4` and `255`.
6//! - **[RCP-002](docs/requirements/domains/receipt/specs/RCP-002.md)** — [`Receipt`] field layout (tx id, height, index, status, fees, state).
7//! - **[NORMATIVE](docs/requirements/domains/receipt/NORMATIVE.md)** — receipt domain obligations.
8//! - **[RCP-003](docs/requirements/domains/receipt/specs/RCP-003.md)** — [`ReceiptList`]: storage, Merkle [`ReceiptList::root`], accessors.
9//! - **[RCP-004](docs/requirements/domains/receipt/specs/RCP-004.md)** — [`ReceiptList::len`], success/failure counts, [`ReceiptList::total_fees`].
10//! - **[SPEC §2.9](docs/resources/SPEC.md)** — receipt payload context.
11//! - **[HSH-008](docs/requirements/domains/hashing/specs/HSH-008.md)** — receipts Merkle algorithm (same as this module’s root helper; see note below).
12//!
13//! ## Rationale
14//!
15//! - **`#[repr(u8)]`:** Stable single-byte tags for bincode payloads and receipt Merkle leaves ([RCP-001](docs/requirements/domains/receipt/specs/RCP-001.md) implementation notes).
16//! - **`Failed = 255`:** Leaves `5..=254` for future specific failure codes without renumbering existing wire values.
17//! - **`ReceiptStatus::from_u8`:** Unknown bytes map to [`ReceiptStatus::Failed`] so forward-compatible decoders never panic (RCP-001 implementation notes).
18//! - **`ReceiptList::push` without immediate root update:** Batch amortization per [RCP-003](docs/requirements/domains/receipt/specs/RCP-003.md); callers must [`ReceiptList::finalize`] (or use [`ReceiptList::from_receipts`]).
19//! - **`compute_receipts_root`:** [HSH-008](docs/requirements/domains/hashing/specs/HSH-008.md) algorithm lives in this module (bincode
20//! leaf + [`MerkleTree`]); re-exported as [`crate::compute_receipts_root`] from the crate root ([`types::receipt`](crate::types::receipt))
21//! to avoid `merkle_util` ↔ `Receipt` dependency cycles while keeping one normative implementation for [`ReceiptList`].
22//! - **Aggregates ([RCP-004](docs/requirements/domains/receipt/specs/RCP-004.md)):** `failure_count` is any status other than [`ReceiptStatus::Success`]; [`ReceiptList::total_fees`] sums [`Receipt::fee_charged`] for all rows (fees still charged on failed execution per spec notes). Used by checkpoint / epoch summaries ([CKP-006](docs/requirements/domains/checkpoint/specs/CKP-006.md) when implemented).
23
24use chia_sdk_types::MerkleTree;
25use chia_sha2::Sha256;
26use serde::{Deserialize, Serialize};
27
28use crate::constants::EMPTY_ROOT;
29use crate::primitives::Bytes32;
30
31/// Outcome of applying one transaction in a block ([SPEC §2.9](docs/resources/SPEC.md), RCP-001).
32///
33/// **Wire:** Use [`Self::as_u8`] / [`Self::from_u8`] for deterministic `u8` ↔ enum mapping; serde derives are
34/// retained for schema evolution ([SER-001](docs/requirements/domains/serialization/specs/SER-001.md)) and may be
35/// tuned in SER-* tasks for integer tagging.
36#[repr(u8)]
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
38pub enum ReceiptStatus {
39 /// Transaction executed successfully.
40 Success = 0,
41 /// Sender balance insufficient for the transaction cost.
42 InsufficientBalance = 1,
43 /// Nonce did not match account sequence.
44 InvalidNonce = 2,
45 /// Cryptographic signature verification failed.
46 InvalidSignature = 3,
47 /// Sender account missing from state.
48 AccountNotFound = 4,
49 /// Generic or reserved-range execution failure (`255` — see module docs).
50 Failed = 255,
51}
52
53impl ReceiptStatus {
54 /// Discriminant as a single byte (same as `self as u8` with [`#[repr(u8)]`](ReceiptStatus)).
55 #[inline]
56 #[must_use]
57 pub const fn as_u8(self) -> u8 {
58 self as u8
59 }
60
61 /// Decode a wire / stored byte into [`ReceiptStatus`].
62 ///
63 /// **Unknown values:** Any byte other than `0..=4` or `255` maps to [`Self::Failed`] so new failure codes
64 /// can be introduced later without breaking old decoders (they will classify unknowns as failed execution).
65 #[must_use]
66 pub fn from_u8(byte: u8) -> Self {
67 match byte {
68 0 => Self::Success,
69 1 => Self::InsufficientBalance,
70 2 => Self::InvalidNonce,
71 3 => Self::InvalidSignature,
72 4 => Self::AccountNotFound,
73 255 => Self::Failed,
74 _ => Self::Failed,
75 }
76 }
77}
78
79/// Result of executing one transaction inside a block ([RCP-002](docs/requirements/domains/receipt/specs/RCP-002.md), SPEC §2.9).
80///
81/// ## Field semantics
82///
83/// - **`tx_id`:** Transaction hash this receipt attests to (often spend-bundle / tx commitment — exact preimage in HSH-*).
84/// - **`tx_index`:** Zero-based position in block body (RCP-002 implementation notes).
85/// - **`post_state_root`:** State trie root **after** this tx; enables per-tx light-client checkpoints.
86/// - **`cumulative_fees`:** Running sum of `fee_charged` for receipts `0..=tx_index` in the same block; execution must keep this
87/// consistent when appending receipts ([RCP-002](docs/requirements/domains/receipt/specs/RCP-002.md) implementation notes).
88///
89/// **Serialization:** [`Serialize`] / [`Deserialize`] for bincode ([SER-001](docs/requirements/domains/serialization/specs/SER-001.md)).
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct Receipt {
92 /// Hash identifying the executed transaction.
93 pub tx_id: Bytes32,
94 /// Block height containing this transaction.
95 pub block_height: u64,
96 /// Zero-based transaction index within the block.
97 pub tx_index: u32,
98 /// Execution outcome ([`ReceiptStatus`], RCP-001).
99 pub status: ReceiptStatus,
100 /// Fee debited for this transaction.
101 pub fee_charged: u64,
102 /// State root after applying this transaction.
103 pub post_state_root: Bytes32,
104 /// Sum of `fee_charged` for all receipts up to and including this one in the block.
105 pub cumulative_fees: u64,
106}
107
108impl Receipt {
109 /// Construct a receipt with all NORMATIVE fields ([RCP-002](docs/requirements/domains/receipt/specs/RCP-002.md)).
110 ///
111 /// **Note:** Callers must ensure `cumulative_fees` matches the monotonic fee aggregate for the block; this crate does not
112 /// recompute it here (single-receipt constructor only).
113 pub fn new(
114 tx_id: Bytes32,
115 block_height: u64,
116 tx_index: u32,
117 status: ReceiptStatus,
118 fee_charged: u64,
119 post_state_root: Bytes32,
120 cumulative_fees: u64,
121 ) -> Self {
122 Self {
123 tx_id,
124 block_height,
125 tx_index,
126 status,
127 fee_charged,
128 post_state_root,
129 cumulative_fees,
130 }
131 }
132}
133
134/// Merkle root over ordered receipts: SHA-256(bincode(`Receipt`)) per leaf, then [`MerkleTree`] ([HSH-008](docs/requirements/domains/hashing/specs/HSH-008.md)).
135///
136/// **Public API:** Also exported as [`crate::compute_receipts_root`] for callers that hold a `[Receipt]` slice without a
137/// [`ReceiptList`] wrapper (structural validation, tooling).
138///
139/// **Empty list:** [`EMPTY_ROOT`] ([BLK-005](docs/requirements/domains/block_types/specs/BLK-005.md)).
140///
141/// **Tagged hashing:** [`MerkleTree`] applies leaf/node domain separation per [HSH-007](docs/requirements/domains/hashing/specs/HSH-007.md) (inherited from `chia-sdk-types`).
142#[must_use]
143pub fn compute_receipts_root(receipts: &[Receipt]) -> Bytes32 {
144 if receipts.is_empty() {
145 return EMPTY_ROOT;
146 }
147 let hashes: Vec<Bytes32> = receipts
148 .iter()
149 .map(|r| {
150 let bytes =
151 bincode::serialize(r).expect("Receipt bincode serialization should not fail");
152 let mut hasher = Sha256::new();
153 hasher.update(&bytes);
154 Bytes32::new(hasher.finalize())
155 })
156 .collect();
157 MerkleTree::new(&hashes).root()
158}
159
160/// Ordered block receipts with a commitments root ([RCP-003](docs/requirements/domains/receipt/specs/RCP-003.md), SPEC §2.9).
161///
162/// **Aggregates:** [`Self::len`], [`Self::success_count`], [`Self::failure_count`], [`Self::total_fees`] ([RCP-004](docs/requirements/domains/receipt/specs/RCP-004.md)).
163///
164/// **Wire:** [`Serialize`] / [`Deserialize`] include both `receipts` and `root`; consumers should re-validate or
165/// call [`Self::finalize`] after deserializing if they distrust the stored root.
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167pub struct ReceiptList {
168 /// Receipts in **block order** (must align with `tx_index` / execution order).
169 pub receipts: Vec<Receipt>,
170 /// Merkle root over [`Self::receipts`] — see [`Self::finalize`].
171 pub root: Bytes32,
172}
173
174impl Default for ReceiptList {
175 /// Same as [`Self::new`] — keeps [`crate::AttestedBlock::new`] and tests working with [`Default::default`].
176 fn default() -> Self {
177 Self::new()
178 }
179}
180
181impl ReceiptList {
182 /// Empty list, root = [`EMPTY_ROOT`] ([RCP-003](docs/requirements/domains/receipt/specs/RCP-003.md) `new()`).
183 #[must_use]
184 pub fn new() -> Self {
185 Self {
186 receipts: Vec::new(),
187 root: EMPTY_ROOT,
188 }
189 }
190
191 /// Take ownership of `receipts` and set [`Self::root`] via [`compute_receipts_root`].
192 #[must_use]
193 pub fn from_receipts(receipts: Vec<Receipt>) -> Self {
194 let root = compute_receipts_root(&receipts);
195 Self { receipts, root }
196 }
197
198 /// Append a receipt **without** updating [`Self::root`] — call [`Self::finalize`] when done ([RCP-003](docs/requirements/domains/receipt/specs/RCP-003.md)).
199 pub fn push(&mut self, receipt: Receipt) {
200 self.receipts.push(receipt);
201 }
202
203 /// Recompute [`Self::root`] from the current [`Self::receipts`] vector.
204 pub fn finalize(&mut self) {
205 self.root = compute_receipts_root(&self.receipts);
206 }
207
208 /// Borrow receipt at `index`, or `None` if out of bounds.
209 #[must_use]
210 pub fn get(&self, index: usize) -> Option<&Receipt> {
211 self.receipts.get(index)
212 }
213
214 /// First receipt whose [`Receipt::tx_id`] matches, or `None` ([RCP-003](docs/requirements/domains/receipt/specs/RCP-003.md) — linear scan).
215 #[must_use]
216 pub fn get_by_tx_id(&self, tx_id: Bytes32) -> Option<&Receipt> {
217 self.receipts.iter().find(|r| r.tx_id == tx_id)
218 }
219
220 /// Number of receipts in this list ([RCP-004](docs/requirements/domains/receipt/specs/RCP-004.md)).
221 #[inline]
222 #[must_use]
223 pub fn len(&self) -> usize {
224 self.receipts.len()
225 }
226
227 /// `true` when there are no receipts (same as `len() == 0`).
228 #[inline]
229 #[must_use]
230 pub fn is_empty(&self) -> bool {
231 self.receipts.is_empty()
232 }
233
234 /// Count of receipts whose [`Receipt::status`] is exactly [`ReceiptStatus::Success`].
235 ///
236 /// **RCP-004:** Complement is [`Self::failure_count`]; together they sum to [`Self::len`].
237 #[must_use]
238 pub fn success_count(&self) -> usize {
239 self.receipts
240 .iter()
241 .filter(|r| matches!(r.status, ReceiptStatus::Success))
242 .count()
243 }
244
245 /// Count of receipts with any **non-success** status (all variants except [`ReceiptStatus::Success`]).
246 ///
247 /// **Rationale:** Checkpoint and metrics code treat “failure” as “not Success” ([RCP-004](docs/requirements/domains/receipt/specs/RCP-004.md) implementation notes).
248 #[must_use]
249 pub fn failure_count(&self) -> usize {
250 self.receipts
251 .iter()
252 .filter(|r| !matches!(r.status, ReceiptStatus::Success))
253 .count()
254 }
255
256 /// Sum of [`Receipt::fee_charged`] over every receipt (success and failure — fees may still be levied).
257 #[must_use]
258 pub fn total_fees(&self) -> u64 {
259 self.receipts.iter().map(|r| r.fee_charged).sum()
260 }
261}