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}