fix(collateral): a degraded source can no longer seal a low requirement - #406
Draft
MichaelTaylor3d wants to merge 5 commits into
Draft
fix(collateral): a degraded source can no longer seal a low requirement#406MichaelTaylor3d wants to merge 5 commits into
MichaelTaylor3d wants to merge 5 commits into
Conversation
…a low requirement Refs: #405
…r-count A degraded chain view can only OMIT coins from a census, never invent them, so it describes a network smaller than the chain holds and the requirement derived from it comes out LOW -- the direction that leaves stores under-collateralised. Two properties made that permanent, each individually correct. `put` never let a later record supersede one this node censused, which is what stops a lying peer overwriting a node's own answer; and `catch_up` never re-censused an epoch it had already recorded, which is what kept the walk cheap. Together, one badly-timed ten-minute window sealed the figure for a seven-day epoch. The repair admits exactly one new superseding pair: a census this node took, replaced by a LATER census OF ITS OWN, of the same epoch, at the SAME census height, counting strictly MORE stores. The direction is the whole of the rule -- raising the count requires somebody to have posted collateral on chain, while dropping one requires only silence, so the upward direction is unforgeable and the downward one is free. The walk now re-censuses its TARGET epoch and no earlier one, so history is still computed exactly once and a cold-start walk is unchanged; the steady state costs one census of one epoch per pass instead of none. Refs: #405
…he repair chore(release): bump to 0.167.0 The probe dig-node#405 asked for: the same chain presented twice, once hiding one of two mirror coins and once not, with the real walk run against each. Asserting on hand-written records would prove only that `put` compares integers the way its own unit tests already say it does. Every coin is built by a genuine CAT spend, reusing dig-mirror-coin 0.7.0's published `tests/support/mod.rs` verbatim: the census authenticates a candidate by executing its creating spend, so a hand-built CoinRecord never reaches the path under test. The thin view hides ONE of two coins and keeps the other. Hiding both reads as the harsher fixture and is exactly the one that cannot tell a repair from a walk that recorded nothing, because no surviving figure would be left to move. Refs: #405
…ropagates
Two defects the first run of these tests found, both real.
The walk re-entered `record_one` for the genesis epoch, which is derived from
nothing and has no predecessor, so it stopped at `PriorEpochMissing { epoch: 0 }`
-- an epoch that cannot exist -- on every pass, forever, on the one node state
that is unambiguously correct. `refresh` now returns early for genesis and for
any record that is not a census, since `put` would refuse to supersede those and
the read would be spent to reach a refusal.
And the steady-state test targeted GENESIS, so it could never reach a chain read
at all: it asserted the repair path while exercising the guard that skips it. It
now seeds and targets epoch 2, with the genesis case split into its own test.
The store-layer fixture asserted the repair moves `required_per_store`, which is
a function of the multiplier and the OWNER count -- so an under-counted `stores`
moves nothing in its own epoch and the assertion compared 5000 to 5000. That is
the more interesting fact: the harm is in the SUCCESSOR, because `advance` reads
the seed's census to derive the next multiplier, which is why sealing a low count
was permanent rather than merely wrong for one epoch. The test now advances both
records with the same following census and shows the charge differs.
Refs: #405
…g witnessed Eight mutations, six red. The two survivors were both real gaps. Relaxing `>` to `>=` in the repair predicate survived every test, because they all vary the store count and the two operators differ only where it is EQUAL. That case is not a curiosity: a record carries `owners` beside `stores`, hiding an owner is as free to a thin source as hiding a store, and the requirement is derived from the owner count directly -- so under `>=` a re-census keeping the store count while reporting fewer owners would supersede, which is the downward direction wearing the shape of a repair. Removing the `epoch <= GENESIS_EPOCH` arm ALSO left the suite green, because it sat in front of the provenance arm and could never fire: genesis is held as a bootstrap record, and no path writes a censused epoch 1. Two guards where one masks the other cannot both be witnessed, so the masked one is gone and the comment on the surviving arm records that it is what protects genesis. Refs: #405
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
DRAFT — DO NOT MERGE. Nothing in this branch has been compiled or executed: the machine is at
ENOSPC (see "What I could not prove" below). The gate round has not run.
Closes #405. Blocker on #377, which is the core of DIG-Network/dig_ecosystem#3166.
The defect
A transient degraded chain read wrote a permanently uncorrectable low collateral
requirement. Three individually-correct properties composed into it:
EpochRecordStore::putnever let a later record supersede one this node had censused — theprotection that stops a lying peer overwriting a node's own answer.
catch_upnever re-censused an epoch it had already recorded — what made the walk idempotentand cheap.
A thin view can only OMIT coins, so it under-counts, so the requirement comes out LOW — the
direction that leaves stores under-collateralised — and the two mechanisms that would repair it are
the two protecting the record from a peer. Today every record is
(0,0,0)and correct; #377 is whatmakes the under-count reachable.
The shape chosen, and the two I rejected
Chosen: shape (2) — self-supersession, narrowed to a one-bit, one-way ladder.
putnow admitsexactly one further superseding pair: a record this node censused, replaced by a later census of
its own, of the same epoch, at the same census height, counting strictly more stores.
catch_upre-censuses its target epoch and no earlier one, which is what turns that admissioninto a live repair path rather than a dormant mechanism.
Why the upward direction only — and why this needs no total order. A source can OMIT a coin for
free, by staying quiet. To add one it must have been posted on chain against the requirement, with
a creating spend the census executes. One direction is unforgeable, the other is free, so admitting
only the unforgeable one gives a strict monotone ladder — which is the answer to #405's stated
objection to this shape, that "strictly better" needs an order that does not exist. It does not: it
needs one bit, and the bit is a store count on a fixed block.
Rejected — (1) route the census through NC-12 corroboration. Not implementable in this repo.
chia-query0.19 grades only the by-coin-id reads (peer::OptAnswer); the census's down-directionlever is the population read,
coin_records_by_puzzle_hash, which is ungraded(
router.rs:673,peer_then_coinset). And the node has no second independent view to corroborateagainst: its one fabric is oracle-first, so
dig-wallet/src/sage/sources.rs::independence_group_forcorrectly places it in coinset's own group, and building a second pool is the defect
dig_ecosystem#2761 records. Shape (1) is a
chia-querychange plus a release-first cascade — realwork, but not this blocker, and it composes with what lands here rather than replacing it.
Rejected — (3) refuse to record when the source is degraded. No such signal exists, and the
obvious candidate is a trap.
Exclusions::unreadable > 0looks like the degradation flag; readingdig-mirror-coin0.7.0census.rs:673-681shows it means "not a mirror coin at all" — a stranger'sdust at the shared puzzle hash, which anyone can produce for nothing. Refusing on it would deny the
census permanently on free noise. The genuinely degraded case it resembles, an absent creating
spend, is already fail-closed upstream:
Unauthenticatedis anErrthat ends the census, andthe crate says so in the same file. What remains is a source omitting coins coherently, and no
in-census signal can see that.
Blast radius
The gitnexus index for this repo is stale, so this was measured with grep + direct read
(§2.0 bound 2, and #405's own note).
own_census_supersedesandEpochRecordStore::puthave onecall path each into
collateral_census::record_one;catch_uphas exactly one production caller,server.rs::spawn_collateral_census, plus the crate's own tests.CatchUpgained a field and isconstructed in one module and read in one. Nothing outside
dig-node-servicenames any of them.The change
crates/dig-node-service/src/collateral.rsown_recensus_supersedes, and theputarm that must be checked before the evidence-strength arm — both censuses carry the same provenance, so an arm ordering evidence reads them as equal and answersAlreadyPresent, silently keeping the low countcrates/dig-node-service/src/collateral_census.rsrefresh, called only for the walk's target;CatchUp::supersededcrates/dig-node-service/src/server.rsSPEC.md§24.8, §24.8a, §24.10Both invariants, and how each is held
A peer still cannot supersede anything. Both sides of the new arm must carry
Censused, andcollateral_sync::adoptdiscards whatever provenance a responder sent and stampsAdoptedFromPeers. Asserted directly, in the direction that would hurt:a_peer_record_counting_more_stores_cannot_supersede_this_nodes_own_census— a peer able to raisethe count inflates every operator's collateral just as surely as one able to lower it.
The walk stays cheap where cheapness matters. No epoch before the target is ever re-censused, so
history is computed exactly once and #404's cold-start cost is untouched
(
no_epoch_before_the_target_is_ever_re_censusedpins this by asserting the stop names the targetand not an earlier held epoch). What does change: the steady state goes from zero chain reads to
one census of one epoch per pass. That is the price of the repair, it is stated in SPEC §24.8a
rather than glossed, and it buys the attacker's cost going from one badly-timed window per
seven-day epoch to sustained degradation for the whole epoch — because a lower re-census cannot
supersede a higher one.
Tests
tests/collateral_census_degraded_source.rsis the probe #405 asked for: it degrades the sourcemid-sequence, not the record, and runs the real walk against it — thin view records 1 store →
healthy view of the same block supersedes to 2 and moves the requirement → thin view again
changes nothing. Every coin is built by a genuine CAT spend, reusing
dig-mirror-coin0.7.0'spublished
tests/support/mod.rsverbatim, because the census authenticates a candidate by executingits creating spend and a hand-built
CoinRecordnever reaches that path.The degraded view hides one of two coins and keeps the other. Hiding both would read as the
harsher fixture and is exactly the one that cannot distinguish a repair from a walk that recorded
nothing, since no surviving figure would be left to move.
Store-layer tests assert the ladder from both sides separately rather than as one negation: the
repair, the refused under-count, a re-census of a different block still being a conflict (so the
same-height requirement is load-bearing and not decoration), and the peer refusal.
What I could not prove, stated plainly
Nothing here has been compiled or run. The machine hit
ENOSPC(os error 112) —C:at 100%with 519 MB free of 1.9 T. I reclaimed this lane's own
target/twice per CLAUDE.md §1.6 and touchedno sibling lane's; a cold rebuild of this crate needs roughly 7 GB and got to
dig-walletbefore thedisk reached zero. Cross-repo reclaim is the orchestrator's call (
loop-disk-reclaimer), not alane's. So there is no red-then-green evidence, no coverage figure, and the code may not even
compile. Treat every claim above as reasoned, not measured.
The version bump to
0.167.0was taken now because the lane cannot reach a "last step"; it must bere-checked against
mainbefore this is undrafted, since a peer merges into this repo from anothermachine.