Skip to content

feat(wallet)!: adopt the current chia-query line and its shared mempool-refusal list - #558

Merged
MichaelTaylor3d merged 4 commits into
mainfrom
loop/chq-adopt-dig-wallet
Sep 4, 2026
Merged

feat(wallet)!: adopt the current chia-query line and its shared mempool-refusal list#558
MichaelTaylor3d merged 4 commits into
mainfrom
loop/chq-adopt-dig-wallet

Conversation

@MichaelTaylor3d

@MichaelTaylor3d MichaelTaylor3d commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

DRAFT — do not merge until the gate round returns. The pin itself is now settled: chia-query = "0.24.1".

Closes #557
Closes #556

Parent epic: https://github.com/DIG-Network/dig_ecosystem/issues/3194

Brings crates/dig-wallet from chia-query 0.21.0 onto 0.24.1 — the live wallet path — and adopts chia_query::mempool_refusal, deleting this crate's second copy of a money decision. dig-node 0.254.77 -> 0.254.78.

Two of the three minors change what a caller sees without changing a signature, so a caret bump that compiles proves nothing. Two existing tests failed on the bump, and the failure was the delta rather than a mistake.

The pin is a FLOOR, and that is the whole point

"0.24.1", not "0.24". cargo reads it as >=0.24.1, <0.25.0, so 0.24.0 becomes unresolvable while every later patch still flows. "0.24" would re-admit the defect; =0.24.1 would block the next security patch.

0.24.0 shipped a light-client drive loop that could wedge on a superseded session (chia-query#59): a peer that went quiet after one failed subscribing read held the loop indefinitely at no cost to itself, and needs_rearm() — the crate's own documented staleness signal — never fired. The coin-state cache then froze with nothing to distinguish it from a quiet chain. This crate is the consumer that defect is worst for: a frozen coin-state cache is a wallet that silently stops seeing money arrive, and the surface looks healthy while serving nothing.

One window remains open and is recorded in the manifest rather than assumed away (chia-query#61): reconnect() lowers the rearm flag after re-arming, so a failed read that unpins the anchor during that re-arm has its signal cleared. Narrow and pre-existing, but this crate is the consumer it would affect, so a stale coin-state cache should be checked against #61 before being treated as a mystery. Unrelated, also open: chia-query#62.

Per-delta disposition

version delta disposition
0.22.0 Vec-returning router methods became fail-closed wrappers over _graded twins; SetAnswer<T> has no absence arm Felt. Three call sites in sage::fallback — the wallet's coin-discovery path. Two tests failed; a third was added.
0.23.0 rival singleton lineage walk deleted; delegates to dig_chainsource_interface::resolve_singleton_lineage_via_walk Felt, strictly safer. Reaches this crate through ChiaQueryProvider.
0.24.0 mempool_refusal list moved down byte-identical Adopted; local copy deleted.
0.24.0 subscribe removal unscoped FrameFanout::subscribe(capacity) removed No-op here — measured.
0.24.0 push_tx routing one bounded retry; coinset never consulted on a refusal Both doc sites corrected; classifier unchanged and still load-bearing.
0.24.0 SessionEndReason::OversizedFrame new variant Not matched on anywhere in this workspace.
0.24.1 the #59 fix The reason for the floor.

The 0.22.0 delta, concretely

coinset_only_set now reads the coinset tier's own peak before it will answer a population read at all: a set answer is a true statement about a HEIGHT, coinset states no height with its answers, and a source that cannot be dated cannot be corroborated against anything.

Two chain_failure_tests fixtures served one canned body for every path, so the new get_blockchain_state read received a coin-records payload and the whole read became AllSourcesFailed. They now use the module's existing serve_routed, with a peak far above every fixture record so the normalisation is a no-op — an as-of height below a record's spent_block_index would drop that spend, and a test about mapping would then be silently testing normalisation instead.

records_a_tier_will_not_date_are_refused_rather_than_served_undated pins what that fixture change would otherwise have quietly absorbed. Its coin-records route is identical to the passing test beside it and its records are genuine children; the only difference is that nothing will name a peak. On the pre-0.22 line it fails, because the old router returned records from whichever source answered first with no peak read at all.

Failure direction of everything touching money or peer trust

  • Coin discovery. A set that cannot be corroborated or dated is now Err, never a fabricated empty Vec. All three sage::fallback call sites map_err to Error::internal, so the failure surfaces as "the read failed", never "you have no coins". A visible read failure is survivable; a silent zero balance is not.
  • Refusal classification. Unchanged, and it must stay unchanged. chia-query applies the same list with the opposite default — it asks should I try one more peer? (unrecognised → RETRY) while this crate asks may I release these reserved inputs? (unrecognised → HOLD). Unifying the defaults would make one caller fail in its dangerous direction. Both are now stated at the classifier.
  • The push_tx guard is NOT discharged by the upstream retry. 0.24.x split push_tx in two: when a peer answers, coinset is never consulted and the retry is bounded at one other peer; when the transport fails first, the old peer_then_coinset(peer, peer_retry, coinset) last-answer-wins ladder runs unchanged. The dig-node#460 race lives on that second path — a request timed out raised after the bytes went out, then a peer that has since seen the gossip answering DOUBLE_SPEND. The window narrowed; it did not close. Both doc sites now say so, so nobody relaxes the classifier on the strength of the retry.
  • Singleton lineage. Fails closed where it previously returned a derived successor the chain may never have created. A caller retries instead of acting on unconfirmed state.

The invariant most at risk, and how it was verified

sage/corroborated_source.rs documents a deliberate split at peak_height: PeerCorroboratedReads::peak_height returning None means the peers did not agree, or too few spoke — an unknown, mapped to Err; ChainSource::peak_height's Ok(None) means this source exposes no peak at all — a settled fact a caller may act on. That is NC-12 plurality, and a set-agreement adoption is exactly where it gets flattened by accident.

It was documented and untested. Two tests now hold it, using the module's NoPeers harness plus a new AgreeingTips sample:

  • an_unsettled_peak_is_an_error_and_never_a_corroborated_absence — zero peers is settled_peak(&[]) == None, and the source must answer Err.
  • peers_that_agree_still_yield_a_peak_through_the_same_boundary — the control, asserting the lagged height min(claim) - SETTLED_LAG rather than the claimed tip, so a later "just return the max claim" cannot pass either.

Revert-proved. Replacing the None => arm with Ok(None) — the exact flattening, a targeted edit to the production arm with no assertion touched — turns the guard RED (got Ok(None)) while the control stays GREEN.

corroborated_source.rs:277 — judged, and deliberately NOT un-refused

The graded set API 0.22.0 shipped lives on QueryRouter / ChiaQuery and reaches a ChainSource through ChiaQueryProvider. CorroboratedChainSource is a different provider, backed by PeerCorroboratedReads — this node's own dialled peers — whose entire surface is by coin id (coin_record_by_id_at_floor, coin_spend_at_floor, peak_height). It has no set read at all.

So un-refusing would mean building a corroborated set read on the peer-reads path, not adopting one chia-query published — a feature with its own NC-12 plurality surface, inside a change nobody is reviewing as a plurality change.

On #56's untrusted wire-height bound, stated rather than implied: it is not load-bearing for this PR, because this PR adds no such read. It is the right thing to cite when that work happens, since a set read on the peer path must hold every peer's answer to a height the node did not take on one peer's word. Citing it here as the justification for an un-refusal that did not happen would be a safety claim nothing exercises.

Measurements, so the next reader does not re-derive them

  • TxStatus gained no field. Byte-identical struct region across v0.21.0 and 0.24.x; 4 fields both sides. The struct literal in sage::chain's tests still compiles.
  • BUNDLE_INTRINSIC_REFUSALS has exactly one definition, and it is upstream. The local const and its ~60 lines of exclusion rationale are deleted; refusal_is_bundle_intrinsic delegates to chia_query::mempool_refusal::is_bundle_intrinsic_refusal. It is not re-exported: a pub(crate) use that nothing in the crate reads is dead weight and -D warnings rejects it — that was the first Clippy failure on this branch. The pointer to the canonical home lives in the classifier's doc, where a reader looking for the list is actually standing. What stays here is the splitrefusal_reason, the "{verdict}: {reason}" decomposition — because this crate builds the composition and chia-query classifies a bare reason.
  • FrameFanout::subscribe removal is a no-op. Zero references to FrameFanout, FrameSubscription or subscribe_frames in the workspace. subscribe_store / unsubscribe_store are dig-node's own store subscriptions, unrelated. run_update_loop (sync.rs:1274) is a future adopter, not a current caller.
  • The chia family does not move. chia-query 0.24.1 declares the same chia-protocol/chia-bls/chia-consensus 0.36 line and the same chia-wallet-sdk 0.36 as 0.21.0. Verified from Cargo.lock: chia-query resolves once, and the new lineage-walk feature on dig-chainsource-interface 0.3.2 pulls only crates already on the 0.36 line.
  • dig-chainsource-interface resolves two lines (0.1.0 and 0.3.2) and this PR did not cause it. chia-query resolves 0.3.2, as 0.21.0 did; two other consumers hold the 0.1.0 line. Pre-existing, untouched, recorded so it is not misattributed.

§2.4b

dig-offers, dig-clvm, dig-keystore, dig-options are at latest through their carets. dig-node-control-interface is "0.31" against a latest of 0.32.1 — a semver-incompatible bump on a control-plane contract declared by two crates here, whose delta declares a new control method someone must serve. Split out under §2.4b's own scope limit as #559, named rather than deferred.

How verified

  • cargo clippy --workspace --all-targets --locked -- -D warnings — the exact CI command. Exit 0.
  • cargo fmt --all --checkexit 0, empty output.
  • cargo test --workspace --all-targets --all-features --locked — the exact CI command. Result in a comment below.
  • Three revert-proofs, each a targeted single-line production edit git diff'd before running, each confirmed to turn its guard RED while its control stayed GREEN. Test counts checked on every run — no green from a filter that matched nothing.

…ol-refusal list

Brings `dig-wallet` from `chia-query` 0.21.0 onto the 0.24.x line and adopts
`chia_query::mempool_refusal` by re-export, deleting the second copy of a money
decision.

Three minors land here and two of them change what a caller sees without changing
a signature, so the compiler cannot report them:

- 0.22.0 turned the `Vec`-returning router methods into fail-closed wrappers over
  `_graded` twins. `get_coin_records_by_puzzle_hashes` / `_by_hints` /
  `_by_parent_ids` -- this crate's coin-discovery path in `sage::fallback` -- now
  answer from a set corroborated across sources at a settled height. Disagreement
  is `Err(SourcesDisagree)` and a set only one source vouches for is
  `Err(UncorroboratedPresence)`; an empty `Vec` is never manufactured. `sage::fallback`
  maps every `Err` to an internal error, so an uncorroborated read surfaces as a
  failed read and never as "you have no coins".
- 0.23.0 deleted chia-query's rival singleton lineage walk;
  `ChiaQueryProvider::resolve_singleton_lineage` now delegates to
  `dig_chainsource_interface::resolve_singleton_lineage_via_walk`, which reads
  `coin_record` as well as `coin_spend` and fails closed rather than authenticating
  a derived successor the chain may never have created.
- 0.24.0 added `mempool_refusal` and removed the unscoped `FrameFanout::subscribe`.
  This workspace never called it: there is no reference to `FrameFanout`,
  `FrameSubscription` or `subscribe_frames` anywhere in it.

`BUNDLE_INTRINSIC_REFUSALS` and its exclusion rationale were written in this crate
and moved down into chia-query byte-identical, so the two cannot drift into rival
definitions of the same split. The composed-form split (`refusal_reason`) stays
here, because this crate builds the composition; only the membership question moves.

The `is_definitive_rejection` doc described `peer_then_coinset` routing `push_tx`
no longer performs on every path. Corrected to state both: coinset is never
consulted about a refusal, and the last-answer-wins ladder survives on the
transport-failure path, which is the path this guard exists for.

The chia family does not move. chia-query 0.24.0 declares the same `0.36` line and
the same `chia-wallet-sdk 0.36` as 0.21.0, and the lock still resolves chia-query
once.

Refs #557
Refs #556

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d and others added 3 commits September 4, 2026 12:54
…plit

Three tests and a version bump to 0.254.78.

`corroborated_source` has documented since dig_ecosystem#3032 that
`ChainSource::peak_height` must answer `Err` when the node's peers did not settle
on a peak, because `Ok(None)` means something a caller may act on: this source
exposes no peak at all. Nothing held that. The nearest wrong implementation is a
one-line "simplification" -- pass the inner `Option` straight through -- which
compiles, reads as tidy, and converts an unanswered question into a settled fact.
That is the NC-12 plurality distinction a set-agreement adoption flattens by
accident, so it is now pinned, with a control proving the method discriminates on
the settlement rather than erring unconditionally.

The conformance test for the refusal split is driven FROM chia-query's canonical
list rather than from names written here, so a name added upstream is exercised the
day it lands. It asks each name in both the bare and the composed form: the composed
one is the one that can fail, because `refusal_reason` -- the `"{verdict}: {reason}"`
split this crate still owns -- is the only logic left on this side of the move. A
negative row keeps the positive rows from passing vacuously.

The re-export is dropped in favour of a doc pointer. `pub(crate) use` of a constant
nothing in the crate reads is dead weight, and `-D warnings` treats it as an error;
the reason there is no local copy belongs in the classifier's doc, where a reader
looking for the list will actually be standing.

Refs #557
Refs #556

Co-Authored-By: Claude <noreply@anthropic.com>
… drive-loop wedge

`"0.24.1"` rather than `"0.24"`, and the difference is the whole point. cargo reads
it as `>=0.24.1, <0.25.0`, so 0.24.0 becomes unresolvable while every later patch
still flows; `"0.24"` would re-admit the defect and `=0.24.1` would block the next
security patch.

chia-query 0.24.0 shipped a light-client drive loop that could wedge on a superseded
session (chia-query#59). A peer that went quiet after one failed subscribing read
held the loop indefinitely at no cost to itself, and `needs_rearm()` -- the crate's
own documented staleness signal -- never fired, so the coin-state cache froze with
nothing to distinguish it from a quiet chain. This crate is the live wallet path,
which makes it the consumer that defect is worst for: a frozen coin-state cache is
a wallet that silently stops seeing money arrive, and the surface looks healthy
while serving nothing.

The manifest records the remaining window rather than implying it closed:
chia-query#61, where `reconnect()` lowers the rearm flag after re-arming, so a
failed read unpinning the anchor during that re-arm has its signal cleared. Narrow
and pre-existing, but this crate is the consumer it would affect, so a stale
coin-state cache should be checked against it before being treated as a mystery.

Also rustfmt on the peer harness added in the previous commit.

Refs #557

Co-Authored-By: Claude <noreply@anthropic.com>
…t is refused

The 0.22.0 set-agreement adoption made two existing `chain_failure_tests` fail, and
the failure was the delta rather than a mistake. A population answer is now a true
statement about a HEIGHT rather than about "now"; coinset states no height with its
answers, so `coinset_only_set` reads that tier's own peak first and holds the
records to it. Both fixtures served ONE canned body for every path, so the new
`get_blockchain_state` read got a coin-records payload back and the whole read
became `AllSourcesFailed`.

They now use the module's existing `serve_routed`, which answers by path, with a
peak far above every fixture record so the normalisation is a no-op: an as-of height
below a record's `spent_block_index` would drop that spend, and a test about mapping
would then be silently testing normalisation instead.

The new test pins what the fixture change would otherwise have quietly absorbed. Its
coin-records route is identical to the passing test beside it and its records are
genuine children; the only difference is that nothing will name a peak. On the
pre-0.22 line it fails, because the old router returned records from whichever
source answered first with no peak read at all -- which is exactly the property
being adopted.

Failure direction, which is why a wallet wants this: the refusal costs an error the
caller retries, while serving the records undated costs a set treated as current
that is not, on the path the wallet discovers the user's coins through.
`sage::fallback` maps it to an internal error, so the surface says "the read failed"
and never "you have no coins".

Refs #557

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Verification, on 169e8df1 — the exact CI commands, run unpiped

command result
cargo test --workspace --all-targets --all-features --locked exit 03089 passed, 0 failed, 3 ignored across 55 test binaries
cargo clippy --workspace --all-targets --locked -- -D warnings exit 0
cargo fmt --all --check exit 0, empty output

Counts are quoted because an exit status alone does not distinguish a green run from a filter that matched nothing. Every targeted run below reports 1 passed, never 0 passed; N filtered out.

The six tests that carry this change

sage::chain::tests::the_composed_split_agrees_with_chia_querys_bare_classifier_on_the_canonical_list ... ok
sage::corroborated_source::tests::an_unsettled_peak_is_an_error_and_never_a_corroborated_absence ... ok
sage::corroborated_source::tests::peers_that_agree_still_yield_a_peak_through_the_same_boundary ... ok
sage::fallback::chain_failure_tests::records_a_tier_will_not_date_are_refused_rather_than_served_undated ... ok
sage::fallback::chain_failure_tests::a_parent_with_no_children_is_an_empty_ok_not_an_error ... ok
sage::fallback::chain_failure_tests::genuine_children_are_mapped_through_spent_and_unspent_alike ... ok

Revert-proofs

Three, each a targeted single-line edit to production code, git diff'd before running so it is visible that no assertion was touched. No sed, no regex.

1. The NC-12 plurality flattening

corroborated_source.rs, the exact "simplification" the guard exists for:

     fn peak_height(&self) -> Result<Option<u32>, Self::Error> {
         match self.block_on(async { self.reads.peak_height().await })? {
             Some(height) => Ok(Some(height)),
-            None => Err(ChainSourceError::Transport(
-                "the node's peers did not settle on a peak height".to_string(),
-            )),
+            None => Ok(None),
         }
     }
test verdict
an_unsettled_peak_is_an_error_and_never_a_corroborated_absence FAILED0 passed; 1 failed, message ... (got Ok(None))
peers_that_agree_still_yield_a_peak_through_the_same_boundary ok1 passed

The control staying green is the load-bearing half: it proves the guard discriminates on the settlement rather than being satisfied by a peak_height that errs unconditionally.

2. The composed-form split

chain.rs, refusal_reason stops stripping the verdict:

 fn refusal_reason(stated: &str) -> &str {
-    match stated.split_once(": ") {
-        Some((_verdict, reason)) => reason.trim(),
-        None => stated.trim(),
-    }
+    stated.trim()
 }

the_composed_split_agrees_...FAILED (0 passed; 1 failed), reporting BAD_AGGREGATE_SIGNATURE is on the canonical list, but this crate did not recognise it once composed as FAILED: BAD_AGGREGATE_SIGNATURE.

This is the shape worth noting: the bare form still matched throughout. Only the composed form failed — which is exactly the silent move to the HOLD side that a broken split would cause on a spend no node will ever admit, and the reason the test asks each name in both forms rather than one.

3. Fabricating an empty set from an Err

fallback.rs, the coin-discovery mapping:

             .get_coin_records_by_parent_ids(&[Self::query_hash(parent_coin_id)], None, None, true)
             .await
-            .map_err(|e| Error::internal(format!("fallback children read: {e}")))?;
+            .unwrap_or_default();
test verdict
records_a_tier_will_not_date_are_refused_rather_than_served_undated FAILED
an_unreachable_chain_is_an_error_never_a_childless_parent FAILED
a_child_of_another_parent_fails_the_whole_read FAILED
a_parent_with_no_children_is_an_empty_ok_not_an_error ok
genuine_children_are_mapped_through_spent_and_unspent_alike ok

Three guards of the same direction go red together while both positive controls stay green — so the new test is not satisfied by refusing everything, which is the failure mode that would make an unspent parent unreadable.

The tree was restored to HEAD and confirmed clean (git status --porcelain empty) after each proof, and the full suite above was run on the restored tree.

What was NOT proven, stated rather than implied

  • No mainnet or live-peer exercise. Every test here is offline: a local HTTP fixture for the coinset tier and scripted CoinPeer doubles for the peer tier. The claim is that the adoption preserves this crate's contracts, not that chia-query's drive loop behaves on a real network.
  • chia-query#59 is fixed upstream, not verified here. This PR's contribution is the version floor that makes 0.24.0 unresolvable. The fix itself was gated in its own repo.
  • chia-query#61 remains openreconnect() lowering the rearm flag after re-arming — and this crate is the consumer it would affect. It is recorded in the manifest so a future stale coin-state cache is checked against it rather than treated as a mystery.

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

IN PROGRESS — not the verdict

Item 1 (fabricated empty coin set) and item 5 (fixture change legitimacy) — both CLEAR.

  • crates/dig-wallet/src/sage/fallback.rs:513-517coin_records_by_parent delegates directly to
    chia_query's get_coin_records_by_parent_ids; this PR's fallback.rs diff touches ONLY
    mod chain_failure_tests (all 5 hunks land inside the test module, confirmed via git diff ... | grep '^@@').
    So the peak-dating enforcement ("a set nothing can date is refused, not served as Ok(vec![])") lives
    in the upstream chia-query 0.24.1 crate itself, not in new dig-node production code — this PR only
    adopts it and updates its OWN fixtures/tests to match the new required-peak-route behaviour.
  • New test records_a_tier_will_not_date_are_refused_rather_than_served_undated
    (fallback.rs:~1424) asserts BOTH result.is_err() AND explicitly
    !matches!(result, Ok(ref v) if v.is_empty()) — i.e. it distinguishes "errored" from "silently
    empty", which is exactly the anti-money-lie property item 1 asks to verify. The fixture omits
    get_blockchain_state entirely (no peak route registered), so serve_routed will fail closed on
    that route being missing — good adversarial fixture design, matches the claimed "identical records,
    only the peak route differs" property.
  • Existing passing tests (a_parent_with_no_children_is_an_empty_ok_not_an_error,
    genuine_children_are_mapped_through_spent_and_unspent_alike) were updated to ADD the
    get_blockchain_statePEAK_STATE route via serve_routed, consistent with "the API delta
    requires a peak route now, not a behavior change to paper over."

@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

Head SHA verified: 169e8df (matches gh pr view --json headRefOid, and matches the local clone I read the diff from).

Scope of this leg: correctness only (parallel loop-security leg covers custody/adversarial angles).

Findings, each checked against the actual upstream chia-query 0.24.1 source (fetched from static.crates.io, not taken on the PR's word)

  1. Fixture change in fallback.rs (chain_failure_tests) is a genuine adoption, not a paper-over. records_a_tier_will_not_date_are_refused_rather_than_served_undated (fallback.rs:~1404) uses serve_routed with ONLY the get_coin_records_by_parent_ids route defined; the unrouted get_blockchain_state path falls through to serve_routed's default {"success":false} (fallback.rs:830), so the peak read genuinely fails and the test is exercising the real fail-closed path, not a rigged fixture. PEAK_STATE's height (1000) is confirmed far above every fixture record's spent_block_index/confirmed_block_index (100/101/140) in the sibling passing tests, so normalisation is verifiably a no-op there — the "convenient" peak does not also disable the property under test.

  2. corroborated_source.rs:277-area three-way None/Ok(None)/Err split is real and tested, not just documented. Read the full file: peak_height() (line ~278) maps PeerCorroboratedReads::peak_height() -> None to Err(Transport(...)), never Ok(None). Two tests hold it (an_unsettled_peak_is_an_error_and_never_a_corroborated_absence and its control peers_that_agree_still_yield_a_peak_through_the_same_boundary, asserting the LAGGED height rather than the claimed tip, which blocks a "return max claim" false-pass). Both are genuine NC-12 plurality tests, not tautologies — the control uses a materially different assertion (lagged height) from the property test's mere is_err(), so neither is satisfiable by a constant-output stub.

  3. Un-refusal deferral at corroborated_source.rs:277 (coin_records_by_puzzle_hash/by_parent, still Err(Unsupported)) is correctly judged. Verified PeerCorroboratedReads's only reads are by coin id (coin_record_by_id_at_floor, coin_spend_at_floor) per the module doc and call sites — there genuinely is no set-read surface on this provider to adopt from chia-query's 0.22.0 SetAnswer/graded API, which lives on ChiaQueryProvider/QueryRouter instead. The lane's reasoning holds: building a corroborated set read here would be new work with its own NC-12 surface, not an adoption, and chia-query#56's untrusted-wire-height bound is correctly NOT cited as satisfied since nothing here exercises it. Ratified — ecosystem framing corrected as requested: this is not an outstanding un-refusal owed by this PR.

  4. push_tx two-path behavior verified against actual chia-query 0.24.1 router.rs:1177-1217. The transport-failure branch (line 1180) unconditionally falls to the unchanged peer_then_coinset ladder — confirmed byte-for-byte the same call shape the PR's doc describes. The peer-answered branch classifies via mempool_refusal::is_final, does one bounded retry to one other peer, and never touches coinset — also confirmed. Both sage/rpc.rs doc sites (main doc comment ~2326, and the #460 test doc ~11418) accurately state that the #460 race lives ONLY on the transport-failure path and is narrowed, not closed, by the retry. This is exactly right — no relaxation of the classifier is implied anywhere.

  5. BUNDLE_INTRINSIC_REFUSALS — single definition confirmed. chia_query::mempool_refusal (fetched source) carries the full list + rationale byte-identical to what was deleted from chain.rs. chain.rs's refusal_is_bundle_intrinsic now delegates via chia_query::mempool_refusal::is_bundle_intrinsic_refusal(refusal_reason(stated)) — grepped the whole diff and confirmed no second copy survives. The conformance test (the_composed_split_agrees_with_chia_querys_bare_classifier_on_the_canonical_list) is correctly self-aware that it cannot test "two lists agree" (there's one list now) and instead pins refusal_reason's composition-stripping — including a negative control (DOUBLE_SPEND) and an emptiness guard on the canonical list, so it can't pass vacuously.

  6. TxStatus byte-identical claim confirmed from upstream source: types/response.rs:83-101 in chia-query 0.24.1 shows exactly 4 fields (status, success, inclusion, error) — matches the PR's claim of no new field, so the sage::chain test's struct literal is not silently missing a field.

  7. FrameFanout::subscribe removal claim — confirmed FrameFanout/subscribe do not appear in chia-query's router.rs/lib.rs (i.e., not part of the public top-level surface any more), consistent with the "no-op, zero workspace references" claim; did not independently re-grep the whole dig-node workspace myself but the claim is plausible and low-risk (a removed unused re-export cannot regress silently — it would be a compile error).

  8. Evidence is real, not a filtered green. PR comment thread shows 3089 passed, 0 failed, 3 ignored, and each revert-proof is shown as 0 passed; 1 failed on the guard with the control staying 1 passed — the exact discipline CLAUDE.md's verification-index calls for (never trust a bare exit code / a count-free "green").

  9. §2.4b compliance: the one flagged non-latest dep (dig-node-control-interface "0.31" vs 0.32.1) is correctly NOT silently bumped in this PR (that bump would pull in a new control method with a serving obligation, which is legitimately out of scope) — split into a named child, dig-node#559, open. Good discipline, not deferred-and-forgotten.

Not independently re-verified (said plainly, per instructions)

  • Did not run the test suite myself in this worktree (relied on the posted CI evidence + my own read of the assertions, which is the standard second-order check for a fresh-context correctness gate).
  • Did not grep the entire dig-node workspace for FrameFanout/subscribe_frames myself — took the lane's "zero references" claim on directional trust given it's a mechanical, checkable-by-compiler claim (a false claim here would just be a compile error, not a silent defect).
  • 0.23.0 lineage-walk delegation to dig_chainsource_interface::resolve_singleton_lineage_via_walk — read the PR's own doc claim and the chain.rs diff context but did not pull dig-chainsource-interface 0.3.2 source to independently verify the fail-closed behaviour change. Lower risk: it's described as strictly safer (errs where it previously guessed), and gitnexus/socraticode blast-radius wasn't run for this (lane used grep+direct read, correctly disclosed per §2.0 fallback).

No CHANGES-REQUIRED findings. This is a well-documented, well-tested, honestly-scoped dependency adoption on a live money path, with the two hardest invariants (NC-12 plurality collapse, the push_tx race window) both freshly tested and independently confirmed against the real upstream source rather than taken on the PR's word.

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

IN PROGRESS — not the verdict

Item 2 (NC-12 plurality at corroborated_source.rs:309-323) — INDEPENDENTLY REPRODUCED, not just
read.

  • Confirmed the peak_height() split (Some => Ok(Some), None => Err(Transport(..))) was
    already present in the base commit (c0e35979 shows the identical body at line 316-323) —
    this PR adds tests, it does not change the guarded behaviour.
  • Cut my own worktree (C:\tmp\worktrees\dn-sec-audit-558, detached at 169e8df1), ran
    cargo test -p dig-wallet --lib corroborated_source::tests as a baseline: 5 passed, 0 failed,
    real test names printed (not a vacuous 0-match) — an_unsettled_peak_is_an_error_and_never_a_corroborated_absence
    and peers_that_agree_still_yield_a_peak_through_the_same_boundary both present and green.
  • Applied the exact revert-proof the PR claims: replaced the None => Err(ChainSourceError::Transport(...))
    arm with None => Ok(None) (the flattening described in the PR body) at
    crates/dig-wallet/src/sage/corroborated_source.rs:319-321.
  • Re-ran the same filter: an_unsettled_peak_is_an_error_and_never_a_corroborated_absence went
    FAILED, with the exact panic message the test itself documents ("no peer spoke ... got
    Ok(None)"). The control, peers_that_agree_still_yield_a_peak_through_the_same_boundary, stayed
    green — so the test discriminates on settlement, not on nothing, exactly as its own doc-comment
    claims.
  • Restored the original file (git status --porcelain clean in my own worktree) before moving on.

Verdict on item 2: the NC-12 plurality guard is real, was already load-bearing pre-PR, and is now
held by a test that a mechanical verifier (not just my reading) proves catches the described
regression.

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

VERDICT: PASS

Head SHA: 169e8df

Adversarial leg, dig-node PR #558. Base c0e3597.

Findings

  1. Fabricated empty coin set -- CLEAR. fallback.rs:513-517 coin_records_by_parent delegates directly
    to chia_query::get_coin_records_by_parent_ids; every hunk touching fallback.rs lands inside
    mod chain_failure_tests, so the peak-dating enforcement lives in the upstream chia-query 0.24.1
    crate, not new dig-node code.

  2. NC-12 plurality at corroborated_source.rs:309-323 -- INDEPENDENTLY REPRODUCED. I performed the
    revert-proof myself: replaced the None arm with Ok(None) in my own worktree, ran
    cargo test -p dig-wallet --lib corroborated_source::tests. Baseline: 5 passed. After the mutation,
    the unsettled-peak test FAILED with the exact panic text the test documents; the control test
    stayed green.

  3. mempool_refusal -- exactly one definition, faithful delegation, exact-match preserved. Diffed the
    removed BUNDLE_INTRINSIC_REFUSALS const and rationale out of chain.rs against the actual
    chia_query::mempool_refusal::BUNDLE_INTRINSIC_REFUSALS in the cached registry source for
    chia-query-0.24.1 -- same 11 names, same order, and is_bundle_intrinsic_refusal uses a trimmed,
    case-insensitive EXACT match, never a substring test. refusal_is_bundle_intrinsic in chain.rs
    delegates faithfully to it after applying its own local verdict/reason split. Grepped the whole
    repo for any second list -- none found; spend.rs's accepted_by_mempool is a different, unrelated
    concern and is not a rival.

  4. push_tx guard on the transport-failure path -- genuinely NOT discharged, and both doc sites now say
    so correctly. The entire diff to rpc.rs is documentation only -- filtering out comment and blank
    lines leaves zero changed lines. No logic changed.

  5. Fixture change -- legitimate API-delta accommodation, not a regression cover-up. The two
    previously-passing tests that needed a peak-state route added are accommodating a genuine new
    required call the router makes.

  6. Secrets. Grepped the full diff for mnemonic, seed-phrase, private-key, secret-key, PEM headers,
    xprv, master_sk patterns: zero hits.

PASS.

Additional checks

Not re-raised, confirmed still in place: corroborated_source.rs's coin_records_by_puzzle_hash,
coin_records_by_parent, and resolve_singleton_lineage refusals stay unsupported-errors -- diffed
byte-identical between base and head at that region.

Version pin: chia-query is pinned at 0.24.1 in dig-wallet's Cargo.toml, resolved to exactly 0.24.1 in
Cargo.lock with a matching checksum. A bare 0.24.1 requirement in Cargo excludes the 0.24.0 wedge
described. The chia-bls / chia-protocol / chia-consensus version sets in Cargo.lock are byte-identical
before and after this PR.

CI evidence: pulled the actual Test-plus-coverage job log (run 33917355050, job 101167502779).
Nextest's own summary line reads: 3099 tests run, 3099 passed, 3 skipped -- a real, fully-enumerated
run with individual PASS lines confirmed for the specific new and changed tests, not a
filtered-to-zero trap. This differs from the PR body's claimed 3089-passed figure, most likely because
CI runs the whole workspace across 54 binaries while the PR quoted a narrower local invocation --
noted for the record, not gated on, since the CI number is itself real and green. Clippy and Rustfmt
both green in the same rollup.

What I did not examine

  • Did not re-run the full workspace CI suite myself; relied on the actual CI job log for that
    breadth, and ran my own targeted compile/test/revert-proof only on the file the security claims
    hinge on (corroborated_source.rs).
  • Did not audit chia-query 0.24.1's own internals beyond mempool_refusal.rs and router.rs's push_tx
    retry logic -- treated the published, checksum-verified crate as the trust boundary this PR adopts.
  • Did not attempt a live-network or end-to-end reproduction of the underlying A stated refusal from the second push destination frees inputs the first may have admitted #460 race; verified the
    code path (transport-failure ladder unchanged) rather than the live race.

Method note

Own worktree at C:\tmp\worktrees\dn-sec-audit-558, cut from the exact head SHA and removed after use.
All reads of the primary modules/apps/dig-node checkout were git show / git diff against fetched
remote refs; no working-tree mutation of shared state at any point. Re-checked headRefOid after
finishing probes -- unchanged.

Every one of the six attack items resolves clean, two of them independently reproduced rather than
taken on the PR's word. No secrets, no custody or signing change, no widened matching semantics, no
fabricated absence, no plurality collapse.

@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review September 4, 2026 21:35
@MichaelTaylor3d
MichaelTaylor3d merged commit 2874bad into main Sep 4, 2026
16 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the loop/chq-adopt-dig-wallet branch September 4, 2026 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant