fix(wallet): make the provider registry real, and stop reporting a third party's peak - #354
Conversation
ba325db to
4157700
Compare
loop-security — pre-merge audit STARTEDAuditing head Scope: full tier. NC-12 (untrusted dialled peers, agreement across independently-held sessions), the Findings will be posted to this PR as they form, followed by the verdict. Read-only: no merge, no |
SECURITY FINDING 1 — gap 2's guard is bypassable by the exact regression it namesAudited head: The defect
let mut in_tests = false;
for (ix, line) in text.lines().enumerate() {
if line.contains("#[cfg(test)]") { in_tests = true; }
if !in_tests && line.contains(CONSTRUCTOR) { sites.push(...); }
}The latch is set by the first
The doc comment states the opposite as its safety argument:
That claim is false for Measured, not reasonedI replicated Baseline: Then I applied the regression the guard exists to catch — a second, unregistered peer fabric built Both assertions still pass with a live second fabric in the transport. Exploit scenarioState: this PR merges; NC-12 clause 3 is recorded as a machine-enforced property. Cheap fixLatch only on a module-level if line.starts_with("#[cfg(test)]") { in_tests = true; }In Second, smaller scope hole (same finding, lower rank)The walk covers |
Finding 1 — cargo confirmation (not just the replication)Ran the real test, in my own worktree at The crate compiled with the second fabric — so this is production code, not a fixture — and both the |
SECURITY FINDING 2 — the two "independence groups" are not independent: BOTH answer from api.coinset.orgAudited head: The defectThe registry registers two providers in two independence groups:
chia-query defines an independence group as "sources that could fail or lie together — e.g. two views The chain, in chia-query 0.6.2:
pub async fn get_blockchain_state(&self) -> Result<BlockchainState, ChiaQueryError> {
// Try coinset first for full state.
if self.coinset_fallback_enabled {
if let Ok(state) = self.coinset.get_blockchain_state().await {
return Ok(state);
}
}
// Fallback: return a minimal state from the peer-tracked peak.
So the provider registered as this node's peers asks api.coinset.org first and consults peers only MeasuredI enabled the operator opt-in the module docs describe, as a one-line mutation in my own worktree: and ran the custody test against the fixture's
Exploit scenarioState: this PR merges. An operator (or a later lane) reads Two doc claims that are false as written
It does not invert it.
The classification asserted here is not the one the code produces. Why it is not live at this headTwo things hold it back, both of which should be stated rather than relied on:
Point 2 is worth sitting with, because the module doc says "A path that wants chain data asks it" and Recommended remedyPick one, and say which:
Either way the two doc claims above need to match the code. |
SECURITY FINDING 3 — "it never leads it" is false, and the peak path does not inherit the 6-of-10 analysisAudited head: The claim
Stated unconditionally. It holds only while the attacker cannot own the credibility band. MeasuredTwo probes added to
Both probes have been removed; Why this matters, in the words of your own SPEC
The second half: the published risk table does not describe this path
Nothing here is wrong in mechanism. The defect is that a reader of Recommended remedy (documentation + one constant decision, not a redesign)
|
loop-security VERDICT: CHANGES-REQUIREDHead audited: No LIVE vulnerability found. Nothing here lets a remote party move money, read a key, or reach a privileged action today. I am gating anyway, on two findings, because both are defects in the deliverable itself rather than incidental hardening: this PR's purpose is to convert a vacuously satisfied NC-12 clause into an enforced property, and as shipped the enforcement does not cover the regression it names, while the trust classification it publishes is factually wrong. Merging as-is records NC-12 clause 3 as satisfied when it is not. Both fixes are one line plus a doc correction. Direct answers to the three questions askedCan any single source still determine the peak? NO, and coinset is genuinely gone from that path. Verified by tracing every production route to Is gap 2 non-vacuous, can I make its test pass vacuously? YES, I made it pass vacuously. See finding 1. A second, live peer fabric compiled into Does the peak path depend on any stale count? NO. GATING1. 2. 3. NON-GATING (follow-up tickets, do not hold the merge)
Areas checked and CLEAR
Verification hygieneBaseline reproduced in my own worktree ( The shared |
Gate fixes landed — all three GATING findings, head
|
RE-GATE — the primary question, answered: the registry is an INVENTORY, not a controlHead audited: The fix lane's self-report is correct and I reproduce it independently:
What that means for the PR's stated purpose — split it in three, because the three parts differ
So the PR is not vacuous — but the thing doing the enforcing is the sweep, not the registry. NC-12 clause 3 That is a fine outcome. What is not fine is that two module-doc paragraphs assert the opposite, and both |
RE-GATE FINDING A — finding 1's SECOND hole is not closed. Measured: a production fabric at
|
| file | latch set at | total lines | net depth at EOF | why |
|---|---|---|---|---|
rpc.rs |
4783 | 10090 | +1 | :8233 be.dispatch("get_coins", "{ not json") |
tipping.rs |
1103 | 2039 | +2 | :1842 b"{ this is not valid json ]", :1938 b"{ not: valid" |
types.rs |
1899 | 2196 | +1 | :2106 r#"{"option_id":"abc","summary""# |
watchlist.rs |
254 | 382 | +1 | :367 b"{not json" |
autoseed.rs |
652 | 1144 | +1 | :963 b"{ not json" |
5 of 38 files. The cause is the ordinary "feed it malformed JSON" test fixture, which is a pattern this
crate uses everywhere and will keep adding. Every one of those files is blind from its first column-0
#[cfg(test)] to EOF, permanently, and nothing reports it.
The doc claim at sources.rs:407-408 is measurably false
"a stray unpaired
{inside a test module keeps the latch set past that module's end (silent
under-report) ... Neither shape exists in this crate today."
It exists in five files today. That sentence is the fix's own safety argument, and it is the same kind of
statement — a documented conservatism the code does not have — that made the original finding 1 a gating
defect rather than a nit.
Why the new regression test cannot see this
production_code_below_a_test_module_is_still_swept (:534-550) uses a five-line fixture whose braces are
perfectly balanced, so the latch clears and the test passes. The fixture cannot express the shape that
actually occurs in this crate. It proves the latch can clear, not that it does on any real file.
Exploit scenario
State: this PR merges; NC-12 clause 3 is recorded as machine-enforced.
Action: a later change adds a ChiaQuery::new in production code anywhere in rpc.rs below line 4783 — or
appends any new module to one of the five files below its test module. rpc.rs is the crate's largest
production file and this PR already edits it.
Impact: the node holds a second peer fabric the registry never names, outside the operator's provider
configuration, and the guard reports green. Identical end state to the defect this PR was gated on.
Cheap fixes, pick one
- Ignore braces in string literals and line comments — strip
//...and quoted spans before counting.
Still a heuristic, but it removes the idiom that actually occurs. - Clear the latch on the next column-0 non-
}line instead of on brace balance. A column-0 item after a
test module is unambiguously out of it, and needs no brace tracking at all. - Assert the residual — add a check that no file ends with the latch still set (that is precisely the
diagnostic that found this), and fix the five files or exempt them explicitly. Whatever else you do, the
claim at:407-408must stop asserting the shape does not exist, because it does.
Probe was run in my own worktree C:/tmp/worktrees/sec354rg and against a copy of the source tree in
C:/tmp/secregate354/. No file in the worktree, the shared checkout, or the lane's worktree was modified.
RE-GATE — findings 2 and 3 VERIFIED, and a byte-level regression proofHead audited: Regression risk is structurally zero, and that is cheap to proveEvery file the previous gate cleared is byte-identical to the head it cleared. So the corroborated-peak FINDING 2 — VERIFIED CORRECT, and the "structural" claim holdsChecked against the real
I also checked the classifier's OTHER direction, because a classifier that is wrong for The wiring test does fire under the revert. One residual, NON-GATING and not fixable in this repo. Nothing network-free pins that FINDING 3 — VERIFIED CORRECT, including the new normative paragraphThe new paragraph is the likeliest defect in a fix, so I checked each clause against the code rather than
Both new tests traced by hand against SPEC.md swept, not just edited. The |
loop-security RE-GATE VERDICT: CHANGES-REQUIREDHead audited: No LIVE vulnerability. Nothing here lets a remote party move money, read a key, or reach a privileged Direct answer to the primary questionThe independence grouping is NOT load-bearing on any production path. It is a declared inventory. That does not make the PR vacuous, and the distinction matters:
So the honest summary is: the sweep is the enforcement, and the registry gives the sweep its target. Finding GATINGA. Cause: B. This is the third instance of the class the lane swept for, and it is the strongest one. Two false doc claims I am not gating on NON-GATING (follow-up tickets — do not hold the merge)
Areas checked and CLEAR
Verification hygiene — read this, it bounds the findingsI could not run cargo. This machine is at 100% disk, 0 bytes free. So I did not reproduce the lane's 646 -> 653/0/1 baseline, cannot vouch for it, and did not identify the What I did instead, and why it settles finding A: I extracted Shared state. I created my own worktree |
Both re-gate findings fixed — head
|
loop-security re-gate (round 3) — interim, head
|
loop-security RE-GATE (round 3) VERDICT: CHANGES-REQUIREDHead audited: No LIVE vulnerability. Nothing in this delta lets a remote party move money, read a key, or reach a privileged action, and the delta changes zero production code — every non-doc changed line sits inside GATING A is fully cleared. GATING B is cleared as written. I am gating on one item: a measured, rustfmt-stable bypass of the new sweep that defeats both round-2 remedies at once, and the three sentences that assert it cannot exist. GATING —
|
836de62 to
3e026a8
Compare
Round-3 finding fixed, branch rebased — head
|
loop-security round-4 re-gate — IN PROGRESSHead being audited: Scope per §1.10: the round-3 MEDIUM only ( Verdict to follow on this PR. Worktree is my own ( |
loop-security round 4 — INTERIM finding (verdict still to come)Head: The round-3 finding IS fixed — I reproduced the shipped fixture's exact result. But the same probe The round-3 fix works (control)
That is the fixture's asserted The unnamed silent shape: a terminator carrying a trailing comment
Fixture text, dumped from the compiled probe source rather than retyped: The latch never clears at the module end, so every production line below is dropped — and because rustfmt preserves it byte-for-byte (rustfmt 1.9.0-stable), which is the part that matters: So The doc's own example does the opposite of what the doc says
Measured on that literal string: The reason is in That shape then latches and swallows a following multi-line item: This one IS fmt-suppressed — rustfmt splits Why this lands where round 3 landedNeither is reachable today: no The problem is the closed claim. The doc says The remedy here is not another heuristic. Stripping a trailing Verdict comment to follow once I have re-run the revert-proof, the blind-file count, the rebase lock |
loop-security RE-GATE (round 4) VERDICT: CHANGES-REQUIREDHead audited: No LIVE vulnerability, and the PR is strictly safer than main. The delta changes zero Everything the brief asked me to re-verify checks out — except the doc, which is where round 3 The round-3 finding IS fixed — measured, not acceptedRevert-proof re-run in my own worktree. Exactly the claimed result. Round 3's F1/F2/F4 are closed, and a standalone probe over the One qualification on the fixture, non-gating. It does assert both halves, as claimed — but the GATING —
|
| line | claim | measured |
|---|---|---|
:26 |
"sweep names every shape it mis-reads and the direction each one fails in" |
the trailing-comment terminator is unnamed; the one-line-attribute case is named with the wrong direction |
:508 |
heading: "The one shape that IS silent" | there are two, and the fmt-stable one is not the one named |
:501 |
"#[cfg(test)] fn f() { ChiaQuery::new(c); } fails here. Loud" |
sites=[] — silent |
:504 is what makes this structurally misleading: "A column-0 #[cfg(test)] item that never
appears to close is not silent either." True — but an item that appears to close late is
exactly the silent case, and it is the one left unlisted.
This is the paragraph NC-12's "Satisfied by" record is written from. Round 3 gated because it claimed
an unbounded property; that headline is now correctly bounded ("that its classifier can SEE",
:24) and I verified it is true. What remains is a closed enumeration claim that is not closed —
and it actively tells a maintainer who writes a commented terminator that the guard still sees below
it.
The remedy is the doc, NOT another heuristic
Stripping a trailing comment would be the brace-counting mistake in a third costume — it has its own
hole (a comment marker inside a literal) and would need a fifth round to characterise. Do not add it.
What closes this:
:508— retitle to the plural and add the trailing-comment terminator: a column-0 line that ends
an item but carries a trailing comment does not clear the latch, so the region below is dropped
silently, and it survivescargo fmt.:499-503— correct the example. The reported case is the item on the line after the attribute
(which the fixture pins); the attribute-and-item-on-one-line case is dropped, not reported, and is
fmt-suppressed.:26and:492— say the enumeration is the set of shapes known today, not "every shape".:758— "nothing was invisible" is delivered byunread.is_empty(), which only means no file
ran out of text while latched. That is narrower, and the shape above is precisely an invisible
region it does not flag. State the narrower thing.
No code change is required to discharge this, and none should be re-gated if none is made.
On the question the brief asked directly: leaving the column-0 #[cfg(test)]-as-string-content
shape unhandled but named, with its silence and its reason stated, is honest — that is the right
call, and it is not a hole wearing a disclaimer. A residual you name, whose direction you state, and
whose detection you decline for a stated reason, is a documented limit. The defect is not that shape;
it is that the enumeration around it claims to be complete when it is not.
Reachability today: NONE, verified rather than assumed. No dig-wallet/src file has a column-0
#[cfg(test)] carrying trailing content (zero hits across 43 column-0 attributes in 32 files), and
no column-0 closing brace carries a trailing comment — the only such hits are }; closing use
blocks, which are real terminators. Today's green is genuine. This is a latent hole plus a false
claim, which is why it is MEDIUM and not HIGH.
Areas checked and CLEAR
The rebase is exactly what was claimed — verified from the resolved lock, not the carets. The
branch's entire contribution to Cargo.lock versus f364d065 is two lines:
name = "dig-node-service" -version = "0.152.0" +version = "0.153.0"
name = "dig-wallet" -version = "0.34.0" +version = "0.35.0"
Zero dependency version lines moved. chia-query resolves to 0.6.2 on both sides and is
declared identically at crates/dig-wallet/Cargo.toml:94 on both sides; dig-node-core is 0.57.2
on both sides, so the conflict really was resolved to main's value, twice. The only manifests in the
delta are Cargo.toml (workspace 0.153.0) and crates/dig-wallet/Cargo.toml (0.35.0).
Nothing this audit or the previous three rested on can re-resolve. I also diffed the branch's
whole pre-rebase contribution (dffb264..836de62) against its post-rebase contribution
(f364d065..3e026a8): the only content differences are those two version bumps, hunk-offset shifts
in SPEC.md, and the sources.rs growth that is the fix. The rebase moved nothing else.
The 5-blind-files retraction is correct — confirmed independently. I recovered the pre-fix
classifier with git show b0e5fb1:crates/dig-wallet/src/sage/sources.rs, extracted brace_balance
and production_lines verbatim with sed (both arms present, including the one_liner_ended arm the
lane had dropped), and walked the real tree:
files scanned: 38
PRE-FIX blind files (5): ["autoseed.rs", "rpc.rs", "tipping.rs", "types.rs", "watchlist.rs"]
Exactly the retracted list. peer_reads.rs was never blind, so the corroborated-peak path was
never exposed. The shipped docs say five in three places (:463, :486, :696) and name the right
five at :627-628 — the durable artifact was always accurate; only the earlier PR comment
overstated, and it has been retracted on the record.
Baseline reproduced independently, every gate UNPIPED, read from the test result: line.
cargo test -p dig-wallet --lib EXIT=0
test result: ok. 655 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
cargo fmt --all -- --check EXIT=0 (zero output)
cargo clippy -p dig-wallet --lib --tests -- -D warnings EXIT=0 (zero error/warning lines)
The single ignored test is pre-existing on MAIN, not introduced here.
sage::sync_supervisor::tests::live_mainnet_default_install_corroborates_and_follows_the_chain, the
only #[ignore] in the crate. Byte-identity by blob hash rather than by an empty diff —
6e8b751972705fd48a1caefa4c9c035046b03756 at 41577001, 836de62, 3e026a8 and f364d065.
Nothing was ignored to make this suite green.
Custody, secrets, dependencies, exposure. No key, seed or signing capability touched. No
credential, token, .test-credentials value or projectId added, logged or printed. No new endpoint,
RPC, permission or remote entry point — nothing in the delta is reachable by any peer, so the authz
and amplification questions do not arise for it. No pin loosened: the only .toml/.lock lines in
the delta are the two version bumps above. Version-increment gate satisfied (0.153.0 over main's
0.152.0; dig-wallet 0.35.0 over 0.34.0; manifest and lock agree).
All 15 required checks report SUCCESS at this head (one SKIPPED: "Attach packages to the
release"), mergeable: MERGEABLE, still DRAFT — correctly, since a gate round was in flight.
NON-GATING (do not hold the merge on these)
- The new fixture's flag assertion is non-discriminating for the acceptance revert — the flag is
falsein both the fixed and the reverted state, so only the sites assertion fires. Correct as
documentation; not a second independent proof. :758's "nothing was invisible" overstates whatunread.is_empty()delivers, which is only
no file ran out of text while latched. Folded into remedy 4 above.an_indented_cfg_test_does_not_blind_the_sweepand
a_stray_brace_in_a_test_fixture_does_not_hide_the_production_code_below_itstill discard the
flag — round 3's non-gating note, unchanged, still harmless.dig-gossip v0.25.0resolves from a git source, not crates.io (NC-7). Pre-existing on main,
untouched by this delta, out of scope for this re-gate — noted only so it is not lost.
Verification hygiene
Everything ran in my own worktree, C:/tmp/worktrees/sec354r4, cut with
git worktree add --detach at 3e026a8. Final state:
HEAD=3e026a8d18953d58d6ecd07ceab1e43ab19431e9, git status --porcelain empty, git clean -nd
empty, no local commits, no stash created by me (the three present are pre-existing and belong to
other branches). Two source mutations — the acceptance revert and the trailing-comment probe test —
were applied there, measured, then restored from a pristine copy taken before each; both restorations
verified against the HEAD blob.
The shared dig-node checkout was never mutated — no checkout, reset, stash or edit; all PR content
read via git show, git diff, git rev-parse and gh.
C:/Users/micha/workspace/dig_network/worktrees/dig-node-2790 and C:/tmp/worktrees/fix354 were not
touched, and no target/ outside my own worktree was deleted. One cargo process at a time. Disk
stayed at 199 GB free. I did not merge, undraft, or edit any code on this PR.
Round-4 finding fixed, doc-only — head
|
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
CHANGES-REQUIRED — round 5, doc-only scope
Head reviewed: c5aad5338ec334119643b52f0642ff9cfd4a9bb3 (resolved from the remote at audit time, not taken from the dispatch).
Recorded as a COMMENT review: a verdict-bearing event is HTTP 422 on a same-identity PR. The open inline thread below is what bars the merge.
What I verified, by measurement rather than by reading the prose
Diff 3e026a8d..c5aad533 is one file, +40/-16, doc plus one test comment - zero production lines. Confirmed.
I re-ran every inline number in the doc against the real compiled sweep, via probes appended to mod sole_owner_tests in my own worktree:
| doc claim | measured |
|---|---|
:501 attr on its OWN line, one-line item + ctor, production line below -> sites=[2, 3] |
sites=[2, 3] flag=false PASS |
:501 attr INLINE with the item, ctor on it -> sites=[2], construction dropped |
sites=[2] flag=false PASS |
:508 bare terminator control -> sites=[5] |
sites=[5] flag=false PASS |
:508 } // done terminator -> sites=[] |
sites=[] flag=false PASS |
string-content attribute, bound evidence -> sites=[7] |
sites=[7] flag=false PASS |
The :508 control pair genuinely discriminates - bare terminator reports the production site, commented terminator reports nothing, on the identical fixture. The :501 correction is now the right way round: the inline-attribute form latches on its own line, and a latching line is never passed to ends_a_column_0_item, so its construction is correctly dropped as test code.
:26 - the retraction is a real sweep, not a spot edit. grep -n "LOUDLY" over the file returns one hit, the qualified form at :494 ("fail LOUDLY wherever that is achievable without parsing"). The unqualified "every imprecision fails LOUDLY" survives nowhere.
The new bound claim holds. The mis-latch does clear, and the site after the window is still reported. Better than the doc states: the truly-unbounded case is LOUD - sites=[] flag=true, the refusal fires. One sizing caveat in a non-gating note below.
Gates, re-measured at this head
cargo test -p dig-wallet --lib->test result: ok. 655 passed; 0 failed; 1 ignored(parsed from thetest result:line, run unpiped). Reproduces the stated baseline exactly.cargo fmt --all --check-> RC 0.cargo clippy -p dig-wallet --lib --all-features-> 0 errors.check-merge-preconditions.sh-> all five required contexts SUCCESS by name (Lint commit messages, Check version increment, Rustfmt, Clippy, Test + coverage),unresolvedReviewThreads=0,mergeStateStatus=CLEAN, BLOCKED ondraft=truealone -> GREEN-BUT-DRAFT.- Rebase/version re-measured myself:
origin/main=f364d065,git merge-base --is-ancestor origin/main HEADpasses, workspace 0.153.0 vs 0.152.0,dig-wallet0.35.0 vs 0.34.0. All current.
The one gating finding
A comment ADDED in this round claims a live assertion is non-discriminating. It is not - measured flag=true under the revert the test is named for. Detail and reproduction are in the inline thread. It is a small, precise fix, and it must not be "fixed" by deleting the assertion.
Everything else in this round's doc delta is true as written.
|
Fixed in So the assertion FIRES under revert A — the one this test is named for, and the one my own doc at The comment now names which revert is which, carries both measurements inline (the standard every other claim in this file already meets), and records WHY the correction exists: a comment that calls a working assertion "documentation" is how a working assertion gets deleted. The assertion itself is untouched. Also took the non-gating note. "Bounded" now says explicitly that it is not the same as small — a false latch on string content nested in an |
…fabric NC-12 asks that no path construct its own peer fabric outside the registry. That clause held for the worst possible reason: there was no registry. `ProviderRegistry` had no production construction site in dig-node, so the property was satisfied by the absence of the thing it governs. `NodeChainSources` is now the sole production caller of `ChiaQuery::new`, and it registers what it built: the node's own peers ahead of the public oracle, in separate independence groups, both untrusted, with public-quorum custody off so the custody view fails closed on a default install. Refs: DIG-Network/dig_ecosystem#2790
A revert-proof run showed all three tests failing on the VALUE assertion, so the assertion that pins the oracle out of the path was never evaluated and proved nothing. Reordered, the same revert fails each test on the placement assertion by its own message. Refs: DIG-Network/dig_ecosystem#2790
Also bumps the workspace to 0.151.0 and dig-wallet to 0.34.0: a compatible new capability, with one behaviour change on a chain read that a caller can only observe as an honest unknown where it previously got a third party's number. Refs: DIG-Network/dig_ecosystem#2790
The rebase onto main silently absorbed the earlier bump: dig-node#344 landed 0.151.0 / 0.34.0, the exact values this branch had written, so both sides wrote something plausible and git resolved it with no conflict. The increment gate caught it, not a reviewer. Refs: DIG-Network/dig_ecosystem#2790
The `in_tests` latch tripped on the FIRST `#[cfg(test)]` anywhere in a file and never cleared. chain.rs gates a test helper inside `impl ChainTransport` at line 157, so lines 158-711 — `peak_height()`, `push()`, the `ChainFallback` impl — were invisible to the sweep, and a live second peer fabric compiled into `ChainTransport::peak_height` left the guard green. Latch only on a column-0 attribute, and clear the latch when the item it introduced ends. Documented as the heuristic it is, with each imprecision's failure direction named. Two fixture tests pin the classification. Co-Authored-By: Claude <noreply@anthropic.com>
… type `ChiaQueryProvider` reads through chia-query's router, whose first act is "try coinset first for full state" with `coinset_fallback_enabled: true` in the default config the fabric is built from. Registering it as its own independence group made a 2-of-2 independent-group custody quorum satisfiable by one HTTPS endpoint: measured on a `max_peers: 0` client — a node holding no peers at all — the custody view returned a peak. Derive the group from the config the fabric was built with, so a fabric that can fall through to the oracle shares the oracle's group and only a peers-only fabric counts as independent. A client handed in from outside describes nothing about what it can reach and is grouped conservatively. Corrects the two module doc claims the code contradicted. Co-Authored-By: Claude <noreply@anthropic.com>
…onal one "it never leads it" was stated without qualification and is false: eligible keeps claims within PEAK_LAG_TOLERANCE of their own MEDIAN, so claimants who are a majority of those who answered own the band, evict the honest claims and place the result where they like — measured leads of +998 and +498. State the weaker true property: the result is the MINIMUM of the credible claims, so it cannot lead the tip while any credible claim is honest. Pinned from both sides — an outvoted liar cannot move it, a colluding majority can. Also record that settled_peak runs on the QUORUM_SAMPLE-wide held sample and never calls hold_best, so band_kept_a_majority's 6-of-10 table describes a different path; and restore common_height's doc comment, which the settled_peak block had been inserted in front of. Co-Authored-By: Claude <noreply@anthropic.com>
…literals
The latch cleared on brace BALANCE, and `brace_balance` counted braces inside
string literals. The crate's ordinary malformed-JSON fixture — `"{ not json"`
at rpc.rs:8233 and four more — left the balance permanently positive, so five
of 38 files were blind from their first column-0 `#[cfg(test)]` to EOF. A
column-0 production fn appended to rpc.rs passed the sweep.
Clear on the next column-0 item end instead: a bare `}` or a `;` at column 0.
Literal content is almost always indented, so refusing to look at it is both
simpler and stricter than parsing it out.
The sweep now also reports when it ran out of text still inside a
`#[cfg(test)]` item, and the assertion REFUSES on such a file. When the
classifier cannot tell, it says so rather than returning an empty stray list
that reads identically to a clean file.
The regression test's fixture is widened to carry the unbalanced brace inside
a string; the balanced one could not express the shape at all.
Co-Authored-By: Claude <noreply@anthropic.com>
…stry The module doc claimed the registry was LOAD-BEARING and that its custody view refuses "on a default install", which reads as a gate in a live path. It is not one: NodeChainSources::registry() has no production caller — ChainTransport asks only for client(), and nothing calls trusted() or any(). NC-12 clause 3's "Satisfied by" record gets written from this module doc, so an overstatement here becomes the ecosystem's record of a discharged obligation. State the true, weaker thing: the sole-owner sweep is the enforcement, the registry gives that sweep its target, the classification is a correct inventory awaiting its first consumer, and the read that genuinely removed the third party is the corroborated peak next door. Co-Authored-By: Claude <noreply@anthropic.com>
ends_a_column_0_item cleared only on a line that IS `}`, so a column-0 item
written on one line — `fn helper() {}`, `mod probe {}`, `impl X {}` — never
cleared the latch. That shape defeated both of the previous round's remedies at
once: the sweep went blind for the rest of the file AND ended_inside_a_test_item
stayed false, so nothing reported it. rustfmt preserves such one-liners, so
cargo fmt does not suppress it. No file in this crate has the shape today.
Accept any column-0 line ending in `}` or `;`, and judge the clearing line like
any other line, so a constructor written ON a one-line test item is reported as
production rather than vanishing with the line that cleared the latch.
Bound the three sentences that claimed the sweep catches a second fabric
"anywhere in this crate": a green means no VISIBLE second owner and nothing
invisible, which is the strongest claim a source sweep can make. The one
remaining silent shape — a column-0 `#[cfg(test)]` as string content — is named
with its failure direction rather than left out.
Co-Authored-By: Claude <noreply@anthropic.com>
… complete
Three statements were measurably false, and the worked example was backwards.
The sweep doc claimed every imprecision fails loudly and that one shape is
silent. Two are: a column-0 `#[cfg(test)]` appearing as string content, and a
terminator carrying trailing content (`} // done`), which ends_a_column_0_item
does not recognise because it judges the raw line. Measured: the same fixture
gives sites=[5] with a bare `}` and sites=[] with the comment, flag false in
both. rustfmt preserves the trailing comment.
The one-line-item bullet claimed `#[cfg(test)] fn f() { ChiaQuery::new(c); }`
fails loudly. It does not — a latching line is never passed to
ends_a_column_0_item, so that construction is dropped as the test code it is.
The loud case is the attribute on its own line with the item below, measured
sites=[2, 3]. Both forms are now stated with their measurements.
No fourth heuristic. Four rounds found four shapes, each invisible to the
fixture written for the one before it, so the list is documented as OPEN and
the module doc no longer implies otherwise. What bounds the damage is stated:
a mis-latch lasts only to the next column-0 terminator, not to EOF.
Also notes that the one-line fixture's flag assertion documents rather than
catches — it is false in both states.
Co-Authored-By: Claude <noreply@anthropic.com>
…mentation The comment added last round said ended_inside_a_test_item is false before and after the one-line-item fix and that the assertion documents rather than catches. That is true only of the narrower revert. Measured on that fixture against the compiled sweep: revert A (require a bare } again, the fix this test is named for): sites=[] flag=true revert B (continue past the clearing line): sites=[4] flag=false Under A the assertion FIRES — the latch never clears, the sweep reaches EOF still inside the test item, and the fail-closed flag is what says so. Both measurements now sit inline, which is the standard every other claim in this file already meets. A comment that calls a working assertion documentation is how a working assertion gets deleted, so the correction is recorded rather than just applied. Also: bounded is not small. A false latch on string content nested in an impl holds for the whole block — 40 constructors dropped in a 130-line fixture — and the genuinely unbounded case is the loud one, since a latch that never clears reaches EOF and the refusal fires. Co-Authored-By: Claude <noreply@anthropic.com>
main took 0.153.0 independently (PR#362), so the branch's own 0.153.0 bump was dropped by the rebase as already upstream and the version stopped incrementing. dig-wallet stays 0.35.0, above main's 0.34.0; dig-node-core is main's 0.58.0. Third time this branch's bump has gone stale across a gate round — re-check against origin/main before every push, not only at open. Co-Authored-By: Claude <noreply@anthropic.com>
b32d523 to
b1d3381
Compare
Round-5 finding fixed, thread resolved — head
|
The version-increment gate held both numbers and reported only pass/fail. A stale bump is invisible everywhere else -- `git status`, the diff, fmt, clippy and the test suite are all green when `main` has taken your version number, and a rebase can drop the bump commit entirely as "already upstream". It went stale three times on one branch (PR #354) across a five-round gate, and each time the lane had to rediscover at merge what the comparison even was. Writes a table to `$GITHUB_STEP_SUMMARY` on EVERY run, success or failure: the base ref actually compared (short SHA of `main`, plus the head SHA), a row per manifest with base, head and verdict, the must-match row where both exist, and the overall verdict spelled out. Being able to READ the comparison while the gate is green is the point -- that is what turns a merge-time surprise into something a long-lived review round can notice. What the gate ENFORCES is unchanged: same predicate, same exit code, same error annotations. `${GITHUB_STEP_SUMMARY:-/dev/null}` keeps the script runnable off Actions, which is how the three cases below were exercised. Bounded to dig-node deliberately (#364, CLAUDE.md 1.3c rule zero). 25 other repos carry a byte-identical copy and 6 carry a diverged one; propagating is a named follow-up with that finite list, not "every repo". Verified by running the extracted script unpiped against scratch repos: passing (RC=0), the stale-bump failure with `main` holding the same version (RC=1), and the both-manifests-agree case (RC=0) -- each producing its summary. Closes #364 Co-Authored-By: Claude <noreply@anthropic.com>
The version-increment gate held both numbers and reported only pass/fail. A stale bump is invisible everywhere else -- `git status`, the diff, fmt, clippy and the test suite are all green when `main` has taken your version number, and a rebase can drop the bump commit entirely as "already upstream". It went stale three times on one branch (PR #354) across a five-round gate, and each time the lane had to rediscover at merge what the comparison even was. Writes a table to `$GITHUB_STEP_SUMMARY` on EVERY run, success or failure: the base ref actually compared (short SHA of `main`, plus the head SHA), a row per manifest with base, head and verdict, the must-match row where both exist, and the overall verdict spelled out. Being able to READ the comparison while the gate is green is the point -- that is what turns a merge-time surprise into something a long-lived review round can notice. What the gate ENFORCES is unchanged: same predicate, same exit code, same error annotations. `${GITHUB_STEP_SUMMARY:-/dev/null}` keeps the script runnable off Actions, which is how the three cases below were exercised. Bounded to dig-node deliberately (#364, CLAUDE.md 1.3c rule zero). 25 other repos carry a byte-identical copy and 6 carry a diverged one; propagating is a named follow-up with that finite list, not "every repo". Verified by running the extracted script unpiped against scratch repos: passing (RC=0), the stale-bump failure with `main` holding the same version (RC=1), and the both-manifests-agree case (RC=0) -- each producing its summary. Closes #364 Co-Authored-By: Claude <noreply@anthropic.com>
The version-increment gate held both numbers and reported only pass/fail. A stale bump is invisible everywhere else -- `git status`, the diff, fmt, clippy and the test suite are all green when `main` has taken your version number, and a rebase can drop the bump commit entirely as "already upstream". It went stale three times on one branch (PR #354) across a five-round gate, and each time the lane had to rediscover at merge what the comparison even was. Writes a table to `$GITHUB_STEP_SUMMARY` on EVERY run, success or failure: the base ref actually compared (short SHA of `main`, plus the head SHA), a row per manifest with base, head and verdict, the must-match row where both exist, and the overall verdict spelled out. Being able to READ the comparison while the gate is green is the point -- that is what turns a merge-time surprise into something a long-lived review round can notice. What the gate ENFORCES is unchanged: same predicate, same exit code, same error annotations. `${GITHUB_STEP_SUMMARY:-/dev/null}` keeps the script runnable off Actions, which is how the three cases below were exercised. Bounded to dig-node deliberately (#364, CLAUDE.md 1.3c rule zero). 25 other repos carry a byte-identical copy and 6 carry a diverged one; propagating is a named follow-up with that finite list, not "every repo". Verified by running the extracted script unpiped against scratch repos: passing (RC=0), the stale-bump failure with `main` holding the same version (RC=1), and the both-manifests-agree case (RC=0) -- each producing its summary. Closes #364 Co-Authored-By: Claude <noreply@anthropic.com>
…, executed sweep examples (#368) * chore(wip): batch #360 #361 #363 #364 #367 — stub for lane ownership Co-Authored-By: Claude <noreply@anthropic.com> * refactor(wallet): delete the dead oracle-first ChainTransport constructor `ChainTransport::with_sources` had no callers anywhere in the workspace, was `pub` + `#[must_use]`, and built `peer_reads: None` — a transport whose `peak_height` takes `chia-query`'s router path, which asks `api.coinset.org` FIRST. It read like the obvious way to build a transport over the node's own fabric while quietly being the one shape NC-12 exists to prevent: the node's headline chain fact decided by a single third party. Deleted rather than repaired: a constructor kept "in case someone needs it" is how it comes back, and the only production construction (`sage/service.rs:173`) already chains `with_peer_reads`. Makes the deletion durable with a fail-closed source guard: the constructors that may build `peer_reads: None` are a CLOSED enumeration of two (`new`, and the `#[cfg(test)]` `with_client`), and a third one fails the suite by name. Proved load-bearing by reintroducing `with_sources` — the guard reports it. Closes #360 Co-Authored-By: Claude <noreply@anthropic.com> * fix(test): stop the serve harness leaking a ~57 MB temp tree per node `content_serve.rs` built its temp path by hand -- `env::temp_dir().join(format! ("dig-node-serve-test-{pid}-{n}"))` -- and nothing ever removed it. Each node seeds a real compiled `.dig` module and warms a cache, so a run cost ~57 MB per node. 1,123 trees reached 62.5 GB and took the dev machine to 81 MB free on a 1.9 TB disk, producing a machine-wide ENOSPC that stopped an unrelated lane mid-build. It is self-concealing: it grows fastest when the suite runs most, so it reads like a build-cache problem (the first diagnosis blamed worktree `target/` dirs, which were not the cause). - A `NodeCache` RAII guard OWNS the tree (`tempfile::TempDir`), so removal happens in `Drop` -- including on an unwind. Ownership is the point, not the deletion: a cleanup line at the end of a test is skipped by every failing assertion, i.e. exactly the runs a developer repeats. - Both leaking sites in the file are covered (`dig-node-serve-test-*` and `dig-node-origin-test-*`); the latter now shares the guard. - The node's detached axum task still holds `wallet.sqlite` open when the test body returns, and Windows will not unlink an open file, so `TempDir::drop` cannot remove that last ~1 MB. `Drop` cannot cancel the task (it cannot `.await`), so the residue is BOUNDED rather than pretended away: a once-per- process sweep removes trees idle for 15 minutes, which no live run can be. Measured, same machine, full suite: per-run residue 970 MB -> 17 MB, and the 62.5 GB accumulation is gone. Two tests hold it: one asserts the tree is gone after a normal drop AND after a `catch_unwind` panic; one asserts the sweep removes an abandoned tree while sparing a live one and a stranger's directory. Each was proved load-bearing by reverting only its own fix. Closes #361 Co-Authored-By: Claude <noreply@anthropic.com> * ci: report the version comparison the gate made, on every run The version-increment gate held both numbers and reported only pass/fail. A stale bump is invisible everywhere else -- `git status`, the diff, fmt, clippy and the test suite are all green when `main` has taken your version number, and a rebase can drop the bump commit entirely as "already upstream". It went stale three times on one branch (PR #354) across a five-round gate, and each time the lane had to rediscover at merge what the comparison even was. Writes a table to `$GITHUB_STEP_SUMMARY` on EVERY run, success or failure: the base ref actually compared (short SHA of `main`, plus the head SHA), a row per manifest with base, head and verdict, the must-match row where both exist, and the overall verdict spelled out. Being able to READ the comparison while the gate is green is the point -- that is what turns a merge-time surprise into something a long-lived review round can notice. What the gate ENFORCES is unchanged: same predicate, same exit code, same error annotations. `${GITHUB_STEP_SUMMARY:-/dev/null}` keeps the script runnable off Actions, which is how the three cases below were exercised. Bounded to dig-node deliberately (#364, CLAUDE.md 1.3c rule zero). 25 other repos carry a byte-identical copy and 6 carry a diverged one; propagating is a named follow-up with that finite list, not "every repo". Verified by running the extracted script unpiped against scratch repos: passing (RC=0), the stale-bump failure with `main` holding the same version (RC=1), and the both-manifests-agree case (RC=0) -- each producing its summary. Closes #364 Co-Authored-By: Claude <noreply@anthropic.com> * test(wallet): execute the sweep guard's worked examples instead of asserting them A doc example in `sources.rs` was exactly backwards -- it claimed the attribute-inline shape failed loudly, when it measures `sites=[2]` and drops the construction -- and it survived FOUR adversarial gate rounds. Everything else in that file was measured; only the examples were prose, and that is where the false statement lived. Reviewers read a doc example as documentation rather than as a claim to test, so the remedy is to make the claim executable. The worked examples are now `json` blocks carrying their fixture AND their expected `sites` / `ended_inside_a_test_item`, and a test extracts every one of them from this file's own source and runs the real classifier over it. Doing this as a rustdoc doctest is not possible and would have been worse than prose: rustdoc does not run doctests on `#[cfg(test)]` items, and this whole module is one, so a ```rust example would compile in nobody's build while reading exactly like a passing test. Fail-closed twice over: an unterminated block is an error, and the example COUNT is pinned, so deleting an inconvenient example -- the cheapest way to green -- fails rather than passing quietly. Found while doing it: the trailing-comment example asserted `sites=[5]` and `sites=[]` for "the same fixture" WITHOUT ever writing that fixture down, so its numbers were unreproducible by construction. The fixture is now explicit and the measured values are `[6]` and `[]`. A claim whose input is missing cannot be checked by anyone, which is worse than a claim that is merely wrong. No classifier heuristic is added. Five rounds established that the durable artifact is the fail-closed flag plus an honestly open enumeration, and `sweep` is untouched -- only the file's statements ABOUT it are now enforced. Each of the four examples was proved load-bearing: altering any expected value, and flipping any documented flag, turns the suite red naming that example. Closes #363 Co-Authored-By: Claude <noreply@anthropic.com> * chore(wallet): bump dig-wallet to 0.36.0 for the removed pub constructor `ChainTransport::with_sources` was `pub`, so deleting it is a breaking change to this crate's surface even though it had no callers. SemVer minor on 0.x. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
Closes #249
Parent: https://github.com/DIG-Network/dig_ecosystem/issues/2790 (NC-12, gap 2)
Direct answers to the three questions
1. Coinset is DEMOTED TO A REGISTERED DISCOVERY SOURCE. It is neither the peak's source nor its
corroborator, and it is not removed.
production transport —
ChainTransport::peak_heightnever callschia-query's router, sorouter.rs:711-717's coinset-firstget_blockchain_stateis never reached for this read.untrusted peers would let it decide the number at exactly the moment corroboration failed — the
single-source dependency in its most dangerous form, not its mildest.
PublicOraclein its own independence group, at a priority BEHINDthis node's peers, so non-custody discovery reads can still use it and the operator can still see
and reach it. Deleting it would remove a source the registry exists to name.
chia-queryitself is not modified. Invertingrouter.rsthere is a cross-repo release-firstcascade; the node-side fix needs no release.
What the node reports when peers disagree: NO HEIGHT.
control.wallet.peakanswerspeak_height: null, which the SPEC already defines as UNKNOWN and forbids reading as height zero.No repaired value, no plurality-wins arm, no fallback. The same applies to a sample that has
collapsed to one voice. The height when they DO agree is
min(credible claims) − SETTLED_LAGoverclaims within
PEAK_LAG_TOLERANCEof the median — so it can lag the true tip and can never leadit, which is the safe direction for every confirmation comparison built on it.
2. Yes — and the proof is that a path building its own fabric turns something RED, not that a
registry exists.
sole_owner_tests::only_the_registry_owner_constructs_a_peer_fabricsweeps every productioncall site of
ChiaQuery::newin the crate and fails on any outsidesources.rs. It was REDwhen written, naming
["chain.rs:131"]— the exact path that falsified the clause. Add a secondfabric anywhere in dig-wallet today and that test fails by name and line.
Two guards against the vacuity this ticket is about:
the_sweep_can_find_a_construction_site_at_all) fails if the sweep finds zeroChiaQuery::newanywhere, so a broken file walk or a renamed constructor cannot make it pass bymeasuring nothing;
What it catches that today's code would not: an ornamental registry — constructed, never
consulted — satisfies NC-12's wording exactly as vacuously as no registry at all. That is what
custody_fails_closed_testspins: the custody view must refuse with the variantChainSourceError::NoProvider, while the discovery view must not beNoProvider. The pairseparates "populated registry that refuses custody" from "empty registry" — and from "registry
that would happily accept the oracle for custody and merely could not reach it", which any
is_err()assertion would have passed.This is deliberately the dig-node#356 standard: not a doc comment asserting the property, but a
one-line violation that turns a test red. The peak half meets the same bar — reverting only the
placement fails all three transport tests by their placement message.
3. #351 does NOT block this, and this change does not depend on the number it mis-reports.
chia_peer_countcomes fromchia_query::ChiaQuery::peer_count()— the fabric's pool size, on adifferent accessor and a different path from anything here. Nothing in this change reads it.
The distinction that matters: the corroborated peak counts claims actually received in this
round, one per peer that answered, not a reported pool size. So a count stuck at 5 while the node
holds zero cannot inflate the plurality — a node in that state receives no claims, and
settled_peakreturnsNone.three_silent_peers_leave_one_voice_and_that_is_not_agreementisexactly that case pinned as a test: four peers drawn, one speaking, refusal.
#351 remains a real defect on its own surface, and its wider point stands — it is why the plurality
here is evidenced by received claims rather than by any reported count.
The problem, stated precisely
NC-12's third acceptance clause reads "no path constructs its own peer fabric outside the
registry". It held because there was no registry.
chia_query::provider_registry::ProviderRegistryhad no production construction site anywhere in dig-node — before PR #339 or after — so the clause
was satisfied by the absence of the thing it governs. A clause that passes because its subject does
not exist is not a property, and it must not read as discharged.
Second, and separately: the node's headline chain fact was a third party's notion.
chia-query'srouter.rs:711get_blockchain_statetriesapi.coinset.orgFIRST and consults this node's ownpeers only when that fails, so
ChainTransport::peak_heightreturned one HTTPS endpoint's view ofthe chain even on a node holding five peers — and that number divides into the confirmation counts
served over RPC.
The shape, and why
Unify the OWNER, keep the VOICES.
1.
dig_wallet::sage::sources::NodeChainSourcesis now the only production caller ofChiaQuery::newin dig-wallet, and it registers what it built:DigPeers(→ untrusted)chia-peersPublicOracle(→ untrusted)coinset.orgallow_public_quorum_custodyis left off, so the custody view fails closed on a defaultinstall: no public oracle and no randomly dialled peer may decide where money goes merely by
answering first.
ChainTransportholds the sources and reads through them.The registry is built per call rather than held —
ProviderRegistrycomposesdyn ChainSourceProvider, which chia-query does not boundSend + Sync(its providers are ablocking facade by design), so a field would make
NodeChainSourcesnon-Syncand infect everyasync caller. Building it boxes two wrappers around the fabric that already exists and dials nothing.
2.
ChainTransport::peak_heightstops going through the router. It asks the peers this nodedialled itself, concurrently, and settles them with a new
quorum::settled_peak.The reported height is
min(credible claims) − SETTLED_LAG, over claims withinPEAK_LAG_TOLERANCEof the median (a median cannot be moved by one outlier; a maximum can, and is the single most
attacker-friendly aggregate available). Quorum floor is
CORROBORATION_FLOORclaimants; a bandfailing
band_kept_a_majorityis a split. On a split, or a collapse to one voice, the node reportsno height — never a repaired number and never a fallback to the oracle, because falling through
would let one endpoint overrule the peers at exactly the moment corroboration failed.
It is deliberately not the tip. It is a height every credible peer has passed, so it can lag by
a few blocks and can never lead — the safe direction for every "is this coin buried yet" comparison
built on it. Stated as such in the doc comment and in SPEC §
control.wallet.peak.3.
ChiaCoinPeernow HOLDS its session's inbound stream instead of dropping it, so each peer'sclaimed tip stays current across the 5-minute sample lifetime rather than being frozen at handshake.
await_peakis refactored intoawait_peak_from+peak_fromso the corroborator and the heldsample share ONE parser — a second parser of the same message would be free to disagree with this
one about what a peer claimed.
What this does NOT do
ChiaQuorumCorroboratorandDialedPeerSampleare untouched and NOT collapsed. They dialindependently on purpose; one dialler is the goal, one voice is a regression.
chia-queryis not modified. Invertingrouter.rsthere is a cross-repo release-firstcascade; the node-side fix needs no release, so the ordering is settled here.
trustedflag is set on any dialled peer. That maps to a custody grant, and this changegets nowhere near it.
Blast radius checked
gitnexus has no index for this worktree, so blast radius was taken by grep + direct read, as §2.0
permits and requires me to say.
ChiaQuery::new— 3 sites in dig-wallet, 1 production (chain.rs:131, relocated) and 2#[cfg(test)]. Zero elsewhere in the workspace.ChainTransport::peak_height/ChainFallback::peak_height— one production consumer,sage/rpc.rs:1916, thecontrol.wallet.peakfallback arm. Its stale "outbound call to thethird-party tier" comment is corrected in the same diff.
await_peak— one caller (sync_supervisor.rs:1979), preserved as a wrapper.ChainTransport.clientfield — private; 3 test reads, migrated toexisting_client().CoinPeer— 2 implementors (ChiaCoinPeer, the testScriptedPeer), both updated.control.wallet.peak's fallback arm now answerspeak_height: nullwhere peers disagree insteadof a coinset number. That is the honest direction and the one this ticket asks for; the replica
arm, which serves most reads, is untouched.
Evidence
cargo test --workspace --no-fail-fast: 0 failed in every target; dig-wallet lib 625 passed(now 636 with the new tests).
cargo clippy --workspace --all-targetsclean. Passed-counts readfrom the
test result:lines, not from a bareok.The property is proven, not asserted
sole_owner_tests::only_the_registry_owner_constructs_a_peer_fabricsweeps every productionChiaQuery::newsite and fails on any outsidesources.rs. It was RED when written, naming["chain.rs:131"]. It ships with a control —the_sweep_can_find_a_construction_site_at_all— because a sweep over a broken file walk or a renamed constructor reports clean while measuring
nothing.
custody_fails_closed_testsshows the registry DECIDES something. The custody refusal assertsthe variant
ChainSourceError::NoProvider, not merely that it failed: an unreachable networkalso errors, so a test satisfied by any error would pass against a registry that accepted the
oracle for custody and simply could not reach it. The paired assertion — discovery is not
NoProvider— is what separates "populated registry that refuses custody" from "empty registry",and it holds identically with a network or without one, so it needs neither.
Plurality is proven behaviourally
a_single_peer_yields_no_height_and_no_fallback_to_the_oracle(collapse to one → refusal),three_silent_peers_leave_one_voice_and_that_is_not_agreement(four DRAWN, one speaking — the casea draw-size check misses),
a_lying_peer_does_not_move_the_peak_the_node_reports(asserts equalitywith the liar-free answer, since "a height came back" is produced identically by a max-of-claims
implementation), plus controls at both layers so no refusal is satisfied by a function that
always refuses.
Revert-proof — and it caught a false green in my own tests
Reverting only the peak placement first made all three transport tests fail on their value
assertion, so the assertion pinning the oracle out of the path was never evaluated and proved
nothing. Reordered to assert placement first, the same revert fails each test by its own placement
message. Recorded in the test module docs so it is not re-introduced.
What today's code would pass that these tests catch
A
peak_heightthat reads the oracle first passes every value assertion whenever the oracle isreachable and agrees — which is the normal case, and is why this went unnoticed. The placement
assertions catch it because they observe that no chain client was ever built. Likewise, an
ornamental registry constructed and never consulted satisfies NC-12's wording exactly as vacuously
as no registry at all; the custody-variant assertion catches that.
Known gaps, deliberately left (§2.6 — logged, not fixed here)
chia_peer_countis not a live measurement) is real and does not block this.It reads
ChiaQuery::peer_count(), a different accessor on a different path; nothing here consultsit, and the corroborated peak counts claims actually received this round rather than a reported
pool size, so a stale count cannot inflate the plurality this change relies on.
chia-query(0.6.2 → 0.16.0),
dig-node-control-interface(0.19 → 0.21),dig-offers(0.2 → 0.3) anddig-clvm(0.2 → 0.3) — all semver-incompatible0.xsteps, and the chia-query one almostcertainly carries the chia 0.36 line that dig-node#308 records as not currently attemptable. They
dwarf this change; smuggling them in would make this diff unreviewable.
Version
Workspace
0.151.1→0.152.0, dig-wallet0.34.0→0.35.0. Minor: a compatible new capability,with one behaviour change a caller can only observe as an honest unknown where it previously
received a third party's number. No API was removed or renamed.
Rebase note (the bump had to be redone twice)
Branched at
90ed299.mainthen took #344 (015b1c6, 0.151.0 / dig-wallet 0.34.0) and#353 (
dffb264, 0.151.1). The first rebase silently absorbed this branch's bump with noconflict, because #344 had written the exact same values this branch had — both sides wrote
something plausible, so there was nothing for git to flag. The version-increment gate caught it, not
a reviewer. Re-bumped past
mainto 0.152.0 / 0.35.0.Release hazard
dig-node's cron cuts a stable tag at midnight UTC unattended, so this merge gate is the last line
of defence. Do not merge until the gate round has returned.