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 dig_merkle::{melt, mint_datastore_with_kind, update_root, StoreKind};
25
26use crate::error::DigStoreResult;
27use crate::size::SizeBucket;
28use crate::types::{Bytes32, Coin, DataStore, DigDataStoreMetadata, MerkleCoinSpend};
29
30/// Who is authorized to spend a store coin — the p2 ("inner") puzzle that guards it.
31///
32/// Re-exported verbatim from [`dig_merkle::Owner`] so a caller uses ONE owner type across the whole
33/// DataLayer surface: [`StoreOwner::Standard`] is the common single-key case (dig-merkle builds the
34/// standard layer; the spend requires one `AGG_SIG_ME` over the key), and [`StoreOwner::Custom`] is
35/// the escape hatch for a pre-built inner spend (a DID-authorized delegated puzzle, a multisig, a
36/// vault).
37pub use dig_merkle::Owner as StoreOwner;
38
39/// The parameters that describe a store's on-chain metadata at creation (SPEC §3.1).
40///
41/// `root_hash` and `size` are required — every store anchors its size so the SIZE PROOF (SPEC §4) can
42/// gate downloads. Every other field is optional and omitted-when-absent on chain (NC-8).
43#[derive(Debug, Clone)]
44pub struct CreateStoreParams {
45    /// The first anchored merkle root (the `.dig` root of generation 0).
46    pub root_hash: Bytes32,
47    /// The store's size, anchored as a power-of-2 bucket so clients can gate downloads (SPEC §4).
48    pub size: SizeBucket,
49    /// An optional human label (`dig-merkle` metadata `"l"`).
50    pub label: Option<String>,
51    /// An optional human description (`dig-merkle` metadata `"d"`).
52    pub description: Option<String>,
53    /// An optional CLVM tree-hash of a program/puzzle associated with the store (`dig-merkle` `"p"`).
54    pub program_hash: Option<Bytes32>,
55    /// The reserve fee (mojos) to attach to the launch spend.
56    pub fee: u64,
57}
58
59/// Launches a new store coin from a funding parent, anchoring the first root (SPEC §3.1).
60///
61/// `parent_coin` funds + parents the launcher, so `launcher_id == store_id` derives from its
62/// `coin_id`. `owner` authorizes the parent spend; `owner_puzzle_hash` is the store owner recorded in
63/// the singleton (and the target of the owner-discovery hint + any change). `params` carries the
64/// first root, the required size bucket, and optional metadata. The store is minted with the file
65/// launcher discriminator ([`StoreKind::File`]), byte-identical to existing on-chain DIG stores.
66///
67/// To root a store in a DID, pass the DID-authorized coin as `parent_coin` with a
68/// [`StoreOwner::Custom`] inner spend satisfying the DID puzzle — owner discovery
69/// ([`crate::get_store_did_owner`]) then resolves the DID via `dig-merkle`. Returns the UNSIGNED
70/// launch spend.
71///
72/// # Errors
73///
74/// Returns a [`DigStoreResult`] error if the spend cannot be constructed (invalid metadata / size /
75/// fee overflow).
76pub fn create_store(
77    parent_coin: Coin,
78    owner: StoreOwner,
79    owner_puzzle_hash: Bytes32,
80    params: CreateStoreParams,
81) -> DigStoreResult<MerkleCoinSpend> {
82    Ok(mint_datastore_with_kind(
83        StoreKind::File,
84        parent_coin,
85        owner,
86        params.root_hash,
87        params.label,
88        params.description,
89        None, // size_proof: superseded by the size bucket (NC-8), never emitted by dig-store.
90        params.program_hash,
91        Some(params.size),
92        owner_puzzle_hash,
93        Vec::new(), // delegated puzzles: added additively in a later unit (SPEC §3).
94        params.fee,
95    )?)
96}
97
98/// Spends the store's tip coin to recreate it anchoring `new_root` — a new generation (SPEC §3.2).
99///
100/// `store` is the current confirmed tip (from [`crate::get_store_singleton_tip`]); the spend consumes
101/// it and recreates the singleton with `new_root`, PRESERVING every other anchored metadata field
102/// (label, description, size bucket, program hash) and the store identity (`store_id`, owner,
103/// delegation set). Returns the UNSIGNED spend.
104///
105/// Note: attaching a reserve fee to a modify spend is a `dig-merkle` future unit (its `fee` module is
106/// a documented stub); this builder recreates the coin at its current amount.
107///
108/// # Errors
109///
110/// Returns a [`DigStoreResult`] error if the spend cannot be constructed.
111pub fn modify_store(
112    store: &DataStore<DigDataStoreMetadata>,
113    owner: StoreOwner,
114    new_root: Bytes32,
115) -> DigStoreResult<MerkleCoinSpend> {
116    let new_metadata = DigDataStoreMetadata {
117        root_hash: new_root,
118        ..store.info.metadata.clone()
119    };
120    Ok(update_root(store, owner, new_metadata)?)
121}
122
123/// Terminally spends (melts) the store's tip coin, leaving no successor (SPEC §3.3).
124///
125/// Closes the store: the singleton is spent with no recreation, so no future generation can be
126/// anchored. `store` is the current tip (from [`crate::get_store_singleton_tip`]). Returns the
127/// UNSIGNED melt spend.
128///
129/// # Errors
130///
131/// Returns a [`DigStoreResult`] error if the spend cannot be constructed.
132pub fn melt_store(
133    store: &DataStore<DigDataStoreMetadata>,
134    owner: StoreOwner,
135) -> DigStoreResult<MerkleCoinSpend> {
136    Ok(melt(store, owner)?)
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use chia_puzzle_types::standard::StandardArgs;
143    use chia_wallet_sdk::test::Simulator;
144
145    /// Mints a store on the simulator and returns the owner keypair + the settled eve DataStore, so
146    /// lifecycle tests start from a real on-chain store.
147    fn minted_store(
148        sim: &mut Simulator,
149        size: SizeBucket,
150    ) -> anyhow::Result<(
151        chia_wallet_sdk::test::BlsPairWithCoin,
152        DataStore<DigDataStoreMetadata>,
153    )> {
154        let owner = sim.bls(1_000_000);
155        let owner_ph: Bytes32 = StandardArgs::curry_tree_hash(owner.pk).into();
156        let built = create_store(
157            owner.coin,
158            StoreOwner::Standard(owner.pk),
159            owner_ph,
160            CreateStoreParams {
161                root_hash: Bytes32::new([0x5a; 32]),
162                size,
163                label: Some("docs".into()),
164                description: None,
165                program_hash: None,
166                fee: 0,
167            },
168        )?;
169        sim.spend_coins(built.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
170        Ok((owner, built.child.expect("mint yields a child")))
171    }
172
173    /// create_store anchors the first root + size bucket and validates on the simulator; the eve
174    /// store hydrates back with both preserved.
175    #[test]
176    fn create_store_anchors_root_and_size() -> anyhow::Result<()> {
177        let mut sim = Simulator::new();
178        let size = SizeBucket::from_exponent(7).unwrap();
179        let (_owner, store) = minted_store(&mut sim, size)?;
180
181        assert_eq!(store.info.metadata.root_hash, Bytes32::new([0x5a; 32]));
182        assert_eq!(store.info.metadata.size_bucket, Some(size));
183        assert_eq!(store.info.metadata.label, Some("docs".into()));
184        Ok(())
185    }
186
187    /// modify_store recreates the store with a NEW root, PRESERVES the anchored size bucket + label
188    /// (wholesale-replacement carry-forward), keeps the store id, and validates on the simulator.
189    #[test]
190    fn modify_store_updates_root_and_preserves_metadata() -> anyhow::Result<()> {
191        let mut sim = Simulator::new();
192        let size = SizeBucket::from_exponent(5).unwrap();
193        let (owner, store) = minted_store(&mut sim, size)?;
194
195        let new_root = Bytes32::new([0x77; 32]);
196        let built = modify_store(&store, StoreOwner::Standard(owner.pk), new_root)?;
197        let child = built.child.clone().expect("modify yields a child");
198
199        assert_eq!(child.info.metadata.root_hash, new_root);
200        assert_eq!(
201            child.info.metadata.size_bucket,
202            Some(size),
203            "the anchored size bucket is preserved across a modify"
204        );
205        assert_eq!(child.info.metadata.label, Some("docs".into()));
206        assert_eq!(child.info.launcher_id, store.info.launcher_id);
207
208        sim.spend_coins(built.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
209        Ok(())
210    }
211
212    /// melt_store yields no successor and the melt validates on the simulator: the store is closed.
213    #[test]
214    fn melt_store_closes_the_store() -> anyhow::Result<()> {
215        let mut sim = Simulator::new();
216        let size = SizeBucket::from_exponent(3).unwrap();
217        let (owner, store) = minted_store(&mut sim, size)?;
218
219        let built = melt_store(&store, StoreOwner::Standard(owner.pk))?;
220        assert!(built.child.is_none(), "a melt leaves no successor");
221
222        sim.spend_coins(built.coin_spends.clone(), std::slice::from_ref(&owner.sk))?;
223        Ok(())
224    }
225
226    /// The unsigned create spend requires exactly one `AGG_SIG_ME` over the owner's key — the custody
227    /// contract inherited from dig-merkle (a compromised dig-store cannot move funds, INV-2).
228    #[test]
229    fn create_store_requires_a_single_owner_signature() -> anyhow::Result<()> {
230        use chia_wallet_sdk::prelude::MAINNET_CONSTANTS;
231        use chia_wallet_sdk::signer::{AggSigConstants, RequiredSignature};
232
233        let mut sim = Simulator::new();
234        let owner = sim.bls(1_000_000);
235        let owner_ph: Bytes32 = StandardArgs::curry_tree_hash(owner.pk).into();
236        let built = create_store(
237            owner.coin,
238            StoreOwner::Standard(owner.pk),
239            owner_ph,
240            CreateStoreParams {
241                root_hash: Bytes32::new([0x01; 32]),
242                size: SizeBucket::from_exponent(0).unwrap(),
243                label: None,
244                description: None,
245                program_hash: None,
246                fee: 0,
247            },
248        )?;
249
250        let constants = AggSigConstants::from(&*MAINNET_CONSTANTS);
251        let required = dig_merkle::required_signatures(&built.coin_spends, &constants)?;
252        assert_eq!(required.len(), 1, "one AGG_SIG_ME expected");
253        match &required[0] {
254            RequiredSignature::Bls(bls) => assert_eq!(bls.public_key, owner.pk),
255            RequiredSignature::Secp(_) => panic!("standard owner uses a BLS key"),
256        }
257        Ok(())
258    }
259}