feat(collateral): add BalanceUnreadable to CollateralUnknownReason - #39
Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
dig-node's mirror pass has a BondState::FundsUnknown for the case where the node reads chain fine but cannot read its own $DIG balance. The contract could not express it: MirrorBondState has no fitting variant, and all four existing CollateralUnknownReason variants are about the census, the record, or the chain. Reusing one of the four would tell an operator their census is broken while it is working -- the remedy misdirection the withheld/disabled/reclaiming split exists to prevent. Reporting Unfunded would assert a shortfall the node has no evidence for, on the surface dig-app#300 reads to decide whether to alarm. The state maps onto the existing per-row Deferred, keeping the answer per-row: a call-level unknown would blank a whole page including known-good Bonded rows when only one input was unreadable. ALL is a hand-written list that as_wire's exhaustive match does not force to grow, so a second KAT indexes the variants through an exhaustive match: a new variant now fails to compile until it is listed. Co-Authored-By: Claude <noreply@anthropic.com>
|
IN PROGRESS — not the verdict. Gate on head Confirmed so far, by reading the branch (not the brief):
Still open: the |
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
CHANGES-REQUIRED
Head reviewed: b6737f9 (resolved from the remote; isDraft: true).
One gating finding, and it is a documentation contract split rather than a code defect. The code change
itself is correct and minimal, and its tests are load-bearing — verified by mutation, not by reading.
Ranked findings
- GATING —
README.md:154enumerates four reasons whileSPEC.md:980now says five. Born false
in this commit. Inline thread anchored onsrc/results.rs:3169. - Judgement — do NOT add
#[non_exhaustive]. The premise ("zero references outside this crate")
is measurably false: two wildcard-free downstream matches exist, and dig-app's is deliberately
exhaustive so this change is a compile error there. Detail in its own thread; non-blocking, resolved
by me. - Non-gating — the two literal
7s atkats.rs:5429,5432and the
MirrorBondStatesUnknownReason::ALLhole. Recommend closing here; not blocking. Resolved by me.
What I verified, and how
The three things it must not have done — all clear. src/results.rs touches
CollateralUnknownReason only (one variant, one ALL entry, one as_wire arm). MirrorBondState
still has exactly seven variants (results.rs:2245-2310). MirrorBondStatesUnknownReason
(results.rs:2368) is byte-unchanged. The four existing reasons' docs, tokens and order are unchanged.
The mutations WERE executed — instrument confirmed attached first. Each KAT run alone on the
unmutated tree produced a named test ... ok line plus 1 passed; 186 filtered out. Full suite on the
unmutated tree: 187 passed, 0 failed. Then, tree restored and git diff --quiet asserted after
every revert:
| mutation | result |
|---|---|
#[serde(rename = "record_unreadable")] on the new variant — the reuse defect itself |
a_balance_unreadable_bond_is_deferred_not_unfunded FAILED at kats.rs:4900, left: {"reason": "record_unreadable"} vs right: {"reason": "balance_unreadable"} |
variant dropped from ALL |
every_collateral_unknown_reason_is_listed_in_all FAILED ("ALL has drifted from the variant set", kats.rs:4870); the balance KAT failed too |
a 6th variant added with its as_wire arm and its ALL entry — an author who did everything right except the KAT |
error[E0004]: non-exhaustive patterns: CollateralUnknownReason::ProbeSixth not covered |
The third is the strongest evidence: the new KAT forces a compile failure even when every other
obligation was met, so the drift hole is genuinely closed rather than merely re-described.
The first names the property — this reason is its own token, distinguishable from the
census/record/chain reasons — rather than an outcome, and the nearest wrong implementation (reusing
record_unreadable) is exactly what it kills.
Also verified: Cargo.toml declares only serde, serde_json, async-trait, semver — no
dig-*/chia-* edge, so §2.4b has nothing to bump. closingIssuesReferences is [] per the API,
so #412 cannot close on this merge. 0.27.0 is the correct bump: minor is the breaking slot on a 0.x
line and this is breaking downstream, so patch would have been wrong. CHANGELOG.md correctly
carries no hand-written entry.
Not covered: clippy/fmt and the repo's CI gates — that is
check-merge-preconditions.sh's job, and the PR is a draft.
The CollateralUnknownReason taxonomy for control.collateral.requirement was updated in SPEC.md to include five reasons, with balance_unreadable added. README.md:154 still enumerated only the original four; bring it into alignment with the current SPEC.md:980 contract. Co-Authored-By: Claude Haiku <noreply@anthropic.com>
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Fixed: README.md:154 now lists all five reasons including balance_unreadable, matching SPEC.md:980.
Siblings checked:
- SPEC.md:980: Already lists the five reasons correctly ✓
- SPEC.md:1073: The four buffer reasons (requirement_unknown, served_set_unknown, reclaim_state_unknown, balance_unknown) — correctly left untouched as they are a separate taxonomy ✓
- README.md has no other enumeration of the requirement reasons
Change made:
- OLD:
(not_censused \| behind_finality_depth \| record_unreadable \| no_chain_source) - NEW:
(not_censused \| behind_finality_depth \| record_unreadable \| no_chain_source \| balance_unreadable)
Adds a
CollateralUnknownReason::BalanceUnreadablevariant so the contract can express dig-node'sBondState::FundsUnknown: the node can read chain fine but cannot read its own $DIG balance.Step 6 of DIG-Network/dig-node#412, under epic
https://github.com/DIG-Network/dig_ecosystem/issues/3166. Release-first: the contract publishes
before dig-node serves it. This PR deliberately closes NOTHING (parser-confirmed:
closingIssuesReferences: []).Why a new reason rather than reusing one
The four existing reasons are all about the census, the record, or the chain. Reusing e.g.
record_unreadablewould tell an operator their census is broken while it is working — the sameremedy-misdirection defect that produced the
withheld/disabled/reclaimingsplit in 0.26.0.Reporting
unfundedwould assert a shortfall the node has no evidence for, on the surfacedig-app#300 reads to decide whether to alarm.
Shape
{"bond_state": "deferred", "reason": "balance_unreadable"}— mapped onto the EXISTING per-rowDeferred. No eighthMirrorBondStatevariant, no per-row unknown, no change to the four existingreasons' meanings.
Compatibility
CollateralUnknownReasonis NOT#[non_exhaustive]. Nothing existing changes shape, but:adding a variant is source-breaking for a consumer matching it exhaustively, and a 0.26 consumer
receiving
balance_unreadablefails to DECODE (serde errors on an unknown variant).Blast radius checked
gitnexus has no index for this crate (29 repos indexed, not this one), so this was done by grep
across
modules/per §2.0's fallback bound, plus direct reads.dig-app-core/src/collateral/node.rs— COMPILE BREAK, by design.CollateralUnknown::of_wireis a total match whose own doc says a fifth upstream reason must be acompile error here. Its test also asserts a hardcoded
mapped.len() == 4. dig-app owes a new armwith its own remedy sentence — a required sibling adoption (§1.3b).
dig-node-service/src/collateral.rs— does not break. Constructs variants only; noexhaustive match.
= "0.24", so^0.24cannot resolve 0.27 and the dig-app break is LATENT until eachbumps.
Tests
Two KATs in
src/kats.rs, each proven load-bearing by mutation (committed first; tree verifiedclean after each revert):
#[serde(rename = "record_unreadable")]on the new variant — the reuse defect itselfas_wiretoken →"balance_unreadabl"ALLevery_collateral_unknown_reason_is_listed_in_allis new hardening:ALLis hand-written andas_wire's exhaustive match does not force it to grow, so the KAT indexes the variants through itsown exhaustive match — a future variant now fails to COMPILE until it is listed.
cargo fmt/clippy --all-targets -D warnings/test --allgreen (187 lib + 9 doc).§2.4b: this crate declares no
dig-*orchia-*dependencies — onlyserde,serde_json,async-trait,semver. Nothing to bump.Status: DRAFT until the gate returns.