dig-did 0.8.0

The DIG Network canonical Chia DID expert crate: a pure, key-free, network-free SpendBundle-builder for Chia Decentralized Identifiers. Builds the exact CoinSpends for every DID lifecycle operation and reports the exact signatures a caller must produce — never holds a key, never signs, never broadcasts.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# dig-did — Normative Specification

`dig-did` is the DIG Network canonical **Chia DID expert crate**: a pure, key-free, network-free
library that builds the exact `CoinSpend`s for every Chia Decentralized Identifier (DID) lifecycle
operation and reports the exact signatures a caller must produce. This document is the authoritative
contract an independent reimplementation could be built against. It describes the COMPLETE designed
surface; the crate ships incrementally by unit (U1 = foundation), but the contract below is final.

Key words MUST, MUST NOT, SHOULD, MAY are used per RFC 2119.

---

## §1 Scope & invariants

dig-did is a **spend-builder**. It transforms caller-supplied on-chain state (coins, puzzles,
lineage) into unsigned `CoinSpend`s and tells the caller what to sign. It is not a wallet, not a
node, and not a signer.

Four invariants hold across the entire crate:

- **INV-1 — No network.** dig-did performs NO network or chain I/O. Every public function is a pure
  transform of its inputs. The caller is responsible for fetching coins/puzzle reveals and for
  broadcasting the assembled `SpendBundle`.
- **INV-2 — No keys.** dig-did MUST NOT accept, hold, derive, persist, or log a secret key. It
  computes the messages that must be signed (§4); the caller's signer produces the signatures. No
  function in this crate takes a `SecretKey`.
- **INV-3 — Unsigned output.** Every operation returns an unsigned `DidSpend` — the coin spends plus
  the recreated child DID. Assembling and signing the `SpendBundle` is always the caller's
  responsibility.
- **INV-4 — SDK byte-source-of-truth.** Every puzzle, layer, and coin-spend byte is produced by
  `chia-wallet-sdk` (pinned to the **0.34 / chia-protocol 0.36** family — the version the whole DIG
  on-chain line rides). dig-did MUST NOT re-implement a DID puzzle or hand-roll a spend bundle; it
  adds DID-workflow ergonomics over the SDK primitives.

---

## §2 DID model

A Chia DID is a **singleton** whose inner puzzle is the DID layer, whose inner puzzle is in turn the
owner's p2 ("standard") puzzle:

```
SingletonLayer( DidLayer( p2_puzzle ) )
```

### 2.1 `DidInfo`

The information needed to construct a DID's outer puzzle (re-exported verbatim from
`chia-wallet-sdk`, INV-4):

| Field | Type | Meaning |
|---|---|---|
| `launcher_id` | `Bytes32` | Coin id of the launcher coin that created this DID's singleton. Stable identity of the DID for life. |
| `recovery_list_hash` | `Option<Bytes32>` | Hash of the recovery list (§3 recovery). `None` where the wallet allows it. |
| `num_verifications_required` | `u64` | Number of recoverer attestations required to recover. |
| `metadata` | `HashedPtr` | The DID layer metadata. Freely updatable, but must be confirmed by a settle spend (§3). |
| `p2_puzzle_hash` | `Bytes32` | Hash of the inner (owner) puzzle. Bech32m-encoded, this is the current owner's address. |

`DidInfo::inner_puzzle_hash()` derives the DID layer puzzle hash from these fields via
`DidArgs::curry_tree_hash(p2_puzzle_hash, recovery_list_hash, num_verifications_required,
SingletonStruct::new(launcher_id), metadata_tree_hash)`. This derivation is the SDK's; dig-did MUST
use it rather than recomputing.

### 2.2 `Did`

`Did = Singleton<DidInfo>` = `{ coin: Coin, proof: Proof, info: DidInfo }`. The `Proof` (an
`EveProof` for a freshly launched DID, a `LineageProof` thereafter) is required in the singleton
solution to spend the coin. A `Did` carries everything needed to spend the DID EXCEPT the inner
puzzle+solution, which the `Owner` (§2.4) supplies.

### 2.3 `did:chia:1…` string (codec)

A DID's canonical string form is `did:chia:` followed by the **bech32m** encoding of its
`launcher_id`, using the same address codec as `chia-sdk-utils` `Address` (hrp `did:chia:`). The
codec MUST byte-agree with that SDK codec (§9); dig-did MUST NOT hand-roll bech32m. Malformed input
yields `DidError::InvalidDidString`.

### 2.4 `Owner`

`Owner` names the p2 puzzle that authorizes a DID spend:

- `Owner::Standard(PublicKey)` — the standard single-key p2 puzzle. dig-did curries the
  `StandardLayer` over the (synthetic) key; the resulting spend requires one `AGG_SIG_ME` over that
  key (§4).
- `Owner::Custom(Spend)` — a caller-supplied, already-built inner `Spend` for any p2 puzzle (custom
  vault, multisig, delegated puzzle). dig-did passes it through unchanged; the caller owns its
  signature requirements.

An operation that must add conditions of its own MUST refuse `Owner::Custom` with
`DidError::UnsupportedOwner`, naming the alternative. A pre-built spend emits one fixed condition
set, so those conditions would be silently dropped and the operation would return a well-formed
bundle that creates none of the coins it reports. This binds `create_did`, `create_eve_did_only`
(both add launcher conditions computed inside the call) and `spend_did_with_conditions` (which adds
the DID's recreation condition).

---

## §3 Operations

Every operation returns `Result<DidSpend, DidError>` where `DidSpend = { coin_spends:
Vec<CoinSpend>, child: Option<Did> }`. `child` is the DID as it will exist after the spends confirm
(`None` only for a terminal operation). Unless stated otherwise, a standard-owner operation requires
exactly one `AGG_SIG_ME` over the owner key (§4); a custom-owner operation, where the operation
accepts one (§2.4), requires whatever the caller's inner spend requires. Where an operation emits
caller-supplied conditions (**Spend-with-conditions**), any `AGG_SIG_*` among those conditions is an
ADDITIONAL requirement reported by `required_signatures`, so the stated count is the operation's own
minimum, not a total.

Caller-supplied conditions MUST be judged by an ALLOWLIST, applied to the conditions RE-PARSED from
their CLVM form rather than to the variants the caller typed. `Condition` is `#[non_exhaustive]` and
carries a catch-all variant that serializes verbatim, so a caller can present any condition under a
name a list of refusals does not recognise; only a guard that refuses everything it does not
explicitly permit can fail closed, and it stays closed when a future variant is added. A
DID-preserving spend MUST permit only: `REMARK`, even-amount `CREATE_COIN`, `RESERVE_FEE`, the
announcement and message conditions, the `ASSERT_*` assertions (including timelocks and `ASSERT_MY_*`),
and the `AGG_SIG_*` kinds bound to this spend's coin lineage (`AGG_SIG_ME`, `AGG_SIG_PARENT`,
`AGG_SIG_PARENT_AMOUNT`, `AGG_SIG_PARENT_PUZZLE`). A coin id is unique to one coin; a parent id is
NOT — every coin created by one spend shares it — so an `AGG_SIG_PARENT` signature emitted by a DID
spend is reusable by any SIBLING of that DID coin: the other outputs of the DID's PREVIOUS spend,
not the coins this spend creates. Those kinds are permitted nonetheless, because that set is
bounded: the signature cannot reach a later generation of the DID (each generation has a different
parent id) and cannot become an off-domain assertion. It is NOT confined to coins the caller
controls — see "Scope of the guarantee" below. Everything else MUST be refused
(`DisallowedCondition`), including `SOFTFORK`, the magic `CREATE_COIN` forms (`MELT_SINGLETON`,
`RUN_CAT_TAIL`, the NFT/data-store updaters), any condition the SDK cannot name, and
`AGG_SIG_PUZZLE`/`AGG_SIG_AMOUNT`/`AGG_SIG_PUZZLE_AMOUNT` — a self-recreating DID keeps the puzzle
hash and amount identical every generation, so a signature bound only to those is replayable in a
later spend. `AGG_SIG_UNSAFE` MUST be refused with its own error (`AggSigUnsafeInConditions`): it is
signed with no coin binding and no domain separation, so the signature it induces under the DID
owner's key is replayable against any other spend.

A permitted `CREATE_COIN` MUST additionally carry an amount encoded canonically as chia encodes a
CLVM integer: the empty atom for zero, no leading byte with the sign bit set, a leading zero byte
only where it prevents the next byte reading as a sign bit, and no more significant bytes than a
`u64` holds. An amount outside that encoding MUST be refused (`NonCanonicalCreateCoinAmount`). CLVM
integers are signed while the typed condition surface decodes the amount unsigned, so without this
rule a negative or redundantly-encoded amount is admitted, reported as a child DID, and then
rejected at mempool admission — the opaque failure the guard exists to prevent. The rule mirrors
chia's own encoding requirement exactly and therefore refuses no amount the chain would accept.

**Scope of the guarantee.** The allowlist bounds the KIND of authority a DID spend may create. It
MUST NOT be described as bounding value or as making a hostile condition set safe. A permitted
even-amount `CREATE_COIN` moves a caller-chosen amount of the caller's own bundled funds to a
caller-chosen puzzle hash, and a permitted `CREATE_PUZZLE_ANNOUNCEMENT` is emitted by the DID coin
verbatim — announcements are Chia's authority-granting primitive, so a permitted announcement is a
grant of the DID's authority to another spend in the bundle, not merely a constraint on this one.
Neither is every permitted shape confined to the bundle's lifetime, nor to the spend's own coin set.
An `AGG_SIG_PARENT` signature is bound to the DID coin's PARENT id, so it stays satisfiable by any
future spend of any coin sharing that parent — the outputs of the DID's PREVIOUS spend, not anything
this spend creates (a coin created here carries THIS coin's id as its parent id, a different value).
That set was fixed before this spend was built and MAY include a coin an earlier caller paid to a
third party, under a puzzle that third party chose. For the EVE generation the previous spend is the
LAUNCHER's, whose only output is the eve coin itself, so the sibling set is empty and the exposure is
nil; it opens from the first ordinary spend onwards. The bound that does hold: such a signature can
never reach a later generation of the DID, and can never become an off-domain assertion. The guard
does not sanitize a hostile caller, and no allowlist over a conditions passthrough can. A caller
composing conditions from an untrusted source MUST review the bundle before signing, and where an
`AGG_SIG_PARENT` is present MUST also account for what the DID's PREVIOUS spend created — which this
bundle does not show.

| Operation | Unit | Inputs | CoinSpends produced | Recreated child | Signature |
|---|---|---|---|---|---|
| **Create** | U2 | funding coin, owner, `recovery_list_hash`, `num_verifications_required`, metadata | launcher spend (from funding coin) + eve DID spend + owner settle spend | the new `Did` |`AGG_SIG_ME` (owner: one over the funding-coin spend, one over the settle spend) |
| **Update-metadata** | U3 | `Did`, owner, new metadata | DID update spend recreating the DID with new metadata | `Did` with new `metadata` |`AGG_SIG_ME` (owner) |
| **Settle** | U3 | `Did`, owner | DID update spend with unchanged metadata/p2 (confirms metadata for wallets) | `Did` unchanged in shape |`AGG_SIG_ME` (owner) |
| **Set-recovery** | U4 | `Did`, owner, new `recovery_list_hash`, `num_verifications_required` | DID update spend recreating the DID with new recovery config | `Did` with new recovery fields |`AGG_SIG_ME` (owner) |
| **Recover** | U4 | `Did`, recoverer attestations, new p2 puzzle hash | DID recovery spend rotating owner to the new p2 | `Did` with new `p2_puzzle_hash` | attestations per `num_verifications_required` |
| **Transfer** | U5 | `Did`, owner, new p2 puzzle hash | DID update spend creating the DID under the new owner (hinted) | `Did` with new `p2_puzzle_hash` |`AGG_SIG_ME` (CURRENT owner) |
| **Launch-from-DID** (child DID / NFT / datastore) | U6 | `Did`, owner, launch parameters | DID spend emitting the launch's announcement assertion + the dependent singleton's launch spend(s), whose launcher is parented to an ordinary coin (see the one-odd-output note below) | `Did` (unchanged) + the launched primitive |`AGG_SIG_ME` (owner) |
| **Melt** | U7 | `Did`, owner | DID spend with no odd-amount successor (terminal) | `None` |`AGG_SIG_ME` (owner) |
| **Announce-as-DID** (attest) | U8 | `Did`, owner, announcement message/target | DID update spend emitting the announcement condition | `Did` (unchanged) |`AGG_SIG_ME` (owner) |
| **Hydrate** | U9 | parent coin, parent puzzle reveal, parent solution, child coin | — (parse only) | the spendable `Did` ||
| **Resolve-XCH-address** | U10 | `launcher_id` (or `did:chia:` string), address `prefix`, `ChainSource` | — (chain READ only) | the owner's payment `Address` (or `None` if unlaunched/melted) ||

Notes:
- **Create** builds the eve DID via `Launcher::create_eve_did`, then performs the settle spend
  itself (via `Did::spend` with an `Owner`-derived inner [`Spend`], SPEC §2.4) rather than the SDK's
  typed `Launcher::create_did`/`Did::update`, which require a concrete `SpendWithConditions +
  ToTreeHash` inner layer. The settle step is the no-condition case of **Spend-with-conditions**
  below — one code path, so the recreation condition is emitted identically either way. All three
  resulting spends (funding, launcher, settle) are returned together as one `DidSpend`. Create
  requires `Owner::Standard` (§2.4). `create_eve_did_only` is the lower-level primitive that stops
  after the launcher spend, for a caller that wants to fold its own follow-up spend into the same
  bundle.
- **The funding coin becomes the DID, in full.** A launch gives the singleton the funding coin's
  ENTIRE amount; this crate emits no change output, because deciding where change goes is caller
  policy, not a spend builder's. Two consequences bind every create entry point (`create_did`,
  `create_simple_did`, `create_eve_did_only`):
  - The amount MUST be ODD. A singleton is the odd-amount output of its launcher, so an even-amount
    funding coin yields a bundle that spends the coin and creates no singleton at all — a total,
    silent loss rather than a rejected spend. Creation MUST refuse it with `EvenSingletonAmount`.
    The proof is carried by the `SingletonAmount` newtype, whose only constructor validates and
    which every launch site MUST go through. EVERY SDK route by which a launcher can take a raw
    amount — each of its constructors AND any amount mutator on the built launcher — could bypass
    the newtype, so ALL of them MUST be denied to new call sites by a lint (`disallowed-methods` in
    `clippy.toml`, with CI running clippy as `-D warnings`), leaving one annotated production
    exemption at the chokepoint. Denying only the primary constructor is NOT sufficient and MUST NOT
    be read as conformance; an SDK upgrade that adds a route MUST extend the denial in the same unit
    of work. The newtype states the rule; the lint is what
    makes bypassing it fail the build rather than merely break a convention.
  - Any excess above the intended singleton amount is LOCKED in the identity coin permanently. The
    caller MUST pass a coin pre-split to exactly the amount the DID should carry; `dig-account`'s
    exact 1-mojo split is the reference pattern.
- **Spend-with-conditions** (`spend_did_with_conditions`) spends the DID emitting the caller's
  conditions IN ADDITION to the recreation `CREATE_COIN` that preserves the DID unchanged (same
  inner puzzle hash, same amount, same owner hint). The recreation condition MUST be emitted FIRST,
  before the caller's conditions, and MUST NOT be replaced or omitted. The ordering is load-bearing:
  a successor DID is identified by scanning the emitted conditions for the first odd-amount
  `CREATE_COIN`, and that scan ABORTS — reporting no successor rather than skipping ahead — at the
  first odd-amount `CREATE_COIN` carrying no memos. A recreation emitted after such a condition is
  therefore unreachable, and the spend reports no successor DID even when it is otherwise valid. The
  spend is staged into the caller's
  `SpendContext`; the caller's `Conditions` MUST have been built in that same context. This is the
  primitive **Launch-from-DID** and **Announce-as-DID** are expressed in terms of.
- **One odd-amount output.** A singleton's inner puzzle MUST emit exactly one odd-amount
  `CREATE_COIN`, and a DID's recreation occupies it. A singleton launcher is an odd-amount coin, so a
  foreign singleton MUST NOT be parented to the DID coin. `spend_did_with_conditions` therefore MUST
  refuse, with `OddAmountCreateCoin`, any caller condition that is an odd-amount `CREATE_COIN`: the
  recreation already holds the singleton's one odd-amount output, so such a bundle can never be
  valid. Refusing at build time is required because the alternative failure is opaque — the bundle
  assembles and reports a child DID, and is rejected only at mempool admission (it never enters a
  block, so no fee is paid, but the caller learns nothing about why). **Launch-from-DID** therefore
  parents the launcher to an ordinary coin and binds it to the DID
  by other means: an announcement asserted by the DID's own spend in the same bundle, and/or the
  launched singleton's owner puzzle hash.
- **Melt** (`melt`) is TERMINAL and irreversible. It spends the DID with a single `MELT_SINGLETON`
  magic condition (`(51 () -113)`) instead of a recreation condition, so the singleton top layer
  creates no successor and the lineage ENDS; the returned `DidSpend.child` MUST be `None`, and an
  implementation that yields a successor MUST refuse rather than report the spend as a melt. A
  melted launcher id can never be recreated, so every `did:chia:` reference to it becomes
  permanently unresolvable.
  - **Authority MUST be checked before the spend is built.** `melt` MUST refuse, with `NotTheOwner`,
    an `Owner::Standard` key that does not curry to the DID's current `p2_puzzle_hash`, and MUST
    refuse `Owner::Custom` with `UnsupportedOwner` (the melt condition is built inside the call, so a
    pre-built inner spend would emit none of it). Because the operation is unrecoverable, neither
    refusal may be deferred to signing or to mempool admission.
  - **The melted amount is an implicit fee, not a payout.** The singleton layer permits exactly one
    odd-amount `CREATE_COIN` and the melt magic condition occupies it, so the amount MUST NOT be
    recovered to a caller-supplied puzzle hash in this spend; the output-under-input difference is a
    fee. The executed spend emits no `CREATE_COIN` at all.
- **Update/Settle/Transfer/Launch/Melt/Attest** all build on the SDK `Did::update*` / `Did::spend` /
  `Did::transfer` methods with the inner spend from the `Owner` (§2.4).
- dig-did MUST NOT sign or broadcast any of these; it returns the `CoinSpend`s only (INV-3).

---

## §4 Signing boundary

`required_signatures(coin_spends: &[CoinSpend], constants: &AggSigConstants) ->
Result<Vec<RequiredSignature>, DidError>` is the sole bridge between dig-did's unsigned output and a
caller's signer.

- It runs each coin spend's puzzle against its solution in a private `Allocator`, collects every
  `AGG_SIG_*` condition, and returns the precise `RequiredSignature` set (public key + raw message +
  appended coin/domain info) — the exact bytes the caller must sign.
- It is **pure and key-free** (INV-2): it takes no secret key and computes only what must be signed.
- `AggSigConstants` is derived from the network's `AGG_SIG_ME` additional data, e.g.
  `AggSigConstants::from(&*MAINNET_CONSTANTS)`.
- **Owner operations use `AGG_SIG_ME`** (bound to the specific coin id). dig-did MUST NOT produce a
  spend that requires `AGG_SIG_UNSAFE` over caller-supplied bytes — an `AGG_SIG_UNSAFE` requirement
  would let a signature be replayed against an unrelated message.
- Errors: `DidError::Signer` if a puzzle fails to evaluate or an `AGG_SIG` condition carries an
  infinity public key.

The delegation to `chia_sdk_signer::RequiredSignature::from_coin_spends` guarantees byte-agreement
with the SDK's signature-message construction (INV-4, §9).

---

## §5 Hydration & lineage (fail-closed)

Reconstructing a spendable `Did` from chain data is **fail-closed**: dig-did returns an
error rather than a degraded or guessed DID.

- A DID child is parsed from its parent coin spend (SDK `Did::parse_child`), which relies on the
  child being **hinted** and carrying the **same metadata** as the parent.
- If the parent spend does not establish a lineage proof for the child, hydration MUST return
  `DidError::MissingLineage`.
- If the owner **hint memo** required to recreate the child is absent, hydration MUST return
  `DidError::MissingHint`.
- A puzzle that parses but is not a DID singleton MUST yield `DidError::NotDid`; a puzzle that should
  have been a DID but fails to parse MUST yield `DidError::Parse`.
- Hydration MUST NOT fabricate a lineage proof or a hint. A DID that cannot be proven spendable is an
  error, never a partially-populated success.

### 5.1 Lineage proof (`prove_lineage``AncestryProof`)

`prove_lineage(coin_id, did, chain)` answers one question — *is `coin_id` a coin the DID identity owns
/ is rooted in?* — over a caller-supplied `ChainSource` (§10). It returns an unforgeable `AncestryProof`
(private fields, accessor-only) via **exactly two accepted models**, both reduced to DID-singleton
**lineage membership**:

- **`Direct`**`coin_id` authenticates as a state of the DID singleton itself: its authenticated
  launcher id equals `did.launcher_id`.
- **`LaunchedFrom { launcher, did_parent }`**`coin_id` is a *distinct* singleton whose launcher
  coin's `parent_coin_info` (`did_parent`) is a **member** of the DID singleton's lineage. Membership,
  NOT tip-equality: launching from a DID recreates the DID coin in the same spend, so the launcher's
  parent is a past DID coin `Cn` while the tip is already `Cn+1`.

**The curry-commitment authentication rule (the soundness crux).** A coin is authenticated as a genuine
singleton ONLY by walking its parent-spend chain (`ChainSource::parent_spend`): at each hop the parent
puzzle MUST parse as the SDK `SingletonLayer` (proving the parent is itself a singleton and yielding its
*curried* `launcher_id`); the parent's inner puzzle is run and its odd-amount successor re-wrapped in the
singleton curry (`SingletonArgs::curry_tree_hash(launcher_id, inner)`), and that computed successor MUST
equal the child under authentication. The walk terminates at the singleton **launcher** coin
(`SINGLETON_LAUNCHER_HASH`), yielding the authenticated `launcher_id`. dig-did MUST NEVER authenticate a
coin on `coin.puzzle_hash == singleton_puzzle_hash(...)` alone (a pay-to coin can wear any puzzle hash),
nor on a bare `parent_coin_info` field without the parent's spend proving the recreation.

**Out of scope — MUST fail.** An ordinary payment/change coin whose `parent_coin_info` merely happens to
be a DID coin is REJECTED (`NotASingleton`): a DID spend may pay anyone, so a non-singleton output is not
owned by the DID. The discriminator is singleton structure.

**Fail-closed.** Every gap/mismatch is an error, never a soft `true`: no DID lineage →
`NoIdentitySingleton`; not a genuine singleton → `NotASingleton`; a genuine singleton neither the DID nor
launched from it → `NotDidRooted`; a walk past `MAX_LINEAGE_DEPTH` → `LineageTooDeep`; a `ChainSource`
read failure → `Chain` (never "assume owned").

`walk_did_lineage_to_tip(chain, launcher_id)` is the forward companion: it resolves the DID singleton's
current unspent tip and reconstructs it as a `DidTip { coin, info, proof }` (SDK `Did::parse_child`), or
`None` when the DID is unlaunched/melted. It fails closed (`NotDid`) when the tip's creating spend does
not parse as a DID.

---

## §6 Error taxonomy

`DidError` (a `thiserror` enum; the crate result alias is `DidResult<T> = Result<T, DidError>`):

| Variant | Raised when |
|---|---|
| `Driver(DriverError)` | A chia-wallet-sdk driver op failed (currying, spend construction, CLVM eval). Wrapped verbatim. |
| `Signer(String)` | The signing calculator failed (invalid puzzle/solution, infinity public key). Underlying signer error as a string, so the signer's error type does not leak. |
| `Parse(String)` | A coin/puzzle/solution could not be parsed as the expected shape. |
| `NotDid` | A puzzle parsed but is not a DID singleton. |
| `NotTheOwner` | An irreversible operation (`melt`) was given an `Owner` key that does not curry to the DID's current `p2_puzzle_hash`, so the caller cannot prove it controls the singleton. Refused before any spend is built (§3, fail-closed). |
| `InvalidDidString(String)` | A `did:chia:1…` string was malformed / failed bech32m decoding. |
| `InvalidRecovery(String)` | An inconsistent recovery configuration was supplied. |
| `UnsupportedOwner(&'static str)` | The operation must add conditions of its own and cannot honour `Owner::Custom` (§2.4). The message names the alternative. |
| `EvenSingletonAmount(u64)` | A create entry point was given a funding coin with an even amount. The singleton is the odd-amount output of its launcher, so the launch would spend the coin and create no DID — the whole funding coin lost silently (§3, fail-closed). The `u64` is the offending amount. |
| `OddAmountCreateCoin` | A caller passed an odd-amount `CREATE_COIN` to `spend_did_with_conditions`; the singleton's one odd-amount output is the DID's recreation, so the spend could never be valid on chain (§3, fail-closed). |
| `AggSigUnsafeInConditions` | A caller passed an `AGG_SIG_UNSAFE` to `spend_did_with_conditions`. It is signed with no coin binding and no domain separation, so it induces a replayable signature under the DID owner's identity key over caller-chosen bytes (§3, fail-closed). |
| `NonCanonicalCreateCoinAmount(String)` | A caller passed a `CREATE_COIN` whose amount atom is not chia's canonical integer encoding (negative, a redundant leading zero, or too many significant bytes). The typed condition surface decodes the amount unsigned, so such a spend would otherwise assemble here and be rejected at mempool admission (§3, fail-closed). The string renders the offending atom. |
| `DisallowedCondition(String)` | A caller passed a condition outside the allowlist of shapes a DID-preserving spend may carry (§3, fail-closed). The string renders the offending condition. |
| `MissingLineage` | Hydration could not establish the lineage proof (fail-closed, §5). |
| `MissingHint` | A parsed DID coin was missing the owner hint memo (fail-closed, §5). |
| `Chain(String)` | A chain-level precondition was violated, or a `ChainSource` read failed (§10) — surfaced verbatim, never degraded to "assume owned". |
| `NoIdentitySingleton` | The DID has no current on-chain coin — unlaunched or melted (§5.1, fail-closed). |
| `NotASingleton` | A coin under `prove_lineage` is not a genuine singleton (a payment/change coin, or a pay-to coin wearing a singleton puzzle hash with no genuine recreation parent spend) (§5.1). |
| `NotDidRooted` | A coin authenticates as a singleton but is neither the DID nor launched from a coin in the DID's lineage (§5.1). |
| `LauncherMismatch` | `resolve_xch_address`'s tip authenticated as a genuine singleton, but its GENUINE launcher (walked from the parent-spend chain) is not the requested launcher — a dishonest source echoing another DID's tip. Fails closed rather than routing a payment to the wrong recipient (§3, §5.1). |
| `LineageTooDeep` | The singleton parent-spend walk exceeded `MAX_LINEAGE_DEPTH` — a DoS guard (§5.1/§10). |

Error messages MUST be descriptive and MUST NOT include secret material.

---

## §7 Security properties

- **No custody.** dig-did never holds a key (INV-2). A caller compromise cannot leak a key *through*
  dig-did because dig-did never possesses one.
- **Explicit signing surface.** Every signature a caller must produce is enumerated by
  `required_signatures` — there is no hidden signing requirement. What the caller signs is exactly
  and only what the returned spends require.
- **`AGG_SIG_ME` binding.** Owner operations bind their signature to the specific coin being spent
  (§4), preventing signature replay across coins. dig-did never emits an `AGG_SIG_UNSAFE` over
  caller bytes.
- **Bounded, NOT eliminated, exposure through the conditions passthrough.**
  `spend_did_with_conditions` emits caller-supplied conditions under the DID's authority, judged by
  an allowlist (§3). The allowlist bounds the KIND of authority created; it bounds neither the VALUE
  moved nor the lifetime of what it creates. Two residual exposures are properties of the design,
  not defects: a permitted even-amount `CREATE_COIN` pays caller-chosen amounts of the caller's own
  bundled funds to caller-chosen puzzle hashes, and a permitted `AGG_SIG_PARENT` induces a signature
  bound to the DID coin's PARENT id — satisfiable at any future time by any coin sharing that parent
  (the outputs of the DID's PREVIOUS spend, which MAY include a coin paid to a third party under a
  puzzle that party chose), though never by a later generation of the DID and never as an off-domain
  assertion. A caller composing conditions from an untrusted source MUST review the bundle before
  signing, and where an `AGG_SIG_PARENT` is present MUST also account for what the DID's previous
  spend created — which the bundle does not show (§3 "Scope of the guarantee").
- **Fail-closed hydration.** Ambiguous or under-specified chain data is an error, not a guess (§5),
  so a caller never signs against a mis-reconstructed DID.
- **Deterministic byte output.** Given identical inputs, dig-did produces identical `CoinSpend`
  bytes (INV-4 delegation to the SDK), making spends auditable and reproducible.
- **Unforgeable lineage proof.** An `AncestryProof` has private fields and accessor-only reads — it
  cannot be minted by a struct literal; the only constructor, `prove_lineage`, authenticates every
  field against the chain (§5.1). A value of the type is therefore evidence the proof genuinely holds.
- **Sound singleton authentication.** A coin is proven owned ONLY by the curry-commitment walk (§5.1) —
  never by puzzle-hash equality or a bare `parent_coin_info`. A non-singleton coin (a payment routed
  through a DID spend) fails closed as `NotASingleton`, so a DID spend that pays an attacker never
  launders that coin into the DID's authority.
- **Bounded work.** The parent-spend walk is capped at `MAX_LINEAGE_DEPTH`, so a malicious
  `ChainSource` cannot force unbounded computation (`LineageTooDeep`).

---

## §8 Backwards compatibility

Per CLAUDE.md §5.1 (additive-only), a published DID is a permanent on-chain artifact and MUST stay
spendable and parseable by every later dig-did:

- **Additive only.** New operations, new optional parameters, and new fields MAY be added. Existing
  operation signatures, produced `CoinSpend` byte shapes, and parsing behavior MUST NOT be removed,
  renumbered, or repurposed.
- **Newer parses older.** A newer dig-did MUST hydrate/resolve every DID a prior version built. A
  version bump means "new builders MAY emit new shapes", never "readers reject older DIDs".
- **Golden fixtures.** Each release keeps golden `CoinSpend` fixtures of the DID operations. A change
  MUST include a test proving the new builder reproduces the prior fixtures byte-identically and the
  new parser decodes older fixtures. A byte-shape change without such a test is incomplete.
- **SDK pin discipline.** The chia-wallet-sdk version is a byte contract (INV-4). Bumping it is a
  deliberate, fixture-verified event — a resulting byte change is a breaking (major) protocol event
  with a migration path, never a silent break.

---

## §9 Conformance

An implementation conforms to this spec when:

- Every produced `CoinSpend` **byte-agrees with chia-wallet-sdk 0.34** for the equivalent operation
  (INV-4). This crate satisfies it by construction (it calls the SDK).
- The `did:chia:1…` codec **byte-agrees with `chia-sdk-utils` `Address`** (§2.3) — same hrp, same
  bech32m, round-trips every launcher id.
- `required_signatures` output **byte-agrees with `chia_sdk_signer::RequiredSignature`** for the same
  coin spends and constants (§4).
- The datastore launch authorization (§3, U6) **byte-agrees with `chip35`** for the equivalent
  DID-authorized datastore launch, so a DID launched via dig-did and one launched via chip35 are
  indistinguishable on chain.
- All fail-closed hydration rules (§5) hold: missing lineage/hint/DID-shape are errors, never
  degraded successes.
- `prove_lineage` accepts exactly the two models of §5.1 and rejects everything else with the stated
  fail-closed error; the curry-commitment authentication rule holds (no puzzle-hash-equality shortcut).

---

## §10 The `ChainSource` seam & trust model

dig-did performs **no network or chain I/O** (INV-1), yet lineage authentication needs to *read* chain
state. `ChainSource` is the seam that reconciles the two — the caller supplies an honest reader; dig-did
supplies all the trust logic. The trait is the **ONE canonical `dig-chainsource-interface` contract**
(published to crates.io) — dig-did consumes and re-exports it (so `dig_did::ChainSource` and
`dig_did::SingletonLineage` remain stable), never a per-crate copy that could byte-drift. There is **no
default impl** in dig-did: the consumer implements it over its own backend (coinset.org, a local full
node, `chia-query`), keeping dig-did no-network and wasm-buildable.

```rust
// re-exported from dig-chainsource-interface
pub trait ChainSource {
    type Error: core::fmt::Display;
    fn coin_record(&self, coin_id: Bytes32) -> Result<Option<CoinRecord>, Self::Error>;
    fn coin_records_by_puzzle_hash(&self, puzzle_hash: Bytes32, include_spent: bool)
        -> Result<Vec<CoinRecord>, Self::Error>;
    fn coin_records_by_parent(&self, parent_coin_id: Bytes32)
        -> Result<Vec<CoinRecord>, Self::Error>;
    fn coin_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error>;
    fn parent_spend(&self, coin_id: Bytes32) -> Result<Option<CoinSpend>, Self::Error>; // default: coin_record + coin_spend
    fn resolve_singleton_lineage(&self, launcher_id: Bytes32)
        -> Result<Option<SingletonLineage>, Self::Error>;
    fn peak_height(&self) -> Result<Option<u32>, Self::Error>;
    fn block_timestamp(&self, height: u32) -> Result<Option<u64>, Self::Error>;
}
```

The two methods dig-did's lineage + resolve logic consume are `resolve_singleton_lineage` and
`parent_spend`:

- `resolve_singleton_lineage` MUST be a genuine forward walk from the DID launcher to its current
  unspent tip, returning every coin id on the walk as a `SingletonLineage` (membership set + tip), or
  `None` when the launcher never existed / the singleton is fully spent. It MUST NOT echo a
  caller-supplied coin.
- `parent_spend(coin_id)` MUST return the coin spend that CREATED `coin_id` (the spend of its parent
  coin), or `None` when no such spend is known. It performs NO authentication — dig-did's walk parses
  and verifies it.

**Trust boundary.** The `ChainSource` MUST be the caller's OWN honest view of the chain, never an
attacker-controlled channel. dig-did assumes the source reports real chain state; it cannot defend
against a source that fabricates the chain itself. Given honest chain data, dig-did guarantees no coin
can launder itself into a DID's authority (§5.1, §7). Every read failure or gap fails closed
(`DidError::Chain` / the §5.1 errors), never an "assume owned" default.

**Coherence.** `SingletonLineage` (tip + membership `contains`) is the `dig-chainsource-interface` type
itself, re-exported — so every ecosystem consumer (dig-did, dig-identity, chia-query) shares ONE shape
that cannot byte-drift.