Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
94f5510
chore(wallet): open lane for CAT discovery staging (#390)
MichaelTaylor3d Aug 28, 2026
fe3b3b5
feat(wallet): the cat_admission_pending staging table and its promote…
MichaelTaylor3d Aug 28, 2026
b4e612c
fix(wallet): unmake staged CAT admissions with the coins they describ…
MichaelTaylor3d Aug 28, 2026
c1a1dd8
feat(wallet): discover CAT coins by derived puzzle hash and stage eve…
MichaelTaylor3d Aug 28, 2026
9995ea5
feat(wallet): promote staged CAT coins on a lineage proof, off the fr…
MichaelTaylor3d Aug 28, 2026
44c3d25
test(wallet): the five defect classes CAT staging must exclude
MichaelTaylor3d Aug 28, 2026
36c09c8
test(wallet): pin the #380 ingestion drop at apply_coin_states
MichaelTaylor3d Aug 28, 2026
865ba9d
test(wallet): assert the catch-up subscribes addresses AND their deri…
MichaelTaylor3d Aug 28, 2026
2c1d327
test(wallet): real-wallet measurement harness for the #380 acceptance…
MichaelTaylor3d Aug 28, 2026
7eb6ddd
docs(spec): SPEC 18.11a -- CAT discovery is not CAT authenticity
MichaelTaylor3d Aug 28, 2026
e75149a
chore(wallet): drop the lane scratch file
MichaelTaylor3d Aug 28, 2026
44bacb6
chore(wallet): re-lock for the dig-wallet 0.40.0 bump
MichaelTaylor3d Aug 28, 2026
dd07c6a
fix(wallet): the catch-up coverage set is not the coin-admission set
MichaelTaylor3d Aug 28, 2026
08d8105
test(wallet): pin the catch-up coverage/admission split from both sides
MichaelTaylor3d Aug 28, 2026
2f96767
fix(wallet): bound the promotion read rate and stop the staging queue…
MichaelTaylor3d Aug 28, 2026
e8bff4a
test(wallet): pin the starvation and the re-read bound, and stop weak…
MichaelTaylor3d Aug 28, 2026
e113262
fix(wallet): a hint is a claim, so the point-read tier stages instead…
MichaelTaylor3d Aug 28, 2026
910dc4e
fix(wallet): prove a hinted CAT from its parent spend instead of drop…
MichaelTaylor3d Aug 28, 2026
6bac817
test(wallet): pin the hint tier from both ends
MichaelTaylor3d Aug 28, 2026
69a1c11
fix(wallet): claim the staged row before writing the coin, and stop t…
MichaelTaylor3d Aug 28, 2026
0a85ea4
docs(spec): rewrite 18.11a against what the code does, not what it in…
MichaelTaylor3d Aug 28, 2026
46fa77b
style(wallet): cargo fmt
MichaelTaylor3d Aug 28, 2026
c27fabc
fix(wallet): stop the point-read tier deleting the wallet's NFTs and …
MichaelTaylor3d Aug 28, 2026
be4e12c
docs(spec): four promotion outcomes, and the four cases Disproven covers
MichaelTaylor3d Aug 28, 2026
904f506
test(wallet): report nfts/dids and point-read routing on the real rep…
MichaelTaylor3d Aug 28, 2026
a92dcf9
fix(wallet): bind a reconstructed DID to the coin it claims to be
MichaelTaylor3d Aug 28, 2026
2ebc305
test(wallet): a DID hinted to us but locked to a stranger is refused
MichaelTaylor3d Aug 28, 2026
66782d1
docs(spec): a reconstructed singleton must reproduce its own coin
MichaelTaylor3d Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ edition = "2021"
# the ROOT manifest (`[workspace.package].version`), so it MUST be set here for a
# release to fire (§3.6). The library crates (dig-node-core/dig-runtime/dig-wallet)
# keep their own independent versions — only the released binary tracks the workspace version.
version = "0.160.0"
version = "0.161.0"

# Release hardening, matching digstore: keep integer-overflow checks ON in release.
# The node parses untrusted serialized input and does offset/length arithmetic over
Expand Down
120 changes: 120 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -5405,6 +5405,126 @@ The sync loop runs this attribution as a post-apply step (`sync::CatAttributor`,
newly-synced candidate coins, so a synced CAT coin — stored initially with `asset_id: None` — gains its
TAIL and surfaces in `get_cats` (this is how `$DIG` resolves from the node).

18.11a. **CAT discovery is not CAT authenticity — staged admission (#380, #394).** A `CoinState` carries a
parent, a puzzle hash and an amount, and **no hint**, so a wallet cannot recognise its own CAT coins from
the frame that delivers them. The node therefore DERIVES, for each address it follows and each asset id it
knows, the outer hash `cat_puzzle_hash(owner_p2, asset_id)`, and REQUESTS those hashes alongside the
addresses. Without this the peer never sends the wallet its CAT coins at all: a CAT coin does not sit at
its owner's address.

Requesting a derived hash is **discovery** and MUST NOT be read as ownership of that asset. The
derivation is injective — it commits to the CAT2 module, the asset id and the inner p2 together — but it
establishes only *"if this coin is ever spent, only this wallet can spend it, as this asset"*. It does
NOT establish that the coin is a unit of that asset, because `CREATE_COIN` is unconstrained in its
destination: anybody may place a coin at any puzzle hash, at a cost of one mojo per displayed base unit,
knowing nothing but the victim's public address. The same is true of a coin's HINT, which is likewise
chosen freely by whoever creates it.

**The coverage set and the admission set are different sets, and MUST NOT be the same value.**

- The **coverage** set is what the node asks a peer about: the addresses UNION the derived hashes. It
determines what the wallet can SEE.
- The **admission** set is the wallet's own p2 addresses, and nothing else. It determines what may be
written to `coins`.
- The union that produces the coverage set MUST be performed at the point that issues the request, and a
derived hash MUST NOT be admissible even if one is supplied to that point as an address. One vector
serving both roles is the defect this clause exists to exclude: it admitted every coin at a derived
hash and typed it `asset_id: None`, which means XCH.
- The recorded coverage a replica claims (§18.13's authoritative-read routing) is the ADDRESS set, since
that is the set its readers ask about.

The two states are held in **different tables**, and this separation is normative:

- A coin arriving at a derived hash, on ANY tier, MUST be written to `cat_admission_pending` and never to
`coins`. This binds the peer catch-up, the peer frame path, and the point-read tier alike. The one
exception is a coin already PRESENT in `coins` — one that has cleared promotion — whose later states,
including its spend, MUST update `coins` as any other coin's would.
- A coin discovered by HINT rather than at a derived hash MUST be staged on the same terms. A hint is a
claim its creator chose; admitting a hinted coin as an untyped row is the same fabricated-balance
primitive reached without a peer.
- A coin sitting at one of the wallet's OWN p2 hashes is an ordinary XCH coin and is admitted directly.
This is the only direct admission.
- A coin MUST enter `coins` only when a read of its parent spend reconstructs it as a CAT, and:
- the reconstruction's coin id equals the staged row's; AND
- where the coin was discovered at a derived hash, the reconstructed asset id and inner p2 hash both
equal the ones the derivation predicted; OR
- where the coin was discovered by hint and nothing was predicted, the reconstructed inner p2 hash is
one the wallet controls.

It is then written **fully attributed**, from the reconstruction's values and never from the
derivation's or the hint's.
- `coins` retains exactly the semantics it has without this feature. No reader of `coins` — the balance,
the spend-input selector, `get_cats`, the arrivals notifier — is required to apply a predicate to
remain correct.
- Only the address set is presented to the arrivals notifier (§18.13), which reports payments to a user.

**Promotion** runs off the peer frame path, in the same out-of-band pass as §18.11 attribution:

- The frame path performs **zero** chain reads. Routing is a membership test against locally derived
hashes, and the staging write takes no `LineageSource`.
- A pass reads at most `MAX_CAT_PROMOTIONS_PER_PASS` parent spends.
- **Promotion is terminal per VERDICT, not per coin, and the read cost is bounded by RATE.** A coin that
is proven or disproven is never read again, because its staged row is deleted. A coin whose parent
cannot be read reaches no verdict and MUST remain staged, so it will be read again — which is why a
per-coin bound cannot be claimed. The queue is therefore served ordered by attempt count first and
arrival order second, and a row is eligible only if it has not been read within
`PROMOTION_RETRY_COOLDOWN`. Together these give: a row that never resolves can never hold the head of
the queue against one that has never been tried, and the total read rate is bounded by
`staged rows / cooldown` rather than by `cap` per pass.
- **A never-existing parent is NOT distinguished from an unreadable one, deliberately.** A source
answers identically for a spend it has never heard of and one it is merely behind on, so a terminal
refusal built on that answer would convert a brief outage into permanent erasure of a real coin. The
cost is bounded instead of the cause classified.
- The four outcomes are distinct. **Proven** promotes into `coins`. **Resolved** — the parent read
reconstructs the coin as an NFT or DID singleton owned by a p2 hash the wallet controls — writes it to
`nfts`/`dids` and deletes the staged row. **Disproven** — a parent read that SUCCEEDED and refutes the
claim — deletes the staged row. **Unavailable** — a read that could not be performed, or that the
source answered emptily — leaves the row staged for retry, and MUST NOT delete it; treating an
unavailable answer as a disproof would let a peer erase real money by withholding parent spends.
- **A proven non-CAT MUST NOT be refused.** *Resolved* and *Disproven* are separate outcomes because one
says the derivation was true about something the CAT path does not itself handle, and the other says
the derivation was a lie. Collapsing them makes a routing gap indistinguishable from a security verdict
and deletes real assets terminally — the point-read tier is the only production path that reaches
§18.11 reconstruction, so a wallet's NFTs and DIDs vanish and their chain reads are re-paid on every
refresh. A singleton MUST NOT be written to `coins`: it carries no asset id, where absence means XCH.
- **A resolved singleton MUST be owned.** Admission requires the inner p2 hash the RECONSTRUCTION names
to be one the wallet controls — the same test an unpredicted CAT is held to, and never the hint, which
anybody may write.
- **A reconstructed singleton MUST reproduce its own coin.** The reconstruction is only a proof of
ownership if the p2 hash it names is one the coin's puzzle is actually locked to, so §18.11
reconstruction MUST recompute the singleton puzzle hash from the parsed info and refuse the coin unless
it equals the child coin's own puzzle hash. Without this the preceding clause is vacuous for DIDs: the
DID driver's read path takes the owner from the parent spend's `CREATE_COIN` memo hint and stores it
verbatim, so anybody able to spend any DID could name any wallet as owner of their singleton. The NFT
path needs no separate check — its child coin is derived from the parsed info, and the coin-id equality
that path already requires commits to it.
- **Disproven covers five cases**, all terminal deletions: a coin already spent on chain (dropped without
a parent read, since it can neither be counted nor selected); a staged row whose coin id does not bind
its own parent, puzzle hash and amount; a reconstruction that disagrees with the derivation, or whose
hint names an address the wallet does not control; a singleton owned by another p2 hash; and a coin
whose parent spend reconstructs to nothing the wallet may hold at all.
- A promotion write MUST claim its staged row before writing the coin: the staging row is deleted first
and the write proceeds only if exactly one row was removed, all in one transaction. Promotion spans a
network round trip, and a reorg rollback inside that window must not be overwritten by a coin the
replica has already decided to forget.
- A promotion failure MUST NOT propagate into the peer update loop. A chain read fails for reasons a peer
can arrange, and an error reaching the update loop would end a live session.
- `cat_admission_pending` MUST be bounded, evicting oldest-first. The bound MUST delay and MUST NOT
error: the staging write is on the frame path, and a peer that can fail a frame can deny a catch-up.
- Staged rows are rolled back with the coins they describe. A reorg deletes every staged row created
above the fork and clears any spend recorded above it.

**Where promotion runs today.** On the shipped node the promotion pass runs on the POINT-READ tier
(`refresh_tracked_coins`) only. The peer frame path's promotion is reachable but its `CatAttributor` is
constructed under `cfg(test)` alone, so it does not run in production; wiring it is #382. A staged coin
therefore becomes spendable on a point-read refresh rather than on the frame that delivered it. This is a
statement of current behaviour, not a licence: nothing above is relaxed by it.

**The stated failure mode is INCOMPLETENESS.** A real coin that cannot yet be proven is *absent* — not
counted as its asset, and in particular not counted as XCH, which `asset_id IS NULL` means and which
feeds coin selection. A wallet may under-report; it must never report a figure that is wrong.


18.12. **Live broadcaster bring-up — real mainnet $DIG spends behind a config gate (#428).** The
node-custodied wallet BUILDS + SIGNS + VALIDATES spends (§18.9/§18.21) and the tip engine (§18.23)
reserves + caps them, but on the shipped node NO broadcaster is attached, so no `$DIG` moves. This
Expand Down
2 changes: 1 addition & 1 deletion crates/dig-wallet/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dig-wallet"
version = "0.39.0"
version = "0.40.0"
edition = "2021"
license = "GPL-2.0-only"
description = "DIG Browser built-in Chia wallet sidecar: a local axum server (using digstore-chain + chia-wallet-sdk over coinset.org) that serves a Sage-mirroring wallet UI. Native Rust so BLS signing works; the browser opens it at 127.0.0.1."
Expand Down
Loading
Loading