dig-merkle
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.
dig-merkle constructs the exact CoinSpends for every DataLayer-coin lifecycle operation and
reports the exact signatures a caller must produce. It never holds a secret key, never signs, and
never touches the network. The consumer signs the reported messages, assembles the SpendBundle,
and broadcasts.
[]
= "0.2"
What it is
A DataLayer coin is a CHIP-0035 singleton whose launcher_id IS the DIG store_id. Its
metadata (DigDataStoreMetadata) carries the anchored .dig capsule merkle root_hash plus
optional label/description/size-proof, the additive program_hash (the CLVM tree-hash of an
associated program/puzzle — stored and echoed, never computed here), and the store size as a
size_bucket (a SizeBucket — a power-of-2 bucket, k ∈ 0..=10 ↔ 2^k MB, 1 MB..1 GB, CLVM key
sz) that REPLACES the SDK's exact-byte "b" field (dig-merkle never emits "b"). With
size_bucket and program_hash both None a mint is byte-identical to a plain DataLayer store. Its
delegated-puzzle list grants admin/writer/oracle
authority. Spending the coin recreates it with a new root, a new delegation set, or a new owner — or
melts it. Publishing a new capsule root IS a DataLayer update. dig-merkle builds each such spend,
unsigned.
dig-merkle is the DIG-Network expert wrapper over
chia-wallet-sdk's DataLayer primitives (the
byte-source-of-truth): it adds workflow ergonomics and a hard custody boundary, never a
re-implemented puzzle.
Invariants
- INV-1 — No network. No network or chain I/O; every function is a pure transform. The caller fetches coins and broadcasts bundles.
- INV-2 — No keys. Never accepts, holds, derives, or logs a secret key. It computes what must be signed; the caller's signer produces the signatures.
- INV-3 — Unsigned output. Every operation returns an unsigned
MerkleCoinSpend(coin spends + the recreated childDataStore). - INV-4 — SDK byte-source-of-truth. Every byte comes from
chia-wallet-sdk(0.34 / chia-protocol 0.36.1,chip-0035feature); the SDK's DataStore types are re-exported verbatim.
Consumer pattern
build an unsigned MerkleCoinSpend
-> required_signatures(&spend.coin_spends, &constants)
-> caller signs each reported message
-> assemble SpendBundle
-> broadcast
use ;
use MAINNET_CONSTANTS;
// (build a MerkleCoinSpend via a mint/update/... operation — see the operation surface below)
let constants = from;
let required = required_signatures?;
// sign each `required[i]` under its public key, aggregate, assemble the SpendBundle, broadcast.
# Ok::
Operation surface
Each operation returns an unsigned MerkleCoinSpend and states its signing requirement.
U2 (v0.2.0) ships the mint builder + the owner-discovery hint on top of the U1 foundation (type
surface, error taxonomy, inner-spend helpers, signing boundary). The remaining operations are the
designed surface; each lands in its own unit.
| Function | Semantics | Signing |
|---|---|---|
mint::mint_datastore(parent_coin, owner, root_hash, label, description, size_proof, program_hash, size_bucket, owner_ph, delegated, fee) |
shipped — launch a new DataLayer store anchoring a root, byte-identical to on-chain stores | owner's AGG_SIG_ME |
size::SizeBucket (from_exponent/for_byte_len/exponent/megabytes/byte_len) |
shipped — the canonical .dig size-bucket ladder (k ∈ 0..=10 ↔ 2^k MB, 1 MB..1 GB); CLVM key sz, replaces the exact-byte "b" |
— |
digstore_owner_hint(owner_ph) / DATASTORE_LAUNCHER_HINT / DIGSTORE_OWNER_HINT_DOMAIN |
shipped — the owner-discovery hint (SPEC §9) | — |
read::did_ref_from_spend(&coin_spend) |
shipped — recognise a DID coin spend, returning its DidRef { launcher_id } (a non-DID puzzle is None; a reveal the coin did not commit to is Err(Chain)) |
none |
read::resolve_owner_did(store_id, &chain) |
recover the DID that owns a store via a ChainSource lineage walk (SPEC §3.7) — pending dig-chainsource-interface crates.io publish |
none |
update::update_root(store, owner, new_metadata) |
recreate the coin with a new merkle root | owner or writer/admin AGG_SIG_ME |
delegation::set_delegated_puzzles(store, owner, set) |
grant/revoke admin/writer/oracle authority (admin-only) | owner or admin AGG_SIG_ME |
oracle::oracle_spend(store) |
read the coin on-chain for the fixed oracle fee | none (keyless oracle puzzle) |
melt::melt(store, owner) |
terminally spend the coin (no child) | owner AGG_SIG_ME |
read::read(store) |
parse current on-chain state (no spend) | none |
hydrate::* |
reconstruct a spendable DataStore from a parent spend (fail-closed) |
— |
lineage::* |
derive the LineageProof a child spend needs |
— |
required_signatures(...) |
shipped — the signing boundary (§4) | — |
The two-memo launcher hint (byte-identity)
mint_datastore overrides the launcher CREATE_COIN memos to exactly
[digstore_owner_hint(owner_ph), DATASTORE_LAUNCHER_HINT] — the first the indexed owner-discovery
hint (sha256("dig:datastore:owner:v1" ‖ owner_ph)), the second the global launcher hint
(sha256("datastore")). This replicates chip35_dl_coin and digstore-chain exactly, so a store
minted here is byte-identical to (and interchangeable with) the stores those already publish
on-chain. It is the default behaviour, verified by a golden test.
DID composition
A DIG store can be rooted in a DID without a dig-did dependency. The composable path is:
- Build the launcher for your parent's shape. A DID is a singleton, whose inner puzzle may emit
exactly ONE odd-amount
CREATE_COIN— its own successor — so it cannot create the 1-mojo launcher directly; the bundle would build cleanly and be rejected on chain. Interpose an intermediate coin:IntermediateLauncher::new(did.coin.coin_id(), 0, 1).create(&mut ctx)?. (An ordinary, non-singleton parent usesLauncher::new(parent_coin.coin_id(), 1).) Both are re-exported here. - Call
mint_datastore_launch_with_kind(&mut ctx, kind, launcher, ..)— this stages the launcher and eve-DataStore spends intoctxand returns aDatastoreLaunchwhoseparent_conditionscarry theCREATE_COINthat starts the launch (the intermediate coin, or the launcher itself for a direct launch) plus the announcement assertions that the DID-authorized parent spend must emit. - Build your DID-authorized parent-coin spend on the same
ctx, folding inDatastoreLaunch::parent_conditions. - Drain
ctxonce to get the complete spend bundle.
The launcher is created at 1 mojo by a zero-amount intermediate, so the bundle must carry that mojo from another spend; Chia balances a bundle in aggregate, not per coin.
The two-memo owner-discovery hint lives on the launcher CREATE_COIN, which an intermediate launch
emits from its own fixed puzzle — so a store launched this way carries no launcher memos and is
not found by a launcher-memo scan. The kind discriminator rides on those same memos, so it too is
accepted but not written on this path — the launch reports launcher_memos_written == false so a
caller can see it. Such a store is discovered by resolve_owner_did instead (below).
The two shapes trade memo-scannability against lineage-resolvability, and a DID-rooted launch has
to pick one. The intermediate shape is resolvable by resolve_owner_did but writes no memos. The
alternative — DID coin -> ordinary EVEN-amount coin -> launcher -> store, where the DID creates an
ordinary even-amount coin and THAT coin launches directly — does write the memos, but is not
resolvable: the launcher's creator is an ordinary coin, which is neither a DID nor the recognised
intermediate launcher, so resolve_owner_did returns None for it today (known gap, #2463). The
odd-coin restriction binds the singleton's inner puzzle, not an ordinary coin, so both compositions
are legal on chain.
Note also that the owner-discovery memo encodes the owner puzzle hash, not a DID — so a memo scan never yields a DID reference on either path; DID attribution comes only from the lineage walk.
mint_datastore_with_kind (the all-in-one wrapper) accepts only Owner::Standard and rejects
Owner::Custom with MerkleError::UnsupportedOwner — use the composable API above for DID-rooted
stores. update_root and melt reject it for the same reason: each builds the conditions its spend
must emit inside the call, and a pre-built inner spend cannot contain them. In practice 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. The dependency edge stays one-way (dig-identity → dig-merkle); dig-merkle depends on no
dig-* crate except the canonical leaf dig-chainsource-interface (a reference-DOWN pure read
interface BELOW dig-merkle, for §3.7 — pending its crates.io publish).
Owner-DID discovery
A store launched through an intermediate launcher (the composable path above) can be traced back
to its owning DID: resolve_owner_did walks the store's launcher lineage up — one creator hop, or two
through that intermediate coin — and recognises a DID creator, delegating ALL chain reads to a
caller-supplied ChainSource (the canonical dig_chainsource_interface::ChainSource), so dig-merkle
stays network-free (INV-1). A store launched directly from an ordinary coin resolves to None, even
when a DID created that coin (#2463):
use ;
use ChainSource; // canonical read interface (pending crates.io publish)
// Implement ChainSource over your own client (RPC / full node / cache):
// resolve_owner_did walks store_id -> launcher.parent -> creator spend, fail-closed to None:
let owner: = resolve_owner_did?;
// The pure detection core ships today (no ChainSource needed):
let did_ref: = did_ref_from_spend?;
# Ok::
resolve_owner_did lands when dig-chainsource-interface publishes to crates.io (dig-merkle allows
no git dependencies); did_ref_from_spend + DidRef are available now.
Module map
types—MerkleCoinSpend,Owner, and the re-exported SDK types (DataStore,DataStoreMetadata,DataStoreInfo,DelegatedPuzzle,Bytes32,Coin,CoinSpend,LineageProof,Proof).metadata—DigDataStoreMetadata, the SDK metadata with"b"replaced bysize_bucket("sz") + the additiveprogram_hash(shipped, SPEC §2).size—SizeBucket, the canonical.digsize-bucket ladder (shipped, SPEC §2).error—MerkleError/MerkleResult(the error taxonomy, SPEC §6).sign—required_signatures(the signing boundary, SPEC §4).mint—mint_datastore(shipped, SPEC §3.1).hint—digstore_owner_hint+ the two hint constants (shipped, SPEC §9).read—did_ref_from_spend+DidRef(shipped, SPEC §3.6/§3.7); theresolve_owner_didChainSourcewalk is pending the interface's crates.io publish.update/delegation/oracle/melt/hydrate/lineage/fee— the remaining DataLayer operation modules (doc-only stubs; each filled in its own unit).
Custody guarantee
dig-merkle holds no key, signs nothing, and does no network I/O. A caller cannot leak a key through this crate because it accepts none. The signing boundary returns only the public (public-key, message) pairs a signer needs.
License
Licensed under either of Apache-2.0 or MIT at your option.
See SPEC.md for the full normative contract.