Skip to main content

dig_store/
types.rs

1//! The shared identifier + value types of the store surface.
2//!
3//! The coin/identity types are re-exported VERBATIM from `dig-merkle` (which re-exports the
4//! `chia-wallet-sdk` byte-source-of-truth) so a consumer depends on ONE canonical shape and never a
5//! shadow copy that could byte-drift:
6//!
7//! - [`Bytes32`] — a 32-byte identifier (a `store_id` / `launcher_id`, a merkle root, a DID id);
8//! - [`Coin`] / [`CoinSpend`] — the Chia coin + confirmed spend;
9//! - [`DataStore`] / [`DigDataStoreMetadata`] — the hydrated DataLayer coin + its on-chain metadata;
10//! - [`DidRef`] — a reference to an owning DID by its launcher id;
11//! - [`MerkleCoinSpend`] — the unsigned result of a lifecycle operation (coin spends + child store);
12//! - [`Proof`] / [`LineageProof`] — a singleton's eve-or-lineage proof and the lineage proof a child
13//!   spend must carry (see [`crate::child_lineage_proof`]);
14//! - [`DelegatedPuzzle`] — an admin/writer/oracle delegated puzzle a store may carry (SPEC §5).
15//!
16//! Two `dig-store`-owned view types are added here:
17//!
18//! - [`RootHistory`] — the ordered list of merkle roots a store has anchored across its generations,
19//!   produced by the on-chain lineage walk (SPEC §5);
20//! - [`CapsuleIdentity`] — the `(store_id, root_hash)` a capsule declares, recovered OFF-CHAIN from a
21//!   compiled `.dig` module's bytes (SPEC §5/§11). It is the `dig-store`-native view (canonical
22//!   [`Bytes32`]) of a `dig_capsule::capsule::Capsule`, so the whole store surface speaks ONE byte
23//!   type rather than exposing `dig-capsule`'s separate `Bytes32`.
24
25use serde::{Deserialize, Serialize};
26
27pub use dig_merkle::{
28    Bytes32, Coin, CoinSpend, DataStore, DelegatedPuzzle, DidRef, DigDataStoreMetadata,
29    LineageProof, MerkleCoinSpend, Proof,
30};
31
32/// The lifecycle state of a store as seen on chain (SPEC §5).
33///
34/// The three outcomes of the single lineage walk that backs [`crate::get_store_status`]:
35///
36/// - [`Live`](StoreStatusKind::Live) — the launcher resolved and the walk reached an UNSPENT tip;
37/// - [`Melted`](StoreStatusKind::Melted) — the launcher resolved but the lineage ends in a terminal
38///   melt (no live tip); the store still has a root history but no current content;
39/// - [`NotFound`](StoreStatusKind::NotFound) — no launcher spend exists on chain for this store id.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum StoreStatusKind {
43    /// The store is live: its singleton has an unspent tip anchoring the current root.
44    Live,
45    /// The store has been melted: its lineage ends in a terminal melt, no live tip remains.
46    Melted,
47    /// No store with this id exists on chain (no launcher spend found).
48    NotFound,
49}
50
51/// How deeply the live tip is buried under the current chain peak (SPEC §5).
52///
53/// `have` is the number of blocks between the tip's confirming height and the current peak
54/// (`peak.saturating_sub(confirmed_height)`); `target` is the caller-chosen confirmation depth at
55/// which the tip is considered settled (see [`crate::DEFAULT_CONFIRMATION_TARGET`]). Present only when
56/// the chain source exposed BOTH a peak height and the tip's confirmed height.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58pub struct Confirmations {
59    /// The confirmation depth observed (`peak_height - tip_confirmed_height`).
60    pub have: u32,
61    /// The confirmation depth the caller treats as settled.
62    pub target: u32,
63}
64
65/// The aggregate on-chain status of a store, produced by [`crate::get_store_status`] from ONE
66/// consistent lineage walk plus a single supplementary read on the resolved tip (SPEC §5, NC-9).
67///
68/// Every identifier field is a bare lowercase-hex string byte-identical to the URN body form (so
69/// `store_id` equals the id in the store's `urn:dig:chia:<store_id>`), making the whole snapshot
70/// directly JSON-serializable for a CLI/RPC surface without leaking a chia byte type across serde.
71///
72/// # Field availability by [`status`](StoreStatus::status)
73///
74/// - `NotFound` — every optional field is `None`, `verified` is `false`, `generation_count` is 0.
75/// - `Melted`  — identity fields (`owner_puzzle_hash`/`live_root`/`program_hash`/`coin_id`) and
76///   `confirmations` are `None`, `verified` is `false`; `generation_count` still counts every root
77///   the store anchored while live.
78/// - `Live`    — `live_root`/`owner_puzzle_hash`/`coin_id` are always present; `program_hash`,
79///   `confirmations`, and `verified` reflect what the metadata + supplementary tip read carried.
80///
81/// `head_signature` is ALWAYS `None`: a per-coin BLS head signature is structurally unavailable
82/// through the [`ChainSource`](crate::ChainSource) surface (it yields `CoinSpend` — coin/puzzle/
83/// solution — with no signature). The field ships present-but-`None` for forward-compat; a routed
84/// follow-up sources it out-of-band (SPEC §5/§7).
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct StoreStatus {
87    /// The store's lifecycle state on chain.
88    pub status: StoreStatusKind,
89    /// The store's launcher id as bare lowercase hex (the URN body form).
90    pub store_id: String,
91    /// The live tip's confirmation depth vs the chain peak, when both heights are known.
92    pub confirmations: Option<Confirmations>,
93    /// The live tip's owner puzzle hash as bare lowercase hex, when live.
94    pub owner_puzzle_hash: Option<String>,
95    /// The latest anchored merkle root as bare lowercase hex, when live.
96    pub live_root: Option<String>,
97    /// The store's on-chain program hash as bare lowercase hex, when live and set.
98    pub program_hash: Option<String>,
99    /// The live tip's head BLS signature — always `None` (structurally unavailable, see type docs).
100    pub head_signature: Option<String>,
101    /// The live tip coin's id as bare lowercase hex, when live.
102    pub coin_id: Option<String>,
103    /// Whether the resolved tip's coin record confirms it is unspent (never self-asserted; `false`
104    /// when no coin record was available). Cross-checked against the walk (NC-9 fail-closed).
105    pub verified: bool,
106    /// The number of generations (root anchorings) the store has had across its whole lineage.
107    pub generation_count: usize,
108}
109
110/// The ordered history of merkle roots a store has anchored, oldest first.
111///
112/// Each entry is proven on chain by walking the singleton's lineage from the launcher forward (NC-9,
113/// SPEC §5). The last element is the latest root. A live store is never empty (the mint anchors
114/// generation 0); a fully-melted store's history still lists every root it anchored while live.
115#[derive(Debug, Clone, PartialEq, Eq, Default)]
116pub struct RootHistory {
117    /// The anchored roots, oldest → newest.
118    pub roots: Vec<Bytes32>,
119}
120
121impl RootHistory {
122    /// The most recently anchored root, if any.
123    pub fn latest(&self) -> Option<Bytes32> {
124        self.roots.last().copied()
125    }
126
127    /// The number of generations (root anchorings) the store has had.
128    pub fn generation_count(&self) -> usize {
129        self.roots.len()
130    }
131}
132
133/// The identity a capsule declares: one immutable store generation, the pair `(store_id, root_hash)`.
134///
135/// Recovered OFF-CHAIN from a compiled `.dig` module's bytes by [`crate::get_capsule_identity`] /
136/// [`crate::open_capsule`] (SPEC §5/§11). This is `dig-store`'s canonical-[`Bytes32`] view of a
137/// `dig_capsule::capsule::Capsule`.
138///
139/// # `store_id` is NOT self-verified
140///
141/// `root_hash` is proven internally consistent by the reader (it recomputes the merkle root from the
142/// module's committed leaves and rejects a forged one). `store_id`, however, is the store's on-chain
143/// Chia launcher id, baked into the module at compile time and NOT self-verifiable from the module
144/// bytes alone — nothing in the bytes binds them to that launcher. Treat a `store_id` from
145/// [`crate::get_capsule_identity`] as a CLAIM until cross-checked against a trusted anchor (the URN you
146/// resolved, the on-chain singleton, or a verified `ChainSource`). [`crate::open_capsule`] performs
147/// that cross-check for you.
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
149pub struct CapsuleIdentity {
150    /// The store's on-chain launcher id (a CLAIM until cross-checked — see the type docs).
151    pub store_id: Bytes32,
152    /// The merkle root of this capsule generation, proven internally consistent by the reader.
153    pub root_hash: Bytes32,
154}
155
156impl CapsuleIdentity {
157    /// The capsule URN `urn:dig:chia:<store_id>:<root_hash>` pinning this exact generation.
158    pub fn capsule_urn(&self) -> String {
159        crate::urn::capsule_urn(self.store_id, self.root_hash)
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    fn id(b: u8) -> Bytes32 {
168        Bytes32::new([b; 32])
169    }
170
171    #[test]
172    fn capsule_identity_formats_its_pinning_urn() {
173        let identity = CapsuleIdentity {
174            store_id: id(0xaa),
175            root_hash: id(0xbb),
176        };
177        assert_eq!(
178            identity.capsule_urn(),
179            format!("urn:dig:chia:{}:{}", "aa".repeat(32), "bb".repeat(32))
180        );
181    }
182
183    #[test]
184    fn root_history_reports_latest_and_generation_count() {
185        let empty = RootHistory::default();
186        assert_eq!(empty.latest(), None);
187        assert_eq!(empty.generation_count(), 0);
188
189        let history = RootHistory {
190            roots: vec![id(1), id(2), id(3)],
191        };
192        assert_eq!(history.latest(), Some(id(3)));
193        assert_eq!(history.generation_count(), 3);
194    }
195}