feat(wallet)!: scope coin reads by requested CAT asset id (#3077) - #238
Conversation
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>
b4cc25d to
2c0e1e9
Compare
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
|
Correctness gate started on head |
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
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::DIGisCat(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 canonicalDIG_ASSET_IDcurrently has copies indigstore-chain,chip35_dl_coin,dig-constantsanddig-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_typeassertsControlAsset::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.
DO NOT MERGE — gate round not yet run
Consumer half of dig_ecosystem#3077: adopt
dig-node-control-interface0.17.0's widenedAsset(Xch | Cat(AssetId)) and makecontrol.wallet.coins/ balance reads scope to the REQUESTED asset id rather than $DIG only.WIP.