feat(wallet)!: adopt the current chia-query line and its shared mempool-refusal list - #558
Conversation
…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>
…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>
Verification, on
|
| command | result |
|---|---|
cargo test --workspace --all-targets --all-features --locked |
exit 0 — 3089 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 |
FAILED — 0 passed; 1 failed, message ... (got Ok(None)) |
peers_that_agree_still_yield_a_peak_through_the_same_boundary |
ok — 1 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
CoinPeerdoubles 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 open —
reconnect()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.
|
IN PROGRESS — not the verdict Item 1 (fabricated empty coin set) and item 5 (fixture change legitimacy) — both CLEAR.
|
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
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)
-
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) usesserve_routedwith ONLY theget_coin_records_by_parent_idsroute defined; the unroutedget_blockchain_statepath falls through toserve_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'sspent_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. -
corroborated_source.rs:277-area three-wayNone/Ok(None)/Errsplit is real and tested, not just documented. Read the full file:peak_height()(line ~278) mapsPeerCorroboratedReads::peak_height() -> NonetoErr(Transport(...)), neverOk(None). Two tests hold it (an_unsettled_peak_is_an_error_and_never_a_corroborated_absenceand its controlpeers_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 mereis_err(), so neither is satisfiable by a constant-output stub. -
Un-refusal deferral at
corroborated_source.rs:277(coin_records_by_puzzle_hash/by_parent, stillErr(Unsupported)) is correctly judged. VerifiedPeerCorroboratedReads'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.0SetAnswer/graded API, which lives onChiaQueryProvider/QueryRouterinstead. 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. -
push_txtwo-path behavior verified against actual chia-query 0.24.1router.rs:1177-1217. The transport-failure branch (line 1180) unconditionally falls to the unchangedpeer_then_coinsetladder — confirmed byte-for-byte the same call shape the PR's doc describes. The peer-answered branch classifies viamempool_refusal::is_final, does one bounded retry to one other peer, and never touches coinset — also confirmed. Bothsage/rpc.rsdoc sites (main doc comment ~2326, and the#460test 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. -
BUNDLE_INTRINSIC_REFUSALS— single definition confirmed.chia_query::mempool_refusal(fetched source) carries the full list + rationale byte-identical to what was deleted fromchain.rs.chain.rs'srefusal_is_bundle_intrinsicnow delegates viachia_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 pinsrefusal_reason's composition-stripping — including a negative control (DOUBLE_SPEND) and an emptiness guard on the canonical list, so it can't pass vacuously. -
TxStatusbyte-identical claim confirmed from upstream source:types/response.rs:83-101in chia-query 0.24.1 shows exactly 4 fields (status,success,inclusion,error) — matches the PR's claim of no new field, so thesage::chaintest's struct literal is not silently missing a field. -
FrameFanout::subscriberemoval claim — confirmedFrameFanout/subscribedo not appear in chia-query'srouter.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). -
Evidence is real, not a filtered green. PR comment thread shows
3089 passed, 0 failed, 3 ignored, and each revert-proof is shown as0 passed; 1 failedon the guard with the control staying1 passed— the exact discipline CLAUDE.md's verification-index calls for (never trust a bare exit code / a count-free "green"). -
§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_framesmyself — 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.
|
IN PROGRESS — not the verdict Item 2 (NC-12 plurality at
Verdict on item 2: the NC-12 plurality guard is real, was already load-bearing pre-PR, and is now |
VERDICT: PASSHead SHA: 169e8df Adversarial leg, dig-node PR #558. Base c0e3597. Findings
PASS. Additional checksNot re-raised, confirmed still in place: corroborated_source.rs's coin_records_by_puzzle_hash, Version pin: chia-query is pinned at 0.24.1 in dig-wallet's Cargo.toml, resolved to exactly 0.24.1 in CI evidence: pulled the actual Test-plus-coverage job log (run 33917355050, job 101167502779). What I did not examine
Method noteOwn worktree at C:\tmp\worktrees\dn-sec-audit-558, cut from the exact head SHA and removed after use. Every one of the six attack items resolves clean, two of them independently reproduced rather than |
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-walletfromchia-query0.21.0 onto 0.24.1 — the live wallet path — and adoptschia_query::mempool_refusal, deleting this crate's second copy of a money decision. dig-node0.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.1would 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
Vec-returning router methods became fail-closed wrappers over_gradedtwins;SetAnswer<T>has no absence armsage::fallback— the wallet's coin-discovery path. Two tests failed; a third was added.dig_chainsource_interface::resolve_singleton_lineage_via_walkChiaQueryProvider.mempool_refusalsubscriberemovalFrameFanout::subscribe(capacity)removedpush_txroutingSessionEndReason::OversizedFrameThe 0.22.0 delta, concretely
coinset_only_setnow 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_testsfixtures served one canned body for every path, so the newget_blockchain_stateread received a coin-records payload and the whole read becameAllSourcesFailed. They now use the module's existingserve_routed, with a peak far above every fixture record so the normalisation is a no-op — an as-of height below a record'sspent_block_indexwould 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_undatedpins 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
Err, never a fabricated emptyVec. All threesage::fallbackcall sitesmap_errtoError::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.chia-queryapplies 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.push_txguard is NOT discharged by the upstream retry. 0.24.x splitpush_txin two: when a peer answers, coinset is never consulted and the retry is bounded at one other peer; when the transport fails first, the oldpeer_then_coinset(peer, peer_retry, coinset)last-answer-wins ladder runs unchanged. The dig-node#460 race lives on that second path — arequest timed outraised after the bytes went out, then a peer that has since seen the gossip answeringDOUBLE_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.The invariant most at risk, and how it was verified
sage/corroborated_source.rsdocuments a deliberate split atpeak_height:PeerCorroboratedReads::peak_heightreturningNonemeans the peers did not agree, or too few spoke — an unknown, mapped toErr;ChainSource::peak_height'sOk(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
NoPeersharness plus a newAgreeingTipssample:an_unsettled_peak_is_an_error_and_never_a_corroborated_absence— zero peers issettled_peak(&[]) == None, and the source must answerErr.peers_that_agree_still_yield_a_peak_through_the_same_boundary— the control, asserting the lagged heightmin(claim) - SETTLED_LAGrather than the claimed tip, so a later "just return the max claim" cannot pass either.Revert-proved. Replacing the
None =>arm withOk(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-refusedThe graded set API 0.22.0 shipped lives on
QueryRouter/ChiaQueryand reaches aChainSourcethroughChiaQueryProvider.CorroboratedChainSourceis a different provider, backed byPeerCorroboratedReads— 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
TxStatusgained no field. Byte-identical struct region across v0.21.0 and 0.24.x; 4 fields both sides. The struct literal insage::chain's tests still compiles.BUNDLE_INTRINSIC_REFUSALShas exactly one definition, and it is upstream. The localconstand its ~60 lines of exclusion rationale are deleted;refusal_is_bundle_intrinsicdelegates tochia_query::mempool_refusal::is_bundle_intrinsic_refusal. It is not re-exported: apub(crate) usethat nothing in the crate reads is dead weight and-D warningsrejects 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 split —refusal_reason, the"{verdict}: {reason}"decomposition — because this crate builds the composition and chia-query classifies a bare reason.FrameFanout::subscriberemoval is a no-op. Zero references toFrameFanout,FrameSubscriptionorsubscribe_framesin the workspace.subscribe_store/unsubscribe_storeare dig-node's own store subscriptions, unrelated.run_update_loop(sync.rs:1274) is a future adopter, not a current caller.chia-protocol/chia-bls/chia-consensus0.36line and the samechia-wallet-sdk 0.36as 0.21.0. Verified fromCargo.lock: chia-query resolves once, and the newlineage-walkfeature ondig-chainsource-interface 0.3.2pulls only crates already on the 0.36 line.dig-chainsource-interfaceresolves 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-optionsare at latest through their carets.dig-node-control-interfaceis"0.31"against a latest of0.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 --check— exit 0, empty output.cargo test --workspace --all-targets --all-features --locked— the exact CI command. Result in a comment below.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.