# dig-store — normative specification
`dig-store` is the DIG Network **DataLayer store manager**: the crate that composes the on-chain
anchor (`dig-merkle`) and the off-chain `.dig` data plane (`dig-capsule`) into ONE store abstraction.
This document is the normative contract an independent reimplementation can be built against. The
voice is normative (IS / MUST / SHOULD); it describes the current contract, not history or roadmap.
Cross-references: the ecosystem interaction map (`SYSTEM.md`), the `dig-store-format` skill (the
`.dig` / DIGS layout + build prereqs), `dig-merkle` `SPEC.md` (the on-chain byte layout), and
`dig-capsule` `SPEC.md` (the `.dig` format). NC-8 / NC-9 are the ecosystem MUST-DO items on minimal
on-chain encoding and on-chain proof (the `normative-contract` skill).
## 1. Scope + invariants
A **store** is the pair of:
- an **on-chain anchor** — a CHIP-0035 DataLayer singleton whose `launcher_id` IS the `store_id` and
whose metadata carries the current `.dig` merkle root plus `label` / `description` / `size_bucket`
/ `program_hash`. Owned by `dig-merkle`.
- an **off-chain data plane** — the `.dig` capsule (`(store_id, root_hash)`). Owned by `dig-capsule`.
`dig-store` composes the two; it MUST NOT re-implement either plane's bytes. The following invariants
hold across the whole crate:
- **INV-1 — No network.** `dig-store` performs NO chain or network I/O itself. On-chain reads take a
caller-supplied [`ChainSource`] (§7); lifecycle operations are pure transforms of their inputs.
- **INV-2 — No keys, unsigned output.** `dig-store` never accepts, holds, derives, or logs a secret
key. Every lifecycle operation returns an UNSIGNED spend; signing + broadcasting are the caller's.
- **INV-3 — Minimal on-chain encoding (NC-8).** The store's on-chain footprint is delegated wholesale
to `dig-merkle`, which owns the minimal byte layout. `dig-store` MUST NOT add its own on-chain
fields; the size is a single-byte power-of-2 bucket (§4).
- **INV-4 — On-chain proof always (NC-9).** Every getter that returns chain-anchored data MUST prove
it against the chain via the `ChainSource`. Trust MUST NOT come from a self-declared field, a cached
value, a curried id, or an unverified peer. A `ChainSource` used for custody-grade reads MUST be a
trusted source (the user's own verified node or a trusted/threshold provider set), never a single
untrusted remote (NC-9 F1).
- **INV-5 — `.dig` back-compat (CLAUDE.md §5.1).** The capsule surface MUST read every older `.dig`
format identically. The public API is extended additively; an existing item's meaning never changes.
## 2. Identifiers
- `store_id`: the DataLayer singleton `launcher_id`, a 32-byte value, permanent for the store's life.
- `root_hash`: the anchored `.dig` merkle root, a 32-byte value; changes each generation.
- A `(store_id, root_hash)` pair IS a **capsule** — one immutable generation.
- `Bytes32` is a 32-byte identifier rendered as 64 lowercase hex characters in URNs and logs.
## 3. Lifecycle — a store is a coin that gets spent
Three operations span a store's life; each is a spend of the singleton and returns an UNSIGNED
`MerkleCoinSpend` (INV-2, re-exported verbatim from `dig-merkle`: the coin spends + the recreated
child `DataStore`). The on-chain encoding is `dig-merkle`'s (INV-3). `StoreOwner` is a re-export of
### 3.1 `create_store(parent_coin, owner, owner_puzzle_hash, params) -> MerkleCoinSpend`
Launches a new store coin. `parent_coin` (a full `Coin`) funds + parents the launcher, so
`launcher_id == store_id` derives from its `coin_id`. `owner` authorizes the parent spend;
`owner_puzzle_hash` is the store owner recorded in the singleton. `params` (`CreateStoreParams`)
carries the first `root_hash`, the `size` bucket (§4, REQUIRED — every store anchors its size), and
optional `label` / `description` / `program_hash`, plus the launch `fee`. The store is minted with the
`StoreKind::File` launcher discriminator, byte-identical to existing on-chain DIG stores. Composes
`dig_merkle::mint_datastore_with_kind`.
To root a store in a DID, the caller passes the DID-authorized coin as `parent_coin` with a
`StoreOwner::Custom` inner spend satisfying the DID puzzle; owner discovery (§7) then resolves the
DID via `dig-merkle`. `create_store` MUST anchor `size` in the on-chain metadata so §4 can be checked.
### 3.2 `modify_store(store, owner, new_root) -> MerkleCoinSpend`
Spends the current tip coin to recreate the singleton anchoring `new_root` — a new generation.
`store` is the already-hydrated tip `DataStore` (from §7's `get_store_singleton_tip`), so this builder
stays a pure transform (INV-1). `store_id`, owner, and delegation set are preserved, and every OTHER
anchored metadata field (label, description, size bucket, program hash) is carried forward unchanged
(`dig-merkle` replaces metadata wholesale, so `modify_store` re-sends the existing fields with only
`root_hash` updated). Composes `dig_merkle::update_root`. Attaching a reserve fee to a modify is a
`dig-merkle` future unit (its `fee` module is a stub); the coin is recreated at its current amount.
### 3.3 `melt_store(store, owner) -> MerkleCoinSpend`
Terminally spends the tip `DataStore` with no successor (`child == None`), closing the store. No
future generation can be anchored after a melt. Composes `dig_merkle::melt`.
## 4. Size + size proof (the net-new contract)
A store anchors its `.dig` SIZE on chain so a client can decide, BEFORE downloading, whether to fetch
the artifact.
- **Encoding.** The size is a power-of-2 **bucket**, not an exact byte count (NC-8 minimal encoding):
an exponent `k ∈ 0..=10` mapping to `2^k MB`, where `1 MB = 1 MiB = 2^20 bytes`. The ladder is
therefore 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 MB (1 GB is the ceiling). This is `SizeBucket`
and MUST be byte-identical to `dig_merkle::SizeBucket` (CLVM metadata key `"sz"`).
- **byte → bucket.** `SizeBucket::for_byte_len(n)` MUST return the SMALLEST `k` whose bucket
(`2^(k+20)` bytes) is ≥ `n`. `n = 0` or `1` → `k = 0`; exactly 1 MiB → `k = 0`; 1 MiB + 1 → `k = 1`;
exactly 1 GiB → `k = 10`; `n > 2^30` → error (no bucket).
- **Size proof.** `SizeProof::verify(anchored, actual_bytes)` returns `Accept` iff
`SizeBucket::for_byte_len(actual_bytes) == anchored`, else `Discard`. A `.dig` whose real size does
not fall in the store's anchored bucket — larger OR smaller — MUST be discarded. A byte length above
the ceiling can never match and is `Discard`ed (not errored). `SizeProof::require` returns
`DigStoreError::SizeProofMismatch` on the discard path for `?`-propagation.
- **dig-node enforcement (sibling, dig-node repo).** A dig-node MUST run the size proof against the
on-chain-anchored bucket (read with NC-9 proof) for every downloaded `.dig` and MUST NOT store or
serve a capsule whose real size does not match. A size-mismatched capsule is rejected, never cached.
## 5. Getters + URNs
The getter surface is comprehensive over both planes. On-chain getters (§7) prove every value against
the chain (INV-4).
- URN (no chain read for the rootless form). The scheme, canonical form, and key derivation are owned
by the canonical `dig-urn-protocol` crate (re-exported through the `dig-capsule` `urn` facade);
`dig-store` delegates to it so the scheme lives in one place:
- `get_store_urn(store_id) -> String` — the ROOTLESS `urn:dig:chia:<store_id>`; the stable handle
across all generations.
- `get_latest_root_urn(chain, store_id) -> String` — `urn:dig:chia:<store_id>:<latest_root>`, the
capsule URN pinning the latest generation (reads the tip first).
- `store_urn` / `capsule_urn` format the canonical scheme; `retrieval_key(urn) -> Result<Bytes32>` =
`SHA-256(canonical(urn))`, byte-identical to `dig-urn-protocol` (fails closed on a non-URN input).
- On-chain (NC-9), all generic over the canonical `dig_chainsource_interface::ChainSource` (§7). The
reads share ONE lineage walk (launcher spend → hydrate each generation → follow the singleton to the
unspent tip; a `MissingLineage` hydration marks a melt), fail-closed at every missing hop:
- `get_store_did_owner(chain, store_id) -> Option<DidRef>` — the owning DID, resolved by walking the
launcher's parent spend (`dig_merkle::resolve_owner_did`); `None` for a non-DID mint.
- `get_store_singleton_tip(chain, store_id) -> DataStore<DigDataStoreMetadata>` — the current
confirmed tip, fully hydrated so it feeds `modify_store` / `melt_store` directly; errors if the
store is absent or melted (no live tip).
- `get_root_history(chain, store_id) -> RootHistory` — every anchored root, oldest → newest (a
melted store still reports the roots it anchored while live).
- `get_latest_root(chain, store_id) -> Bytes32` — the root at the tip.
- `get_store_label` / `get_store_description` / `get_store_size_bucket` / `get_store_program_hash` —
the corresponding on-chain metadata field read off the tip (`Option`, omitted-when-absent).
- Off-chain (`.dig` via `dig-capsule`) — **DEFERRED** (§11): `open_capsule` / `get_capsule_identity`
are not in this version because `dig-capsule 0.4.0` exposes no lightweight `bytes → (store_id,
root_hash)` reader. They land once `dig-capsule` ships a `Capsule::from_module_bytes` reader
(release-first). The download-gating SIZE PROOF (§4) needs no capsule open and is complete now.
## 6. Errors
`DigStoreResult<T> = Result<T, DigStoreError>`. Variants: `InvalidSize`, `SizeProofMismatch
{ anchored_k, actual_k, actual_bytes }`, `InvalidUrn`, `Proof`, `Capsule`, `Spend`. Variants are
added additively; an existing variant's meaning never changes.
## 7. The `ChainSource` boundary
On-chain getters are generic over `C: ChainSource` — the ONE canonical
`dig_chainsource_interface::ChainSource` (re-exported at `dig_store::ChainSource`), a caller-supplied
reads-only source of confirmed chain state. The store surface uses its `coin_spend(coin_id) ->
Result<Option<CoinSpend>, C::Error>` fail-closed lookup (`Ok(None)` = the coin is unspent/unknown;
`Err(_)` = the source could not answer, mapped into `DigStoreError::Proof`). Lineage walks (owner
discovery, root history, tip) are repeated `coin_spend` lookups composed with `dig-merkle`'s
`hydrate`. The source MUST be trusted for custody-grade reads (INV-4 / NC-9 F1).
## 8. Security properties
- Unsigned output only (INV-2): a compromised `dig-store` cannot move funds; it emits spends the
caller must still authorize.
- On-chain proof (INV-4): a value returned by a getter reflects the chain, not an attacker-chosen
field. The proof is only as sound as the `ChainSource`; a payment-routing / custody read MUST use a
trusted source (NC-9 F1).
- Size proof (§4): a size-mismatched `.dig` is discarded, so an attacker cannot make a node cache or
serve content the store did not commit to.
## 9. Conformance
- The size ladder + byte→bucket mapping are the re-exported `dig_merkle::SizeBucket`, so they cannot
drift from the on-chain encoding (test `size::tests::re_exported_ladder_is_canonical`).
- The URN scheme + retrieval key are the re-exported `dig-urn-protocol` definition (delegated, so they
match the browser verifier by construction; test `urn::tests::retrieval_key_matches_dig_urn_protocol`).
- Golden `.dig` fixtures of each released format version decode byte-identically — added with the
deferred off-chain capsule getters (§11; INV-5 / CLAUDE.md §5.1).
## 10. Crate hierarchy + publishing
`dig-store` depends on `dig-merkle` + `dig-capsule` (both level `10-primitives`) and
`dig-chainsource-interface` + `dig-urn-protocol` (level `00-foundation`). Depending on two
`10-primitives` crates, it sits ABOVE them at level **`20-domain`** in the crate hierarchy
(reference-DOWN-only). It is consumed by `dig-wallet-backend`, which sits above it. It publishes to
crates.io (no git deps); consumers depend on the crates.io version.
## 11. Composition + the deferred off-chain getters (issue #1247)
The compose pass wired the live crates.io dependencies (`dig-merkle 0.4`, `dig-chainsource-interface
0.1`, `dig-urn-protocol 0.1`) and filled every lifecycle + on-chain-getter body:
- **`dig-merkle 0.4`** — `mint_datastore_with_kind` (create), `update_root` (modify), `melt` (melt),
`hydrate` + `resolve_owner_did` (the on-chain read walk), and the re-exported `SizeBucket` / `Owner`
/ `Bytes32` / `Coin` / `CoinSpend` / `DataStore` / `DidRef` / `DigDataStoreMetadata` /
`MerkleCoinSpend` types (so the ladder + coin shapes live in ONE place).
- **`dig-chainsource-interface 0.1`** — the canonical `ChainSource` every on-chain getter is generic
over (its associated `Error` is mapped into `DigStoreError::Proof`).
- **`dig-urn-protocol 0.1`** — the canonical `DigUrn` the URN helpers delegate to (the same definition
`dig-capsule` re-exports at `dig_capsule::urn`; `dig-store` depends on the foundation owner directly
to avoid pulling `dig-capsule`'s heavy serve stack for URN formatting alone).
### Deferred: the off-chain `.dig` capsule getters
`open_capsule` / `get_capsule_identity` are NOT in this version. `dig-capsule 0.4.0` exposes no
lightweight `bytes → (store_id, root_hash)` reader: `Capsule` offers only `canonical()` /
`from_canonical(&str)`, and reading a compiled `.dig` module's embedded identity requires the full
wasmtime `HostRuntime` (with the `store_id` already injected + a chain source + prover + BLS keys). A
`Capsule::from_module_bytes` reader is therefore being added to `dig-capsule` release-first, and the
capsule getters (plus the golden-fixture back-compat tests, §9) land in a follow-up unit. The
download-gating SIZE PROOF (§4) needs no capsule open and ships now.