Skip to main content

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}