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}