Skip to main content

dig_did/
create.rs

1//! DID creation (SPEC §3 "Create").
2//!
3//! Minting a DID from a funding coin is three coin spends bundled together (SPEC §3 notes): the
4//! **funding coin** spend (which creates the launcher and, per [`Owner`], requires the owner's
5//! signature), the **launcher** spend (which creates the eve DID), and an **owner update/settle**
6//! spend that confirms the DID's metadata so wallets can parse it. All three land in one
7//! [`DidSpend`] — dig-did never splits a create across multiple return values.
8//!
9//! Creation requires [`Owner::Standard`] (§2.4). [`Owner::Custom`] is REFUSED with
10//! [`crate::DidError::UnsupportedOwner`]: both spends in a create emit conditions that are only
11//! computable inside this call, and a pre-built inner spend cannot carry them. See the
12//! `# Owner::Custom` section on [`create_did`] for the three independent reasons.
13
14use chia_protocol::{Bytes32, Coin};
15use chia_puzzle_types::standard::StandardArgs;
16use chia_wallet_sdk::driver::{Did, HashedPtr, Launcher, SpendContext};
17use chia_wallet_sdk::types::Conditions;
18
19use crate::amount::SingletonAmount;
20use crate::context::{drain_coin_spends, inner_spend};
21use crate::error::{DidError, DidResult};
22use crate::types::{DidSpend, Owner};
23
24/// Mints a brand-new DID, fully settled and wallet-parseable, from a funding coin.
25///
26/// Spends `funding_coin` (owned by `owner`) to create the launcher, launches the eve DID with the
27/// given recovery configuration and metadata, then performs the owner-update ("settle") spend that
28/// confirms the DID for wallets. Returns a [`DidSpend`] whose `child` is the fully-created,
29/// spendable [`Did`].
30///
31/// # The funding coin becomes the DID, in full
32///
33/// The singleton's amount IS `funding_coin.amount` — the whole coin, because this crate builds
34/// spends and emits no change output (choosing where change goes is caller policy). Two
35/// consequences the caller owns:
36///
37/// - The amount MUST be ODD, or no singleton is created at all and the coin is spent for nothing.
38///   Refused here with [`crate::DidError::EvenSingletonAmount`].
39/// - Any excess is locked in the identity coin forever. Pass a coin pre-split to EXACTLY the amount
40///   the DID should carry — `dig-account` splits an exact 1-mojo coin off its source coin and calls
41///   in with that, which is the reference pattern.
42///
43/// # Signature
44///
45/// Two `AGG_SIG_ME` signatures are required, both under whichever key/spend `owner` names
46/// (SPEC §3): one over the funding-coin spend (which creates the launcher) and one over the settle
47/// spend (which confirms the DID for wallets). Both are coin-bound `AGG_SIG_ME`, never `AGG_SIG_UNSAFE`.
48///
49/// # Errors
50///
51/// - [`crate::DidError::UnsupportedOwner`] if `owner` is [`Owner::Custom`] — see below.
52/// - [`crate::DidError::EvenSingletonAmount`] if `funding_coin.amount` is even — see above.
53/// - Any chia-wallet-sdk driver failure (currying, spend construction) as
54///   [`crate::DidError::Driver`].
55///
56/// # Owner::Custom
57///
58/// Refused. A create is two spends of two different coins, and a pre-built inner spend is one fixed
59/// `(puzzle, solution)` pair emitting one fixed condition set, so it cannot serve both. Three
60/// independent reasons, each sufficient on its own:
61///
62/// 1. **Different coins, one condition set.** The funding coin must emit the launcher's
63///    create/announcement conditions; the DID coin must emit its own recreation. One spend cannot
64///    emit both.
65/// 2. **Circular.** The recreation condition needs `did.info.inner_puzzle_hash()`, which does not
66///    exist until `create_eve_did` has run *inside* this call. No caller can precompute it.
67/// 3. **Actively wrong, not merely insufficient.** With a custom owner the DID's p2 puzzle IS the
68///    caller's puzzle, so replaying that solution on the DID coin re-emits the FUNDING conditions —
69///    a second launcher `CREATE_COIN` from the DID coin, not a settle.
70///
71/// Additionally, any `AGG_SIG_ME` baked into a custom spend is coin-bound and therefore valid for at
72/// most one of the two coins.
73///
74/// A caller that genuinely needs a custom DID p2 puzzle wants a launch-conditions builder it can
75/// compose into its own parent spend (mirroring `dig_merkle`'s `mint_datastore_launch_with_kind`),
76/// not this end-to-end builder.
77pub fn create_did(
78    ctx: &mut SpendContext,
79    funding_coin: Coin,
80    owner: Owner,
81    recovery_list_hash: Option<Bytes32>,
82    num_verifications_required: u64,
83    metadata: HashedPtr,
84) -> DidResult<DidSpend> {
85    let owner_puzzle_hash = standard_owner_puzzle_hash(owner, "create_did")?;
86
87    let (launch_conditions, eve) = singleton_launcher(funding_coin)?.create_eve_did(
88        ctx,
89        owner_puzzle_hash,
90        recovery_list_hash,
91        num_verifications_required,
92        metadata,
93    )?;
94
95    let settled = settle(ctx, eve, owner)?;
96    spend_funding_coin(ctx, funding_coin, owner, launch_conditions)?;
97
98    Ok(DidSpend::new(drain_coin_spends(ctx), Some(settled)))
99}
100
101/// [`create_did`] with the common defaults: no recovery list, a single required verification, and
102/// nil metadata. The usual entry point for a DID that does not need a recovery configuration.
103///
104/// # The funding coin becomes the DID, in full
105///
106/// This delegates to [`create_did`], so its funding-coin contract applies unchanged: the singleton's
107/// amount is the coin's ENTIRE amount, it MUST be odd, and any excess is locked in the DID forever.
108/// Pass a coin pre-split to exactly the intended amount.
109///
110/// # Errors
111///
112/// See [`create_did`] — including [`crate::DidError::EvenSingletonAmount`].
113pub fn create_simple_did(
114    ctx: &mut SpendContext,
115    funding_coin: Coin,
116    owner: Owner,
117) -> DidResult<DidSpend> {
118    create_did(ctx, funding_coin, owner, None, 1, HashedPtr::NIL)
119}
120
121/// Launches the eve DID WITHOUT the owner-update settle step.
122///
123/// The eve DID this returns is real and spendable on-chain, but most wallets expect the additional
124/// settle spend ([`create_did`] performs it) before they will recognize the DID. Use this lower-level
125/// primitive when the caller intends to perform its own follow-up spend on the eve DID (e.g. to fold
126/// the settle into a larger spend bundle).
127///
128/// # The funding coin becomes the DID, in full
129///
130/// Identical to [`create_did`]: the eve DID's amount is `funding_coin.amount` in full, it MUST be
131/// odd (else the coin is spent and no singleton exists), and any excess is locked in the identity
132/// coin. Pass a coin pre-split to exactly the intended amount — `dig-account`'s 1-mojo split is the
133/// reference pattern.
134///
135/// # Signature
136///
137/// Exactly one `AGG_SIG_ME` is required, over the funding-coin spend, under `owner`'s key/spend.
138///
139/// # Errors
140///
141/// See [`create_did`] — including [`crate::DidError::EvenSingletonAmount`] and the
142/// [`Owner::Custom`] refusal, which applies here for reason 1
143/// (the launcher conditions are produced inside this call and a pre-built spend cannot carry them).
144pub fn create_eve_did_only(
145    ctx: &mut SpendContext,
146    funding_coin: Coin,
147    owner: Owner,
148    recovery_list_hash: Option<Bytes32>,
149    num_verifications_required: u64,
150    metadata: HashedPtr,
151) -> DidResult<DidSpend> {
152    let owner_puzzle_hash = standard_owner_puzzle_hash(owner, "create_eve_did_only")?;
153
154    let (launch_conditions, eve) = singleton_launcher(funding_coin)?.create_eve_did(
155        ctx,
156        owner_puzzle_hash,
157        recovery_list_hash,
158        num_verifications_required,
159        metadata,
160    )?;
161
162    spend_funding_coin(ctx, funding_coin, owner, launch_conditions)?;
163
164    Ok(DidSpend::new(drain_coin_spends(ctx), Some(eve)))
165}
166
167/// The single place this crate builds a [`Launcher`] — the chokepoint that keeps an even funding
168/// amount out of every launch path.
169///
170/// The SDK's `Launcher::new(parent_coin_id, amount)` uses `amount` as BOTH the launcher solution's
171/// amount and the launched singleton's amount, and accepts any `u64`. Routing every launch through
172/// [`SingletonAmount`] means the odd-amount proof is carried in the type rather than repeated as a
173/// guard each launch site must remember.
174///
175/// This is enforced, not merely conventional: `clippy.toml` puts EVERY route to a raw amount on
176/// `disallowed-methods` — all five `Launcher` constructors plus `with_singleton_amount`, which
177/// would otherwise overwrite a proven-odd amount on a launcher obtained from here — and CI runs
178/// `cargo clippy --all-targets -- -D warnings`. A new launch site that reaches the SDK directly
179/// therefore fails the build unless it explicitly opts out, in a diff a reviewer sees.
180fn singleton_launcher(funding_coin: Coin) -> DidResult<Launcher> {
181    let amount = SingletonAmount::from_funding_coin(&funding_coin)?;
182    // The one legitimate production call: `amount` is proven odd immediately above.
183    #[allow(clippy::disallowed_methods)]
184    Ok(Launcher::new(funding_coin.coin_id(), amount.get()))
185}
186
187/// The DID's `p2_puzzle_hash` at creation — the gate that keeps [`Owner::Custom`] out of the create
188/// path (see [`create_did`]'s `# Owner::Custom` section for why it cannot work here).
189///
190/// `operation` names the caller so the refusal message points at the function the user actually
191/// called.
192fn standard_owner_puzzle_hash(owner: Owner, operation: &'static str) -> DidResult<Bytes32> {
193    match owner {
194        Owner::Standard(public_key) => Ok(StandardArgs::curry_tree_hash(public_key).into()),
195        Owner::Custom(_) => Err(DidError::UnsupportedOwner(match operation {
196            "create_eve_did_only" => {
197                "create_eve_did_only requires Owner::Standard; a pre-built custom inner spend \
198                 cannot emit the launcher conditions, which are only computable inside this call — \
199                 build the launch yourself with Launcher::create_eve_did"
200            }
201            _ => {
202                "create_did requires Owner::Standard; a pre-built custom inner spend cannot emit \
203                 both the launcher conditions and the DID's recreation — build the launch yourself \
204                 with Launcher::create_eve_did"
205            }
206        })),
207    }
208}
209
210/// Performs the owner-update ("settle") spend that leaves the DID's metadata/p2 puzzle unchanged but
211/// makes it wallet-parseable — the no-condition case of [`crate::spend_did_with_conditions`], which
212/// owns the recreation logic so there is exactly one code path for it.
213fn settle(ctx: &mut SpendContext, did: Did, owner: Owner) -> DidResult<Did> {
214    crate::update::spend_did_with_conditions(ctx, did, owner, Conditions::new())
215}
216
217/// Spends the funding coin under `owner`, emitting the launcher's create/announcement conditions —
218/// the step that actually creates the launcher coin and requires the owner's `AGG_SIG_ME`.
219fn spend_funding_coin(
220    ctx: &mut SpendContext,
221    funding_coin: Coin,
222    owner: Owner,
223    launch_conditions: Conditions,
224) -> DidResult<()> {
225    let spend = inner_spend(ctx, owner, launch_conditions)?;
226    ctx.spend(funding_coin, spend)?;
227    Ok(())
228}
229
230#[cfg(test)]
231// Tests build launchers directly, on purpose: a fixture needs an arbitrary parent id, and some
232// fixtures need an amount the production chokepoint would (rightly) refuse. The lint guards
233// PRODUCTION launch sites; see the note on `singleton_launcher` in create.rs.
234#[allow(clippy::disallowed_methods)]
235mod tests {
236    use super::*;
237    use chia_wallet_sdk::prelude::MAINNET_CONSTANTS;
238    use chia_wallet_sdk::signer::{AggSigConstants, RequiredSignature};
239    use chia_wallet_sdk::test::Simulator;
240
241    /// Creating a simple DID produces exactly the funding+launcher+settle spends, and the resulting
242    /// child DID is real: it can be broadcast against a simulator and parsed back byte-identically.
243    #[test]
244    fn create_simple_did_produces_a_spendable_settled_did() -> anyhow::Result<()> {
245        let mut sim = Simulator::new();
246        let ctx = &mut SpendContext::new();
247
248        let owner = sim.bls(1);
249        let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
250
251        let child = spend.child.expect("create always returns a child DID");
252        assert_eq!(child.info.recovery_list_hash, None);
253        assert_eq!(child.info.num_verifications_required, 1);
254        assert_eq!(child.info.p2_puzzle_hash, owner.puzzle_hash);
255
256        sim.spend_coins(spend.coin_spends, &[owner.sk])?;
257        Ok(())
258    }
259
260    /// `create_did` requires two `AGG_SIG_ME`s — one over the funding-coin spend (which creates the
261    /// launcher) and one over the settle spend (which confirms the DID for wallets) — both under the
262    /// owner's key, never an `AGG_SIG_UNSAFE` (SPEC §3/§4; corrects the earlier single-signature
263    /// estimate now that the settle step is known to require its own spend of the owner's p2 puzzle).
264    #[test]
265    fn create_did_requires_two_agg_sig_mes_over_the_owner_key() -> anyhow::Result<()> {
266        let mut sim = Simulator::new();
267        let ctx = &mut SpendContext::new();
268
269        let owner = sim.bls(1);
270        let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
271
272        let constants = AggSigConstants::from(&*MAINNET_CONSTANTS);
273        let required = crate::sign::required_signatures(&spend.coin_spends, &constants)
274            .expect("signature calculation must succeed for a well-formed create spend");
275
276        assert_eq!(
277            required.len(),
278            2,
279            "the funding-coin spend AND the settle spend each require one AGG_SIG_ME"
280        );
281        for signature in &required {
282            match signature {
283                RequiredSignature::Bls(bls) => assert_eq!(bls.public_key, owner.pk),
284                RequiredSignature::Secp(_) => panic!("a standard owner signs with BLS, not secp"),
285            }
286        }
287        Ok(())
288    }
289
290    /// A pre-built inner spend cannot emit the launcher conditions, so creation refuses it outright.
291    ///
292    /// Before this refusal, `create_eve_did_only` returned `Ok` with an eve DID while the bundle
293    /// contained no `CREATE_COIN` to `SINGLETON_LAUNCHER_HASH` at all — a DID that could never
294    /// exist. That silent-drop reproduction is preserved as the control below.
295    #[test]
296    fn create_did_refuses_a_custom_owner() -> anyhow::Result<()> {
297        let mut sim = Simulator::new();
298        let ctx = &mut SpendContext::new();
299
300        let owner = sim.bls(1);
301        let prebuilt = prebuilt_inner_spend(ctx, owner.pk)?;
302
303        assert!(matches!(
304            create_simple_did(ctx, owner.coin, Owner::Custom(prebuilt)),
305            Err(DidError::UnsupportedOwner(_))
306        ));
307        assert!(matches!(
308            create_eve_did_only(
309                ctx,
310                owner.coin,
311                Owner::Custom(prebuilt),
312                None,
313                1,
314                HashedPtr::NIL
315            ),
316            Err(DidError::UnsupportedOwner(_))
317        ));
318        Ok(())
319    }
320
321    /// The control that makes the refusal load-bearing: the same custom spend, routed through the
322    /// underlying SDK launcher the way the old code did, produces a bundle with NO launcher
323    /// `CREATE_COIN`. This is the outcome the refusal now prevents; if it ever stops holding, the
324    /// refusal above is guarding a defect that no longer exists and should be re-derived.
325    #[test]
326    fn a_custom_inner_spend_would_silently_drop_the_launcher_conditions() -> anyhow::Result<()> {
327        let mut sim = Simulator::new();
328        let ctx = &mut SpendContext::new();
329
330        let owner = sim.bls(1);
331        let prebuilt = prebuilt_inner_spend(ctx, owner.pk)?;
332
333        let launcher = Launcher::new(owner.coin.coin_id(), owner.coin.amount);
334        let (_dropped_launch_conditions, _eve) =
335            launcher.create_eve_did(ctx, owner.puzzle_hash, None, 1, HashedPtr::NIL)?;
336        // Exactly what the old `spend_funding_coin` did with an `Owner::Custom`: the conditions go
337        // nowhere, because a pre-built spend is used verbatim.
338        ctx.spend(owner.coin, prebuilt)?;
339
340        let coin_spends = drain_coin_spends(ctx);
341        assert!(
342            !crate::test_support::creates_coin_to(
343                ctx,
344                &coin_spends,
345                chia_puzzles::SINGLETON_LAUNCHER_HASH.into(),
346            )?,
347            "nothing in the bundle creates the launcher the eve DID was supposedly launched from"
348        );
349        Ok(())
350    }
351
352    /// A syntactically valid inner spend that emits a condition of its own — the most favourable
353    /// custom spend a caller could plausibly supply.
354    fn prebuilt_inner_spend(
355        ctx: &mut SpendContext,
356        public_key: chia_wallet_sdk::prelude::PublicKey,
357    ) -> DidResult<chia_wallet_sdk::driver::Spend> {
358        inner_spend(
359            ctx,
360            Owner::Standard(public_key),
361            Conditions::new().reserve_fee(0),
362        )
363    }
364
365    /// An even-amount funding coin is refused by EVERY creation entry point, naming the amount.
366    ///
367    /// Without the refusal all three assemble happily: the singleton's amount is the funding coin's
368    /// amount, an even-amount coin is not a singleton, so the bundle spends the coin and creates no
369    /// DID — a total loss with a success return. `sim.bls(2)` is a real, ordinary even coin, the
370    /// shape roughly half of arbitrary wallet coins have.
371    #[test]
372    fn every_creation_entry_point_refuses_an_even_funding_coin() -> anyhow::Result<()> {
373        let mut sim = Simulator::new();
374        let ctx = &mut SpendContext::new();
375
376        let owner = sim.bls(2);
377        assert_eq!(owner.coin.amount, 2, "the fixture must be genuinely even");
378
379        assert!(matches!(
380            create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk)),
381            Err(DidError::EvenSingletonAmount(2))
382        ));
383        assert!(matches!(
384            create_did(
385                ctx,
386                owner.coin,
387                Owner::Standard(owner.pk),
388                None,
389                1,
390                HashedPtr::NIL
391            ),
392            Err(DidError::EvenSingletonAmount(2))
393        ));
394        assert!(matches!(
395            create_eve_did_only(
396                ctx,
397                owner.coin,
398                Owner::Standard(owner.pk),
399                None,
400                1,
401                HashedPtr::NIL
402            ),
403            Err(DidError::EvenSingletonAmount(2))
404        ));
405        Ok(())
406    }
407
408    /// The positive control for the refusal above: the SAME three entry points, given an odd
409    /// funding coin one mojo away from the refused fixture, each produce a bundle the simulator
410    /// accepts. Without this, a refusal that rejected every amount would look identical.
411    #[test]
412    fn every_creation_entry_point_accepts_an_odd_funding_coin() -> anyhow::Result<()> {
413        for build in [
414            (|ctx: &mut SpendContext, coin, owner| create_simple_did(ctx, coin, owner))
415                as fn(&mut SpendContext, Coin, Owner) -> DidResult<DidSpend>,
416            |ctx, coin, owner| create_did(ctx, coin, owner, None, 1, HashedPtr::NIL),
417            |ctx, coin, owner| create_eve_did_only(ctx, coin, owner, None, 1, HashedPtr::NIL),
418        ] {
419            let mut sim = Simulator::new();
420            let ctx = &mut SpendContext::new();
421
422            let owner = sim.bls(3);
423            assert_eq!(owner.coin.amount, 3, "the fixture must be genuinely odd");
424
425            let spend = build(ctx, owner.coin, Owner::Standard(owner.pk))?;
426            let child = spend.child.expect("create always returns a child DID");
427            assert_eq!(
428                child.coin.amount, 3,
429                "the singleton carries the funding coin's ENTIRE amount"
430            );
431
432            sim.spend_coins(spend.coin_spends, &[owner.sk])?;
433        }
434        Ok(())
435    }
436
437    /// A full recovery configuration round-trips through creation untouched.
438    #[test]
439    fn create_did_preserves_a_custom_recovery_configuration() -> anyhow::Result<()> {
440        let mut sim = Simulator::new();
441        let ctx = &mut SpendContext::new();
442
443        let owner = sim.bls(1);
444        let recovery_list_hash =
445            Some(clvm_utils::tree_hash_atom(b"dig-did::create::recovery-list").into());
446
447        let spend = create_did(
448            ctx,
449            owner.coin,
450            Owner::Standard(owner.pk),
451            recovery_list_hash,
452            2,
453            HashedPtr::NIL,
454        )?;
455        let child = spend.child.expect("create always returns a child DID");
456
457        assert_eq!(child.info.recovery_list_hash, recovery_list_hash);
458        assert_eq!(child.info.num_verifications_required, 2);
459
460        sim.spend_coins(spend.coin_spends, &[owner.sk])?;
461        Ok(())
462    }
463
464    /// The lower-level eve-only primitive skips the settle spend, returning just the eve DID — the
465    /// caller is expected to perform its own follow-up spend.
466    #[test]
467    fn create_eve_did_only_skips_the_settle_spend() -> anyhow::Result<()> {
468        let mut sim = Simulator::new();
469        let ctx = &mut SpendContext::new();
470
471        let owner = sim.bls(1);
472        let spend = create_eve_did_only(
473            ctx,
474            owner.coin,
475            Owner::Standard(owner.pk),
476            None,
477            1,
478            HashedPtr::NIL,
479        )?;
480
481        // Two spends: funding coin + launcher — no separate settle spend.
482        assert_eq!(spend.coin_spends.len(), 2);
483
484        let eve = spend.child.expect("create always returns a child DID");
485        assert_eq!(eve.info.p2_puzzle_hash, owner.puzzle_hash);
486
487        sim.spend_coins(spend.coin_spends, &[owner.sk])?;
488        Ok(())
489    }
490}