dig-merkle 0.9.1

The DIG Network canonical CHIP-0035 DataLayer coin expert crate: a pure, key-free, network-free SpendBundle-builder for the Chia DataLayer singleton that anchors a .dig file's merkle root on-chain. Builds the exact CoinSpends for every DataLayer-coin 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
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
# dig-merkle — Normative Specification

This document is the authoritative contract for `dig-merkle`, the DIG Network canonical CHIP-0035
DataLayer coin expert crate. An independent reimplementation could be built against this spec. It is
normative: it states what IS and what an implementation MUST/SHOULD do. Cross-references:
`SYSTEM.md` (cross-repo interaction map) and the docs.dig.net protocol pages.

## 1. Scope & invariants

dig-merkle builds the exact `CoinSpend`s for every lifecycle operation of a Chia CHIP-0035 DataLayer
singleton — the on-chain coin that anchors a `.dig` capsule's merkle root — and reports the exact
signatures a caller must produce. It is a pure library: no keys, no signing, no network.

Four invariants hold across the entire crate:

- **INV-1 — No network.** dig-merkle performs NO network or chain I/O of its own and depends on no
  network or async-runtime crate. Spend-builders are pure transforms of their inputs. Read/resolve
  operations (`resolve_owner_did`) delegate ALL chain access to a caller-supplied `ChainSource` — an
  injected, synchronous read interface the caller implements over its own client — so dig-merkle
  itself still opens no socket and holds no client.
- **INV-2 — No keys.** dig-merkle never accepts, holds, derives, or logs a secret key. It computes
  what must be signed (`required_signatures`); the caller's signer produces the signatures.
- **INV-3 — Unsigned output.** Every operation returns an unsigned `MerkleCoinSpend` — the coin
  spends plus the recreated child `DataStore`. Signatures are 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.1 family, `chip-0035` feature).
  dig-merkle adds workflow ergonomics on top; it never re-implements a puzzle or hand-rolls a spend
  bundle, and re-exports the SDK's DataStore types verbatim (no shadow copy).

## 2. The DataLayer-coin model

A DataLayer coin is a CHIP-0035 **singleton** (an NFT-state-layer singleton with the DataLayer
metadata updater). Its structure:

- **`launcher_id == store_id`.** The singleton launcher coin id IS the DIG `store_id`. It is
  permanent and uniquely names the store for the coin's entire lineage.
- **`DigDataStoreMetadata`** carries the anchored state. It mirrors the SDK's `DataStoreMetadata`
  shape but REPLACES the exact-byte `bytes`/`"b"` field with a power-of-2 `size_bucket` (`"sz"`), and
  adds `program_hash` (`"p"`):
  - `root_hash: Bytes32` — the `.dig` capsule's merkle root (the anchored value). REQUIRED; first atom.
  - `label: Option<String>`, `description: Option<String>` — human metadata (CLVM keys `l`, `d`).
  - `size_proof: Option<String>` — an optional size attestation (CLVM key `sp`).
  - `program_hash: Option<Bytes32>` — the CLVM tree-hash of the program/puzzle associated with the
    store/capsule (CLVM key `p`, appended after `sp`, only when `Some`). dig-merkle STORES and ECHOES
    it only — it never computes it (producers compute it via `clvm_utils::tree_hash`/`ToTreeHash`).
  - `size_bucket: Option<SizeBucket>` — the store's size as a power-of-2 bucket (CLVM key `sz`,
    appended LAST, only when `Some`). This is the ONE size field — it REPLACES the SDK's exact-byte
    `"b"` field: **dig-merkle never emits `"b"`**. A `SizeBucket` is a validated exponent `k ∈ 0..=10`
    mapping to `2^k MB`, where **1 MB = 1 MiB = 2^20 bytes** — so the ladder is 1 MB (k=0) … 1024 MB =
    1 GB (k=10). On the wire the value is the exponent encoded as a MINIMAL CLVM integer (NC-8): the
    empty atom for `k=0`, a single byte `0x01`..`0x0a` for `k=1`..`10`. `SizeBucket::for_byte_len(bytes)`
    is the CANONICAL byte→bucket mapping for the MINT INPUT path (the smallest `k` with `2^(k+20) ≥
    bytes`; 0/1 byte → k=0, exactly 1 GiB → k=10, `> 2^30` → error) so dig-store never re-derives the
    ladder and drifts.
  - There is NO `bytes` field. `"b"` is deliberately dropped (pre-release clean replacement: there are
    no on-chain DIG stores carrying `"b"`).

  **INV-4 byte-identity + SDK interop.** With `size_bucket == None && program_hash == None` the CLVM
  encoding is IDENTICAL to the SDK's `DataStoreMetadata` with `bytes == None` (it emits `l`/`d`/`sp`
  only, never `"b"`), so a plain DIG store is byte-for-byte an ordinary DataLayer store. An SDK-typed
  reader decoding a `p`/`sz`-bearing store ignores the unknown keys; a `DigDataStoreMetadata` reader
  decoding an SDK store parses `root`/`l`/`d`/`sp` and IGNORES the SDK's `"b"` (a foreign `"b"` is not
  a DIG size-proof) — yielding `size_bucket == None`. Reading an SDK store therefore still succeeds
  (it decodes); the honest answer for a non-capsule is simply no bucket.

  **`sz` decode is fail-closed (canonical minimal form).** A decoder accepts ONLY the canonical
  minimal encoding of a value in `0..=10`: the empty atom (k=0) or a single byte `0x01`..`0x0a`. A
  non-minimal encoding (a leading-zero atom like `[0x00]` or `[0x00,0x05]`), a byte `> 10`, or any
  multi-byte atom is REJECTED (`FromClvmError::Custom`) — so the on-wire size has exactly one valid
  representation and no producer can encode it two ways.
- **`delegated_puzzles: Vec<DelegatedPuzzle>`** grants write authority beyond the owner:
  - `Admin(TreeHash)` — full control (may change the delegation set + root).
  - `Writer(TreeHash)` — may update the root but not the delegation set.
  - `Oracle(Bytes32, u64)` — anyone may spend the coin to read it, paying the fixed fee.
- **The owner** is the standard p2 (`Owner::Standard`) or a custom inner puzzle (`Owner::Custom`,
  e.g. a DID-authorized delegated puzzle) that guards spending. `Owner::Custom` carries its own
  conditions, so it is valid only for operations whose conditions the caller can build in advance —
  the mint path rejects it (§3.1).

Spending the coin recreates it as its child with a (possibly) new root, delegation set, or owner —
or melts it (no child). This is the DIG anchor: publishing a new capsule root is a DataLayer update
that recreates the singleton with the new `root_hash`.

## 3. Operations catalogue

Every operation returns an unsigned `MerkleCoinSpend { coin_spends, child }` (INV-3) and states its
AGG_SIG requirement. U1 ships the foundation only; the operations below are the designed surface,
each landing in its own unit against the foundation.

### 3.1 mint

```
mint_datastore(parent_coin, owner, root_hash, label, description, size_proof,
               program_hash, size_bucket, owner_puzzle_hash, delegated_puzzles, fee)
    -> MerkleResult<MerkleCoinSpend>
```

There is NO `bytes` param — size is the `size_bucket`. `program_hash: Option<Bytes32>` and
`size_bucket: Option<SizeBucket>` are optional: with both `None` the mint is byte-identical to a plain
DataLayer store; `Some(h)` anchors the program tree-hash (CLVM key `p`) and `Some(bucket)` anchors the
size bucket (CLVM key `sz`). The returned `MerkleCoinSpend.child` is a `DataStore<DigDataStoreMetadata>`.

Launches a new DataLayer store singleton over `chia_wallet_sdk::driver::Launcher::mint_datastore`
(INV-4). `parent_coin` funds AND parents the launcher: its `coin_id` becomes the launcher's parent,
so `launcher_id == store_id` derives from it. Taking a `parent_coin` (not a full launcher) lets a
DID-authorized launch composed by a caller work here **without a `dig-did` dependency**; the edge
stays one-way (dig-identity → dig-merkle). A DID-rooted launch does NOT use `mint_datastore_with_kind`
with an `Owner::Custom` inner spend — that variant emits only the conditions the caller baked in, and
the launch conditions are built inside this call. `mint_datastore_with_kind` therefore REJECTS
`Owner::Custom` with `MerkleError::UnsupportedOwner`; a custom-owner caller uses
`mint_datastore_launch_with_kind` (§3.1a).

The construction, byte-for-byte:

1. `Launcher::new(parent_coin.coin_id(), 1).mint_datastore(ctx, DigDataStoreMetadata{root_hash,
   label, description, size_proof, program_hash, size_bucket}, owner_puzzle_hash,
   delegated_puzzles)` yields the launch conditions + the eve `DataStore`.
2. **Two-memo launcher-hint override (load-bearing).** The raw SDK mint emits only a single default
   launcher hint, which matches NO store already on chain. dig-merkle rewrites the launcher
   `CREATE_COIN` (the one to the singleton launcher puzzle hash
   `eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9`) so its memos are EXACTLY
   `[digstore_owner_hint(owner_puzzle_hash), launcher_hint_for(kind)]` (first = indexed
   kind-agnostic owner-discovery hint, second = the kind discriminator, §9) — replicating
   chip35_dl_coin `store.rs` and digstore-chain `singleton.rs`. This override is the DEFAULT
   behaviour, not opt-in. `mint_datastore` mints `StoreKind::File`, whose discriminator is the
   unchanged `DATASTORE_LAUNCHER_HINT` — byte-identical to existing stores; `mint_datastore_with_kind`
   selects a kind (#1263).
3. Change above `fee + 1` mojos returns to `owner_puzzle_hash`, hinted. The `fee` is paid
   **implicitly** as (coins in − coins out) — there is NO explicit `RESERVE_FEE`, matching the
   on-chain producers. The `fee + 1` reservation is a CHECKED add: a `fee` so large that `fee + 1`
   would overflow `u64::MAX` fails closed with `MerkleError::Chain` rather than wrapping around.
4. `parent_coin` is spent with `owner`'s inner puzzle (`Owner::Standard``StandardLayer`). An
   `Owner::Custom` mint is REJECTED at step 0 with `MerkleError::UnsupportedOwner`, before any
   construction: a pre-built inner spend cannot emit the conditions built in step 1, so accepting it
   would return a bundle that never creates the launcher coin.

### §3.1a `mint_datastore_launch_with_kind` — the composable launch

```rust
#[non_exhaustive]
pub struct DatastoreLaunch {
    pub parent_conditions: Conditions,
    pub datastore: DataStore<DigDataStoreMetadata>,
    /// Whether this launch WROTE the launcher memos (the owner-discovery hint AND the
    /// `StoreKind` discriminator, §9). `true` for a direct launch; `false` for an
    /// intermediate launch, whose launcher `CREATE_COIN` this crate does not author.
    /// Measured from the rewrite, never inferred.
    pub launcher_memos_written: bool,
}

pub fn mint_datastore_launch_with_kind(
    ctx: &mut SpendContext, kind: StoreKind, launcher: Launcher, root_hash: Bytes32,
    label: Option<String>, description: Option<String>, size_proof: Option<String>,
    program_hash: Option<Bytes32>, size_bucket: Option<SizeBucket>,
    owner_puzzle_hash: Bytes32, delegated_puzzles: Vec<DelegatedPuzzle>,
) -> MerkleResult<DatastoreLaunch>;
```

Performs steps 1–2 above into the CALLER's `ctx` and returns the conditions the caller's parent-coin
spend MUST emit (the launcher `CREATE_COIN` with the two memos, plus the launcher's coin-announcement
assertion) — no change and no fee, which belong to whoever pays.

- The `ctx` MUST be the caller's own: `Conditions` hold CLVM node pointers valid only in the
  allocator that built them, and the launcher-coin and eve-DataStore spends are staged into that same
  context.
- The function MUST NOT drain the context. The caller adds its parent-coin spend and drains ONCE.
- `mint_datastore_with_kind` is defined as this function plus a standard-p2 parent spend carrying the
  change, so both paths emit identical bytes.

**The launcher is the CALLER's, because the legal shape depends on the parent (#2418).** The launch
composition MUST be one of exactly two shapes, and the function verifies before returning that the
built `parent_conditions` actually reach `launcher.coin()`, failing closed with `MerkleError::Chain`
otherwise:

1. **Direct — an ORDINARY (non-singleton) parent.** `Launcher::new(parent_coin.coin_id(), 1)`. The
   `parent_conditions` contain the launcher `CREATE_COIN` itself, and the two-memo owner-discovery
   hint (§9) is written onto it. This is what `mint_datastore_with_kind` uses.
2. **Via an intermediate — a SINGLETON parent** (a DID, another DataStore, a vault singleton).
   `IntermediateLauncher::new(parent_coin.coin_id(), 0, 1).create(ctx)?`. The `parent_conditions`
   contain a `CREATE_COIN` for a ZERO-amount intermediate coin, whose own (already staged) spend
   creates the 1-mojo launcher. The intermediate coin is matched by puzzle hash and amount; the
   launcher coin is verified by full coin id. The parent coin's id is not an input, so a caller
   that names the wrong parent when constructing `IntermediateLauncher` is not detected here.

A singleton's inner puzzle MAY emit exactly ONE odd-amount `CREATE_COIN` — its own successor — so
shape 1 from a singleton parent builds cleanly and is REJECTED on chain (a CLVM raise). Shape 2 is
therefore MANDATORY for a singleton parent, and shape 1 MUST NOT be used with one.

**Launcher legality.** Before building anything, the supplied `launcher` MUST be checked and the
launch MUST fail closed with `MerkleError::Chain` unless BOTH hold:

- `launcher.coin().puzzle_hash` is the singleton launcher puzzle hash. Any other puzzle hash never
  mints a singleton, and the §9 memo override — which matches that same launcher coin — would
  silently write no memos.
- `launcher.singleton_amount()` is ODD. This is the SINGLETON's amount, NOT the launcher coin's: the
  launcher coin's amount is not an invariant (a 0-amount launcher minting a 1-mojo singleton is
  legal), while an EVEN singleton amount produces a store that is ACCEPTED on chain and permanently
  frozen — every later `update_root` and `melt` raises, so it can never be spent again and its mojos
  are burned.

The odd-amount property MUST additionally be re-checked on the amount the launch ACTUALLY minted
(`datastore.coin.amount`) after construction, so the guard holds however the `Launcher` was
configured. Build time is the only point at which a frozen store is still reversible.

**The §9 memo override MUST target the launcher by full COIN ID**, not by the launcher puzzle hash: a
caller-supplied `Launcher` may carry base conditions containing another store's launcher
`CREATE_COIN`, and stamping that one too would index a store this launch does not own under this
owner hint and `StoreKind`.

**A launcher created via an intermediate carries NO owner-discovery memos.** The two-memo hint lives
on the launcher `CREATE_COIN`, which under shape 2 is emitted by the intermediate coin's own fixed
puzzle; dig-merkle cannot write memos onto it. Such a store is NOT discoverable by a launcher-memo
scan (§9) and — this includes its `StoreKind` discriminator — is discovered by the §3.7 lineage walk
instead. The `kind` argument is therefore ACCEPTED BUT NOT HONOURED under shape 2; the launch reports
this as `DatastoreLaunch.launcher_memos_written == false`, which a caller MUST check when
memo-scannability matters.

**The two shapes trade memo-scannability against lineage-resolvability, and a DID-rooted launch MUST
choose one.** Neither shape delivers both, and the choice is the caller's:

- **Shape 2 (intermediate)** is LINEAGE-RESOLVABLE — `resolve_owner_did` (§3.7) traverses the
  intermediate hop and names the owning DID — but NOT memo-scannable: it writes no launcher memos, so
  neither the owner hint nor the `StoreKind` discriminator reaches the chain.
- **`DID coin -> ordinary EVEN-amount coin -> launcher -> store`, with the ordinary coin launching by
  shape 1**, is MEMO-SCANNABLE — both memos are written — but NOT lineage-resolvable: the launcher's
  creator is an ordinary coin, which is neither a DID nor the recognised intermediate launcher, so
  §3.7 returns `Ok(None)` and the store reports as not-DID-owned. This chain is legal on chain
  because the one-odd-`CREATE_COIN` restriction binds the *singleton's* inner puzzle, not an
  ordinary coin (**#2463**).

§3.7 MUST NOT be extended over that ordinary hop by inspection alone: an ordinary coin's outputs are
knowable only by EXECUTING its puzzle — the chain-supplied CLVM the walk exists to never run — and
accepting the parent claim unexamined would let any store whose launcher's parent happened to be
DID-created falsely claim DID ownership.

**Neither shape puts a DID reference on chain in a memo.** The owner-discovery hint encodes
`owner_puzzle_hash` (§9), not a DID, so a memo scan identifies the OWNER PUZZLE, never the DID; DID
attribution comes only from the §3.7 lineage walk.

**Bundle balance.** Under shape 2 a zero-amount coin creates a 1-mojo launcher, so the surrounding
spend bundle MUST supply that mojo from another spend. Chia balances a bundle in aggregate, not per
coin; a bundle without the surplus is rejected as a minting coin.

**Signing.** This function returns no coin spends of its own — the caller composes and signs the
parent spend it authorizes. The staged launcher and eve-DataStore spends require no signature; a
standard-p2 parent spend requires exactly one `AGG_SIG_ME` over the owner's synthetic key (never
`AGG_SIG_UNSAFE`), and a DID-authorized or otherwise custom parent owns its own requirement.

**Root encoding.** The anchored `root_hash` is the first atom of the NFT-state-layer metadata CLVM
`(root_hash . (("l" . label)? ("d" . description)? ("sp" . size_proof)? ("p" . program_hash)?
("sz" . size_exponent)?))`, produced by `DigDataStoreMetadata::to_clvm` (which mirrors the SDK's
`l`/`d`/`sp` keys but NEVER emits `"b"`, then appends `("p" . program_hash)` and finally
`("sz" . size_exponent)` LAST, each only when `Some` — never hand-rolled).

### 3.2 update

`update_root(store, owner, new_metadata)` recreates the coin with a new `root_hash` (and optional
metadata), preserving `launcher_id`, delegation set, and owner. Authorized by the owner OR a
`Writer`/`Admin` delegated puzzle. **Signing:** one `AGG_SIG_ME` over the owner key.
`Owner::Custom` MUST be REJECTED with `MerkleError::UnsupportedOwner`: the metadata-update and
recreation conditions are built inside the call, so a pre-built inner spend cannot contain them and
the returned bundle would ignore `new_metadata` or melt the store by omitting the recreation.
**`program_hash` on update:** metadata is replaced wholesale, so an update that means to KEEP a
store's `program_hash` MUST re-send it in `new_metadata`; omitting it DROPS the anchor (sets it back
to `None`).

### 3.3 delegation

`set_delegated_puzzles(store, owner, new_delegated_puzzles)` grants/revokes `Admin`/`Writer`/`Oracle`
authority. **Admin-only:** only the owner or an `Admin` delegated puzzle may change the set; a
`Writer` attempt MUST fail with `MerkleError::Permission`. **Signing:** the authorizing inner
puzzle's `AGG_SIG_ME`.

### 3.4 oracle

`oracle_spend(store)` spends the `Oracle` delegated puzzle so any party may read the coin on-chain,
paying the fixed oracle fee to the oracle puzzle hash. **Signing:** none from dig-merkle's owner
(the oracle puzzle is keyless); the caller supplies the fee.

### 3.5 melt

`melt(store, owner)` terminally spends the coin, producing no child (`child == None`). **Signing:**
the owner's `AGG_SIG_ME`. `Owner::Custom` MUST be REJECTED with `MerkleError::UnsupportedOwner`: the
`MELT_SINGLETON` condition is built inside the call, so a pre-built inner spend cannot contain it and
the returned bundle would melt nothing while reporting success.

**Authority MUST be gated before the spend is built.** `melt` MUST reject with
`MerkleError::NotTheOwner`, before it constructs a `SpendContext` or any condition, unless
`StandardArgs::curry_tree_hash(pk)` equals `store.info.owner_puzzle_hash` — the same commitment the
store's own puzzle enforces on chain for an owner spend. The gate MUST key on
`info.owner_puzzle_hash` and NOT on `coin.puzzle_hash`: a store carrying delegated puzzles wears a
delegation layer curried OVER the owner's p2 hash, so a gate on the coin's hash would refuse the
legitimate owner of a delegated store. Both store shapes — with and without delegated puzzles — MUST
admit their owner and refuse everyone else.

This is not a theft barrier; the chain is. A melt built with the wrong key can never confirm, because
the caller cannot produce the required `AGG_SIG_ME`. The gate exists because a melt is
**irreversible**: the builder MUST NOT hand back a fully-formed destructive spend against a store the
caller does not own, whose only symptom is an opaque mempool rejection distant from its cause.

**The melted amount is unrecoverable by construction.** The singleton top layer admits AT MOST ONE
odd-amount `CREATE_COIN`, and the melt magic condition `(51 () -113)` occupies it. The coin's amount
therefore cannot be paid out in this spend and becomes an implicit fee to the farmer — one mojo for a
conventional store. No recovery path exists at any layer, and none MUST be added.

**`Owner::Custom` is unusable across the whole public API.** A `Spend` holds CLVM node pointers valid
only in the allocator that built them, and no public operation exposes its `SpendContext` for a caller
to build one in. A custom/DID-authorized parent composes a launch through §3.1a instead, building its
own spend in its own context.

### 3.6 read

`read(store)` / `parse_coin_spend(...)` parse the current on-chain `DigDataStoreMetadata` +
delegation set from a coin/puzzle without spending. No signing.

`did_ref_from_spend(spend) -> MerkleResult<Option<DidRef>>` is the pure, network-free core of
owner-DID discovery (§3.7): it recognises whether a coin spend is a DID spend (via the SDK's
`Did::parse`, INV-4) and, if so, returns its `DidRef { launcher_id }`. No signing, no chain access.

Fail-closed, and the two failures MUST stay DISTINCT: a genuine non-DID puzzle yields `Ok(None)`,
while a spend the coin did not commit to — a `puzzle_reveal` that does not hash to `coin.puzzle_hash`
— yields `Err(MerkleError::Chain)`. "Not a DID" is an answer; "the source lied about the puzzle" is
not.

### 3.7 resolve_owner_did

```
resolve_owner_did<C: ChainSource>(store_id, chain) -> MerkleResult<Option<DidRef>>
```

Recovers the DID that OWNS a store (the complement of `dig-did` #1219: dig-did MINTS a DID-owned
store, this READS the ownership back). A store rooted in a DID has its launcher coin created by
spending a DID-authorized coin; `resolve_owner_did` walks that lineage up — one creator hop, or two
through an intermediate-launcher coin — and recognises the creator as a DID:

1. `chain.coin_spend(store_id)?` → the launcher coin's spend (`store_id == launcher_id`). Missing →
   `Ok(None)`.
2. `parent_id = launcher_spend.coin.parent_coin_info` — the coin that created the launcher.
3. `chain.coin_spend(parent_id)?` → the creator's spend. Missing → `Ok(None)`.
4. `did_ref_from_spend(&creator_spend)``Some(DidRef{launcher_id})` if the creator was a DID.
5. Otherwise, IF the creator is the intermediate-launcher coin of a singleton-parent launch (§3.1a),
   ONE further hop: `chain.coin_spend(creator_spend.coin.parent_coin_info)`   `did_ref_from_spend(..)`. Missing → `Ok(None)`.

Every spend the walk consumes MUST be bound to the coin it claims to be, in BOTH directions: the
spend's `coin.coin_id()` MUST equal the id that was asked for, AND its `puzzle_reveal` MUST hash to
`coin.puzzle_hash`. A coin id is derived from the coin's own fields, so the first binding alone is
satisfied by any reveal whatsoever; without the second, a source that returns a genuine coin paired
with a forged reveal can have a DID attributed to a store that has none. A violation of EITHER binding
MUST yield `Err(MerkleError::Chain)`, never `Ok(None)`.

**The walk is BOUNDED at two creator hops, and the second MUST be earned.** A singleton parent
interposes an intermediate coin (§3.1a), so the DID sits two hops above the launcher; without step 5
a DID-rooted store resolves as NOT DID-owned. Step 5 MUST NOT be a general parent walk: an unbounded
climb over coin records an untrusted `ChainSource` controls is a DoS, and it would attribute an
ordinary store to a DID that merely created its funding coin. The step is taken ONLY when the creator
IS that intermediate, recognised by its PUZZLE:

- the creator coin's amount is 0, and its `puzzle_reveal` hashes to its own `coin.puzzle_hash`;
- that reveal uncurries to the `nft_intermediate_launcher` mod hash with its curried
  `launcher_puzzle_hash` bound to the singleton launcher puzzle hash; and
- the launcher that puzzle NECESSARILY creates — `Coin::new(creator.coin_id(), launcher_puzzle_hash,
  1)`, DERIVED analytically because the puzzle is fixed — IS this store's launcher, by full coin id.

The curried `mint_number`/`mint_total` are deliberately unconstrained: they vary the curried puzzle
hash but not the behaviour.

**Recognition MUST be by puzzle hash with an analytic launcher derivation, and MUST NOT be by shape**
(e.g. "a 0-amount coin whose spend creates exactly one coin"), for two reasons. First, the walk MUST
run NO chain-supplied CLVM: the spend comes from an untrusted `ChainSource`, so evaluating it executes
an attacker's program — a few bytes of non-terminating puzzle burn CPU while the walk still returns
`Ok(None)`, surfacing to the caller as latency and never as an error. Uncurrying parses; it does not
execute. Second, shape is a looser bind: it admits ANY puzzle that happens to emit that one condition,
not just the intermediate launcher the walk means to traverse.

Anything else, and any parse failure, stops the walk at `Ok(None)`. A DID three or more hops above the
launcher is NOT reported. This is the honest-answer path; it is distinct from a source that ANSWERS
with something the coin did not commit to, which is `Err(MerkleError::Chain)` (above).

It is **fail-closed** and **READ-ONLY** — it never signs, spends, or broadcasts. Fail-closed splits
two ways, and the split MUST be preserved: a chain that answers honestly with "no DID" — a missing
spend, a non-DID creator, or a creator the walk may not climb past — is `Ok(None)`, never an error for
"not DID-owned"; a source that cannot be consulted, or that answers with a spend the coin did not
commit to (a wrong `coin_id`, or a `puzzle_reveal` that does not hash to `coin.puzzle_hash`), is
`Err(MerkleError::Chain)`. All chain access is delegated
to the caller-supplied `ChainSource`, the CANONICAL `dig_chainsource_interface::ChainSource` read
interface — a reference-DOWN pure leaf crate below dig-merkle, NOT a local trait — with the single
synchronous method `coin_spend(coin_id: Bytes32) -> MerkleResult<Option<CoinSpend>>` (INV-1: the
caller implements it over its own client; dig-merkle opens no socket).

Both layers ship: the pure DID-detection helper `did_ref_from_spend` and the `resolve_owner_did`
lineage-walk wrapper over `dig_chainsource_interface::ChainSource` (`dig-chainsource-interface` on
crates.io, a reference-DOWN pure leaf — no `git` dependency).

## 4. Signing boundary

`required_signatures(coin_spends, constants) -> MerkleResult<Vec<RequiredSignature>>` is the sole
bridge to a signer. It wraps `chia_sdk_signer::RequiredSignature::from_coin_spends` over a private
`Allocator`, collecting every `AGG_SIG_*` condition each coin spend's puzzle emits and returning the
precise (public key, message) pairs the caller must sign. It is pure and key-free (INV-2); an empty
coin-spend slice yields an empty requirement set (never an error). A puzzle-evaluation failure or an
infinity public key yields `MerkleError::Signer`.

The consumer pattern is fixed:

```text
build MerkleCoinSpend -> required_signatures(&spend.coin_spends, &constants)
  -> caller signs each message -> assemble SpendBundle -> broadcast
```

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

To spend an existing DataLayer coin, a caller reconstructs a spendable `DataStore` from its parent
coin spend (`DataStore::from_spend`) and the `LineageProof` a singleton child requires
(`child_lineage_proof`). Hydration is **fail-closed**:

- A coin whose puzzle does not parse as a DataLayer singleton yields `MerkleError::NotDataStore`.
- A missing lineage proof yields `MerkleError::MissingLineage` — dig-merkle never fabricates one.
- A missing required hint memo yields `MerkleError::MissingHint`.

dig-merkle never guesses missing chain state; the caller supplies the real parent spend.

**Reveal binding is MANDATORY (NC-9).** BEFORE any value is extracted from a parent spend, `hydrate`
MUST verify `tree_hash(puzzle_reveal) == spend.coin.puzzle_hash` and reject a mismatch with
`Err(MerkleError::Chain)`. A `coin_id` binding cannot substitute for this: `coin_id` is derived from
the coin's own fields, so a hostile source can pair a victim's genuine coin with a different store's
`puzzle_reveal` and `solution` and have hydration report the ATTACKER's `launcher_id` and
`root_hash`. Because the check precedes the parse, a forged spend also never reaches the CLVM
execution on the non-launcher branch. The failure is a REFUSAL, never a skip or an empty result — a
caller must not be able to read a substituted answer as "no data on chain".

**Child lineage proof — the DataLayer updater path (INV-4).** `child_lineage_proof` derives the
`parent_inner_puzzle_hash` a child singleton must attest to by reconstructing the store's NFT-state-
layer tree hash the SAME way a real on-chain DataLayer coin is built: currying
`DL_METADATA_UPDATER_PUZZLE_HASH` (the DataLayer metadata updater), NOT the NFT-default updater that
the SDK's generic `DataStoreInfo::inner_puzzle_hash` currys. The inner puzzle under the state layer is
the delegation layer when the store carries admin/writer/oracle delegated puzzles, else the bare owner
puzzle hash. Deriving it any other way yields a `parent_inner_puzzle_hash` that a child spend fails
consensus against (`AssertMyParentIdFailed`).

**Launcher-lineage discovery (§3.7).** Owner-DID discovery is the same fail-closed principle applied
to a READ: `resolve_owner_did` walks the launcher lineage via the injected `ChainSource`
(`coin_spend(store_id)` → its `parent_coin_info` → `coin_spend(parent)`, plus at most one further hop
through an intermediate-launcher coin) and recognises a DID creator with `did_ref_from_spend`. Any
missing spend, or a non-DID creator the walk may not climb past, yields `Ok(None)` — never a
fabricated result and never an error for "not DID-owned". A source that ANSWERS with a spend the coin
did not commit to yields `Err(MerkleError::Chain)` instead (below).

**Store-id binding is MANDATORY per hop (NC-9).** The injected `ChainSource` is trusted only to
return CONFIRMED spends, never to return the RIGHT coin — a hostile or buggy source (the public
`rpc.dig.net` gateway is attacker-influenceable, §5.3) can answer a read with a DIFFERENT store's
valid, DID-rooted launcher. `resolve_owner_did` therefore binds every hop to the requested identity
and fails CLOSED on any mismatch — it MUST NOT return a `Some(DidRef)` it cannot bind to `store_id`:
- The launcher spend returned for `store_id` MUST satisfy `launcher_spend.coin.coin_id() == store_id`
  (a DIG store id IS its launcher coin id). A mismatch is rejected with `Err(MerkleError::Chain)`.
- EVERY creator spend returned for a `coin_id` MUST satisfy `spend.coin.coin_id() == coin_id`,
  including the second hop's. A mismatch is rejected with `Err(MerkleError::Chain)`.
- EVERY spend the walk parses MUST satisfy `tree_hash(puzzle_reveal) == spend.coin.puzzle_hash`. A
  coin id is derived from the coin's own fields, so the two bindings above are satisfied by ANY reveal;
  without this one a source can pair a genuine coin with a real DID's reveal and have that DID
  attributed to a store with no DID link. A mismatch is rejected with `Err(MerkleError::Chain)`.
A substituted answer is deliberately surfaced as `Err(Chain)`, not `Ok(None)`, so a hostile-source
substitution is distinguishable from a genuinely non-DID-owned store.

## 6. Error taxonomy

`MerkleError` (all fallible operations return `MerkleResult<T>`):

| Variant | Raised when |
|---|---|
| `Driver(DriverError)` | a chia-wallet-sdk driver op fails (currying, spend, CLVM eval); wrapped verbatim |
| `Signer(String)` | the signing calculator fails (bad puzzle/solution, infinity key) |
| `Parse(String)` | a coin/puzzle/solution is not the expected shape |
| `NotDataStore` | a puzzle parsed but is not a DataLayer singleton |
| `MissingLineage` | hydration lacks the required lineage proof (fail-closed) |
| `MissingHint` | a parsed coin lacks the required hint memo (fail-closed) |
| `Permission(String)` | a delegated-puzzle op lacks its required authority (e.g. writer→admin) |
| `Chain(String)` | a chain-level precondition is violated (e.g. launcher mismatch, an unbound spend) |
| `UnsupportedOwner(&'static str)` | the operation builds the conditions its spend must emit, so `Owner::Custom` cannot authorize it (§3.1, §3.2, §3.5) |
| `NotTheOwner` | an irreversible operation was asked for with a key that does not curry to the store's `owner_puzzle_hash` (§3.5); raised before any spend is built |
| `EmptyCoins` | an operation was given an empty coin set |
| `InvalidSize(String)` | a size-bucket exponent or byte length falls outside the `0..=10` ladder (§2) |

## 7. Security properties

- **Custody:** dig-merkle holds no key and signs nothing (INV-2). A caller cannot accidentally leak
  a key through this crate because it accepts none. The signing boundary (§4) returns only the
  public data a signer needs.
- **Determinism:** every function is a pure transform (INV-1); given identical inputs it produces
  byte-identical coin spends, so a spend can be independently reproduced and audited.
- **Fail-closed:** hydration and permission checks reject on missing/invalid state (§5, §6) rather
  than producing an unspendable or over-authorized bundle.
- **Irreversible operations refuse in the builder:** `melt` (§3.5) verifies the caller's key controls
  the store BEFORE any spend exists, so an unauthorized destructive spend is never constructed. The
  refusal's warrant is the supplied `DataStore`; a store obtained through `hydrate` carries the real
  owner, because hydration binds a parsed puzzle reveal to the coin's puzzle hash (§5).

## 8. Back-compat (CLAUDE.md §5.1 — additive only)

A `.dig` root coin is a permanent, on-chain-anchored artifact; content published under a store id
stays readable forever. dig-merkle's read/hydrate path MUST therefore be additive and
backward-compatible:

- **Newer readers accept ALL older coins.** The parser dispatches on the on-chain shape and keeps
  handling every prior DataLayer layout — it MUST NOT hard-reject an older coin.
- **The legacy launcher path is retained.** The SDK's `from_memos` / `OldDlLauncherKvList` legacy
  key-value-list launcher parsing MUST remain supported; dig-merkle never drops it.
- **Metadata additions are additive; the `"b"``"sz"` size swap is a clean pre-release replacement.**
  The SDK keys `root_hash`/`l`/`d`/`sp` never change meaning or encoding. The `program_hash` key `p`
  and the `size_bucket` key `sz` are NEW optional keys appended after the SDK keys (`p` after `sp`,
  then `sz` LAST), omitted when absent. dig-merkle NEVER emits the SDK's exact-byte `"b"` key — size
  is the `"sz"` bucket instead; this is a clean replacement, safe because no on-chain DIG store carries
  `"b"` (pre-release). SDK-typed readers ignore `p`/`sz` (the SDK's `_ => ()` tolerance), and a
  dig reader decoding a `p`/`sz`-free store (including an SDK store carrying `"b"`, which is ignored)
  yields `program_hash == None` / `size_bucket == None` — reading an SDK store still succeeds. The
  `sz` value is the size exponent in canonical MINIMAL CLVM form (NC-8); a non-minimal or out-of-range
  encoding is rejected fail-closed.
- **Prove it.** The test suite keeps golden coin-spend fixtures of each released layout; every
  format change MUST include a test decoding the golden fixtures byte-identically. The mint golden
  test (`launcher_carries_the_two_memo_owner_discovery_hint`) pins the launcher `CREATE_COIN` memos to
  `[digstore_owner_hint(owner_ph), DATASTORE_LAUNCHER_HINT]`, and `metadata_clvm_encodes_root_as_first_atom`
  pins the root as the first metadata atom — the proof a minted coin matches stores already on chain.
  The program_hash key is proved byte-identical by `mint_none_program_hash_is_byte_identical` at the
  coin level. The size_bucket replacement is proved by `metadata_none_size_bucket_is_byte_identical_to_sdk`
  (empty → identical to an SDK `bytes == None` store), `sdk_reader_parses_size_bucket_store` (our
  `sz` store is a valid SDK `DataStoreMetadata`), `sdk_store_with_b_still_decodes` (an SDK `"b"` store
  decodes, `"b"` ignored → `size_bucket == None`), the NC-8 pin `sz_atom_is_minimally_encoded`, the
  round-trip `sz_roundtrips`, the key-order `sz_is_last_key`, and the fail-closed
  `sz_decode_rejects_non_minimal_and_oversized`.

## 9. Conformance

- **Byte-agreement with chip35.** dig-merkle's DataLayer coin MUST be byte-identical to the existing
  DataLayer coin in `chip35_dl_coin` (both build over the same `chia-wallet-sdk` primitives, INV-4).
  A coin dig-merkle mints/updates MUST be spendable by, and produce the same on-chain state as, the
  chip35 implementation.
- **Signature construction** MUST match `chia_sdk_signer::RequiredSignature::from_coin_spends`
  exactly (dig-merkle only wraps it).
- **Owner-hint domain.** The owner/delegation hint-memo domain is the fixed constant
  `DIGSTORE_OWNER_HINT_DOMAIN = b"dig:datastore:owner:v1"` (defined in dig-merkle, not imported), and
  MUST match across every DIG consumer that resolves a DataLayer owner hint.
  `digstore_owner_hint(owner_ph) = sha256(DIGSTORE_OWNER_HINT_DOMAIN ‖ owner_ph)` — byte-identical to
  chip35_dl_coin + digstore-chain.
- **Launcher-hint kind discriminator (`memo[1]`).** The second launcher memo names the store's
  `StoreKind`:
  - `StoreKind::File``DATASTORE_LAUNCHER_HINT = sha256("datastore") =
    aa7e5b234e1d55967bf0a316395a2eab6cb3370332c0f251f0e44a5afb84fc68` — the pre-existing default,
    byte-identical across all DIG producers (chip35_dl_coin, digstore-chain).
  - `StoreKind::DidProfile``DID_PROFILE_LAUNCHER_HINT = sha256("dig:datastore:profile:v1") =
    9c1d6b6d5d530dd613f4d7d2ced6b704ae8423377e4d567518493159c1d21d01` (#1263).

  `launcher_hint_for(kind)` maps a kind to its discriminator (write side); `from_launcher_hint(memo)`
  classifies a store by its `memo[1]` (read side), returning `None` for an unrecognised value. The
  kind set is ADDITIVE (SPEC §8/§5.1): the `File` bytes never change, and a legacy store — every
  store minted before #1263 — carries `DATASTORE_LAUNCHER_HINT` and so classifies as
  `StoreKind::File`.
- **Launcher memos.** A minted store's launcher `CREATE_COIN` carries exactly
  `[digstore_owner_hint(owner_ph), launcher_hint_for(kind)]`, in that order — `memo[0]` is the
  kind-agnostic owner hint, `memo[1]` is the kind discriminator. `mint_datastore` mints a
  `StoreKind::File` store (byte-identical to existing on-chain stores); `mint_datastore_with_kind`
  selects the kind.
- **Root metadata shape.** `root_hash` is the first atom of the metadata CLVM
  `(root_hash . optional-kv-pairs)`; optional keys are `l`/`d`/`sp`/`p`/`sz` (dig-merkle never emits
  `"b"`). `p` (program_hash) is appended after `sp`, then `sz` (size_bucket) is ALWAYS appended LAST —
  each only when present.
- **Size-bucket ladder.** `sz` carries the size exponent `k ∈ 0..=10` mapping to `2^k MB` (1 MB =
  1 MiB = 2^20 bytes; 1 MB..1 GB), encoded as a minimal CLVM integer (empty atom for k=0, one byte
  `0x01`..`0x0a` otherwise). `SizeBucket::for_byte_len` is the canonical byte→bucket mapping. This
  ladder is the canonical shared contract dig-store's SIZE PROOF consumes; it MUST NOT drift.
- **Empty byte-identity + SDK interop.** A `DigDataStoreMetadata` with `program_hash == None &&
  size_bucket == None` MUST serialize byte-identically to the SDK's `DataStoreMetadata` with
  `bytes == None` (both emit only the present `l`/`d`/`sp` keys). An SDK reader decoding a DIG store
  drops the unknown `p`/`sz`; a dig reader decoding an SDK store parses `root_hash`/`l`/`d`/`sp`, drops
  the SDK's `"b"`, and yields `size_bucket == None` / `program_hash == None`.
- **Dependency layer.** dig-merkle depends ONLY on `chia-wallet-sdk` +
  `chia-protocol`/`chia-puzzle-types`/`clvm-traits`/`chia-sha2` + external utility crates
  (thiserror, hex-literal), plus the single canonical leaf `dig-chainsource-interface` (the
  `ChainSource` read interface consumed by §3.7; a reference-DOWN pure leaf BELOW dig-merkle,
  crates.io-published). It MUST NOT depend on any other `dig-*` crate, and MUST NEVER depend on
  `dig-identity` (the edge is one-way, dig-identity → dig-merkle — the reverse is a cycle).