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//! Every builder here is generic over [`Owner`] (§2.4): a [`Owner::Standard`] key or a
10//! [`Owner::Custom`] pre-built inner spend both work, because the settle step is built from the raw
11//! [`chia_wallet_sdk::driver::Spend`] primitive (`Did::spend`) rather than a typed inner layer.
12
13use chia_protocol::{Bytes32, Coin};
14use chia_puzzle_types::standard::StandardArgs;
15use chia_wallet_sdk::driver::{Did, HashedPtr, Launcher, SingletonInfo, SpendContext};
16use chia_wallet_sdk::types::Conditions;
17use clvm_utils::tree_hash;
18
19use crate::context::{drain_coin_spends, inner_spend};
20use crate::error::DidResult;
21use crate::types::{DidSpend, Owner};
22
23/// Mints a brand-new DID, fully settled and wallet-parseable, from a funding coin.
24///
25/// Spends `funding_coin` (owned by `owner`) to create the launcher, launches the eve DID with the
26/// given recovery configuration and metadata, then performs the owner-update ("settle") spend that
27/// confirms the DID for wallets. Returns a [`DidSpend`] whose `child` is the fully-created,
28/// spendable [`Did`].
29///
30/// # Signature
31///
32/// Two `AGG_SIG_ME` signatures are required, both under whichever key/spend `owner` names
33/// (SPEC §3): one over the funding-coin spend (which creates the launcher) and one over the settle
34/// spend (which confirms the DID for wallets). Both are coin-bound `AGG_SIG_ME`, never `AGG_SIG_UNSAFE`.
35///
36/// # Errors
37///
38/// Propagates any chia-wallet-sdk driver failure (currying, spend construction) as
39/// [`crate::DidError::Driver`].
40///
41/// # Owner::Custom
42///
43/// When using `Owner::Custom(spend)`, the ONE caller-supplied inner spend is used for BOTH the
44/// funding-coin spend and the settle spend. The caller is responsible for ensuring the custom spend
45/// emits all conditions needed to satisfy both steps; conditions are not added by dig-did for a
46/// custom spend. A custom create-spend that omits required conditions will fail closed with a parse
47/// error, never a custody leak.
48pub fn create_did(
49    ctx: &mut SpendContext,
50    funding_coin: Coin,
51    owner: Owner,
52    recovery_list_hash: Option<Bytes32>,
53    num_verifications_required: u64,
54    metadata: HashedPtr,
55) -> DidResult<DidSpend> {
56    let owner_puzzle_hash = owner_puzzle_hash(ctx, owner)?;
57
58    let launcher = Launcher::new(funding_coin.coin_id(), funding_coin.amount);
59    let (launch_conditions, eve) = launcher.create_eve_did(
60        ctx,
61        owner_puzzle_hash,
62        recovery_list_hash,
63        num_verifications_required,
64        metadata,
65    )?;
66
67    let settled = settle(ctx, eve, owner)?;
68    spend_funding_coin(ctx, funding_coin, owner, launch_conditions)?;
69
70    Ok(DidSpend::new(drain_coin_spends(ctx), Some(settled)))
71}
72
73/// [`create_did`] with the common defaults: no recovery list, a single required verification, and
74/// nil metadata. The usual entry point for a DID that does not need a recovery configuration.
75///
76/// # Errors
77///
78/// See [`create_did`].
79pub fn create_simple_did(
80    ctx: &mut SpendContext,
81    funding_coin: Coin,
82    owner: Owner,
83) -> DidResult<DidSpend> {
84    create_did(ctx, funding_coin, owner, None, 1, HashedPtr::NIL)
85}
86
87/// Launches the eve DID WITHOUT the owner-update settle step.
88///
89/// The eve DID this returns is real and spendable on-chain, but most wallets expect the additional
90/// settle spend ([`create_did`] performs it) before they will recognize the DID. Use this lower-level
91/// primitive when the caller intends to perform its own follow-up spend on the eve DID (e.g. to fold
92/// the settle into a larger spend bundle).
93///
94/// # Signature
95///
96/// Exactly one `AGG_SIG_ME` is required, over the funding-coin spend, under `owner`'s key/spend.
97///
98/// # Errors
99///
100/// See [`create_did`].
101pub fn create_eve_did_only(
102    ctx: &mut SpendContext,
103    funding_coin: Coin,
104    owner: Owner,
105    recovery_list_hash: Option<Bytes32>,
106    num_verifications_required: u64,
107    metadata: HashedPtr,
108) -> DidResult<DidSpend> {
109    let owner_puzzle_hash = owner_puzzle_hash(ctx, owner)?;
110
111    let launcher = Launcher::new(funding_coin.coin_id(), funding_coin.amount);
112    let (launch_conditions, eve) = launcher.create_eve_did(
113        ctx,
114        owner_puzzle_hash,
115        recovery_list_hash,
116        num_verifications_required,
117        metadata,
118    )?;
119
120    spend_funding_coin(ctx, funding_coin, owner, launch_conditions)?;
121
122    Ok(DidSpend::new(drain_coin_spends(ctx), Some(eve)))
123}
124
125/// The puzzle hash of the p2 puzzle `owner` names — the DID's `p2_puzzle_hash` at creation.
126///
127/// [`Owner::Standard`] curries the standard-puzzle tree hash directly (no CLVM run needed);
128/// [`Owner::Custom`] hashes the caller's already-built inner puzzle.
129fn owner_puzzle_hash(ctx: &SpendContext, owner: Owner) -> DidResult<Bytes32> {
130    Ok(match owner {
131        Owner::Standard(public_key) => StandardArgs::curry_tree_hash(public_key).into(),
132        Owner::Custom(spend) => tree_hash(ctx, spend.puzzle).into(),
133    })
134}
135
136/// Performs the owner-update ("settle") spend that leaves the DID's metadata/p2 puzzle unchanged but
137/// makes it wallet-parseable — the same effect as [`Did::update`], generalized over [`Owner`] (which
138/// `Did::update` cannot be, since it requires a typed `SpendWithConditions` inner layer).
139fn settle(ctx: &mut SpendContext, did: Did, owner: Owner) -> DidResult<Did> {
140    let unchanged_inner_puzzle_hash: Bytes32 = did.info.inner_puzzle_hash().into();
141    let memos = ctx.hint(did.info.p2_puzzle_hash)?;
142    let settle_conditions =
143        Conditions::new().create_coin(unchanged_inner_puzzle_hash, did.coin.amount, memos);
144
145    let spend = inner_spend(ctx, owner, settle_conditions)?;
146    did.spend(ctx, spend)?.ok_or_else(|| {
147        crate::error::DidError::Parse("settle spend produced no successor DID".into())
148    })
149}
150
151/// Spends the funding coin under `owner`, emitting the launcher's create/announcement conditions —
152/// the step that actually creates the launcher coin and requires the owner's `AGG_SIG_ME`.
153fn spend_funding_coin(
154    ctx: &mut SpendContext,
155    funding_coin: Coin,
156    owner: Owner,
157    launch_conditions: Conditions,
158) -> DidResult<()> {
159    let spend = inner_spend(ctx, owner, launch_conditions)?;
160    ctx.spend(funding_coin, spend)?;
161    Ok(())
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use chia_wallet_sdk::prelude::MAINNET_CONSTANTS;
168    use chia_wallet_sdk::signer::{AggSigConstants, RequiredSignature};
169    use chia_wallet_sdk::test::Simulator;
170
171    /// Creating a simple DID produces exactly the funding+launcher+settle spends, and the resulting
172    /// child DID is real: it can be broadcast against a simulator and parsed back byte-identically.
173    #[test]
174    fn create_simple_did_produces_a_spendable_settled_did() -> anyhow::Result<()> {
175        let mut sim = Simulator::new();
176        let ctx = &mut SpendContext::new();
177
178        let owner = sim.bls(1);
179        let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
180
181        let child = spend.child.expect("create always returns a child DID");
182        assert_eq!(child.info.recovery_list_hash, None);
183        assert_eq!(child.info.num_verifications_required, 1);
184        assert_eq!(child.info.p2_puzzle_hash, owner.puzzle_hash);
185
186        sim.spend_coins(spend.coin_spends, &[owner.sk])?;
187        Ok(())
188    }
189
190    /// `create_did` requires two `AGG_SIG_ME`s — one over the funding-coin spend (which creates the
191    /// launcher) and one over the settle spend (which confirms the DID for wallets) — both under the
192    /// owner's key, never an `AGG_SIG_UNSAFE` (SPEC §3/§4; corrects the earlier single-signature
193    /// estimate now that the settle step is known to require its own spend of the owner's p2 puzzle).
194    #[test]
195    fn create_did_requires_two_agg_sig_mes_over_the_owner_key() -> anyhow::Result<()> {
196        let mut sim = Simulator::new();
197        let ctx = &mut SpendContext::new();
198
199        let owner = sim.bls(1);
200        let spend = create_simple_did(ctx, owner.coin, Owner::Standard(owner.pk))?;
201
202        let constants = AggSigConstants::from(&*MAINNET_CONSTANTS);
203        let required = crate::sign::required_signatures(&spend.coin_spends, &constants)
204            .expect("signature calculation must succeed for a well-formed create spend");
205
206        assert_eq!(
207            required.len(),
208            2,
209            "the funding-coin spend AND the settle spend each require one AGG_SIG_ME"
210        );
211        for signature in &required {
212            match signature {
213                RequiredSignature::Bls(bls) => assert_eq!(bls.public_key, owner.pk),
214                RequiredSignature::Secp(_) => panic!("a standard owner signs with BLS, not secp"),
215            }
216        }
217        Ok(())
218    }
219
220    /// A full recovery configuration round-trips through creation untouched.
221    #[test]
222    fn create_did_preserves_a_custom_recovery_configuration() -> anyhow::Result<()> {
223        let mut sim = Simulator::new();
224        let ctx = &mut SpendContext::new();
225
226        let owner = sim.bls(1);
227        let recovery_list_hash =
228            Some(clvm_utils::tree_hash_atom(b"dig-did::create::recovery-list").into());
229
230        let spend = create_did(
231            ctx,
232            owner.coin,
233            Owner::Standard(owner.pk),
234            recovery_list_hash,
235            2,
236            HashedPtr::NIL,
237        )?;
238        let child = spend.child.expect("create always returns a child DID");
239
240        assert_eq!(child.info.recovery_list_hash, recovery_list_hash);
241        assert_eq!(child.info.num_verifications_required, 2);
242
243        sim.spend_coins(spend.coin_spends, &[owner.sk])?;
244        Ok(())
245    }
246
247    /// The lower-level eve-only primitive skips the settle spend, returning just the eve DID — the
248    /// caller is expected to perform its own follow-up spend.
249    #[test]
250    fn create_eve_did_only_skips_the_settle_spend() -> anyhow::Result<()> {
251        let mut sim = Simulator::new();
252        let ctx = &mut SpendContext::new();
253
254        let owner = sim.bls(1);
255        let spend = create_eve_did_only(
256            ctx,
257            owner.coin,
258            Owner::Standard(owner.pk),
259            None,
260            1,
261            HashedPtr::NIL,
262        )?;
263
264        // Two spends: funding coin + launcher — no separate settle spend.
265        assert_eq!(spend.coin_spends.len(), 2);
266
267        let eve = spend.child.expect("create always returns a child DID");
268        assert_eq!(eve.info.p2_puzzle_hash, owner.puzzle_hash);
269
270        sim.spend_coins(spend.coin_spends, &[owner.sk])?;
271        Ok(())
272    }
273}