Skip to main content

dig_store/
lifecycle.rs

1//! The store LIFECYCLE (SPEC §3): a store is a coin that gets SPENT.
2//!
3//! A DIG store is a CHIP-0035 DataLayer singleton. Three operations span its life, each a spend of
4//! that coin, composed directly over `dig-merkle` (the byte-source-of-truth for every DataLayer
5//! spend, INV-4):
6//!
7//! - [`create_store`] — launch the store coin from a funding parent, anchoring the first root + its
8//!   size bucket + optional metadata (→ [`dig_merkle::mint_datastore_with_kind`]).
9//! - [`modify_store`] — spend the tip coin to recreate the store with a NEW root, preserving the rest
10//!   of the anchored metadata (→ [`dig_merkle::update_root`]).
11//! - [`melt_store`] — terminally spend the coin, closing the store with no successor
12//!   (→ [`dig_merkle::melt`]).
13//!
14//! Every operation returns an UNSIGNED [`MerkleCoinSpend`] (inherited boundary INV-2/INV-3 from
15//! `dig-merkle`): `dig-store` never holds a key, never signs, never broadcasts. The wallet-backend /
16//! node feeds the reported spends to `dig_merkle::required_signatures`, signs, assembles the
17//! `SpendBundle`, and submits it. The on-chain encoding is minimal (NC-8) — delegated wholesale to
18//! `dig-merkle`, which owns the byte layout.
19//!
20//! `modify_store` / `melt_store` take the already-hydrated tip [`Datastore`] (from
21//! [`crate::get_store_singleton_tip`], which does the single chain read) rather than a chain source,
22//! so these builders stay pure transforms of their inputs (INV-1).
23
24use chia_bls::PublicKey;
25use dig_merkle::{melt, mint_datastore_with_kind, update_root, StoreKind};
26
27use crate::error::DigStoreResult;
28use crate::size::SizeBucket;
29use crate::types::{Bytes32, Coin, Datastore, DigDataStoreMetadata, MerkleCoinSpend};
30
31/// Who is authorized to spend a store coin — the p2 ("inner") puzzle that guards it.
32///
33/// [`StoreOwner::Standard`] is the single-key case: `dig-merkle` builds the standard layer, and the
34/// resulting spend requires exactly one `AGG_SIG_ME` over the key.
35///
36/// # Why this is dig-store's own type and not a re-export
37///
38/// `dig-merkle` 0.6 refuses its `Owner::Custom` (a pre-built inner spend) on ALL THREE lifecycle
39/// operations — mint, `update_root`, and `melt` — with `MerkleError::UnsupportedOwner`, because each
40/// spend must emit conditions produced INSIDE the call, which an opaque pre-built spend cannot carry.
41/// So every use of that variant is a guaranteed runtime error, whether or not a caller can build one.
42///
43/// Re-exporting such a variant is an API that promises a composition that does not work. Owning the
44/// type instead makes the unusable case UNEXPRESSIBLE rather than merely documented — the argument
45/// rests on the refusal above, not on any claim about what a caller could construct. `#[non_exhaustive]` keeps a future owner kind (a real delegated or
46/// multisig authorization, once `dig-merkle` supports one) additive.
47#[derive(Debug, Clone)]
48#[non_exhaustive]
49pub enum StoreOwner {
50    /// The standard single-key p2 puzzle, owned by the given (synthetic) public key.
51    Standard(PublicKey),
52}
53
54impl From<StoreOwner> for dig_merkle::Owner {
55    /// Lowers a store owner onto the `dig-merkle` owner the spend builders take. Total by
56    /// construction: every expressible [`StoreOwner`] maps to an owner `dig-merkle` accepts.
57    fn from(owner: StoreOwner) -> Self {
58        match owner {
59            StoreOwner::Standard(key) => dig_merkle::Owner::Standard(key),
60        }
61    }
62}
63
64/// The parameters that describe a store's on-chain metadata at creation (SPEC §3.1).
65///
66/// `root_hash` and `size` are required — every store anchors its size so the SIZE PROOF (SPEC §4) can
67/// gate downloads. Every other field is optional and omitted-when-absent on chain (NC-8).
68#[derive(Debug, Clone)]
69pub struct CreateStoreParams {
70    /// The first anchored merkle root (the `.dig` root of generation 0).
71    pub root_hash: Bytes32,
72    /// The store's size, anchored as a power-of-2 bucket so clients can gate downloads (SPEC §4).
73    pub size: SizeBucket,
74    /// An optional human label (`dig-merkle` metadata `"l"`).
75    pub label: Option<String>,
76    /// An optional human description (`dig-merkle` metadata `"d"`).
77    pub description: Option<String>,
78    /// An optional CLVM tree-hash of a program/puzzle associated with the store (`dig-merkle` `"p"`).
79    pub program_hash: Option<Bytes32>,
80    /// The reserve fee (mojos) to attach to the launch spend.
81    pub fee: u64,
82}
83
84/// Launches a new store coin from a funding parent, anchoring the first root (SPEC §3.1).
85///
86/// `parent_coin` funds + parents the launcher, so `launcher_id == store_id` derives from its
87/// `coin_id`. `owner` authorizes the parent spend; `owner_puzzle_hash` is the store owner recorded in
88/// the singleton (and the target of the owner-discovery hint + any change). `params` carries the
89/// first root, the required size bucket, and optional metadata. The store is minted with the file
90/// launcher discriminator ([`StoreKind::File`]), byte-identical to existing on-chain DIG stores.
91///
92/// Returns the UNSIGNED launch spend.
93///
94/// # This builder mints from an ORDINARY funding coin, never a DID
95///
96/// A DID is a singleton, so it cannot parent the odd-amount launcher directly — it must interpose an
97/// even-amount intermediate coin, and the resulting launcher is built by the CALLER and handed to
98/// `dig_merkle::mint_datastore_launch_with_kind` (which also returns the parent conditions the DID
99/// spend must emit). `create_store` composes the simple funding-coin path only, so it cannot mint a
100/// DID-rooted store; a caller needing one drives that `dig-merkle` API directly until `dig-store`
101/// exposes a builder for it. [`crate::get_store_did_owner`] still READS the owning DID of any store
102/// minted that way.
103///
104/// # Errors
105///
106/// Returns a [`DigStoreResult`] error if the spend cannot be constructed (invalid metadata / size /
107/// fee overflow).
108pub fn create_store(
109    parent_coin: Coin,
110    owner: StoreOwner,
111    owner_puzzle_hash: Bytes32,
112    params: CreateStoreParams,
113) -> DigStoreResult<MerkleCoinSpend> {
114    Ok(mint_datastore_with_kind(
115        StoreKind::File,
116        parent_coin,
117        owner.into(),
118        params.root_hash,
119        params.label,
120        params.description,
121        None, // size_proof: superseded by the size bucket (NC-8), never emitted by dig-store.
122        params.program_hash,
123        Some(params.size),
124        owner_puzzle_hash,
125        Vec::new(), // delegated puzzles: added additively in a later unit (SPEC §3).
126        params.fee,
127    )?)
128}
129
130/// Spends the store's tip coin to recreate it anchoring `new_root` — a new generation (SPEC §3.2).
131///
132/// `store` is the current confirmed tip (from [`crate::get_store_singleton_tip`]); the spend consumes
133/// it and recreates the singleton with `new_root`, PRESERVING every other anchored metadata field
134/// (label, description, size bucket, program hash) and the store identity (`store_id`, owner,
135/// delegation set). Returns the UNSIGNED spend.
136///
137/// Note: attaching a reserve fee to a modify spend is a `dig-merkle` future unit (its `fee` module is
138/// a documented stub); this builder recreates the coin at its current amount.
139///
140/// # Errors
141///
142/// Returns a [`DigStoreResult`] error if the spend cannot be constructed.
143pub fn modify_store(
144    store: &Datastore<DigDataStoreMetadata>,
145    owner: StoreOwner,
146    new_root: Bytes32,
147) -> DigStoreResult<MerkleCoinSpend> {
148    let new_metadata = DigDataStoreMetadata {
149        root_hash: new_root,
150        ..store.info.metadata.clone()
151    };
152    Ok(update_root(store, owner.into(), new_metadata)?)
153}
154
155/// Terminally spends (melts) the store's tip coin, leaving no successor (SPEC §3.3).
156///
157/// Closes the store: the singleton is spent with no recreation, so no future generation can be
158/// anchored. `store` is the current tip (from [`crate::get_store_singleton_tip`]). Returns the
159/// UNSIGNED melt spend.
160///
161/// # Errors
162///
163/// Returns a [`DigStoreResult`] error if the spend cannot be constructed.
164pub fn melt_store(
165    store: &Datastore<DigDataStoreMetadata>,
166    owner: StoreOwner,
167) -> DigStoreResult<MerkleCoinSpend> {
168    Ok(melt(store, owner.into())?)
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use chia_puzzle_types::standard::StandardArgs;
175    use chia_wallet_sdk::test::Simulator;
176
177    /// The one expressible owner lowers onto the one `dig-merkle` owner the spend builders accept.
178    ///
179    /// The value of this test is what it CANNOT be written against: `dig_merkle::Owner::Custom` is
180    /// refused by mint, `update_root`, and `melt` alike, and no `StoreOwner` can name it — so the
181    /// lowering is total by construction and no lifecycle call can reach `UnsupportedOwner`.
182    #[test]
183    fn store_owner_lowers_to_the_standard_merkle_owner() {
184        let key = PublicKey::default();
185        match dig_merkle::Owner::from(StoreOwner::Standard(key)) {
186            dig_merkle::Owner::Standard(lowered) => assert_eq!(lowered, key),
187            dig_merkle::Owner::Custom(_) => {
188                panic!("a standard owner never lowers to a custom spend")
189            }
190        }
191    }
192
193    /// Mints a store on the simulator and returns the owner keypair + the settled eve Datastore, so
194    /// lifecycle tests start from a real on-chain store.
195    fn minted_store(
196        sim: &mut Simulator,
197        size: SizeBucket,
198    ) -> anyhow::Result<(
199        chia_wallet_sdk::test::BlsPairWithCoin,
200        Datastore<DigDataStoreMetadata>,
201    )> {
202        let owner = sim.bls(1_000_000);
203        let owner_ph: Bytes32 = StandardArgs::curry_tree_hash(owner.pk).into();
204        let built = create_store(
205            owner.coin,
206            StoreOwner::Standard(owner.pk),
207            owner_ph,
208            CreateStoreParams {
209                root_hash: Bytes32::new([0x5a; 32]),
210                size,
211                label: Some("docs".into()),
212                description: None,
213                program_hash: None,
214                fee: 0,
215            },
216        )?;
217        sim.spend_coins(built.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
218        Ok((owner, built.child.expect("mint yields a child")))
219    }
220
221    /// create_store anchors the first root + size bucket and validates on the simulator; the eve
222    /// store hydrates back with both preserved.
223    #[test]
224    fn create_store_anchors_root_and_size() -> anyhow::Result<()> {
225        let mut sim = Simulator::new();
226        let size = SizeBucket::from_exponent(7).unwrap();
227        let (_owner, store) = minted_store(&mut sim, size)?;
228
229        assert_eq!(store.info.metadata.root_hash, Bytes32::new([0x5a; 32]));
230        assert_eq!(store.info.metadata.size_bucket, Some(size));
231        assert_eq!(store.info.metadata.label, Some("docs".into()));
232        Ok(())
233    }
234
235    /// modify_store recreates the store with a NEW root, PRESERVES the anchored size bucket + label
236    /// (wholesale-replacement carry-forward), keeps the store id, and validates on the simulator.
237    #[test]
238    fn modify_store_updates_root_and_preserves_metadata() -> anyhow::Result<()> {
239        let mut sim = Simulator::new();
240        let size = SizeBucket::from_exponent(5).unwrap();
241        let (owner, store) = minted_store(&mut sim, size)?;
242
243        let new_root = Bytes32::new([0x77; 32]);
244        let built = modify_store(&store, StoreOwner::Standard(owner.pk), new_root)?;
245        let child = built.child.clone().expect("modify yields a child");
246
247        assert_eq!(child.info.metadata.root_hash, new_root);
248        assert_eq!(
249            child.info.metadata.size_bucket,
250            Some(size),
251            "the anchored size bucket is preserved across a modify"
252        );
253        assert_eq!(child.info.metadata.label, Some("docs".into()));
254        assert_eq!(child.info.launcher_id, store.info.launcher_id);
255
256        sim.spend_coins(built.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
257        Ok(())
258    }
259
260    /// melt_store yields no successor and the melt validates on the simulator: the store is closed.
261    #[test]
262    fn melt_store_closes_the_store() -> anyhow::Result<()> {
263        let mut sim = Simulator::new();
264        let size = SizeBucket::from_exponent(3).unwrap();
265        let (owner, store) = minted_store(&mut sim, size)?;
266
267        let built = melt_store(&store, StoreOwner::Standard(owner.pk))?;
268        assert!(built.child.is_none(), "a melt leaves no successor");
269
270        sim.spend_coins(built.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
271        Ok(())
272    }
273
274    /// The unsigned create spend requires exactly one `AGG_SIG_ME` over the owner's key — the custody
275    /// contract inherited from dig-merkle (a compromised dig-store cannot move funds, INV-2).
276    #[test]
277    fn create_store_requires_a_single_owner_signature() -> anyhow::Result<()> {
278        use chia_wallet_sdk::prelude::MAINNET_CONSTANTS;
279        use chia_wallet_sdk::signer::{AggSigConstants, RequiredSignature};
280
281        let mut sim = Simulator::new();
282        let owner = sim.bls(1_000_000);
283        let owner_ph: Bytes32 = StandardArgs::curry_tree_hash(owner.pk).into();
284        let built = create_store(
285            owner.coin,
286            StoreOwner::Standard(owner.pk),
287            owner_ph,
288            CreateStoreParams {
289                root_hash: Bytes32::new([0x01; 32]),
290                size: SizeBucket::from_exponent(0).unwrap(),
291                label: None,
292                description: None,
293                program_hash: None,
294                fee: 0,
295            },
296        )?;
297
298        let constants = AggSigConstants::from(&*MAINNET_CONSTANTS);
299        let required = dig_merkle::required_signatures(&built.coin_spends, &constants)?;
300        assert_eq!(required.len(), 1, "one AGG_SIG_ME expected");
301        match &required[0] {
302            RequiredSignature::Bls(bls) => assert_eq!(bls.public_key, owner.pk),
303            RequiredSignature::Secp(_) => panic!("standard owner uses a BLS key"),
304        }
305        Ok(())
306    }
307}