Skip to content

feat(collateral): add BalanceUnreadable to CollateralUnknownReason - #39

Merged
MichaelTaylor3d merged 3 commits into
mainfrom
loop/412-balance-unreadable
Aug 30, 2026
Merged

feat(collateral): add BalanceUnreadable to CollateralUnknownReason#39
MichaelTaylor3d merged 3 commits into
mainfrom
loop/412-balance-unreadable

Conversation

@MichaelTaylor3d

@MichaelTaylor3d MichaelTaylor3d commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Adds a CollateralUnknownReason::BalanceUnreadable variant so the contract can express dig-node's
BondState::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_unreadable would tell an operator their census is broken while it is working — the same
remedy-misdirection defect that produced the withheld/disabled/reclaiming split in 0.26.0.
Reporting unfunded would assert a shortfall the node has no evidence for, on the surface
dig-app#300 reads to decide whether to alarm.

Shape

{"bond_state": "deferred", "reason": "balance_unreadable"} — mapped onto the EXISTING per-row
Deferred. No eighth MirrorBondState variant, no per-row unknown, no change to the four existing
reasons' meanings.

Compatibility

CollateralUnknownReason is 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_unreadable fails 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 dig-app-core/src/collateral/node.rs — COMPILE BREAK, by design.
    CollateralUnknown::of_wire is a total match whose own doc says a fifth upstream reason must be a
    compile error here. Its test also asserts a hardcoded mapped.len() == 4. dig-app owes a new arm
    with its own remedy sentence — a required sibling adoption (§1.3b).
  • dig-node dig-node-service/src/collateral.rs — does not break. Constructs variants only; no
    exhaustive match.
  • Both declare = "0.24", so ^0.24 cannot resolve 0.27 and the dig-app break is LATENT until each
    bumps.

Tests

Two KATs in src/kats.rs, each proven load-bearing by mutation (committed first; tree verified
clean after each revert):

mutation result
#[serde(rename = "record_unreadable")] on the new variant — the reuse defect itself KAT FAILED
as_wire token → "balance_unreadabl" KAT FAILED
variant dropped from ALL both KATs FAILED

every_collateral_unknown_reason_is_listed_in_all is new hardening: ALL is hand-written and
as_wire's exhaustive match does not force it to grow, so the KAT indexes the variants through its
own exhaustive match — a future variant now fails to COMPILE until it is listed.

cargo fmt / clippy --all-targets -D warnings / test --all green (187 lib + 9 doc).

§2.4b: this crate declares no dig-* or chia-* dependencies — only serde, serde_json,
async-trait, semver. Nothing to bump.

Status: DRAFT until the gate returns.

MichaelTaylor3d and others added 2 commits August 29, 2026 22:28
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>
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

IN PROGRESS — not the verdict. Gate on head b6737f912d70b6bd2ab71eaa665f8f1f648d12b0.

Confirmed so far, by reading the branch (not the brief):

  1. Nothing forbidden was done. src/results.rs touches CollateralUnknownReason only: one new
    variant, one ALL entry, one as_wire arm. MirrorBondState still has exactly seven variants
    (results.rs:2245-2310); MirrorBondStatesUnknownReason (results.rs:2368) is untouched; the four
    existing reasons' doc text, tokens and order are byte-unchanged.
  2. §2.4b: nothing to bump. Cargo.toml declares serde, serde_json, async-trait, semver
    only — no dig-* / chia-* edge exists in this crate.
  3. closingIssuesReferences is [] per the API, matching the body's stated intent to close nothing.
  4. The 7 finding is real. kats.rs:5429 and :5432 hardcode the literal, and the states array
    at :5399-5419 is a hand-built fixture — an eighth MirrorBondState variant would compile and go
    green. Not introduced by this PR; judgement on scope in the verdict.

Still open: the #[non_exhaustive] judgement, and confirming the mutations actually executed.

@MichaelTaylor3d MichaelTaylor3d left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. GATING — README.md:154 enumerates four reasons while SPEC.md:980 now says five. Born false
    in this commit. Inline thread anchored on src/results.rs:3169.
  2. 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.
  3. Non-gating — the two literal 7s at kats.rs:5429,5432 and the
    MirrorBondStatesUnknownReason::ALL hole. 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.

Comment thread src/results.rs
Comment thread src/results.rs
Comment thread src/kats.rs
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 MichaelTaylor3d left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review August 30, 2026 05:55
@MichaelTaylor3d
MichaelTaylor3d merged commit 18d7d27 into main Aug 30, 2026
8 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the loop/412-balance-unreadable branch August 30, 2026 05:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant