Skip to main content

dig_coinstore/
types.rs

1//! Domain types for dig-coinstore.
2//!
3//! Core data structures: [`CoinRecord`], [`ChiaCoinRecord`], [`BlockData`], [`CoinAddition`],
4//! [`ApplyBlockResult`], [`RollbackResult`], [`CoinStoreStats`],
5//! [`CoinStoreSnapshot`], [`UnspentLineageInfo`].
6//!
7//! Also defines type aliases: [`CoinId`] = [`Bytes32`], [`PuzzleHash`] = [`Bytes32`] (see API-009).
8//!
9//! # Requirements
10//! - **API-002:** [`CoinRecord`], [`ChiaCoinRecord`], [`CoinId`]
11//! - **API-005:** [`BlockData`], [`CoinAddition`]
12//! - **API-006:** [`ApplyBlockResult`], [`RollbackResult`]
13//! - **API-007:** [`CoinStoreStats`]
14//! - **API-008:** [`CoinStoreSnapshot`]
15//! - **API-009:** [`CoinId`], [`PuzzleHash`], [`UnspentLineageInfo`]
16//!
17//! ## `ChiaCoinRecord` vs `chia_protocol::CoinRecord`
18//!
19//! The upstream streamable type is documented at
20//! [docs.rs `chia_protocol::CoinRecord`](https://docs.rs/chia-protocol/latest/chia_protocol/struct.CoinRecord.html).
21//! This crate pins `chia-protocol` **0.26** together with [`dig_clvm`](https://github.com/DIG-Network/dig-clvm)
22//! for a single `Coin` / [`Bytes32`] identity graph. That protocol release does **not** yet export
23//! `CoinRecord`, so we define [`ChiaCoinRecord`] here with **identical fields and semantics** to the
24//! current Chia reference implementation. When `dig-clvm` upgrades `chia-protocol`, [`ChiaCoinRecord`]
25//! should become `pub use chia_protocol::CoinRecord as ChiaCoinRecord` (STR-005).
26
27use std::collections::HashMap;
28
29use serde::{Deserialize, Serialize};
30
31use crate::Bytes32;
32use crate::Coin;
33use crate::CoinState;
34
35// ─────────────────────────────────────────────────────────────────────────────
36// Type aliases (API-002 / API-009)
37// ─────────────────────────────────────────────────────────────────────────────
38
39/// 32-byte coin identifier: `sha256(parent_coin_info || puzzle_hash || amount)`.
40///
41/// Alias of [`Bytes32`] for readable APIs (`get_coin_record(&CoinId)`).
42///
43/// See: [`Coin::coin_id`], docs/resources/SPEC.md §2.1
44pub type CoinId = Bytes32;
45
46/// Puzzle hash (SHA256 of serialized puzzle program). Same underlying type as [`CoinId`].
47///
48/// Fully specified under API-009; exported early for `CoinRecord::coin_id()` return type clarity.
49pub type PuzzleHash = Bytes32;
50
51// ─────────────────────────────────────────────────────────────────────────────
52// Chia wire-shaped coin row (interop)
53// ─────────────────────────────────────────────────────────────────────────────
54
55/// Row-shaped coin metadata matching Chia full-node / light-wallet `CoinRecord` streamable layout.
56///
57/// **Why this exists:** `chia-protocol` 0.26 (bundled via `dig-clvm`) does not define this struct;
58/// Chia’s reference layout is still the contract for RPC and cross-repo interop. Field names mirror
59/// [`chia_protocol::CoinRecord`](https://docs.rs/chia-protocol/latest/chia_protocol/struct.CoinRecord.html).
60///
61/// **`spent_block_index` sentinel:** `0` means *unspent* in Chia’s encoding; positive values are
62/// spent heights. Fast-forward–eligible rows (Python `spent_index == -1`) are not represented here;
63/// dig-coinstore uses [`CoinRecord::ff_eligible`] instead once ingested.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65pub struct ChiaCoinRecord {
66    /// The coin identity and payload (parent id, puzzle hash, amount).
67    pub coin: Coin,
68    /// Height at which the coin was included (confirmed).
69    pub confirmed_block_index: u32,
70    /// `0` = unspent; otherwise spent at this height.
71    pub spent_block_index: u32,
72    /// Block-reward coin vs transaction output.
73    pub coinbase: bool,
74    /// Block timestamp at `confirmed_block_index`.
75    pub timestamp: u64,
76}
77
78impl ChiaCoinRecord {
79    /// Construct a protocol-shaped row (mainly for tests and RPC adapters).
80    #[inline]
81    pub const fn new(
82        coin: Coin,
83        confirmed_block_index: u32,
84        spent_block_index: u32,
85        coinbase: bool,
86        timestamp: u64,
87    ) -> Self {
88        Self {
89            coin,
90            confirmed_block_index,
91            spent_block_index,
92            coinbase,
93            timestamp,
94        }
95    }
96}
97
98// ─────────────────────────────────────────────────────────────────────────────
99// CoinRecord — authoritative stored row (API-002)
100// ─────────────────────────────────────────────────────────────────────────────
101
102/// Full lifecycle state of one coin in the coinstore.
103///
104/// Persists after spending for history + rollback (SPEC.md §2.2). Prefer [`Option<u64>`] for
105/// [`CoinRecord::spent_height`] over Chia’s `spent_block_index == 0` sentinel to keep Rust matches
106/// exhaustive and avoid double meanings for `0`.
107///
108/// See: [`API-002`](../../docs/requirements/domains/crate_api/specs/API-002.md),
109/// Chia reference: <https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/coin_store.py>
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub struct CoinRecord {
112    /// Immutable coin identity and value.
113    pub coin: Coin,
114    /// Height where the coin was created / confirmed.
115    pub confirmed_height: u64,
116    /// Spend height when spent; [`None`] if still unspent.
117    pub spent_height: Option<u64>,
118    /// Whether this coin came from the block reward (not a normal tx output).
119    pub coinbase: bool,
120    /// Timestamp of the confirming block.
121    pub timestamp: u64,
122    /// Singleton fast-forward candidate (set at ingestion when `same_as_parent`; recomputed on rollback).
123    pub ff_eligible: bool,
124}
125
126impl CoinRecord {
127    /// New **unspent** coin at `confirmed_height` with `ff_eligible = false`.
128    ///
129    /// Callers set [`CoinRecord::ff_eligible`] later (e.g. `apply_block` when `CoinAddition::same_as_parent`).
130    #[must_use]
131    pub fn new(coin: Coin, confirmed_height: u64, timestamp: u64, coinbase: bool) -> Self {
132        Self {
133            coin,
134            confirmed_height,
135            spent_height: None,
136            coinbase,
137            timestamp,
138            ff_eligible: false,
139        }
140    }
141
142    /// `true` iff [`CoinRecord::spent_height`] is present.
143    #[must_use]
144    pub fn is_spent(&self) -> bool {
145        self.spent_height.is_some()
146    }
147
148    /// Mark spent at `height` (struct does **not** assert double-spend; pipeline validates).
149    pub fn spend(&mut self, height: u64) {
150        self.spent_height = Some(height);
151    }
152
153    /// Same digest as [`Coin::coin_id`] on the embedded coin (spec: never reimplement ID math).
154    #[must_use]
155    pub fn coin_id(&self) -> CoinId {
156        self.coin.coin_id()
157    }
158
159    /// Lightweight sync view: maps heights to [`Option<u32>`] per [`CoinState`] wire encoding.
160    ///
161    /// **Truncation note:** `u64` heights are cast to `u32`. For practical chains this fits; debug
162    /// builds assert no loss when truncating `confirmed_height`.
163    #[must_use]
164    pub fn to_coin_state(&self) -> CoinState {
165        debug_assert!(self.confirmed_height <= u64::from(u32::MAX));
166        let created = Some(self.confirmed_height as u32);
167        let spent = self.spent_height.map(|h| {
168            debug_assert!(h <= u64::from(u32::MAX));
169            h as u32
170        });
171        CoinState::new(self.coin, spent, created)
172    }
173
174    /// Ingest a Chia-shaped row into the native coinstore model.
175    ///
176    /// Mapping rules match API-002 / SPEC §2.2:
177    /// - `spent_block_index == 0` → [`None`] spent height
178    /// - `spent_block_index > 0` → [`Some`] as `u64`
179    /// - `ff_eligible` is always reset to `false` (not carried on wire)
180    #[must_use]
181    pub fn from_chia_coin_record(record: ChiaCoinRecord) -> Self {
182        let spent_height = if record.spent_block_index == 0 {
183            None
184        } else {
185            Some(u64::from(record.spent_block_index))
186        };
187        Self {
188            coin: record.coin,
189            confirmed_height: u64::from(record.confirmed_block_index),
190            spent_height,
191            coinbase: record.coinbase,
192            timestamp: record.timestamp,
193            ff_eligible: false,
194        }
195    }
196
197    /// Export to Chia wire-shaped row. Loses [`CoinRecord::ff_eligible`].
198    ///
199    /// # Panics
200    /// If `confirmed_height` or `spent_height` exceed `u32::MAX` (should not occur before ~4B blocks).
201    #[must_use]
202    pub fn to_chia_coin_record(&self) -> ChiaCoinRecord {
203        assert!(self.confirmed_height <= u64::from(u32::MAX));
204        let spent_block_index = match self.spent_height {
205            None => 0,
206            Some(h) => {
207                assert!(h <= u64::from(u32::MAX));
208                h as u32
209            }
210        };
211        ChiaCoinRecord::new(
212            self.coin,
213            self.confirmed_height as u32,
214            spent_block_index,
215            self.coinbase,
216            self.timestamp,
217        )
218    }
219}
220
221// ─────────────────────────────────────────────────────────────────────────────
222// Block application input (API-005)
223// ─────────────────────────────────────────────────────────────────────────────
224//
225// See: docs/requirements/domains/crate_api/specs/API-005.md,
226// docs/resources/SPEC.md §2.4, Chia `coin_store.py` `new_block()` parameters.
227
228/// One transaction-created coin plus ingestion metadata for [`BlockData::additions`].
229///
230/// **`same_as_parent`:** `true` when this coin’s [`Coin::puzzle_hash`] and [`Coin::amount`] match the
231/// spent parent’s puzzle hash and amount — the block pipeline uses this for singleton **fast-forward**
232/// eligibility ([`CoinRecord::ff_eligible`], BLK-007).
233///
234/// **`coin_id`:** For valid blocks this MUST equal [`Coin::coin_id`] on [`Self::coin`]. The struct does not
235/// enforce equality at construction time; BLK-* / `apply_block` validation rejects mismatches so callers
236/// cannot poison the store with an inconsistent ID ([API-005 test plan](docs/requirements/domains/crate_api/specs/API-005.md#verification)).
237///
238/// **Chia reference:** `tx_additions` tuples in
239/// [`coin_store.py`](https://github.com/Chia-Network/chia-blockchain/blob/main/chia/full_node/coin_store.py).
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241pub struct CoinAddition {
242    /// Coin ID (`sha256(parent || puzzle_hash || amount)`) — use [`Coin::coin_id`], never a custom hash.
243    pub coin_id: CoinId,
244    /// The created coin (parent id, puzzle hash, amount).
245    pub coin: Coin,
246    /// Same puzzle hash and amount as the parent coin being spent in this block.
247    pub same_as_parent: bool,
248}
249
250impl CoinAddition {
251    /// Build from a [`Coin`] using [`Coin::coin_id`] as [`Self::coin_id`] (recommended for callers).
252    ///
253    /// **Rationale:** Centralizes the “no custom coin ID” rule ([STR-005](docs/requirements/domains/crate_structure/specs/STR-005.md),
254    /// project `start.md` hard rules).
255    #[must_use]
256    pub fn from_coin(coin: Coin, same_as_parent: bool) -> Self {
257        let coin_id = coin.coin_id();
258        Self {
259            coin_id,
260            coin,
261            same_as_parent,
262        }
263    }
264}
265
266/// Pre-validated block state changes: input to `CoinStore::apply_block` (BLK-*).
267///
268/// The coinstore **does not** run CLVM — the caller extracts additions, removals, coinbase rewards, and
269/// hints from execution results, then fills this struct ([API-005](docs/requirements/domains/crate_api/specs/API-005.md#summary)).
270///
271/// | Field | Role |
272/// |-------|------|
273/// | `height` / `timestamp` / `block_hash` / `parent_hash` | Chain linkage + time (validated in BLK-002, BLK-003) |
274/// | `additions` / `removals` | UTXO delta |
275/// | `coinbase_coins` | Farmer + pool rewards (count rules: BLK-004) |
276/// | `hints` | CREATE_COIN hint bytes for the hint index (HNT-*) |
277/// | `expected_state_root` | Optional post-apply root check (BLK-009) |
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279pub struct BlockData {
280    /// Block height; must be `current_height + 1` when applied ([BLK-002](docs/requirements/domains/block_application/specs/BLK-002.md)).
281    pub height: u64,
282    /// Unix timestamp (seconds) of the block.
283    pub timestamp: u64,
284    /// This block’s header hash (tip / chain tracking).
285    pub block_hash: Bytes32,
286    /// Parent block header hash; must match current tip ([BLK-003](docs/requirements/domains/block_application/specs/BLK-003.md)).
287    pub parent_hash: Bytes32,
288    /// Transaction-created coins (+ metadata); Chia `tx_additions`.
289    pub additions: Vec<CoinAddition>,
290    /// Spent coin IDs from transaction spends in this block.
291    pub removals: Vec<CoinId>,
292    /// Block reward outputs (empty at genesis; ≥ 2 after — [BLK-004](docs/requirements/domains/block_application/specs/BLK-004.md)).
293    pub coinbase_coins: Vec<Coin>,
294    /// Hint bytes per coin id from CREATE_COIN conditions (wallet / subscription index).
295    pub hints: Vec<(CoinId, Bytes32)>,
296    /// If set, `apply_block` verifies the computed state root matches ([BLK-009](docs/requirements/domains/block_application/specs/BLK-009.md)).
297    pub expected_state_root: Option<Bytes32>,
298}
299
300/// Summary returned after a successful [`crate::coin_store::CoinStore::apply_block`] (success path of
301/// `Result<ApplyBlockResult, CoinStoreError>`).
302///
303/// **Source of truth:** [`docs/resources/SPEC.md`](../../docs/resources/SPEC.md) §3.2. Field meanings:
304/// post-apply Merkle **state root**, how many coins were **created** (tx additions + coinbase),
305/// how many were **marked spent**, and the new tip **height** (= input block height when validation passes).
306///
307/// **Chia note:** Chia’s `new_block()` updates storage in place and returns nothing; this struct is the
308/// dig-coinstore contract for observability and tests ([API-006](docs/requirements/domains/crate_api/specs/API-006.md)).
309///
310/// # Requirement: API-006
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
312pub struct ApplyBlockResult {
313    /// Merkle root after inserting additions, marking removals, and batch-updating the tree (BLK-013).
314    pub state_root: Bytes32,
315    /// `block.additions.len() + block.coinbase_coins.len()` after successful apply (API-006 field table).
316    pub coins_created: usize,
317    /// `block.removals.len()` — each removal marks one coin spent at this height.
318    pub coins_spent: usize,
319    /// New chain tip height (same as applied [`BlockData::height`] on success).
320    pub height: u64,
321}
322
323/// Summary returned after a successful rollback ([`crate::coin_store::CoinStore::rollback_to_block`],
324/// [`crate::coin_store::CoinStore::rollback_n_blocks`]).
325///
326/// **`modified_coins`:** Chia’s `rollback_to_block` returns `dict[bytes32, CoinRecord]`
327/// ([`coin_store.py:567`](https://github.com/Chia-Network/chia-blockchain/blob/6e7a4954edccd8ab83fcacf938cfc42ddfcad7f2/chia/full_node/coin_store.py#L567)).
328/// dig-coinstore keeps that map and adds explicit **`coins_deleted`** / **`coins_unspent`** counts
329/// (SPEC §1.6 improvement #11; [API-006](docs/requirements/domains/crate_api/specs/API-006.md)).
330///
331/// **Count invariant (well-formed results):** For each entry in `modified_coins`, the rollback either
332/// **deleted** a coin confirmed after the target height (`coins_deleted`) or **reverted a spend**
333/// for a coin spent after the target (`coins_unspent`). Callers assembling this struct should ensure
334/// `coins_deleted + coins_unspent == modified_coins.len()` when every modified coin is accounted for once.
335///
336/// # Requirement: API-006
337#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
338pub struct RollbackResult {
339    /// Affected coin IDs → post-rollback–relevant [`CoinRecord`] snapshot (deleted or un-spent row).
340    pub modified_coins: HashMap<CoinId, CoinRecord>,
341    /// Coins removed from the store (created strictly after the rollback target height).
342    pub coins_deleted: usize,
343    /// Coins whose `spent_height` was cleared (were spent strictly after target height).
344    pub coins_unspent: usize,
345    /// Chain tip height after rollback (equals target height on success).
346    pub new_height: u64,
347}
348
349/// Aggregated chain + coinset metrics returned by [`crate::coin_store::CoinStore::stats`] (API-007 / QRY-010).
350///
351/// **Design goal (SPEC §1.6 #18):** eventually all aggregate fields are **O(1)** materialized counters
352/// updated in the same write batch as `apply_block` / rollback ([`docs/resources/SPEC.md`](../../docs/resources/SPEC.md)).
353/// Until PRF-003 lands, [`CoinStore::stats`](crate::coin_store::CoinStore::stats) may derive some fields by
354/// scanning `coin_records` (documented on that method) while still returning this single struct shape.
355///
356/// **Operational use:** dashboards, health checks, and mempool admission logic read one snapshot instead of
357/// issuing multiple Chia-style COUNT queries ([`coin_store.py:96-103`](https://github.com/Chia-Network/chia-blockchain/blob/6e7a4954edccd8ab83fcacf938cfc42ddfcad7f2/chia/full_node/coin_store.py#L96)).
358///
359/// # Requirement: API-007
360/// # Spec: docs/requirements/domains/crate_api/specs/API-007.md
361#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
362pub struct CoinStoreStats {
363    /// Current chain tip height (same source as [`crate::coin_store::CoinStore::height`]).
364    pub height: u64,
365    /// Timestamp (seconds) of the current tip block.
366    pub timestamp: u64,
367    /// Count of coins with [`CoinRecord::spent_height`] == [`None`].
368    pub unspent_count: u64,
369    /// Count of coins with [`CoinRecord::spent_height`] present (historical spends retained).
370    pub spent_count: u64,
371    /// Sum of [`Coin::amount`](crate::Coin::amount) over all unspent [`CoinRecord`] rows.
372    pub total_unspent_value: u64,
373    /// Sparse Merkle root over coin record leaves ([`crate::merkle::SparseMerkleTree`]).
374    pub state_root: Bytes32,
375    /// Header hash of the current tip block.
376    pub tip_hash: Bytes32,
377    /// Rows in the forward hint index ([`crate::storage::schema::CF_HINTS`]).
378    pub hint_count: u64,
379    /// Rows in [`crate::storage::schema::CF_STATE_SNAPSHOTS`] (retained checkpoints).
380    pub snapshot_count: usize,
381}
382
383/// Serializable checkpoint of the full coinstate at one instant (fast sync / backup / restore).
384///
385/// **Why it exists:** Chia has no first-class snapshot object; nodes replay from genesis. dig-coinstore
386/// standardizes this struct for bincode persistence and future checkpoint sync ([`SPEC.md`](../../docs/resources/SPEC.md)
387/// §1.6 improvement #6, §3.14; [API-008](docs/requirements/domains/crate_api/specs/API-008.md)).
388///
389/// **`block_hash`:** Tip header hash at capture time (same role as [`crate::coin_store::CoinStore::tip_hash`]
390/// when produced by a future `CoinStore::snapshot` implementation — tracked under PRF-008).
391///
392/// **`coins` / `hints`:** Full materialized rows and `(coin_id, hint)` pairs — same `hints` element type as
393/// [`BlockData::hints`] for consistency across APIs.
394///
395/// **`total_coins` / `total_value`:** Per API-008 field table, `total_coins` SHOULD equal `coins.len()` as `u64`,
396/// and `total_value` SHOULD be the sum of **unspent** [`CoinRecord`] amounts at capture time. The type does
397/// not enforce these invariants; `CoinStore::snapshot` (PRF-008) must populate them coherently.
398///
399/// # Requirement: API-008
400/// # Spec: docs/requirements/domains/crate_api/specs/API-008.md
401#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
402pub struct CoinStoreSnapshot {
403    /// Tip height when the snapshot was taken.
404    pub height: u64,
405    /// Tip block header hash when the snapshot was taken.
406    pub block_hash: Bytes32,
407    /// Sparse Merkle root over coin record leaves at capture time.
408    pub state_root: Bytes32,
409    /// Tip block unix timestamp (seconds).
410    pub timestamp: u64,
411    /// Every coin row (spent and unspent) needed to rebuild indices and history.
412    pub coins: HashMap<CoinId, CoinRecord>,
413    /// Hint pairs carried on coins at capture time (same shape as [`BlockData::hints`]).
414    pub hints: Vec<(CoinId, Bytes32)>,
415    /// Count of coin rows (`coins.len()` when built by [`crate::coin_store::CoinStore::snapshot`]).
416    pub total_coins: u64,
417    /// Sum of unspent coin amounts (mojos) at capture time.
418    pub total_value: u64,
419}
420
421/// Lineage chain for **one unspent coin**, used when resolving singleton **fast-forward** candidates
422/// ([`SPEC.md`](../../docs/resources/SPEC.md) §2.5, [`QRY-008`](../../docs/requirements/domains/queries/specs/QRY-008.md)).
423///
424/// # Field semantics
425///
426/// - **`coin_id`:** The unspent leaf’s identity (`sha256(parent || puzzle_hash || amount)`), same as
427///   [`Coin::coin_id()`](crate::Coin::coin_id).
428/// - **`parent_id`:** That coin’s `parent_coin_info` (the parent coin’s name / id in Chia terminology).
429/// - **`parent_parent_id`:** The parent coin row’s `parent_coin_info` (grandparent link in the lineage chain).
430///
431/// # Genesis / missing rows
432///
433/// Chia encodes coinbase parents with fixed sentinel bytes; there may be **no** grandparent coin row in the
434/// coinset. Callers still store a [`CoinId`] value (often all-zero bytes) for `parent_parent_id` when the
435/// wallet or mempool has no further ancestor ([`API-009.md`](../../docs/requirements/domains/crate_api/specs/API-009.md) implementation notes).
436///
437/// **Serde:** Not required by API-009; QRY-008 returns this in-process. Add `Serialize`/`Deserialize` only if
438/// an RPC boundary needs a stable wire shape.
439///
440/// # Requirement: API-009
441/// # Spec: docs/requirements/domains/crate_api/specs/API-009.md
442#[derive(Debug, Clone, PartialEq)]
443pub struct UnspentLineageInfo {
444    /// This coin’s id (the unspent singleton or leaf being queried).
445    pub coin_id: CoinId,
446    /// `parent_coin_info` of the coin identified by [`Self::coin_id`].
447    pub parent_id: CoinId,
448    /// `parent_coin_info` of the coin identified by [`Self::parent_id`].
449    pub parent_parent_id: CoinId,
450}