Skip to content

feat(wallet)!: scope coin reads by requested CAT asset id (#3077) - #238

Merged
MichaelTaylor3d merged 4 commits into
mainfrom
loop/3077-node-cat
Aug 18, 2026
Merged

feat(wallet)!: scope coin reads by requested CAT asset id (#3077)#238
MichaelTaylor3d merged 4 commits into
mainfrom
loop/3077-node-cat

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

DO NOT MERGE — gate round not yet run

Consumer half of dig_ecosystem#3077: adopt dig-node-control-interface 0.17.0's widened Asset (Xch | Cat(AssetId)) and make control.wallet.coins / balance reads scope to the REQUESTED asset id rather than $DIG only.

WIP.

MichaelTaylor3d and others added 2 commits August 17, 2026 16:47
Co-Authored-By: Claude <noreply@anthropic.com>
WIP salvaged at the watchdog: dependency bump only, compiles clean. The
asset-scoped coin read is not yet implemented.

Co-Authored-By: Claude <noreply@anthropic.com>
BREAKING CHANGE: `BalanceAsset::Dig` is now `BalanceAsset::DIG`, a const
alias for `BalanceAsset::Cat(DIG_ASSET_ID)`, and the enum carries an
arbitrary CAT asset id instead of naming one of two tokens.

Refs DIG-Network/dig_ecosystem#3077
Adds the tagged `{"cat":"<64-hex>"}` request form to `control.wallet.balance`
and `control.wallet.coins`, echoes the scoped asset back on every coin, and
lets `dign wallet coins --asset <64-hex>` reach it from a terminal. SPEC \xc2\xa721
updated; workspace 0.126.0, dig-wallet 0.26.0.

Refs DIG-Network/dig_ecosystem#3077
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Correctness gate started on head 5a5267e. Diff + fixture read; running the suite and the revert probe in an isolated worktree next. Findings will be posted as they form.

@MichaelTaylor3d MichaelTaylor3d left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PASS — correctness gate

Head reviewed: 5a5267e77365f5e51374d1f6ada0c9efe4aeac45 (resolved from the remote myself; matches the dispatched sha). Draft, and it stays draft — this verdict does not authorize a merge.

Everything the brief asked me to verify was verified by measurement, not by reading. Two non-gating notes are posted inline and resolved by me so they cannot block merge.

1. Revert probe — re-run, and it discriminates

In an isolated worktree at 5a5267e I reverted ONLY Self::Cat(id) => Some(id) to Self::Cat(_) => Some(digstore_chain::dig::DIG_ASSET_ID) in crates/dig-wallet/src/sage/rpc.rs:93, changing nothing else. Baseline: 110 passed; 0 failed. With the defect reintroduced:

a_fallback_read_for_an_arbitrary_cat_returns_that_cats_coins ... FAILED
a_fallback_read_for_an_unheld_cat_answers_an_honest_empty_list ... FAILED
test result: FAILED. 108 passed; 2 failed

Both, as claimed. Worktree restored (git status --porcelain empty) and removed; the shared checkout was never mutated.

2. The two-CAT fixture reasoning holds — and is stronger than claimed

hinted_multi_asset_fixture is pre-existing and unmodified by this PR (it already carried the [0x33; 32] foreign CAT for #2879), so the new tests read a fixture they did not shape. It hints four coins to the same p2 hash: hinted-xch, real-dig, foreign-cat, pending-dig.

The lane's pairing argument is sound. Against the two named wrong implementations: filters nothing returns four ids, failing the arbitrary test's list equality; returns an empty list for every CAT also fails it; filters on a fixed $DIG id returns real-dig/pending-dig for a foreign request, failing it. The unheld test is the over-filtering mirror and additionally pins source == Fallback, so an empty list is an ANSWER rather than a suppressed read. A one-CAT fixture genuinely cannot see this — $DIG's own read stays correct under the defect.

3. Backward compatibility — the compat test was NOT edited to accommodate the change

coins_wire_emits_the_published_contract_shape (crates/dig-node-service/src/control.rs:2709) has exactly one change in the diff: the constructor spelling BalanceAsset::Dig to BalanceAsset::DIG, forced by the enum rename. Every assertion is byte-for-byte unchanged, including both "asset": "dig" expectations. It passes.

The emission is structurally guaranteed, not incidental: dig-node-control-interface 0.17.0's Asset has no Dig variant at all, and its hand-written Serialize matches other if other.is_dig() => serialize_str("dig") BEFORE the Cat(id) map arm (params.rs:394-405). So Cat(DIG_ASSET_ID) cannot serialize as a map, and the tagged cat form carrying the canonical id deserializes to the same value as "dig". the_asset_type_round_trips_through_the_published_wire_type pins that equality across the two crates' copies of the id.

4. No default-to-XCH

parse_asset_param (control.rs:1373-1400) branches on params.get("asset") PRESENCE, not on parse success: absent yields Ok(BalanceAsset::Xch); present goes through serde_json::from_value::<ControlAsset> with the error mapped to INVALID_PARAMS. There is no fallback arm anywhere that maps an unknown asset to XCH — I looked for one. an_unparseable_asset_is_refused_while_an_absent_one_defaults_to_xch covers three bad shapes (a bad token, a bad hex payload, a non-string) plus the absent control, which is the right control: a parser that defaulted the bad cases to XCH would satisfy the absent half identically. An explicit "asset": null also refuses, which is the fail-closed side and matches the SPEC sentence.

5. asset: null on coinById / coinSpend / coinsByParent is deliberate, not an oversight

Confirmed. The hardcoded "asset": Value::Null at control.rs:2264 and :2289 is the coin-record mapper for the three reads that classify nothing, it is normative in SPEC.md ("a coin id alone does not reveal whether a coin is XCH, a CAT or a singleton — that needs the puzzle, which this read never inspects"), and it is asserted at control.rs:3402 and :3412. Naming an asset there would be the node asserting a classification it never verified. The widening correctly did not touch it.

6. CLI — cannot accept what the node rejects, nor reject what it accepts

asset_to_wire (control_cli.rs:174-190) applies exactly one transform (a bare 64-hex operand becomes the tagged cat form) and forwards everything else verbatim, so the CLI's accept set is never WIDER than the node's. Checked the asymmetric direction too: uppercase 64-hex is tagged by the CLI and accepted by the node (AssetId::from_hex tolerates uppercase), and a 0x-prefixed id is 66 chars, so it is forwarded as a string and refused by the node — a refusal, never a wrong-token read. Neither xch nor dig is 64 hex chars, so the legacy tokens are unreachable by the transform.

7. Required checks — asserted BY NAME

check-merge-preconditions.sh --repo DIG-Network/dig-node --pr 238, exit code captured before any pipe (EXIT=1):

tip 5a5267e  base main  mergeStateStatus=CLEAN  unresolvedReviewThreads=0
  Lint commit messages               SUCCESS
  Check version increment            SUCCESS
  Rustfmt                            SUCCESS
  Clippy                             SUCCESS
  Test + coverage                    SUCCESS

All five required contexts are PRESENT and SUCCESS on this head. The non-zero exit is draft=true and nothing else — which is the correct state per the dispatch.

On the stale red — settle it here so nobody re-litigates it. A Check Version Increment: completed/failure run from the pre-bump commit is still visible in gh run list on this branch. It is superseded: the required context of that name resolves to SUCCESS on 5a5267e. A glance at the run list is the wrong instrument; the by-name assertion above is the right one, and it is green.

Not proven — and it does not gate this merge

Nothing has been watched against a live node or chain: no dign wallet coins --asset <hex> against a wallet holding a non-$DIG CAT. My judgement is that this does not gate merge, for a specific reason rather than a general one: the filter keeps only coins sitting at cat_puzzle_hash(owner_p2, requested_id), which is a narrowing applied to whatever get_coin_records_by_hints returns. If coinset's real answer is a smaller set than the fixture models, the result is a subset of the truth — the same exposure $DIG already ships with today, unchanged by this PR — and never a coin of the wrong asset. The failure mode this PR exists to remove (a confident empty list for a foreign CAT) is fully reproducible in-process and is now pinned by a test that fails without the fix. Live verification belongs with the dig-app slice that consumes it, where a person can actually watch it, per CLAUDE.md 2.6.

Known, noted, not resolved by me

Lane collision with #237 (loop/dig-logging-0.2.0): both edit crates/dig-node-service/src/control.rs, Cargo.toml, Cargo.lock and SPEC.md. The code regions differ, but both bump the workspace version and both edit SPEC.md, so whichever lands second needs a rebase and a re-check that the version still increments. Flagged, not resolved — that is the orchestrator's sequencing call.

dig-constants check (both directions)

  • Does anything here belong in dig-constants? No new shared value is defined locally. BalanceAsset::DIG is Cat(digstore_chain::dig::DIG_ASSET_ID) — a consumed canonical constant, not a new literal. The only literals introduced are test-local ([0x33; 32], [0x77; 32]).
  • Should anything here be USING dig-constants? The canonical DIG_ASSET_ID currently has copies in digstore-chain, chip35_dl_coin, dig-constants and dig-node-control-interface (the last deliberately, with a documented CONTRACT comment — it is a level-00 crate and may not depend sideways, CLAUDE.md Appendix B). This PR improves that position rather than worsening it: the_asset_type_round_trips_through_the_published_wire_type asserts ControlAsset::DIG == BalanceAsset::DIG, a mechanical cross-crate drift guard that did not exist before. Not gating; the consolidation is pre-existing ecosystem work.

Nothing handed to Copilot

Zero findings delegated. Both inline notes are non-gating, posted and resolved by me, and neither needs an author.

Comment thread crates/dig-wallet/src/sage/rpc.rs
Comment thread crates/dig-wallet/Cargo.toml
@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review August 18, 2026 00:54
@MichaelTaylor3d
MichaelTaylor3d merged commit 188e5a4 into main Aug 18, 2026
15 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the loop/3077-node-cat branch August 18, 2026 00:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant