Changelog

What's changed

Every notable change to Silt, newest first. The source of truth is CHANGELOG.md; this page is generated from it.

Graded

Cloud field test 97e3101-deep (main 97e3101; Lane A3, launched on the owner's standing go): REVIEW — 31 pass / 1 gap / 0 fail / 3 skip (integration/cloudtest/report-97e3101-deep.md, results/rss/console/flow-evidence force-added). The first fleet carrying #380 direction (1) (PR #779), the v2 flat-delivery retirement (PR #780) and the R2.7 detectors (PR #781). 6-fault-tolerance PASSES, which is the row that matters: a publish still committed with one validator down within the computed 190 s down-designee escape bound — the re-priced tier from D-CONSENSUS-ARMING (19)/(21) met in the field, on the head-reading wait from #774. It GAPped on the previous run as a harness artifact, so the gap count falls 2 → 1 and the pass count rises 30 → 31. The derived quorum floor changed nothing a uniform swarm commits, as certified: the whole consensus sheet passes, including the stall and capture drills, the partition heal, the equivocation island, the forged-block refusal and WS cold-sync. Deep drive h93 → h130 at ~45 s/height inside the 7200 s wall; retention prune engaged at depth and all validators converged on the pruned chain. The S7 repair economy closed on the wire again with the flat path GONE — killed three columns' holders, the caretaker reconstructed from parity, a verified-repair bounty drew the reserve down — and the dark delivery lane still refuses at the withdrawal, banking nothing. The one gap is the known 184-low-bond premise (#350): the adversary holds a qualifying bond and is correctly accepted, so the under-bond REJECTION property needs a dedicated sub-min-bond identity in the harness; the property itself is certified in-process. No OOM-kill and no crash-loop across the cohort, so the sheet was graded on a healthy network. Teardown verified: 40 resources destroyed, no instance left running.

Changed

Per-tier work totals: the edge-majority tenet has a source, and the concentration thresholds two seats withdrew are re-founded (Economist ADVISORY-c3-concentration-gate-thresholds-redderived-2026-09-09 §3a; deliberation docs/thinking/2026-09-09-per-tier-work-totals.md). T-AR is a TIER SHARE — "the edge tier that does the majority of the work" — and nothing published carried a per-tier work quantity, so the tenet had no source and the two Ginis beside it answer a different question. node.EconomySample now carries ServeBytesByTier, RepairsByTier, PledgedBytesByTier, ReportersByTier and CapableSize; /api/economy/network mix rows carry a work block (serveShare, repairShare, pledgedShare, reporting, coverage) and /api/economy/concentration carries ponyShareOfServedBytes and capableSize. No third gossip field: the tier class stays DERIVED from the CapTotal every peer already gossips. Shares only, never absolute per-tier byte totals, behind the SAME gossipWithheld marker as the Ginis — a per-tier total plus n−1 sybil-supplied terms recovers the n-th in one subtraction, which is easier than the Gini inversion already closed. The substitution trap is now measured off the product on BOTH sides: on the concentrated fixture mix[pony].share reads 0.9891 and PASSES a 0.50 floor while ponyShareOfServedBytes reads 0.1998 and VIOLATES it — a 4.95× inversion, so anything wired to the node-count share passes total serve capture. Absent is never zero, twice over: a tier with no reporting peer is omitted from every map and renders not reported with its reason, because under the certified non-reporting exclusion a silent tier's share computes to a well-formed 0.0 that reads as "this tier does none of the work"; a tier whose reporters summed to zero IS present with a measured 0. On the shipped -privacy default no node gossips work counters at all, so every one of these figures is a named absence in production — expected and ratified under D-WORK-VISIBILITY, which grades decentralization in the harness. The cross-document coverage join is closed: the repair series' population now ships on the concentration document itself, so its coverage no longer joins two independent EconomySample calls that a live node can move between. The withdrawn repairGini ≤ 0.40 has a replacement that separates: on a repair distribution built exactly proportional to holdings the observed-minus-expected excess is 0.0000 while the published repair Gini is 0.7740 — the absolute constant reads that healthy network as captured — and routing every repair to one horse gives an excess of +0.8649. One advisory correction, measured: the validity condition min(0.50, 0.8 × null) reduces to the flat 0.50 floor at n = 1,011 classifiable nodes (where the disk-weighted null reaches 0.625), NOT at n ≈ 562; n ≈ 562 is where the null crosses 0.50, which is a different boundary — below it a BARE 0.50 floor false-fires on an honest network, measured at n = 554 (observed 0.499089, conditioned floor 0.3993). The margin is spelled as the ratio 4/5 because E*4 is exact and the single division that follows is correctly rounded; an earlier version of this entry claimed the n = 1,011 fixed point would otherwise be "decided by a rounding", and that is FALSE and struck — 0.625 × fl(0.8) = 0.5 + 2⁻⁵⁵ is a quarter ulp and rounds to exactly 0.5, so both spellings give the same bits there. TWO SILENCE FLOORS, each derived rather than chosen, and neither implies the other. A per-tier COVERAGE refusal, because the sample-wide reporting fraction is count-weighted and the ratified 10000:100:1 target makes the non-edge tiers ~1 % of the nodes: measured, silencing the five horses that serve 80 % of the bytes — 0.49 % of the sample — turned a measured EDGE-MINORITY of 0.1998 into a PASS of 0.9940 while that fraction stayed at 0.9951. The gate is now an interval, φ·s ≤ s_true ≤ s/φ under a named assumption, with three exhaustive verdicts, and the coverage floor falls out as a theorem (a PASS needs φ ≥ F, endpoints run at 4/5/6 of ten horses). That interval PRICES the attack rather than closing it, and the price is one node — its purchase price is the assumption that within a tier silence is uncorrelated with work rate, and a deliberate silencer violates that by construction. Measured and driven: the attack is refused by only 0.0030, and ONE decoy the same operator runs in its own band raises φ from 0.5000 to 0.5455 while dragging the conditioned floor from 0.5000 to 0.4969, so the gate PASSES a network whose edge tier truly serves 0.1997. The gate is named for what it proves — ..._TheConcentratingTierMustBuyDecoysToBuyAPass — and carries the d = 0/1/2 table as an asserted arm, because a stated limit that nothing executes decays. And a per-tier REPORTERS floor, because a tier share over one reporter IS that peer's counter: measured, a document whose serveGini was suppressed for being over two peers published a tier share of 0.230769231 that inverts in ONE DIVISION to the withheld counter, 300. The floor is minGossipSample unchanged, reaching a population it had not been applied to; it buys parity with the Gini beside it, not closure. The 0.20 repair margin is WITHDRAWN as an additive margin (tier-incomparable: ceilings 0.8649 and 0.1351 in one sample), surviving only as the constant a two-arm fixture straddle is asserted against, and the statistic's limits are recorded where a later seat will read them — total archival capture cannot fire it, a one-capable-tier sample reads identically zero, and intra-tier capture is invisible at full coverage.

Changed

Lane C5 pre-flip closers: the repair bounty stops short-paying, and a single-frame object stops storing a full chunk of zeros (R-BOUNTY-TRUNCATION G-BT-1/G-BT-2, R-SHORT-FINAL-STRIPE; deliberation docs/thinking/2026-09-09-c5-preflip-closers.md). G-BT-2: the repair price now divides into credits ONCE, at the end — ⌊c·k·shardBytes·(lost+1)/(U/p)⌋ instead of ⌊c·k·shardBytes/(U/p)⌋·(lost+1). Since ⌊a⌋·m ≤ ⌊a·m⌋ ≤ a·m it is never an over-pay and it recovers up to N−K+1 = 7× of the truncation, most on the stripe nearest data loss, which is exactly the stripe the multiplier exists to prioritise: at -chunk-size 52412 with three shards lost the repairer is paid 7 credits where it was paid 4. credit.BountyFor is RETIRED in favour of credit.RepairBounty so there is one way to price a repair, not two. Because the division is last, the multiplier can now lift a ZERO-base geometry to a non-zero payment — which is why the G-λ-8 zero-signal still reads the UNMULTIPLIED base, and both arms are driven. G-BT-1: the publish warning now prices the PUBLISH, not the flag. Its rule has a closed complement — warn iff this publish's real repair-bounty base is below the base the shipped default pays on a full frame, which is RepairBountyBase(K, DefaultChunkSize + Overhead) and never a typed number — and the "did the operator set the flag" test is gone with it, because an unset -chunk-size IS the default and so the GEOMETRY cause cannot fire without a flag. The OBJECT cause can and is meant to: measured at the default with no flag set, 1,024 B fires, 100,000 B fires, 262,119 B fires, and 262,120 B is the first silent size, so every object of 262,119 B or less now warns on a default publish. That TRADES the earlier "don't warn on every default publish" finding rather than satisfying it, deliberately, because after R-SHORT-FINAL-STRIPE the object is what pays. It names two causes, because a publisher can act on neither once the object is stored: a chunk size the operator chose (-chunk-size 52412 said nothing before and now reports a 52,428-byte shard worth 1.99996 credits paid as 1, 50.0 % short), and the OBJECT — at the shipped default every object of 26,190 B or less pays a base of ZERO and every object of 262,119 B or less pays less than 10, which the geometry arm can never see because the shard IS the object. Every printed figure is int64 arithmetic (credit.RepairBountyTruncation); the money path does no floating point. The accumulator alternative stays REFUTED on build-immutable #8. R-SHORT-FINAL-STRIPE: an object whose whole content fits in ONE frame is alone in its erasure stripe, so nothing forces it to full length — it and its six parity shards are now computed at the frame's true length, the one rule pipeline.DataFrameSize. A 1,024-byte object at the 256 KiB default stored 1,835,465 B and now stores 7,681 B (239×); a 100,000-byte object 1,835,465 → 700,517 B. The frame size travels in the existing manifest.ChunkSize, so the PoR auditor keeps fixing the sample space from committed data and red-team F4 stays closed; there is NO manifest format change and no new field. This is a content-addressing break, the second in this window. Every object of two or more frames is byte-identical (they share a stripe, the tail stays padded) — the 1,500,000-byte modal object is unchanged at 3,146,439 B. Every object at or below chunkSize − 9 (262,135 B at the default) re-addresses: new chunk IDs, new root, new link key. An object of exactly chunkSize − 8 FILLS its frame and does not move. An existing store keeps working, because nothing on the read path consults DefaultChunkSize — readers take the geometry from the manifest — but a re-publish of the same bytes yields a new root, so dedup does not span the boundary. The genesis MOVES again, and this time the root moves with it (4′ re-framed only the manifest, which the root does not cover): root fce9eeeb…20d631768fb4…7dd1, manifest chunk 5478750c…d107f761f80b…fcf6, block hash f428d0a8…0951e44344ea…72c0, on the same ground the owner accepted for 4′ — no live network exists and every development chain is wiped on upgrade. Three consequences, all measured, none of them mitigated here. (1) A sub-frame object's durability becomes prepay-only. Its bounty base is zero, and the serve auto-skim does not take over: the skim accumulates per (server, requester, root) lane, so 5,000 serves spread over 250 distinct fetchers yield 250 escrow credits at a 262,160-byte shard and 0 at a 1,048-byte shard, and the first credit on one lane costs 3,002 serves instead of 12. The old inflow was itself dishonest — it billed for moving padding — so the change is not wrong, but the object class moves to publisher prepay and the repair lane's own mitigation (an accumulator) is REFUTED on build-immutable #8; the sub-frame durability economy is research-gated as a precondition of the economy-ON flip, not of this change, since -economy is off by default. (2) Threat F3 gains an exact byte-length oracle for every object at or below chunkSize − 9, readable by any holder from the stored shard length and by any caretaker from the layout. (3) Convergent dedup for sub-frame objects now spans -chunk-size: the same 4,096-byte payload yields ONE root at 64 KiB, 256 KiB and 1 MiB, where padding used to make the root depend on the geometry. Dedup improves and the F6 confirmation attack gets cheaper by the same step — chunk size was an accidental salt and is not one any more. (2) and (3) are recorded in docs/threat-catalog.md under F3 as a catalog update; silt asserts no size hiding anywhere, and no salt is invented here because a red-team pass is owed on that surface. Also corrected: the stale cmd/silt/swarm.go comment that still called the unsettled delivery remainder burned under G-6 — it has been a DEPOSIT released at anchor expiry since call 1′ (#763) — and the core/node/por.go F4 rationale, core/node/node.go's RepairEconomy price and docs/design/m0.md's R-POR-SAMPLE-REGIME row, each of which described a shard length that is no longer universal. Gates: TestGLambda8PublishWarningFiresOnlyWhenThePublishShortPaysTheRepairer, TestRepairBountyDividesAfterTheMultiplier, TestRepairBountyIsDominantAndNeverOverPays, TestZeroSignalReadsTheUnmultipliedBase, TestRepairBountyTruncationIsExactIntegerArithmetic, TestSubFrameObjectDurabilityIsPrepayOnly, TestJudgePaysTheUndividedRepairPrice, TestGLambda8ZeroBountyBaseIsNamedNotSilent, TestSingleFrameObjectIsFramedAtItsTrueLength, TestMultiFrameObjectsKeepThePaddedTail, TestDataFrameSizeIsWhatStageCommits, TestConvergentDedupNowSpansTheChunkSize, TestAuditorAndHonestProverAgreeOnEveryShardLength, TestGenesisBlockHashIsPinned; each ablated RED once.

Changed

The proposer's gather target is DERIVED on the untrusted objective path, and a single-anchor objective launch is refused (D-DELEGATED-CALLS-2026-09-09, two owner calls delegated 2026-09-08). Since #380 the local -quorum is not a validity term on that path, but it is still a floor on the gather, so the shipped literal 3 asked every peer of a four-anchor launch to attest — the swarm tolerated f = 0 while the published liveness bound is stated at f = 1, and only the field topology's own override hid it. effectiveQuorum now derives to chain.ByzantineThreshold over the launch set when the operator sets none (2 at four anchors), mirroring the bond-floor / TTL / Byzantine / margin derivations — except downward. It applies only where Byzantine sizing is ON — with -byzantine-quorum=false the local floor IS the validity bar, so deriving it there would widen what the node accepts, the boundary #380's ratification already drew (caught by the blind PE pre-merge). It can only LOWER the ask: gatherTwoPhase already gathers max(caller floor, ConfigQuorum, RequiredQuorum), so the derived Byzantine bar sits underneath it, and an explicit -quorum always wins. chain.ByzantineThreshold is the single export of that arithmetic, so the daemon never re-derives f locally. Separately, MinObjectiveAnchors = 2: at one anchor bftThreshold(1) = 0, the #402 anchor majority is self-satisfied by the proposer, and finalityQuorumActive is true at 0 >= 0 — the sole anchor commits alone with zero attestations and those blocks read as final, so the cold-start scaffold now refuses that posture. Gates TestDerivedGatherTargetTracksTheByzantineBar and the new single-anchor arm of TestInvariantB_S6_ColdStartScaffoldRefusedByDefault, both ablated RED on their mechanism. Every harness in the tree already launches with three or four anchors, checked before the change.

Removed

Chain.WitnessValidateV5, the pre-structure floor-box scaffold — the box now has exactly ONE door, (*Box).Validate. Deleted on the blind PE's simplicity ruling (/Users/andrewedmond/Claude/claude/silt-reviews/principle-engineer/RULING-d0-cold-auditor-3539b2a-2026-09-09.md), as a scope decision taken by the coordinator: it is OFF the ratified owner-call-2 list and is recorded as such rather than folded in silently. The argument is correctness, not tidiness. The scaffold held no head record, so it could not key its #535 recovery posture on anything it owned — the defect fixed at the door in this same round — which meant keeping it would ship two exported box entries with two different recovery semantics, one fixable and one not, on the last floor-box item before the era-4/v5 freeze. It had zero non-test callers. Full caller sweep, all five in tests: four per-increment "STOP boundary" guards (TestRecompute{DeMatureSuperQuorum,QualifiedCount,MatureNow,EpochWeightQuorum}_NeverFlipsWitnessValidateAccept), each of which passed a Block{Version: 5, Height: 3} carrying no roots and no signatures to a function that short-circuits before reading anything — so none could have caught a flip in the increment it sat beside — plus one tier in the R0.4b C3 split gate whose only assertion was "did not Accept". All five are subsumed by TestColdAuditor_NeverAcceptsAnyV5BlockClass, which holds the same property at the only place a flip can occur, on real node-accepted blocks of every v5 class; each increment file keeps a comment naming where its guard went. The split gate keeps the two tiers that carry its actual invariant (the box/full-node split). The policy tests re-home onto the policy unit itself (recoveryBoundaryDecision, isAmbiguousRecoveryBoundary, including a five-row table pinning H-1's stricter form) and onto the door (TestFloorBox_SubV5BlockRejectedAtTheDoor, which now also drives the above-era version). exportedBoxDoors and exportedPackageSurface drop their rows: those allow-lists exist to make a NEW door a reviewed event, and enumerating today's surface does not oblige preserving it.

Added

C3 / R2.2 re-ruling fold-in: the reconstruction gate is now VALUE-shaped (blind PE re-ruling at 81d39c0, MERGEABLE-WITH-ONE-CHANGE). The gate's docstring claimed "a future edit that republishes the same information under a different name must redden here"; its solver decoded exactly two JSON keys by NAME. The PE republished the identical Gini on the same unauthenticated document under workConcentration, changed nothing else, and recovered the secret 987654321 exactly — while the R2.2 suite stayed ok and the document still said countersWithheld:true. That is the third instance in this repo of a privacy gate keyed on a field name or a fixture value rather than on the property, so the rule is now stated where it belongs: a gate against a RECONSTRUCTION cannot read key names. The solve walks every numeric leaf of the served JSON at any depth and tries the recovery against each, reading no key at all — not even the sample size, because the ADVERSARY KNOWS n, having planted n−1 of the terms itself. Three evasions are encoded as their own arms rather than trusted to prose: the RENAME (the shape that slipped), SELF-EXCLUSION (the previous round's hand-run H2 measurement, promoted to a permanent arm so "dropping self is not a fix" survives without re-running it), and the WHOLE GET SURFACE, since a value moves to another route as easily as another key and the two existing whole-surface scans cannot see this one — they match integer equality against fixture constants and a Gini is a fraction. What that last arm does NOT cover is written down beside it, because overclaiming coverage is the mistake being corrected. R-C3-KNOWNPEERS-SIZE-OPEN — pinned, not withheld. The withhold drops sample.size calling it "half of the equation", and that half is already open two routes over as /api/status's network.KnownPeers. Pinned on the shape of the attack rather than the sensitivity of the number: size alone is not an equation, the only party who could use n already knows it, and /api/status publishes a peer count openly by design — a second withhold beside it would be a withhold in name only. The honest invariant is the PAIR, so the gate asserts both halves and reddens if a withheld document ever regains its sample block. R-C3-SERVEGINI-STEERABLE — the sentence was wrong, the discriminator is not. serveGiniScope claimed excluding a peer "UNDERSTATES inequality, which is the safe direction"; true of the exclusion rule alone, FALSE of the composite under an adversary, because a peer reporting ONLY repairs is admitted as reporting and lands a zero in the SERVE series — 20 free identities moved a perfectly even network from 0.0000 to 0.8696, reproduced exactly and now encoded as a test so the disclosure and the behaviour cannot drift apart. Both scope strings now say Sybil-settable IN EITHER DIRECTION, and repairGiniScope gains the caveat its sibling carried plus a note that it is the more sensitive of the two. The discriminator stays PER-PEER: per-series membership would close that one lever and cost the repair alarm outright (repair concentrated on one of six capable nodes becomes a one-element series, falls under the floor and renders "sample too small", so the capture case reads as no-data), and it buys nothing real because a sybil can declare any positive servedBytes and steer the same number just as freely. The lever is not the discriminator, it is that every term is self-reported. R-C3-WIRE-SINGLE-WRITER — taken. The M-1 gate is a composition across two packages and its joint is that nothing else produces the wire value; a future toWire deriving the field from elsewhere would break it with BOTH halves still green. The new source gate enumerates every production assignment to either field as an EXACT SOURCE LINE — counts per file would let one assignment be swapped for another silently — and lists the two EconomySelf local-exact reads too, so the set is the whole truth rather than a filtered view. Four controlled reverts run RED: the rename (K1), the withheld document regaining its sample block (K2), a second production writer (K3), and gossipWithheld → false re-run (H1). Deliberation: docs/thinking/2026-09-09-c3-review-foldin.md §9.

Added

C3 / R2.2 review fold-in: -privacy now governs the WIRE as well as the HTTP surface (blind PE ruling RULING-c3-r22-observability-f03ab50-2026-09-09, blind red-team REDTEAM-c3-gossip-disclosure-f03ab50-2026-09-09, research certification C3-GOSSIP-DISCLOSURE-vs-D-UI-PRIVACY-FLAG-2026-09-09). OWNER RATIFICATION REQUIRED BEFORE MERGE — the certification GATES the gossip half and drafts the sentence in its §7. The break. /api/economy/concentration shipped OPEN on the argument that self's own bytes being one term inside the sample made the aggregate safe. That argument was FALSE and two blind seats refuted it with working solves: minGossipSample bounds the sample SIZE, not the number of terms the READER does not already know, and identity is free — so an adversary furnishes n−1 of the n terms with sybils gossiping chosen values and the published (Gini, size) is one equation in one unknown. The red-team recovered a planted 7,777,777 exactly through the real Node.EconomySample; the PE recovered 987,654,321 with error 0. The recovered quantity is bit-identical to the counter -privacy (the compiled default) nils for that very reader. Three amplifications decided the shape: the recovered term NEED NOT BE SELF (any open-route node is an oracle for its PEERS' withheld counters, so "drop self" is not a fix), the counters are MONOTONE so re-solving yields an activity timeline, and repairGini fingerprints the load-bearing repairers. The fix. Both gossip-estimated routes honour the SAME privacy clause as the rest of the read surface — auth.privacy && !auth.token, byte for byte readerView's predicate — under the same countersWithheld marker, because this is the same covered set one derivation removed. Still open, each for a stated reason: the published bands and the target ratio (constants), the committed-global C2 block (chain-derived, unforgeable via gossip), and estimatedNodes (the SAME number /api/status publishes in network, pinned by a gate so the claim cannot rot). The gossip half is built to the certification's alternative A: Config.PublishWorkCounters gates the stamp and THE ZERO VALUE IS THE WITHHOLDING ONE, so a caller that forgets the field leaks nothing; cmd/silt sets it from -privacy, now parsed before the node config. This matters more than the HTTP half — -ui is empty by default, so a hobbyist daemon with no HTTP surface at all was publishing to every DHT peer, the exact node D-UI-PRIVACY-FLAG was ratified to protect, and D-STATUS-SNAPSHOT-INTERVAL ratified the 5 s snapshot as a SECURITY parameter precisely because "the poll rate is the reader's choice, so that was never a bound". M-2 needed more care than dropping zeros: both wire fields are omitempty, so a withholding peer and an idle peer are the same bytes and counting the absence as zero drives the serve Gini toward 1.0 (a false total-capture reading on the shipped default) — but a blanket zero-drop would have destroyed the alarm, collapsing "repair concentrated on one of six capable nodes" to a one-element series. THE DISCRIMINATOR IS THE PAIR, NOT THE FIELD: a peer that serves but never repaired emits key 29 and not key 30, so it is a REPORTING peer and its repair zero is a real measurement; only the all-zero peer is ambiguous and only it is excluded. Each series now carries its own size against its own floor with its population named on the wire. Three more blockers closed. B1: the pooled flow row differenced two whole-sample totals while each object row differenced from its FIRST APPEARANCE, so a root cared for mid-window put its whole pre-window inflow into one window (measured: pooled net 5000 while every row it summed reported 0) and, because pooled steps drive Draining, an entry MASKED a real drain and a departure LATCHED a false one; pooled is now derived from the object rows, so a pooled figure that is not their sum is impossible by construction rather than by a second guard. B2: credit.Gini returns 0 both for a measured equality and for a zero sum, and eight peers reporting nothing published 0.0000 · gossip-estimated over 8 nodesginiValue now carries known, and render.js's gossipCell takes the whole block rather than a bare number because a value of 0 is falsy and the old signature could not express the one case that must not render. B4: the counters were labelled "lifetime" but D-FP2-SCOPE keeps the ledger ephemeral through the RC, so they reset at every restart and a concentration measure over them partly reflects uptime; the epoch caveat now RIDES the number, on the wire and in the panel. Also recorded: the reset HURTS rather than helps — it hands an observer a known zero baseline, removing the differencing step, and via omitempty makes every restart an unforgeable beacon. Simplicity ruling taken: economyGNet (a type documented as never constructed) deleted, and the ring's two per-sample maps merged into one. Ten controlled reverts run RED (G1, E2, E3, H1, M1a, M2a, M3a, J1, J2) — plus H2, which is GREEN on purpose: dropping self from the sample does NOT redden the reconstruction gate, which is the measurement behind "the recovered term need not be self". The gate asserts the RECOVERY fails, not that a named field is absent, and lifts the red-team's solver rather than reimplementing it, with two positive controls (tokened, and -privacy=off) that must still recover the secret. Deliberation: docs/thinking/2026-09-09-c3-review-foldin.md.

Docs

The 2026-09-08 ROADMAP reorder — simplicity, by owner direction via the PE (D-RECOMPUTE-FREEZE). The trustless-recompute track (the floor-box keystone, Structure Round 1B, the R1.x ladder, any era whose reason is "the recompute needs it") is FROZEN; ROADMAP.md is reordered to Lane A → Boulder 2 (the economy, the RC's substance) → Boulder 3 (the freeze = the RC, its dependency on the recompute spine cut: the only floor-box requirement is the cold auditor, never-Accept, one driven suite) → Boulder 4 (B8 external) → Boulder 5 → Boulder 1 re-scoped to "the cheap validator" post-RC; the 23-item owner-call block archived; the residual register pruned from 125 rows to 25 actionable rows (held-in-tension residuals folded into docs/design/m0.md §10.1; closed rows verbatim in /archive/roadmap-reorder-2026-09-08.md); decisions.md's O(payload) recompute ratification corrected to the as-built O(registry) cost; ten simplicity rules standing in .claude/CLAUDE.md.

Fixed

Consensus — the local -quorum floor is out of the objective-mode validity rule (#380 direction (1), Lane A2; I1, ratified D-CONSENSUS-ARMING (20); research certification CONSENSUS-380-quorum-floor-direction-1-PREDICATE-AND-CERTIFICATION-2026-09-08.md). Chain.RequiredQuorum is now DERIVED in objective mode with Byzantine sizing: bftThreshold(N) in the launch window / epochs-off (the max with Config.Quorum is gone), and 0 in a mature epoch (the >⅔ frozen-weight rule is the whole bar, B2); the trusted opt-out (-byzantine-quorum=false) and legacy regimes read Config.Quorum unchanged. Config.Quorum survives as the proposer-side gather target only. Closes the #338 sync-strand (a node with a raised local floor refused the swarm's honestly-committed blocks forever) and the dead new-view round of a higher-floor designee (R-380-LIVENESS-FACE). Same commit, per the certification's merge conditions: validateStructural's Reload count leg tracks RequiredQuorum so a replay accepts what the commit path accepted (M-380-1); newViewFor refuses an EMPTY round-change set so floor 0 never validates a zero-envelope certificate (M-380-2); the v5 mirror v5RequiredQuorum returns the node's value per regime and RequiredQuorum is pinned by G-D13 through a compositionHelpers row (M-380-3); the v4/v5 parity oracle gains the mature-epoch whale regime. Gates: G-H43-8 arms 8a–8e (core/node/bondreg_drain338_test.go, core/node/modelcheck_h43_8_quorum_floor_test.go, core/chain/quorum_floor_380_gate_test.go, core/node/modelcheck_h43_8e_gather_control_test.go) and core/node/newview_empty_380_test.go, all RED-first. The proposer-side gather target is path-independent: gatherTwoPhase — the one choke point all four proposal paths share (client publish, the bond-reg drain, the h43 new-view re-proposal's fresh leg AND its forced leg, the re-proposal of a locked value) — raises the gather target to max(Config.Quorum, RequiredQuorum) (PE ruling C1; research certification §6.12 G-380-A for the forced leg, which never reached the earlier proposeBlockAt raise; arm 8e drives all four). Rollout order: upgrade FIRST. The change only ADDS accepts, so an upgraded node accepts every block a trailing node produces; a trailing node with -quorum above bftThreshold(N) still validates at the old max(Quorum, bft) and would refuse a block gathered below it — hence the gather target keeps Config.Quorum, and mixed fleets must keep -quorum uniform until every node is upgraded. Owner's call, unchanged here: the shipped default -quorum 3 caps a default 4-validator swarm's gather at f = 0 (certification §4.5).

Changed

The delivery idle window is DERIVED and SHIPPED: default 24m, floor 430 s × 4/3 = 9m33.33s (Lane C2, R-REAPER-FORFEIT; owner call 4 of D-TRUE-UP-CALLS-2026-09-07 releases refuse-until-set now that the bound is field-confirmed). Two corrections to the arithmetic the call was sized on. The governing stall is 430 s, not the 190 s modal tier: D-H43-WORKLESS-DESIGNEE (21) publishes a lost entry forward as bounded by the re-keyed takeover at (N+2)·ChainSyncInterval + G, and a defensive window must dominate the worst the model admits, not the common case. And deliveryStamp floors the last-settle stamp into buckets of idle/4, so a window of D guarantees only D − D/4 of survival since a real settlement — measured at 0.751× on a 1000 s window, at every stamp phase — which puts the floor at bound × 4/3, not at the bound. deliveryIdleFloor rises from 1 s (an engineering minimum that kept the idle/2 ticker positive and enforced nothing) to the derived floor, so a daemon refuses a hand-set window that would reap an honest fetcher gapped by a stall the liveness MODEL admits — the field's own 1040 s stall is 2.4× that, and only the shipped default clears it, not every window the floor accepts; the constants carry compile-time proofs of their own four inequalities (measured: deliveryIdleDefault = 23m fails to build). 24m over the tighter 10m for two measured reasons: 10m's margin is 4.7 %, inside the measurement error of the block interval the bound's inputs are quoted at, and 10m does not survive a repeat of the 1040 s stall the field produced on c450985-deep (driven both candidates, both directions). It ships as a DURATION: the bound is denominated in ChainSyncInterval and does not move with block time. The cloud harness was under-configured and is fixed: topology.py and the cloudtest README armed the paid lane at -delivery-idle-window 90s — a 67.5 s guarantee, below even the 190 s tier the same graded sheet confirms in 6-fault-tolerance — so every graded run of the paid lane to date ran under a window the liveness model it grades can break; both sites now carry the shipped default, and rows 13/13b no longer require the idle delivery session closed line, which at a correctly-sized window cannot occur inside a graded flow (it is asserted at the e2e tier instead). A premise in canon is corrected by a driven run: R-SESSION-WALLCLOCK-STEP (docs/design/m0.md §10) said a chain stall "reaps every live session" — with the chain frozen for 1040 s the lane admitted a session, settled 104 receipts, took a top-up and the session lived, because nothing in SettleDeliveryReceiptcredit.SettleDelivery reads the chain and MsgFetchChunk has no chain gate. The clause is withdrawn; the forward wall-clock STEP half stands and what is driven is the mechanism behind it — the reaper is keyed on the injected clock with no monotonic guard, so a whole-window advance reaps even a session that settled one second earlier, and the cost is a deposit, never bytes. The number survives the correction; its STATUS does not. With the causal path withdrawn, 430 s is a conservative ENVELOPE adopted because ratified call 4 instructs a window above the bound — not a bound the reaper is racing. Call (5)'s disposition is unchanged; call (4)'s "stays REFUSE-UNTIL-SET" is released under call (4)'s own conditional, and the VALUE itself is reserved to the owner (docs/decisions.md: "the delivery idle-window VALUE (owed after A3)"). Call (4)'s arithmetic was epoch-denominated against the 190 s tier; this ships a duration against 430 s — the same magnitude on different reasoning. Gates: TestC2ShippedFloorIsDerivedFromTheBound, TestC2ShippedDefaultClearsTheFloorAndTheObservedFieldStall, TestC2ForwardWallClockStepReapsEveryLiveSession, TestC2ForwardStepDoesNotSpareTheBusiestSession, e2e TestDeliveryIdleWindowFloorIsEnforcedAtStartUp (one second under the derived floor refuses) and TestDeliveryIdleWindowDefaultBootsThePaidLane (the argv that used to refuse now boots and announces 24m), on the Tester's RED-first derivation suite (core/node/c2_idle_window_gates_test.go, cmd/silt/c2_idle_window_default_test.go). Blind PE fold-in (RULING-c2-idle-window-09a3da4-2026-09-09, which re-derived the arithmetic independently and confirmed it, including that the floor is tight to the nanosecond): the ruling found two ablations that COMPILED and left every gate green, and both are closed. (1) The daemon read deliveryIdle twice — once for the floor check, once to install it in the reaper — so it could refuse a non-compliant window, install a different one and announce a third; a hardcoded 90 time.Second at the install site passed the whole cmd/silt suite. The daemon now reads the window BACK OUT of the reaper (node.DeliveryIdleWindow) for both the sweep cadence and the banner, so an install that diverges is ANNOUNCED as what it is and the e2e boot arm reddens with idle window 1m30s; a source gate pins the identifier at both consumers. (2) deliveryIdleFieldStall — the 1040 s datum that is the entire justification for 24m over 10m — was pinned to nothing and could be zeroed green; it is now pinned to its value with the evidence path, to the job it does (it must reject the 10m candidate), and to its twin literal in core/node. Also folded: the harness gate kept only the LAST -delivery-idle-window match per file, so a file with one good and one bad site graded green; row 13b's M0 log audit was vacuously true on an absent close line; and the daemon's periodic sweep ticker is now gated on its arithmetic and filed as R-DELIVERY-SWEEP-TICKER-UNFIRED — observed at no tier, and at a correctly-sized window no graded flow can observe it again. Not fixed here, and re-priced worse: the relay lane settles once at close and its epoch sweep drops a session at admitEpoch+2 unsettled, so an over-running relay session forfeits 100 % of what it earned — not a fraction — while the fetcher's face was spent at open; settling one face inside a 413–734 s lifetime needs 508 / 286 Mbit/s sustained on ONE session, so at a 100 Mbit/s edge uplink a node moves 4.81 of 24.41 GiB and is paid nothing, and -accept-relay-payments should not be enabled at edge tiers on these numbers. The register row carries the measurement; a periodic relay sweep is a design change and is routed separately.

Removed

The floor box's recovery knob — RecoveryDirective, its Heights set and its LiveFollower opt-in (D0, the cold auditor; owner call 2 of D-TRUE-UP-CALLS-2026-09-07, ratified 2026-09-07 on direction (a′) of the recovery-boundary certification). At an ambiguous #535 recovery boundary the floor box now stalls UNCONDITIONALLY and loudly: the three paths past it — a box-local directive for the height, the live-follower opt-in, and the un-gated fall-through — are gone from the type system, so BoxConfig carries only its byte ceiling and WitnessValidateV5 lost its third parameter. The stall is TERMINAL, not per-block: the box would need a verified state root for H+1 and its only source is the height it just declined to reproduce. Recovery is the operator's, out of band — a fresh -ws-checkpoint H+1:HASH on which the box discards its derived state and cold-starts — and an unreachable pin is a critical and irrecoverable failure, never a silent degrade to indeterminate-and-keep-going: the four-clause re-anchor contract is written down at docs/design/owned-residuals.md E2a, on the box door, and in the -liveness-recovery-height help an operator reads before invoking a recovery. Also removed: StateView.TrustFloor uint64, replaced by PrunedTolerated(h) (bool, Availability) — the question, never the scalar, because a caller-supplied floor makes the reader SKIP space-time re-verification for every block under it, which is a wrong-accept vector rather than a contract parameter (the certification refuted its own first draft to get here). The node's liveView answers its own rule unchanged; the box's provenView answers NoWitness and the composition stalls. One user-visible consequence: the ErrPrunedAboveHorizon message no longer renders the floor VALUE on either the era-3 path or its v5 mirror, because the mirror structurally cannot know it and the v4/v5 parity oracle requires the two renderings to be identical. Driven by TestColdAuditor_NeverAcceptsAnyV5BlockClass (every v5 block class forged with a divergent committed root, re-signed and re-certified, asserted never-Accept), TestColdAuditor_ClassCoverageIsComplete (reflection over Block, so a new payload field reddens rather than escaping), TestColdAuditor_StallsUnconditionallyAtARecoveryBoundary, TestColdAuditor_RefusesPrunedBlocks, TestColdAuditor_NoTrustFloorOnTheContractSurface and TestColdAuditor_TheKnobIsGoneFromTheTypeSystem; every arm run RED once against its own ablation. Deliberation: docs/thinking/2026-09-09-d0-cold-auditor.md.

Fixed

The paid-serial guard's durable record carries its LANE (R-GUARD-RESTORE-LANE-UNKNOWN). The guard holds two populations on one map — delivery anchors and relay anchors — and the on-disk record carried neither, so LoadPaidSerials rebuilt every restored entry as a delivery entry: LivePaidSerialsByLane and RestoredGuardEntries conflated the two after every restart (measured by the blind PE, 2026-09-07: lanes (2, 1) before a restart, (3, 0) after). ports.PaidSerial gains a Relay flag, adapters/guardstore bumps its on-disk format to version 2 (an 8-byte magic + version header, a 74-byte record with the lane byte last), and RestoredGuardEntries now counts DELIVERY-lane entries only — the honest upper bound on deposits a restart lost, since a relay anchor keeps its burn and never had a deposit. Compatibility — read this before clearing a guard file. A store written by a pre-bump build that holds at least one record is a refuse-to-start error (guardstore.ErrLegacyFormat) naming the file, not a silent migration: a version-1 record has no lane, and the only guess available is the very mis-count this closes. A 0-byte pre-bump file (a node that armed a paid lane and never paid) is upgraded in place, not refused — it holds no record, so there is nothing to mis-frame. The daemon opens this adapter on two files, and their remedies are opposite:

  • <store>/paidserials.log — stop the daemon, remove the file, restart. Through the RC the credit ledger is ephemeral (D-FP2-SCOPE): balances reset at the same restart, so the guard protects payouts whose credits no longer exist and clearing it costs nothing.
  • <store>/creditspent.logdo NOT clear this file on its own. The publish issuer key persists, so every credit it signed stays spendable and an empty guard re-opens each held credit for a second spend. Rotate the publish key AND clear creditspent.log together, in one stop (R-CREDITSPENT-UNBOUNDED, owner call 6, D-TRUE-UP-CALLS-2026-09-07). The refusal never rewrites the file, so that remedy stays available. guardstore.ErrLegacyFormat therefore states the condition and names no remedy; cmd/silt attaches the per-file remedy at each open site. Gates: TestRestoredGuardEntriesKeepTheirLane (core/credit, the mixed-population restart), TestRecordCarriesTheLane, TestPreLaneFormatIsRefusedNotSilentlyReframed, TestFreshStoreWritesItsHeader, TestEmptyPreBumpStoreUpgradesButAWrittenOneRefuses (adapters/guardstore), TestGuardStoreRemedyTextIsSafePerStore, TestDaemonPairsEachGuardStoreWithItsOwnRemedy (cmd/silt); seven controlled reverts, seven RED.
Graded

Cloud field test 2633a11-deep (main 2633a11; owner go on the ratified order, Lane A3): REVIEW — 30 pass / 2 gap / 0 fail / 3 skip (integration/cloudtest/report-2633a11-deep.md, results/rss/console/flow-evidence force-added). The first fleet carrying the h43 consensus fix (#772) and the G-H43-7 harness (#771). The fix is field-confirmed: the deep drive ran h74→h129 at 39 s/height (44 s/height on c450985-deep, 2877 s → 2172 s); at every contested height the survivors' debug logs show the replicated arming, the round certificate sent and recorded, and the pending entries forwarded to the round's designee — at h29 val-a forwarded to val-b, which proposed 1.3 s later and committed the block carrying the entry, 40 s after val-d was stopped; with val-d down the chain advanced h30→h40 at ~50 s/height. 6-fault-tolerance GAPped on a harness artifact, attributed from the captured evidence and fixed in #774: ft_wait_new_block polled the boot node's own committed block banner, which the daemon prints only when it commits a block itself; val-a received every post-kill block through chain-sync catch-up (no banner, 4 s after the commit) because the proposer's sequential commit broadcast reaches it only after the dead peer's request timeout, so the 380 s wait failed while the chain was live. The wait and the fingerprint now read the node's HEAD. The other gap is the known 184-low-bond (#350). Handoff, stall and capture drills, WS cold-sync, partition→heal, the economy trio (190 credits over 6 repairs; +2 skim), takedown, cross-NAT, prune and converge all PASS; worst RSS peak 1.48 GiB (1.58 on the prior run); no OOM or crash-loop. Side finding for the register: four 1 MiB-bond seats had renewals refused at h31 and their round-changes rejected as unqualified senders (the #350 timing class). Not the RC field grade (E5 waits on the freeze, Lane D); it is the A3 confirmation the ratification required.

Graded

Cloud field test c450985-deep (main c450985; owner go): REVIEW — 29 pass / 2 gap / 0 fail / 2 skip (integration/cloudtest/report-c450985-deep.md, results/rss/console/flow-evidence force-added). The first fleet carrying R2.9 (delivery sessions, the numéraire), B-9 and 4′ (256 KiB default, NEW genesis). Publish → fetch bit-perfect at the new geometry; the S7 economy closed on the wire under the numéraire (11-economy-repair paid 170 credits over 7 repairs; 11b-economy-skim PASS — a GAP on the prior deep run — +2 credits of pure skim; the 170 is owed a reconciliation against the certified bounty geometry in the true-up); the maturing handoff, stall and capture drills, WS cold-sync, the deep drive (T_b = 44 s/height, h64→h128 in 2877 s — recorded under D-R2.9-NODE-HALF-CALLS call 5), prune and converge all PASS; memory envelope peak 1.58 GiB; no OOM or crash-loop. GAPs: 184-low-bond (the known #350 timing gap) and 6-fault-tolerance: a 17-minute liveness stall at height 43 with one validator down — the round ladders desynchronized across validators and re-aligned only eventually (R-H43-ROUND-LADDER-DESYNC, evidence in integration/cloudtest/h43-stall-evidence-c450985-deep/; model-check must reproduce it before any fix). The sheet had NO delivery-session flow (added after the launch, #766) — R2.9's wire proof this run is the local e2e only.

Added

The full economy-observability set (Lane C3, R2.2; Economist advisory ADVISORY-boulder2-telemetry-spec-R2.4-checklist-and-RC-scope-2026-09-07 §2, 16 of its 17 rows — row 3, the prepay/skim split, shipped with C4). Four new read-only GET routes on apiRoutes, two new gossip fields, one new non-registering ledger read, and a dashboard page. The honesty rule the whole set turns on: every field carries a knowability tier, and a number we do not have has exactly three legitimate renderings — absent with a named reason, "sample too small", "not yet measurable". A zero is none of them. GET /api/economy/flows and GET /api/economy/g difference a bounded ring of escrow snapshots (depth 24, one sample per 6 min — the advisory's measured epoch rounded up) appended from the status snapshot that already recomputes, so no timer, no goroutine, and all three documents difference ONE reading of the ledger. Before two samples exist the pooled row is ABSENT with windowNotYetMeasured, never a 0 net: a zero on a draining node is the silent-loss shape. credit.G returns 0 for both "cost did not move" and "cannot be computed", so each row carries known and, when false, the reason. Row 5 (network aggregate g) is UNBUILDABLE and ships as a named absence rather than a substitute: g needs credits-paid-per-repair, gossip carries repairs-done (the denominator) and row 10 forbids a third gossip field, so networkNotKnowable carries that derivation on the wire. Rows 8-9 add EXACTLY TWO gossip fieldsports.Message.ServedBytes / RepairsDone, CBOR slots 29/30, omitempty, additive (an old decoder skips them). They ride WITH the capacity pledge and are filed under the existing CapTotal > 0 condition, so the work sample IS the capacity sample and every member is classifiable; the bound is the existing peerCaps / evictPeerInfoIfFull / maxPeerInfo, two int64s per existing entry and no new peer-keyed map. Both are node-wide with no object axis and no fetcher axis, so neither carries the (fetcher × object) join Don't #3 forbids. Row 10 adds no third field: the tier class is DERIVED from the gossiped CapTotal against bands published on the wire with their source — pony < 16 GiB, horse to < 1 TiB, archival above, read off the Economist tier table's "16+ GB disk" and "TBs". A presentation band over a self-reported number: not a role, not a security parameter, and a node can lie into any band for a wrong dashboard. GET /api/economy/concentration carries the advisory §2.1 correction: the design doc's own network-wide repair-Gini gate is VACUOUS, and the build measures it rather than quoting it — on the healthy vision shape (60 ponies at zero repairs, 6 horses repairing evenly) the network-wide repair Gini is 0.9091 while the repair-capable subset's is 0.0000, so a threshold that passes one fails the other on the same healthy network. The repair series is therefore scoped to the horse+archival subset, carries that scope on the wire, and still reddens on total capture (0.8571). Serve-work stays over the whole sample. C2 rides beside them as committed-global, absent-with-a-reason without a chain, and publishes weightUniformity because HHI, Gini and TopShare are all blind to an equal-bond split. minGossipSample = 3 is derived, and is a privacy floor as much as an honesty one: at n=1 a Gini is 0 by construction and at n=2 it INVERTS to the ratio of two named peers' counters, so below 3 no estimate is published at all — which is what lets concentration and network stay open on the unauthenticated wire while flows and g are token-gated in full, pooled row included (on a one-object node the pooled delta IS that object's delta — the mistake selfFunding shipped with). credit.Ledger.WorkSample is a NON-registering read, classified neutral: ServedBytes/RepairsDone go through acctRegister, which creates an account and, on a faucet-configured ledger, increments grantsPending — so stamping gossip through them would mint an account as a side effect of sending a FindNode. Panels 1-4 ship as cmd/silt/ui/economy.html over pure render.js functions, so the honesty rules are testable and a withheld block cannot abort the page: horizon renders "not yet measurable" and never "perpetual" and never green; cost is an operator query parameter and never persisted; the wash panel says "suspected" and never claims a finding. The whole-surface route constant moved 7 → 11 with each route's examination written beside it, and because both existing scans pin ONE instant (so the ring holds one sample and the gated routes are vacuous there), TestR22FlowsAndGAreTokenGatedAcrossAMeasuredWindow drives a real four-sample window before walking the untokened surface. Thirteen controlled reverts run RED, including the one that matters — leaving the pooled row open while withholding only the array — and one vacuous gate was caught in the act: an omitempty assertion comparing frame LENGTHS stayed green under the revert that deletes omitempty, and was replaced by an exact scan for keys 29/30 in the raw CBOR map. Deliberation: docs/thinking/2026-09-08-c3-r22-observability-build.md.

Added

R2.7 blocking telemetry — the affordability floor (Lane C4, Economist advisory ADVISORY-boulder2-telemetry-spec-R2.4-checklist-and-RC-scope-2026-09-07 §1.3). spendRefusedInsufficientCredit (every refusal) and spendRefusersDistinct (identities refused at least once) on credit.Ledger. The ledger has THREE spend gates and all three refusal DECISIONS are counted: ChargePublish and FundEscrow count at their own refusal branch, and CanPublish — which is a PREDICATE and must not count, because sim/economy.go calls it for display and a counter that moves when a dashboard reads it is worse than a missing one — is counted at the one place that turns its false into a refusal, registry.Gated.Publish, through the exported Ledger.NoteSpendRefused reached by an optional interface so ports.CreditLedger stays the consensus-relevant surface. Missing that third decision made the floor under-read beneath a HARD canary abort (ROADMAP C6 aborts on any rise in spendRefusersDistinct), and sim/economy.go grades FreeloadersRejected off exactly those refusals — so the floor would have read zero beside a non-zero rejection count in the tier R2.7's adversarial workload runs in. Distinct identities are counted the way grantDenied already does: ONE BOOL on the account, never a side set, so it cannot become a grow-only map. Every other counter in the ledger measures a flow that happened; this is the only one that measures the flow that was REFUSED at the affordability floor, which is build-immutable #4's exact failure mode and the failure R2.7 is most likely to miss — an economy reads solvent when nobody can afford to transact. Surfaced in the faucet block on GET /api/status beside grantsDenied. It ships on BOTH branches of that block, configured faucet or not: these are refusals for want of CREDIT, not for want of a token, and dropping them on the unconfigured branch would be a silent loss (Don't #4) on exactly the default posture. spendRefusalNote ships beside the numbers so no reader can quote them without the caveat — this is a FLOOR detector: a non-zero value proves honest demand is being refused somewhere and can abort a canary; a zero value certifies NOTHING, because an adversary inflates the number at will with underfunded identities. Gates, each run RED under a controlled revert: TestSpendRefusalsCountDistinctIdentitiesNotRetries (five retries by one identity count five and one; a later success decrements neither), TestAffordabilityFloorReachesTheStatusSurface (the wire half, on the unconfigured-faucet branch) and TestGatedPublishRefusalMovesTheAffordabilityFloor (the third gate, with an arm asserting that three pure CanPublish READS move nothing).

Added

R2.7 blocking telemetry, detector A4 — escrow laundering by self-repair (Lane C4, Economist advisory ADVISORY-boulder2-telemetry-spec-R2.4-checklist-and-RC-scope-2026-09-07 §1.2). The advisory's bountyPaidToEscrowFunder is DEGENERATE — every credit entering an escrow on a silt ledger is placed there by that node itself, so "the payee also funded this escrow" reduces to "the payee is this node" — and it is replaced by three parts, none of which needs a (fetcher × object) join (that join is the access record Don't #3 forbids). A4-1: objectEscrow.funded splits into fundedPrepay (FundEscrow) and fundedSkim (RecordServeToObject, SettleDelivery); reverseLane claws back the SKIM leg only, because a reversal only ever undoes a serve's own auto-skim. The published funded is now DERIVED from the two legs rather than a third accumulator, and its value does not move. The wash loop's recoverable money is the skim, so without this split "escrow recovered by self-repair" has no denominator and the S5 qualifier cannot be evaluated at all. A4-2: Stats.BountyPaidToSelf and Stats.BountyCreditsPaidToSelf on the NODE (the ledger does not know its own id), fired in settleRepairVerdict where the bounty pays and both n.id and claim.Holder are in hand. claim.Holder is attacker-declared on an inbound MsgRepairClaim and nothing refuses a claim naming the judge itself, so this is reachable; an honest judge never pays itself, so any non-zero value is the self-dealing shape. A4-3: BountyToPriorFetcher on credit.Ledger — bounties released to a repairer with fetchedBytes > 0 at payment time, read off account state that already exists, adding no map. It is a SHAPE, not a detection: a repairer may legitimately have fetched survivor shards from this judge, so it ships with its honest limit on the wire and is never a slashing or disbursement input. Surfaces: A4-1 on durability.objects[] (fundedPrepay / fundedSkim) and on /api/economy/self as prepayIn / autoSkimIn beside the combined skimIn, both already token-gated; A4-2 on the stats block; A4-3 on economyRevenue, which is REBUILT rather than passed through in withheldEconomySelf — the allow-list sits at the economySelf field level, so a field added inside economyRevenue would otherwise ship open, and a bounty-out figure on a one-root node is that root's withheld objects[].bountyOut. Gates, each run RED under a controlled revert: TestEscrowFundedSplitsPrepayFromSkim, TestBountyPaidToSelfCountsTheJudgeAsHolder (its third-party arm is the ablation), TestBountyToAPriorFetcherIsFlaggedNotBlocked (both payments settle identically — flagged is never blocked), and TestR27A4PriorFetcherCreditsAreTokenGatedOnTheWholeSurface, the wire gate on the token gate itself: a fixture that pays a real bounty to a prior fetcher, a TOKENED positive control first, then the untokened 0 / 0 plus the named withhold, then a scan of every number in every untokened GET body on the real apiRoutes table. It is a SIBLING of the existing whole-surface F2 scan rather than an edit to it: that scan's strength is that at paid == 0 the per-object reserve, net and skimIn are all aliases of funded, so four aliases ride one scan — and paying a bounty in its fixture would destroy three of the four. The two share one r29aWholeSurfaceGETRoutes constant, so adding a GET route reddens both until it has been examined against each scan's property.

Added

R2.7 blocking telemetry, detector A2 — supersede suppression (Lane C4, Economist advisory ADVISORY-boulder2-telemetry-spec-R2.4-checklist-and-RC-scope-2026-09-07 §1.1). Three node-wide int64 counters on credit.LedgerserveBytesObjectAware (RecordServeToObject), serveBytesWitnessed (SettleDelivery, on the clamped ack, before the full/partial branch) and serveBytesLaneEvicted (laneFor, the FIFO confiscation at the maxProvisional cap) — plus the live-lane sum taken on the walk ServeMintStats already makes. They satisfy an exact conservation identity, objectAware == witnessed + laneEvicted + inFlight, and that identity is the test. servedBytesUnwitnessed, servedBytesUnwitnessable and receiptCoverage are DERIVED at the read and stored nowhere: a second accumulator for a residual invites drift between two write paths. Coverage's denominator is object-aware bytes, never total served bytes, so a node serving manifest chunks (no root, never witnessable) is not penalised. The Economist's spec named a fourth counter, serveBytesSupersededFlat, on the flat leg's supersede block and said in the same sentence that it "is deleted with the leg" — Lane C1 deleted the leg, so the identity has three terminal terms and no permanently-zero counter ships. All seven fields ride the existing serveMintWithheld + countersWithheld markers on GET /api/status; no new marker, no new withhold clause, and no identity or object axis anywhere (Don't #3). Gates: TestServedByteSplitIsExactAcrossEveryTerminalState, TestEvictedLaneBytesAreCountedForfeitedNotWitnessed, TestSuppressionShowsAsCoverageBelowOne (the RED-first proof that the two arms do not read the same), each run RED under a controlled revert of its increment site. serveMint.laneOn ships beside the coverage figure: a zero coverage means one of THREE things, and the third is the DEFAULT posture — a node that does not run -accept-delivery-receipts never calls SettleDelivery, so it prints receiptCoverage: 0 beside a large serveBytesObjectAware while behaving perfectly. ROADMAP C6 makes coverage below 0.75 a machine-read canary abort and the RC ships default-OFF, so without the lane state every honest RC-default node would trip it. Same shape and same reason as economySelfFunding.bountyOn for -economy; gated by TestCoverageZeroIsDistinguishableFromTheLaneBeingOff, whose two arms differ only in the lane flag. Coverage is documented as a LOWER BOUND, not a rate: in-flight and evicted bytes sit in the denominator and never the numerator, so the honest reading is the band [witnessed, witnessed+inFlight] / objectAware — both ends published — and an abort belongs on the upper end. The formula is deliberately not re-based on terminal bytes only, because a single re-based number would hide the band's width from the grader whose rule is the band. TestServedByteSplitIsExactAcrossEveryTerminalState now also drives the PLAIN path, which is the clause that justifies the object-aware denominator and had no test: a manifest serve must move servedBytesUnwitnessable and leave a fully-witnessed node's coverage at exactly 1.0 — under a denominator of total served bytes it falls to 0.727 on a node doing honest work.

Added

Cloud field test: the R2.9 paid delivery lane is on the graded sheet (flow_delivery_lane, rows 13-delivery-lane / 13b-delivery-settlement). topology.py arms -accept-delivery-receipts -delivery-idle-window 90s -grant-capacity 64 -grant-per-hour 64 on the boot validator (the token issuer; the faucet bucket sits 5× below the daemon's derived capacity cap, not at 78 % of it); the flow publishes from fetch-1, fetches back, and presents swarm receipt against the boot validator and against a lane-off store. It is ERA-AWARE and honest: on every real network until the R3.4 stamp raise era-4 is dark, so no E→key binding can commit and the positive settlement has no live seam — row 13 grades the lane's field contract (armed by the unit's argv + announced by the boot banner read over the WHOLE journal; the client refused at the withdrawal naming the committed binding, nothing spent, the server's debug.log carrying no banked line after the flow's baseline; the lane-off server answering with the NOT-banked marker; the two refusals never conflated), and row 13b is a SKIP behind that era probe (the RC gate stays reachable) that grades the wire settlement at the stamp raise with no harness change (delivery receipt banked + the idle delivery session closed, both debug.log lines — n.logf output never reaches journald). Substrate noise GAPs, never FAILs; the client call is retried before it is classified. Blind PE ruling silt-reviews/principle-engineer/RULING-cloudtest-delivery-lane-flow-34114c4-2026-09-07.md (BLOCK on the first cut: the banner read through the 800-line journal window and every server-side marker read from the wrong surface) folded in full. LOCAL proof: the new e2e TestPaidDeliveryLaneArmsInTheHarnessPosture (the harness's exact objective-path argv, epoch clock DERIVED, the real CLI, the refusal named, nothing banked) plus TestPaidDeliveryLaneRefusesWithoutACommittedKeyBinding, TestDeliveryReceiptRefusedWhenLaneOff, TestPaidDeliverySessionEndToEnd, and a LOCAL=1 docker drive of the flow. Owner ask 2026-09-07: "create the necessary cloud tests before the next billable run".

Fixed

Crash-safety — a torn chain.cbor no longer restarts the validator from genesis (#558, Lane B8, scope call S3). chainstore.Save now writes a temp file, fsyncs it, renames it over chain.cbor and fsyncs the directory (the markstore pattern), so a power loss or OOM-kill mid-write cannot leave a truncated store. At boot the daemon replays through chainstore.Recover: a replay that would DISCARD finalized history — a torn file, or a block failing structural verification — REFUSES TO START (exit 3) and names the loss, the kept prefix and the recovery (-ws-checkpoint below the prune horizon); the new -accept-chain-loss flag is the operator's explicit way past, keeping the longest valid prefix and first MOVING the original, untouched, to chain.cbor.rejected-<unix> (a rejected file may be byte-perfect and merely unreadable by this binary — a downgrade, the #572 no-verifier guard — so acceptance never destroys it). Before this the failure was printed and the node continued — from genesis when nothing decoded — and re-entered consensus holding its frozen-epoch seat with a history it did not have (the a434494-deep shape: an INTACT file an era-2 replay bug rejected, restarted at genesis; the replay bug itself was fixed earlier under #558). Gates: RED under the always-accept ablation — adapters/chainstore TestRecoverRefusesATornTail, TestRecoverRefusesACorruptSuffixKeepsPrefixOnlyWhenAccepted and the daemon-level e2e TestDaemonRefusesToStartOnAnUnreplayableChain (exit 3, file untouched); RED under the drop-the-move ablation — TestRecoverAcceptedLossPreservesTheOriginal and TestDaemonAcceptedChainLossPreservesTheOriginal (the preserved copy holds the original bytes). TestSaveLeavesNoTempAndDecodes is a shape pin only; the fsync has no runtime oracle. The two e2e gates skip under -short, and the multi-process e2e CI job is NOT a required check — making it required is an admin action owed (PE re-ruling residual). Blind PE ruling: /Users/andrewedmond/Claude/claude/silt-reviews/principle-engineer/RULING-b8-558-chainstore-refuse-to-start-2026-09-07.md. Note for the owner: the rule refuses on ANY structural-verification failure at replay, not only a torn tail — a strictly larger surface than S3's sentence (kept, per the PE: a stale-prefix restart holding a frozen seat is a safety problem, #560); the reload path is now start-blocking, which matters at the Lane D stamp raise.

Changed

The publish default moves from 64 KiB to 256 KiB, and manifests are framed at TRUE length — one content-addressing break (D-R2.9-NODE-HALF-CALLS call 4′, ratified 2026-09-07). pipeline.DefaultChunkSize = 262,144: one chunk is one delivery credit (credit.DeliveryBytesPerCredit, pinned in cmd/silt), a k = 10 stripe pays a repair-bounty base of exactly 10 (the certified D-S7 threshold of 36 retrievals per repair; the 64 KiB former default paid 2 of an exact 2.5 — R-BOUNTY-TRUNCATION STAYS OPEN, reopened on the blind PE's item 3: the floor mechanism is unfixed and -chunk-size 65536 still under-pays 20 % unwarned), and 256 KiB is the largest power of two at which a PoR audit samples every block. pipeline.ManifestFrameSize: a sealed manifest that fits in one chunk is framed as ONE frame of its own length + 8 (manifests carry no parity, so padding them to the chunk size bought nothing — 87.6 % of the first production store was 1.4 KB manifests padded to 65,536 B, R-MANIFEST-PADDING, closed); larger manifests keep the chunk size. Data frames stay padded to the chunk size (erasure shards are equal-length within a stripe), so a small FILE pays the padding — PRICED: a 1 KB object stores 2,097,264 B, +300 % over the 524,400 B it stored at 64 KiB, and the erasure-efficiency floor K × chunkSize moves 640 KiB → 2.5 MiB (Economist advisory §4); the short-final-stripe fix is filed as R-SHORT-FINAL-STRIPE (ACTIONABLE, Builder). Already-published roots are unchanged; a NEW publish of already-published bytes produces a different root on either side of the boundary (convergent dedup does not span it). The GENESIS block hash moves with this change — accepted by the owner (2026-09-07: no live network exists to fork). chain.Block.Hash covers entry.ManifestChunks, so the framing alone moves the height-0 hash from 7becf754…32ce to f428d0a8…0951 — a fresh node and a node with a persisted pre-4′ chain disagree at height 0 with no refusal and no log line (blind PE ruling silt-reviews/principle-engineer/RULING-default-chunk-256k-manifest-framing-b365f10-2026-09-07.md, item 1, measured; the PE recommended pinning the old frame, the owner chose the new genesis). Every development chain built before this commit is stale: wipe stores/chains on upgrade. TestGenesisBlockHashIsPinned holds the new hash, the root and the manifest chunk ID as literals, so from here height-0 identity moves only by an explicit, recorded decision; pipeline.Options.ManifestFrameBytes (new; 0 derives, non-zero pins) reproduces the pre-4′ framing on demand. Whether height-0 identity sits inside the R3.4 freeze surface is filed (R-GENESIS-HASH-FREEZE-SURFACE). A second break class, named (item 2): the framing moves entry.ManifestChunks under an UNCHANGED root (manifest.Root covers data + parity IDs only), and registry.Publish answers ErrDupPublish for exactly that shape — a re-publish of pre-change content at the same explicit chunk size collides at the registry after the scatter has shipped the bytes (TestReframedManifestUnderAnUnchangedRootIsADupPublish). The B_bootstrap census export header now carries publishDefaultChunkSize (instrument class, a compiled constant) so a future analyst can tell a bin shift that is traffic from one that is geometry (the Economist's one census ask). Gates TestManifestIsFramedAtTrueLength, TestDefaultChunkSizeIs256KiB, TestDefaultChunkIsOneDeliveryCredit, TestGenesisBlockHashIsPinned, TestReframedManifestUnderAnUnchangedRootIsADupPublish. Deliberation: docs/thinking/2026-09-07-default-chunk-256k-manifest-framing.md.

Changed

The credit numéraire (G-R212-7, certified and owner-ratified 2026-09-06): the unwitnessed serve mints ONE credit per 393,216 bytes served (was one per byte), the repair-bounty base is priced in the witnessed fetch price (c·k·shardBytes/262,144 credits), and R2.9's delivery price pair (U, p) = (262,144 B, 1 credit) is pinned. core/credit/numeraire.go derives Dλ = ⌈3U/(2p)⌉ so STRICT parity holds by construction below and Don't #7 against the relay price holds above (T-NUMERAIRE); minting is a floor over a per-lane byte accumulator (remainder on the provisional LANE, never the account — a witnessed supersede would otherwise double-pay), with two floors on the object path so the escrow skim accumulates across serves. A geometry whose k·shardBytes is below one credit of fetch pays a ZERO bounty: the judge counts it (Stats.BountyBaseZero) and journals it, and silt add / swarm publish warn below it (corrected 2026-09-07 to the real geometry — a shard is a whole ciphertext chunk, threshold ≈ 26 KB at k = 10; the 64 KiB default pays 2 — see Fixed above). serveMint telemetry on /api/status (token holders only). Byte observables (ServedBytes/FetchedBytes) are unchanged. Gates TestGLambda1…9, TestGLambda8ZeroBountyBaseIsNamedNotSilent, TestGLambdaServeMintTelemetry; the pre-numéraire conservation gates re-expressed in mint units. The silt sim run economy defaults move to 4 MiB single-chunk objects and a SIM-SCALE fee of 8 credits (a production token is 20.93 GiB of unwitnessed serving; the freeloader demonstration needs a fee a host can earn inside the run). Blind PE MERGE-AFTER folded in: silt sim run economy -fee defaults to 0 (= the scenario default) so the CLI demonstrates what the test does; the serveMintWithheld marker is gated; serveMint reports both remainder legs and a reversedCredits counter (the mint counters are gross of reversal); durability.bountyBaseZero surfaces zero-base releases; the publish warning fires only for an EXPLICIT -chunk-size below 262,144.

Changed

-economy flag help states the economy's status in one sentence (built and running in shadow; payout is opt-in until the delivery price lands, ROADMAP R2.4). The README carries the same sentence.

Changed

Relay lane re-priced: RelayIncrementBytes 4,096 → 524,288 (512 KiB); MaxChainLength and MaxSessionBytes DERIVED from the anchor face (G-R212-2, certified 2026-09-06, owner-ratified the same day). At 4 KiB the starter grant bought 1.9 GiB of relayed fetch, 23.4× below the 44.7 GiB structural floor; at 512 KiB it buys 244 GiB. T-RELAY-GRAN: MaxSessionBytes = MaxChainLength × RelayIncrementBytes (24.4 GiB) so nothing a fetcher pays for is burned past the cap; MaxAnchorsPerSession derives to 1; the relay adapter's shared free/paid per-splice cap defaults to the protocol ceiling and Serve refuses a lower cap (a coherence refusal, never a free/paid differential). Face-neutral: ShippedAnchorFace is unchanged. Nine sites moved in one PR; gates TestRelayMaxAnchorsPerSessionCoversTheSessionCeiling (now also the T-RELAY-GRAN pin), TestRelaySettlementIgnoresForwardedBytesIsBoundedByAnchor.

Fixed

statehash.Root refuses an empty leaf value (G-R31-5, owner-ratified 2026-09-06). The SMT library treats an empty update as a delete, so an empty value would have silently dropped its key from the state root. Every committed field encodes non-empty, so no honest root changes. EmptyValueError; gate TestRootRejectsEmptyLeafValue. R3.1 closes as an owned residual.

Fixed

-grant-deny-floor defaults to an ADVANCE of one publish fee (R2.12, owner-ratified 2026-09-06). The default -1 resolves to the ledger's fee; 0 opts into deny; the sentinel is inert while the faucet is unconfigured. grantDenyFloorOneFee; gate TestR212FaucetFlagsRefuseHalfAndUnsafeConfigurations.

Fixed

NetGet pulls parity per STRIPE, in deficit order, instead of every parity column of the whole object. One withheld data chunk used to make the fetcher pull all N−K parity columns of the entire file — a 1.6× draw on a single missing shard (R-PARITY-AMPLIFICATION). The fallback is now a deficit walk: per stripe, shards missing = real data shards − present; each parity column is consulted in turn for the stripes still in deficit and the walk stops at the first column that clears them — typically one lookup, and never more parity than the damage (a short final stripe counts its real shards, never K). The dead parityForMissing helper is removed. Not a closure claim: a provider that returns a CORRUPT shard has already transferred the bytes before they are verified, so the 64 GiB grant/r pin's worst case is unchanged. Presence in the deficit is READ-AND-VERIFIED (the disk store's existence check is a stat; its read verifies), so a bit-rotten local shard counts as missing and is routed around — at a measured cost of two reads and three hashes over the data per retrieval instead of one and one (~46 ms per 64 MiB chunk vs 2.5 µs; ~21 s at a 30 GB object), taken for correctness with the pay-on-failure redesign filed as R-PS-PRESENCE-COST. Gates TestPS* (G-PS-1…6, G-PS-8) on the 80 KiB / 4 KiB rig; new counters stats.ParityColumnLookups, stats.ParityShardsPulled. Deliberation: docs/thinking/2026-09-06-parity-fetch-per-stripe.md.

Fixed

R3.1 — two latent defects on the floor box's state-root fold surface, closed behind gates (2026-09-06; Researcher certification R3.1-SMT-domain-separation-disjoint-preimage-RESEARCH-CERTIFICATION-2026-09-06.md). (1) FoldChangedPaths seeded the SMT library's node store with witness-supplied (Digest, Preimage) delete siblings without checking that the digest is the hash of the preimage; the library dispatches node type on the first byte and trusts the lookup key as the digest, so a forged node cost zero hash work (the audit's Issue #2 arriving on the verify side). Every sibling is now bound by the library's own rule — SHA-256 for leaf/inner preimages, the expansion root for an extension preimage — and the fold stalls with ErrFoldSiblingUnbound otherwise. (2) A proof whose NonMembershipLeafData does not begin with the leaf prefix made the library PANIC in checkPrefix (no recover in core/): a 33-byte remote crash; and — found by the blind PE code review — an empty SiblingData, or a 0x02 SiblingData shorter than 35 bytes, panicked the library's unbounded hashPreimage slicing (a 154-byte crash through IngestBlockWitnesses). Resolve, FoldChangedPaths and IngestBlockWitnesses now refuse exactly the library's panic set before VerifyProof (measured: zero over-refusals). Both defects were latent — the floor box never Accepts yet. New gates: TestR31UnboundDeleteSiblingStallsTheFold, TestR31ForgedExtensionSiblingDoesNotBind, TestR31MalformedLeafPrefixIsRefusedNotPanicked and TestR31MalformedSiblingDataIsRefusedNotPanicked (their controls capture the raw library panics), plus the scope pins TestR31NoSumTrieAndEverySMTUsesSHA256, TestR31SMTModuleIsPinnedToTheCertifiedVersion, TestR31EveryStateHashTagEndsInExactlyOneNUL. Record: docs/design/state-root-domain-separation.md.

Changed

R2.9: the delivery session's unsettled remainder is a DEPOSIT released at anchor expiry, not a burn (D-R2.9-NODE-HALF-CALLS call 1, amended 1′; certification silt-reviews/research/research-outcome/R2.9-session-remainder-refund-and-live-anchor-cap-RESEARCH-CERTIFICATION-2026-09-06.md). CloseDeliverySession(fetcher, remaining, maxAnchorEpoch) books ONE pending record {durable fetcher, amount, releaseEpoch = maxAnchorEpoch + W + 1} in the same ledger call that accounts the close; release rides the existing at-most-once-per-epoch sweep against the WATERMARK (and ReleaseDueRefunds from the node's session sweep), paying an account that ALREADY EXISTS on the ledger — never the registering lookup (M1: a bearer anchor's presenter need not be its payer, and acct would mint a grant); no account ⇒ burned and counted (R-REFUND-NEEDS-AN-ACCOUNT, ≤ f per session). Release-AT-CLOSE and the per-identity live-anchor cap are REFUTED and not built: a bearer anchor passed down fresh keypairs would fill the guard for zero net credits; locking the deposit for the anchor's window bounds live occupancy by stock/f (T-DEPOSIT) and keeps R2.12's start-up assertion exact. The pending table is bounded at the guard cap, refuse-never-evict. The session carries maxAnchorEpoch; the guard entry and the durable store carry NO fetcher identity. A restart loses pending deposits and live faces (the guard survives): RestoredGuardEntries — the guard entries restored at boot, both lanes, an UPPER BOUND on lost deposits — is printed at boot and served on /api/status (deliverySettlement, token holders only). The 64 GiB pin's composed claim is now TRUE at every consumption ratio; R-REAPER-FORFEIT and R-FACE-BURN-GRIEF close on this lane; R-PIN-VACUOUS-UNDER-QUANTIZATION closes; new R-STOCK-RENEWABLE-OCCUPANCY, R-ANCHOR-BEARER-TRANSFER. DeliverySettlementStats gains RefundedCredits, PendingRefundCredits, RefundsBurnedNoAccount, RefundsBurnedAtCap, RestoredGuardEntries; BurnedCredits counts genuine burns only. The S5 affordability line says so. Gates G-6R-1…10: TestRemainderIsConservedAcrossCloseAndRelease, TestReleaseNeverRegistersAnAccount, TestRemainderIsNotSpendableUntilTheAnchorExpires, TestGuardOccupancyIsBoundedByTheCreditStock (the refutation encoded, written first), TestZeroSettleSessionReturnsItsWholeFace, TestPendingRefundTableIsBoundedAndRefusesNeverEvicts, TestR212AssertionStillBoundsOccupancyUnderTheRelease, TestNoIdentityIsJoinedToAnAnchorSerialInTheDurableStore, TestRestartLosesTheRemainderAndCountsIt, the Invariant-A press; four ablations RED (release at close reddens four gates at once). B-6 and G-λ-8-6 re-expressed as TestDeliveryRemainderIsNeverRoutedToEscrow and TestRemainderIsAccountedOnceAtCloseNotPerSettlement.

Fixed

The repair-bounty geometry: a shard is a WHOLE ciphertext chunk, so the shipped 64 KiB default pays a base of 2 credits, not zero. The G-R212-7 build stated shard = chunk/k and placed the publish warning's threshold at 262,144 B (it fired 10× too eagerly, and its blind PE filed R-DEFAULT-CHUNK-BOUNTY-ZERO on the same model); the Economist's default-chunk advisory caught it and it is verified at the erasure stage and the judge. credit.MinBountyChunkBytes (262,144) is replaced by MinBountyStripeBytes (the stripe must reach one credit of fetch) and MinBountyChunkBytesFor(k, crypto.Overhead) ≈ 26,199 B at k = 10; silt add / swarm publish warn only below THAT, by the judge's own arithmetic; the judge's fix text derives the same number; crypto.Overhead (the 16-byte tag) is pinned against a real encryption. What the default actually does is a 20 % integer-truncation under-pay (R-BOUNTY-TRUNCATION, exact 2.5006 → 2). Gates TestRepairBountyBaseAtTheFormerDefaultIsTwoNotZero, TestCiphertextOverheadIsTheTag; the G-λ-8 warning gate re-expressed on the real geometry.

Added

R2.9 node half — the paid DELIVERY SESSION (core/node/deliverysession.go, core/demand/session.go; G-R212-8 certification 2026-09-06 §3.1, built under G-6 as ratified with the burn-vs-refund seam isolated). A durable fetcher opens one session per server with ONE demand-domain anchor (the demand token it bought there, now spent into the shared paid-serial guard at OPEN — MsgDeliveryOpen), tops it up with fresh anchors (MsgDeliveryFund), and settles INCREMENTALLY with cumulative-count receipts (receipt v3, MsgDeliverySettle: a lower or replayed count pays 0 by arithmetic; settle-once is REFUTED). The session spans objects (per-object lane reversal and skim stay the ledger's), holds no object-keyed collection (Don't #3), and closes on exhaustion or on IDLENESS measured from its last settlement on the node's injected clock (coarse stamp; wall time in production — R-SESSION-WALLCLOCK-STEP; lazy sweep on activity plus SweepDeliverySessions on the daemon's ticker); the byte ceiling and k_max = 1 are DERIVED from the face. -delivery-idle-window is REFUSE-UNTIL-SET (the block interval T_b is unmeasured); the daemon prints the S5 affordability line (B-11) and refuses a priced lane whose 64 GiB pin does not fit in WHOLE faces (G-λ-8-2: 9 of 10 today). Witnessed demand on this lane is DENOMINATED IN SETTLED INCREMENTS (Bank.Witness, WitnessedIncrements; the P3b distinct-bonded-fetcher count is its own surface; the v2 token counter is never shared) — the certified restatement P-SESSION of D-DEMAND's token-level property, whose ratification is the owner's. silt swarm receipt drives the session flow (-increments); the sim positive arm settles two objects on one session; e2e TestPaidDeliverySessionEndToEnd runs the real withdrawal, open, settle and idle close over TCP. The v2 flat receipt path (MsgDeliveryReceipt) stays callable until its retirement PR (B-9). Gates G-λ-8-1/2/3/4/5/7/9, B-7, B-8, B-13 (reap forfeiture), G-DEM-1…8 (TestAckReversalUsesTheBudgetCappedCount closes R-ACK-USES-UNTRUSTED-COUNT); eight node ablations run RED. OPEN: G-λ-8-10 (a zero-settle session spends no face — lift (b) is the G-6 owner call, lift (a) is refuted by G-λ-8-9), the idle-window VALUE, the flat-path retirement. The blind PE's blocker — under settle-monotone the payer chooses the delta and a per-settlement skim floor ⌊value/8⌋ let a face settled in deltas ≤ 7 fund the object escrow with NOTHING while the serve-time skim was clawed back (net −21 at 64 MiB) — is closed as certified (silt-reviews/research/research-outcome/R2.9-settlement-skim-under-fetcher-chosen-deltas-RESEARCH-CERTIFICATION-2026-09-06.md): the skim floors on the SESSION's cumulative settled value, ⌊(prior+value)/8⌋ − ⌊prior/8⌋, with prior the session's own counter passed to SettleDelivery; no remainder is stored; the ratified 1/8 does not move (the G-λ-7 discipline applied to the witnessed leg). Gates G-SKIM-1…6 (TestSkimIsExactOverTheSessionNotPerSettlement, TestSettlementSkimNeverExceedsItsOwnValue, TestSessionSkimIsPartitionIndependent, TestWashPayerPaysTheSkimAtEveryGranularity, TestWitnessingNeverDefundsTheEscrow, TestSkimAggregatesExactlyAcrossObjectsInOneSession), all RED under the per-settlement floor.

Added

R3.4 input — the IssuerKeys-carrier block fraction, measured (2026-09-06; core/node/r34_issuer_key_carrier_fraction_test.go, a logged measurement, not a gate). The floor box marks any block carrying a demand-issuer key registration Indeterminate, so the accept-flip's witness coverage depends on how many blocks carry one. From a cold key schedule over 2V round-robin blocks: before R2.11 every validator carried its own key on its own turn (V carriers, 50 % of blocks at V = 2, 4, 8); after R2.11 the first proposer carries every pending key at once (ONE carrier: 25 % / 12 % / 6 %). R2.11 lowers the Indeterminate fraction. One cold turn; the harness has no epoch turns.

Added

R2.11 — a peer-submit path for a demand-issuer key registration (2026-09-05; closes residual R0.4b-11). A validator that never wins a proposal slot can now get its per-epoch demand-issuer key committed: MsgSubmitIssuerKeyReg (APPENDED to the kind table, never inserted — MsgKind is positional) carries one self-signed IssuerKeyReg to peers every sync sweep while uncommitted; the receiver's arrival gate refuses, loudly and in order, a rate-exceeded sender, a slashed sender, a payload with ≠ 1 reg, a relayed reg (issuer ≠ sender), a bad signature, an out-of-window epoch, an already-committed binding and an UNBONDED issuer (the clause that bounds distinct senders); a queued reg is one slot per (issuer, epoch), latest wins; the next proposer folds it into its v5 block after its own, DROPPING (never deferring) one that is stale, slashed or no longer admissible; the drain driver arms on a FOLDABLE registration so an idle chain carries it. No validity rule moves, so an attester's acceptance is untouched and a mixed-version swarm cannot fork on it. Five TestR211* gates including the end-to-end property (the attest-only node's key commits on both replicas without it ever proposing) and the message-kind number pins. Blind PE code review (MERGE-AFTER) folded in: the arrival-gate fixture was vacuous and is rewritten against a bonded issuer; the staggered-takeover branch now counts issuer-key work (a dead designee no longer stalls a foldable key); the peer queue is pruned every sync sweep; stats.DrainProposalsArmed is a new node-wide counter on GET /api/status (validator-role observability: how often the drain driver armed a proposal; withheld with the other counters under -privacy). Not yet measured: the carrier-block fraction the R3.4 accept-flip needs (the floor box stalls on any IssuerKeys-carrying block). Deliberation: docs/thinking/2026-09-05-r2.11-issuer-key-peer-submit.md.

Added

R2.12 — the faucet rate limit (2026-09-05; owner calls of 2026-09-03 stand; blind PE design ruling STOP → reconciled with the Economist's derivation, then built). Three daemon flags. With -grant-capacity and -grant-per-hour both set, a fresh identity's 500,000 starter grant is applied at its first SPEND (CanPublish / ChargePublish / FundEscrow) only if a continuously-accruing token bucket on the node's own monotonic clock admits; otherwise it stays grant-pending and retries at its next spend — never permanently denied. Non-spend paths (a fetcher credited bytes, a bounty-paid repairer, a bond-challenged peer) take no token, so the bond-audit sweep cannot drain the bucket; the node's own account is granted unmetered. Unset, the faucet is UNLIMITED, byte-for-byte the pre-R2.12 behaviour — there is no shipped default, because the refill interval's epoch-equivalent depends on a block cadence that is demand-driven and only bracketed (40–170 s on cloudtest). -grant-deny-floor selects what an empty bucket hands out: nothing (deny) or N credits once (degrade; the PE recommends one fee). A start-up assertion refuses a capacity whose worst-case paid-serial guard occupancy capacity × (grant/fee) × (W+1) exceeds a quarter of the derived cap, tying four constants in three packages. Telemetry (faucet on GET /api/status: capacity, rate, level, grantsIssued, grantsDegraded, grantsPending) is withheld with the other counters under -privacy; it is NOT the R2.9a arrival series. R2.12 bounds the RATE of fresh grants, never their total — a soft, disclosed deterrent for the unbuilt structural cost of identity. Researcher-certified the same day: the rate is a SECURITY PARAMETER (build-immutable #4 on both sides), so no default ships and none is recommended; the deny floor is an ADVANCE topped up to the full grant, never a settlement; the daemon refuses -accept-delivery-receipts / -accept-relay-payments with the faucet unconfigured; grantsDenied counts distinct refused identities. Fifteen TestR212* gates. Deliberation: docs/thinking/2026-09-05-r2.12-faucet-rate-limit.md.

Added

-privacy on the UI server (D-UI-PRIVACY-FLAG, owner-ratified 2026-09-05; blind PE design review RULING-UI-PRIVACY-FLAG-design-2026-09-05.md folded in). on (the compiled default, every build): the node-wide serve counters — the whole stats block and durability.balance on GET /api/status; revenue, margin and wash on GET /api/economy/self — are withheld from a reader that presents no API token, ABSENT with the sibling marker countersWithheld: true, never a zero; the library's link field (a permanent retrieve-and-decrypt capability) is withheld from a reader that did not present the token in the Authorization header, with linksWithheld: true (live on silt client, which holds the linkbook). The operator's tokened reads are unchanged. off: publish them to any admitted reader, and the node is labelled PRE-RELEASE on its dashboard and observatory and via privacy.{mode,default} on every status response. Any other value refuses to start. Every withhold on the three documents composes in one place (readerView, economyView, libraryView, one readerAuth), each privacy withhold an allow-list. The pages' render logic moved to ui/render.js and a node-run gate proves a withheld document never aborts a render. release.yml asserts the compiled default on the built linux artifact. Compatibility: an OLDER observatory page pointed at a NEW -privacy=on daemon aborts its render (the old inline stats dereference) — upgrade the observing daemon. Blind PE code review (MERGE-AFTER) folded in: the library page renders "link withheld" instead of a get button with an undefined link; a bad -privacy value refuses even with no -ui; the pointer-sharing hazard on the cached document is gated. Deliberation: docs/thinking/2026-09-05-ui-privacy-flag.md.

Added

R2.9a — G-BB-12′ / G-BB-13′ Part A: the B_bootstrap block is served to the OPERATOR and to nobody else (2026-09-05; owner-ratified "refuse at startup", D-R2.9a-RUN-CALLS item 4; mechanism ruled by the blind PE, RULING-R2.9a-G-BB-12-design-2026-09-05.md). Two startup refusals in a tagged daemon: -bbootstrap with a non-loopback -ui bind, and -bbootstrap with a <store>/ui-token readable beyond its owner (a 0644 token would hand every local user the operator predicate). At serve time the block goes only to a request carrying the token in the Authorization HEADER — never ?token=, which lands in logs — and every other reader (a reverse proxy forwarding a loopback Host, a co-tenant, the observatory or any localhost origin) gets the sibling marker bBootstrapWithheld: true and no block, so the three wire states stay distinct key sets. All withholds on GET /api/status now compose in ONE function, uiServer.readerView (the F2 per-object detail today; D-UI-PRIVACY-FLAG's counters when built). Default build untouched: the off twins refuse and withhold nothing. Blind PE code review (MERGE-AFTER) folded in: a second untokened test helper had let a REQUIRED anchor's positive control pass on the marker ("bBootstrap" is a substring of "bBootstrapWithheld") — it reads as the operator and asserts the quoted block key; and the token-file check now also requires the file to be OWNED by the daemon's user (a pre-planted 0600 token from another user was measured adopted and served). Ten tagged gates (TestR29aG12*), all in the CI anchor list. Deliberation: docs/thinking/2026-09-05-r29a-g12-reader-is-operator.md.

Changed

R2.9a — the B_bootstrap byte axis is ONE bin per doubling (G-BB-23, owner-ratified in D-R2.9a-RUN-CALLS item 5). BBootstrapBinsPerOctave 4 → 1, BBootstrapByteBins 164 → 41, the grid 1,312 → 328 counters (≈ 2.6 KiB), the wire byteBinRule restated, the quarter-octave threshold machinery deleted rather than left dead under the tag. Why: the Red-team's F4 measured that at the census sizes the instrument is for, 35–86% of occupied cells hold ONE identity and the count of individually pinned identities is constant in the census size; the Researcher certified the bin count as the only lever that acts on that exposure (merging and rounding refuted at this scale). The 19% → 2× resolution cost has no consumer under the ratified run re-scope (grant/r is pinned structurally). TestR29aByteBinMatchesTheClosedForm pins the ratified value by name; BB-5's payload ceiling is 8 KiB; BB-16's fixture doubles per fetch so each poll crosses one edge on the coarser axis. Blind PE fold-in (MERGE-AFTER): the "about 4×" reduction claim was corrected to the measured R-dependent factor (1.4× at the census floor, 2× at R = 25, 4× only at R ≳ 1,000; the floor and the bin count are weakest in the same band), both touched gates were added to the tagged CI anchor list, and TestR29aTopBinSaturatesThroughTheSnapshot covers the clamp end-to-end at 2^62 bytes. Deliberation: docs/thinking/2026-09-05-r29a-bin-count.md.

Fixed

R2.9a — the four-residuals review fold-in (blind PE, 2026-09-05): the structural gate is a closed tick set, not a name match; two open gates are put back in the owner block; two stale claims are corrected; G-BB-29 is applied. (1) TestR29aBondChallengeStampsNoFirstTouch matched field names containing firstseen; the reviewer re-added the deleted stamp as bondSeenTick with the identical unset-guarded write and core/credit, the CI anchor step and every other gate stayed green. The gate now asserts the account type's set of tick-typed fields (uint64, ports.Time, ports.Duration) is CLOSED, exactly {firstFetchTick, lastBondTick}, and both are present, so a tick under any name fails with that name in the message (bondSeenTick uint64, bondSeenAt ports.Time and a whitelist drift each RED). (2) ROADMAP item 12 filed G-BB-13′ under "ratified, no longer owed"; it is OPEN for tagged builds and moot only for the default build, and every B_bootstrap run is a tagged run. It is restored as open, and G-BB-12′ (the code, not a handoff note, establishes the reader is the operator) is added as required-before-any-run and UNBUILT: the only host check reads the client-controlled Host header and the token gate covers mutating methods only. (3) The tagged bbootstrap CI job is REQUIRED since 2026-09-05 (the main ruleset's fifth context, added sixteen seconds after PR #736 merged), not advisory as ROADMAP said; it is the only check that sees a flag reintroduced under a non-literal name. (4) core/node/bondaudit.go's +1 said "so the first tick is never 0 (unset)"; the unset guard was the deleted firstSeenTick write (its only consumer since the bond ledger's first commit), and nothing reads a zero lastBondTick specially — the comment now says the +1 serves no reader and stays because moving the tick touches retention. (5) G-BB-29 from the DONT3 certification — the D-BB-BUILD-TAG reason text and core/credit/bbootstrap.go's header narrowed from "recording is the break" to "recording SURPLUS is the break" (T-DONT3 prong (a)), because the generalised rule condemned delivery.go's D-S7 provKey — is applied as an appended correction. Also: a stale "(residual R-BB-BOND-STAMP-TUPLE)" in a core/node failure message now says the residual is closed, and the 134× line names the 64 MiB chunk as the pipeline.go COMMENT's stated floor (the code constant is 64 KiB for the sim).

Fixed

R2.9a — the F2 gate on the status surface did not close F2: the pooled selfFunding sum and an uncached sibling endpoint republished the withheld counter at the reader's own rate. Both are closed, the ratified snapshot bound is corrected to say which endpoints it covers, and the one-cache/two-views separation gets the gate it never had. A blind principal-engineer review of the branch measured this on two live daemons: the tokened durability.objects[0].funded and the untokened selfFunding.skimIn on /api/economy/self were the same number (skimIn is Σ objects[].funded, and a node caretaking one object — every node from its first published object to its second — has one term); /api/roots named the root unauthenticated; and /api/economy/self recomputed per request, so polling it at 250 ms across one real fetch recovered a 16,388-credit step, 131,104 bytes of a named root, at 330 ms resolution instead of the 5 s the cache was certified to bound. The gate written to prove the leak closed asserted the leaked number was PRESENT: selfFunding.skimIn == 1024 in a one-object fixture whose sibling assertion said 1024 must be withheld, commented "names no root" — false in the presence of /api/roots. The fixes. (1) /api/economy/self is served from the SAME snapshot as /api/status: one document, taken in one loop pass, recomputed at most once per T, invalidated together after a token-gated mutation, carrying the same snapshotTakenAtUnix / snapshotAgeSec / snapshotIntervalSec stamps. Not a second cache (that doubles the recompute a flood can drive) and not a second interval (two documents on different clocks can be diffed against each other). (2) selfFunding. is token-gated with objects[], and the withholding is an ALLOW-LIST: withheldEconomySelf constructs the open document from named fields, the shape withheldDurability already had, so a field added to the SELF document later ships withheld until someone decides otherwise. objectsWithheld becomes detailWithheld — one flag for one rule, the durability block's name. (3) The false gate now asserts the property: selfFunding ABSENT untokened, present with the number tokened. (4) A WHOLE-SURFACE gate walks the real route table — apiRoutes, which serve registers, with an exact GET-route count so a new route fails until it has been examined — and asserts that for a node with one cared object no unauthenticated response on any route carries objects[0].funded under any name: every number in every body is scanned, no field name is grepped, and a positive control first proves the number IS on both tokened documents. (5) The copy at apiStatus gets its gate: the review replaced out.Durability = … with doc.Durability = … and the suite stayed green in both builds while one anonymous GET stripped the operator's own solvency panel for the rest of the interval; the gate reads tokened, untokened, tokened inside one interval on BOTH documents and requires the third read byte-identical to the first. What is open, by decision and not by omission. The node-wide aggregates — durability.balance, stats.BytesServed, revenue. — stay unauthenticated on both documents: the cross-origin observatory (cmd/silt/ui/observatory.html) reads stats.BytesServed with no token by design, and no cross-origin consumer reads selfFunding.* or /api/economy/self at all (the observatory reads /api/status, /api/roots, /api/registry; nothing under -allow-web-origin reads the SELF document; cloudtest and the e2e tier read /api/status with the token), so the gate breaks no dashboard. On a node holding one root those totals are that root's counters (R-BB-SIBLING-AGGREGATES): now rate-bounded to ⌊uptime/T⌋, not closed, and the whole-surface gate logs the 8x and 7x aliases so the residual is measured rather than assumed. Gating them breaks the observatory's bytes-served panel; that trade is the owner's. The ratified text. D-STATUS-SNAPSHOT-INTERVAL claimed ⌊uptime/T⌋ for "an observer" while the sibling was uncached; the entry keeps its ratified text and carries an appended dated correction naming exactly the two endpoints the bound covers, and the same sentence is corrected at cmd/silt/ui.go (statusSnapshotInterval) and in the core/credit/bbootstrap.go disclosure. Gates, each with a controlled-revert ablation recorded RED: TestR29aF2EconomySelfWithholdsPerObjectDetailWithoutAToken (re-pointed), TestR29aF2NoUnauthenticatedResponseOnTheWholeSurfaceCarriesTheWithheldCounter, TestR29aOneCacheTwoViewsAnAnonymousReadDoesNotStripTheOperatorsView, TestR29aEconomySelfIsServedFromTheStatusSnapshot (cmd/silt, untagged, run in both builds), and a live-daemon arm in TestEconomyEndToEndOnLiveDaemon (e2e), the tier the review found it at. TestEconomySelfIsReadOnlyAndLocalExact now makes the untokened request its comment described. Source: /Users/andrewedmond/Claude/claude/silt-reviews/principle-engineer/2026-09-05-RULING-r2.9a-status-surface-cache-stamp-and-f2-gate.md.

Changed

R2.9a — the B_bootstrap instrument moves BEHIND A BUILD TAG, and inside a tagged build the flag now gates the RECORDING as well as the publication (D-BB-BUILD-TAG, owner-ratified 2026-09-05). A default go build produces a silt binary with no histogram type, no census reader, no age stamping and no -bbootstrap flag; silt daemon -bbootstrap on it fails with "flag provided but not defined", because the mechanism is absent rather than disabled. Measured on the linked binaries from this tree: go tool nm matches 0 bbootstrap symbols in the default build and 10 under -tags bbootstrap; daemon -help mentions the flag 0 times and 2 times respectively. Why a tag and not a better runtime gate. TENETS Part VI Don't #3 is a claim about what silt builds"silt builds no mechanism to observe or link who-fetches-what… The refusal to build surveillance is absolute" — not about who can read the output today, and the shipped binary contained the mechanism and merely declined to print it. Concretely: cmd/silt/daemon.go injected the observability clock unconditionally, with a comment saying so deliberately, so every default-flags node recorded (identity, cumulative bytes, first-seen wall-clock nanosecond) for every requester, in RAM, with no flag to disable it — the when did not exist before R2.9a. Prior art agrees on the direction: go-ethereum resolved the analogous question by removing the personal namespace from the network-facing surface, not by authenticating it better. The trade this reverses, stated plainly: unconditional injection bought the property that flipping the flag on found an already-stamped population. That is gone on purpose — a tagged operator restarts with the flag and waits for the population to re-stamp. It is affordable because G-BB-15 already requires monotone uptime ≥ 2× the read bucket's upper edge, so the wait is the run's own precondition, and because BBootstrapRunPrecondition voids a run carrying any unstamped account rather than fitting a half-stamped one. Rejected alternatives: a token-gated endpoint (silt's status token is a single unscoped secret that also authorises publishing and funding) and a bind check (the guard reads a client-controlled Host header and never the connection's remote address, so a reverse proxy defeats it — the loopback bind remains a sound deployment posture, not an artifact-level claim). Everything R2.9a already shipped still stands, inside the tag: the minimum-requester floor, the G-BB-11′ construct-from-the-instrument-class property, the BB-20 wire-equivalence gate, the two-clock cross-check and the package-scope export gate — all now run ONLY under the tag, which is why CI compiles and tests the tagged variant and asserts twelve named gates actually ran (measured: 41 top-level R2.9a tests tagged, 7 untagged). What the tag does NOT close: with the tag and the flag on, every finding about the instrument's contents is live and unchanged — the census is attacker-mintable for $0, and the object half of who-fetches-what (stats.bytesServed, durability.objects[].funded) is published unconditionally, predates R2.9a and is untouched here. account.firstSeenTick's other writer, in RecordBondChallenge, also predates R2.9a and is preserved exactly, with its own gate — see the correction below for what that writer actually stamps, and for the residual it leaves open on a validator node.

Changed

R2.9a RE-CERT — the floor is RE-CLASSIFIED, and the rule it enforces is now a PROPERTY rather than a field list. A blind review and a re-certification found two defects in the entry above and refuted the FORM of its gate. The claim, corrected first, because it is the part that matters: the floor bounds the published census COUNT. It does NOT bound the anonymity SET. The census population is the set of identities that fetched; an identity is a keypair, the serve path has no admission control beyond freeload/chunkDenied, and Register mints an account for any unseen id — so an observer that can FETCH lifts the floor for nine keypairs and one chunk each, about 576 KiB. Because fetchedBytes never decreases and accounts are never deleted, that purchase is one-time and permanent for the process's lifetime. The floor is retained and re-labelled: it is a fit precondition (below R_min no q-quantile at q ≥ 0.9 is estimable at all) and a defence against a reader that cannot fetch. It is not a privacy mitigation against a capable adversary, and the Don't #3 question is not answered by it. suppressed: true is itself a disclosure — a published upper bound of R_min − 1 on the anonymity set. New open residuals: R-BB-CENSUS-SYBIL-PAD, R-BB-ANONYMITY-SET-SIZE, R-BB-SUPPRESSED-IS-A-DISCLOSURE. The rule is now a property. Partition every published field: an instrument field's value is a function of the injected clock sources, their injection instants and the compiled axis constants alone; a census field's value depends on the ledger's accounts or order. Below R_min the block must be a function of the instrument fields alone, with exactly one named exemption, suppressed. A list had failed three times in one pull request — the certification's three fields, the build's five, and a source gate reading two literal paths — so WithMinRequesterFloor now constructs the suppressed block out of the instrument class instead of clearing a list of census fields. A field nobody has foreseen takes its zero value, which is a compile-time constant: the default flips from published to withheld. Two fields were defects and both are fixed. ageExceedsUptime is a threshold on maxOccupiedAgeEdgeNanos — the very field the floor withholds — so at a census of one it published a lower bound on that identity's age; it is now census class and ABSENT below the floor. clockStepBack fused two arms and is split: the name keeps the instrument arm (the wall clock read earlier than the ledger's own start, which touches no account), and the per-account clamp becomes ageClampedToZero, census class and ABSENT below the floor. An operator loses nothing either way — clockSuspect and the raw signed clockSkewNanos report the same corruption and read no account. The "only seam" source gate is DELETED, not widened. It read two hard-coded paths while claiming a whole-tree property and a reviewer ablated past it in five lines. Widening it to walk the tree would not have closed it either, because the consuming seam is duck-typed on a method name and any such walk must exclude core/credit, which is exactly where a second exported reader would live. The close is the type system: the raw snapshot is now unexported and Ledger.BBootstrapPublish — which floors — is the only route out of the package, so no future publisher can obtain an unfloored census under any method name in any file. Scope stated rather than implied: that is a rule about the histogram OBJECT. FetchedBytes and ServedBytes are pre-existing exported per-identity readers and are unaffected, and a future export returning census-derived scalars of another type is the stated residual R-BB-EXPORT-SCALAR-BYPASS. BBootstrapRunPrecondition needed no raw census — it already keys on Suppressed — and gained an arm for the split flag. Sources: RESEARCH CERTIFICATION R2.9a-minR-floor-RECERT-sybil-pad-and-estimand-steerability-RESEARCH-CERTIFICATION-2026-09-05 §2.5, §5.1, §5.3, §5.4, gates G-BB-11′ and BB-20; RULING R2.9a-minR-floor-3337e8b-2026-09-05 M-1, M-2, B-1.

Removed

B-9 — the flat delivery receipt (token spent at REDEEM) is retired from the node. MsgDeliveryReceipt keeps its kind number and is refused with a named reason (handleDeliveryReceipt parses, banks and pays nothing); the fetcher-side SubmitDeliveryReceipt is deleted (swarm receipt moved to the session flow in R2.9). The build-questions certification (2026-09-04 §2.2) requires it — a fetcher left on the flat path re-creates the suppression break the anchored lane closed — and it closes R-V2-V3-DEMAND-DILUTION by construction (no second surface for one face). The "delivery receipt paid NO credit" S5 marker re-homes to the session lane's refused settlement. The core/demand primitive (Bank.Redeem, SubmittedReceipt, v2 Ack) stays as a leaf library with its own unit tests, documented as having no production caller; it and the ledger's flat leg (RedeemDeliveryCreditReason) retire together in one attended PR. Eight node gates and three sim tests are re-homed to open-and-settle (restart replay → the same anchor cannot open twice; hardness at admission → measured on MsgDeliveryOpen, wall-clock budget re-derived from T-7 for the new driver; P3b → the distinct-bonded-fetcher surface, Node.DistinctBondedFetchers). The v2 cost-to-wash sim retires in favour of its v3 twin. Where the cross-server pump closes MOVED with the re-home (blind PE ruling, 2026-09-07): a session anchor verifies under the server's OWN committed key only (the own-key rule, now gated — a fully operational server B with a foreign issuer's keys pinned refuses a FRESH foreign anchor; ablation RED), so the composed window claim is exercised at the ISSUER (A's own expired token refused at A's keyset, the refusal REASON read so the guard's watermark cannot stand in for the window; the re-dating pump is a same-server pump under sessions and is gated at A). The retirement itself is gated (a well-formed, otherwise-valid v2 receipt → OK=false with the named reason, no credit motion, both demand observables unchanged, the token still opens a session). The guard-full operator signal is re-homed to where it arises — open/fund (delivery anchor refused: guard full, WARN, with serial_guard_refusals; the counter also on /api/status deliverySettlement.guardFullRefusals); the settle WARN fires only for post-authentication refusals (pre-auth classes stay at Debug — one attacker message is no longer one WARN line). G4's positive arm asserts the exact post-settlement balance (the full reversal), not merely "moved". Node.WitnessedDemand is documented as retired with the primitive (permanently zero).

Added

R2.9 ledger half — byte-denominated per-increment delivery settlement on the credit ledger (core/credit/deliveryanchor.go; D-R2.9-DIRECTION, built at the ratified price (U, p) = (262,144 B, 1 credit)). SpendDeliveryAnchors spends verified demand-domain anchors into the shared paid-serial guard at session OPEN, all-or-nothing, and returns the session budget (Σ face); a top-up is the same call with fresh anchors. SettleDelivery(server, fetcher, root, count, budget) pays min(count·p, budget) less the durability skim into the server's balance, routes the skim to the object's escrow, BURNS the remainder, and reverses the provisional self-mint PER INCREMENT (the acknowledged bytes leave the lane's accumulator; the un-acknowledged tail keeps its bytes as the bilateral fallback) — the flat whole-lane supersede left a server strictly worse off than suppression on every partial acknowledgement. At 64 MiB a server that banks the receipt now ends +75 credits ahead of one that suppresses it (R-FLAT-FEE closed structurally). The paid-serial guard's cap derivation is re-based from a per-block serve COUNT to BYTES PER ANCHOR for both populations (12.21 GiB per delivery anchor, 24.4 GiB per relay anchor) against a one-hour block-interval bound at 125 MiB/s — the φ = 1 corner, which the G-R212-8 certification (T-QUANT) shows is NOT the binding bound under spend-at-open; the 65,536 floor is retained and the binding session-count bound stays the R2.12 start-up assertion against the runtime faucet capacity (blind PE item 1). The "256 serves per block" unit is retired. The guard's refusals and live entries are now split per lane (GuardFullRefusalsByLane, LivePaidSerialsByLane, the 2026-09-04 cert §4.4 ruling). DeliverySettlementStats (settlements, settled, sessions closed, burned, increments) is the telemetry for the anchor-quantization residual; the remainder is accounted ONCE at CloseDeliverySession, never per settlement, because the G-R212-8 certification replaces settle-once with settle-monotone (a session settles in deltas and spans objects). Gates TestDeliveryAcceptStrictlyDominatesSuppressionAtEverySize (B-1), TestProvisionalLaneEqualsCreditedBalanceAndReversesPerIncrement (B-2), TestDeliverySettlementIsBoundedByAnchorFaceOnThePayingLedger (B-3), TestDeliveryFundTopUpSpendsFreshAnchorsOnce (B-3b), TestPaidSerialCapDominatesBothPopulations (B-4, unit half; the field half — per-lane guardFullRefusals == 0 on a graded run, T_b and the resident cost measured — is owed to the Tester), TestDeliveryRemainderIsNeverRoutedToEscrow (B-6), TestPaidSerialCapLiteralsMatchTheirSources, TestRemainderIsAccountedOnceAtCloseNotPerSettlement (G-λ-8-6), and the Invariant-A press (B-14); six ablations run RED (the blind PE ran ten). Blind PE MERGE-AFTER (silt-reviews/principle-engineer/RULING-R2.9-ledger-half-45178ff-2026-09-06.md) folded in: the cap text re-stated as the φ = 1 corner, the settlement godoc re-stated as settle-monotone, per-lane guard counters, the payout in WHOLE increments (min(count, ⌊budget/p⌋)·p, exact at any p), the text mismatches. The NODE half (session open, receipt v3 with a cumulative count, the idle reaper, the fetch-path integration, the e2e) is GATED on an owner call: the certification refutes the composed 64 GiB pin claim under G-6 (remainder burned) and names the refund direction — see docs/thinking/2026-09-06-r2.9-delivery-settlement-quantization.md.

Fixed

R2.9a — four residuals closed: the dead bond-path first-seen stamp is deleted (G-BB-28, R-BB-BOND-STAMP-TUPLE CLOSED), the untagged R2.9a gates are name-anchored in CI, the texts that put grant/r on M0's Sybil corner are corrected (G-BB-22), and the owner-decisions block is trued up. (1) RecordBondChallenge wrote account.firstSeenTick from the wall clock and nothing read it in any build: DecayStale reads lastBondTick, Reputation reads neither, the census reads firstFetchTick. A retained when no decided function needs is SURPLUS under T-DONT3 prong (a) (D-DONT3-READING), so the write and the field are gone; lastBondTick, DecayStale and BondMaxAge are untouched. TestR29aBondChallengeStillStampsFirstSeenTick, whose doc block called the write "something else's mechanism" (there was none), is INVERTED as TestR29aBondChallengeStampsNoFirstTouch and reads the account type by reflection, asserting its set of tick-typed fields (uint64, ports.Time, ports.Duration) is exactly {firstFetchTick, lastBondTick}, so a tick added under any name reddens it (a bondSeenTick and a bondSeenAt ports.Time both measured RED; a when declared as a bare int64 is not seen); new TestR29aRetentionReadsLastBondTickInNanoseconds is the ablation that proves the deletion surgical — lastBondTick still advances, a bond lapses one nanosecond past BondMaxAge = 300 * ports.Second, and a counter-valued tick never lapses, which is why lastBondTick must not be re-denominated. Controlled reverts: restoring the field and write, deleting the lastBondTick write, storing seconds, and disabling DecayStale are each RED. TestR29aBondAuditStampsAWallClockNanosecondNotACounter stays green unchanged (it observes the auditor's tick, not the stored field). (2) The default-build go CI job now asserts eighteen named untagged R2.9a gates PASSED and nothing skipped, the same treatment the tagged job already had: those tests carry the load for keeping the instrument out of a default build, and a rename or deletion was visible to no job (renaming one gate and skipping another each fail the step). (3) core/credit/bbootstrap.go (two sites) and the histogram doc block now in cmd/silt/bbootstrap.go say where a wrong grant/r lands: build-immutable #4 from below, Don't #7 / T-AR / build-immutable #8 from above — never M0's Sybil corner, because the grant mints balance and standing is bond-only (Invariant A). The same claim in ROADMAP item 12 (G-BB-9's "viewers-only ⇒ Sybil-dearer") and two lines of the dated 2026-09-04 PACE record carry dated corrections. (4) ROADMAP owner-decisions item 12 is rewritten to what is open: q and P (W is WITHDRAWN — a pure viewer has no income path, so the decision quantity is the cumulative per-server draw and W is not a parameter of it), the byte-axis bin count (G-BB-23, the only lever on a scale-invariant exposure), R-BB-SIBLING-AGGREGATES, the untokened /api/library link key (flagged, pre-existing), and the 134×-short provisional grant/r; the 5-second interval, the build tag and the three-prong reading are marked ratified. Sources: /Users/andrewedmond/Claude/claude/silt-reviews/research/research-outcome/R2.9a-DONT3-READING-AND-BOND-STAMP-TUPLE-RESEARCH-CERTIFICATION-2026-09-05.md (Q2, G-BB-28) and /Users/andrewedmond/Claude/claude/silt-reviews/research/research-outcome/R2.9a-instrument-necessity-geometry-bound-and-tail-merging-RESEARCH-CERTIFICATION-2026-09-05.md (Q1, G-BB-22, G-BB-23). PACE record: docs/thinking/2026-09-05-r29a-residuals.md.

Changed

R2.9a — the B_bootstrap block now has a MINIMUM-REQUESTER FLOOR: below R_min it publishes suppressed: true and no census count at all (G-BB-11). The load-bearing fact is not that cells leak. stats.bytesServed and durability.objects[].funded are published unconditionally and predate this instrument, so at a degenerate census their deltas already say "the single requester fetched X bytes of object Y". What the instrument adds is requestersthe anonymity-set size — and publishing that is what converts a pre-existing aggregate counter into an attributable observation about one identity. Suppressing cells while still publishing requesters would therefore close nothing, so the floor withholds cells, aged, requesters, unstamped and maxOccupiedAgeEdgeNanos (at one requester that last one is the singleton's own age). The counts are absent from the JSON, not published as zero: a zero under a census of nine is a false total a summing reader would take as measured, and a missing key cannot be misread. The clock self-reports, both uptimes and the signed skew survive suppression — they describe the instrument rather than the population, and an operator needs them exactly when the census is too small to publish. R_min = 10 is derived in code, not chosen. Estimating a q-quantile needs at least ⌈1/(1−q)⌉ observations in the read cell, so for any q ≥ 0.90 a census-wide floor of 10 is strictly dominated by the fit's own requirement and costs it nothing; the constant is written as an integer ceiling over the certified q edge so it re-derives if that edge ever moves, guarded by a compile-time assertion that it can never drop below the certified 10. q itself stays UNPINNED — it is the owner's (G-BB-1). This does not contradict the earlier refusal to suppress low-count cells. That ruling forbade suppressing INDIVIDUAL CELLS, because suppression eats exactly the tail the quantile fit reads; this suppresses the WHOLE BLOCK when the census is not a population, where there is no tail to eat. Two different objects, and the code says so where a reader would otherwise fuse them. The floor is applied where the histogram leaves core/credit, and the raw snapshot is UNEXPORTED, so the rule is enforced by the compiler; the operator's own process already holds fetchedBytes keyed by NodeID, so a node-local read gives it nothing it does not have, and the floor is a publication rule. What the floor does NOT close, stated rather than implied: a polled series of cell deltas still yields single-identity bin trajectories at ANY R — one identity crossing a bin edge shows as −1 at one bin and +1 at another — because R governs attribution, not extraction. That is the open residual R-BB-DELTA-TRAJECTORY, bounded by poll rate and by bin crossings per interval, neither of which this instrument controls. And it does NOT bound the anonymity SET — see the re-certification entry below, which corrects this. The block was NOT moved to its own endpoint: an observer polls two endpoints as easily as one, so separation buys nothing and adds surface. No consensus rule, conservation rule or standing calculation reads any of this. Source: RESEARCH CERTIFICATION R2.9a-Bbootstrap-DELTA-contamination-privacy-floor-clock-RESEARCH-CERTIFICATION-2026-09-04 §2.1–§2.4, gate G-BB-11.

Testing

G-FP2-0 — the D-FP2-SCOPE pin: the credit ledger's ephemerality is now a TESTED posture, not an accident (core/credit/gfp20_scope_close_test.go). The owner ratified FP-2 closed by scope on 2026-09-04, which re-arms FP-1, FP-2 and R-F8-RESTART-REWIND automatically on the first of three triggers. This source gate fails when one arrives. T1 parses the credit.Ledger declaration and asserts its durable-store surface is exactly the paid-serial guard — one field, one attach method — so a second store cannot land silently; the clock source is deliberately not counted, because a clock carries no state across a restart. T2 asserts the -economy flag still defaults to false, the R2.4 economy-ON flip being the first named trigger. T3 asserts no durable-append call site under core/credit passes an argument naming a balance. Each failure message routes the change to the FP-2 obligation set rather than to narrowing the gate, and names the certification. The fourth trigger, a shared or multi-operator ledger, is not mechanically checkable from source and is stated as uncovered. Controlled ablation: adding a real store field plus attach method to the Ledger, and flipping the economy default, turned T1 and T2 RED; both were reverted. A teeth test injects all three violations and also asserts the gate does not flag the clock source or the serial-guard append.

Changed

R2.9a — the B_bootstrap clock cross-check now compares TWO INDEPENDENT time sources, so an NTP step is detected instead of cancelling out (blocker F-1 from the blind PE review of f0234be). UptimeNanos and every age were both differences taken from one reading of one ports.Clock, and adapters/walltime returns time.Now.UnixNano, which discards Go's monotonic reading — so a step landed in the minuend of both and cancelled out of the comparison between them. The certified G-BB-4 assertion "largest occupied age edge ≤ uptime" was therefore invariant under exactly the event it existed to detect, and the code comment saying it "cannot be violated by construction" was the defect rather than the proof. Measured in the reviewed build: an 8-day forward step put a 30-second-old identity in the ">7 days" bucket, reported 8 days of uptime, raised no flag, and made BBootstrapRunPrecondition accept a 7-day window on a 60-second-old process; a 2 h 50 m backward step reshaped every bucket unflagged, because ClockStepBack fires only when a subtraction crosses zero. The fix: SetObservabilityClock takes a second source, ports.MonotonicNanos, and stamps both origins in ONE call so "same instant" is structural rather than a call-ordering hope. The daemon supplies a closure over time.Since on a time.Time, which carries Go's monotonic reading; core and ports may not import time (internal/depcheck), so it arrives injected, the same shape SetEpochSource uses. Nothing is measured on it — its only job is to make a step VISIBLE. New payload fields: monotonicSource, monotonicUptimeNanos (the real censoring bound), clockSkewNanos (signed, taken from the raw wall delta before the clamp, so a step past the ledger start reports its full size and the two directions stay distinguishable) and clockSuspect (|skew| ≥ 60 s — DERIVED as the width of the narrowest positive-width age bucket, so below it a divergence cannot displace an identity by a whole bucket). The G-BB-4 assertion and the precondition's W arm now read CensoringBoundNanos, so BB-9 fires on the production path and a run whose monotone uptime does not cover W is refused whatever the wall clock says. With no monotone source injected the check degenerates back into a self-comparison, and the precondition refuses that configuration outright rather than reading a zero. BB-14's degeneracy arm was dead on a wall clock (it required an age of exactly 0 ns) and now fires on the live case: every counted identity in ONE bucket, whichever bucket, because the estimand is a quantile conditioned on age. Also corrected: the false payload-contract comment (Aged + Unstamped == Requesters holds only while the age axis is live), the ClockStepBack comment (it is not the step detector), and the deliberation's "absorbed by clamping and reported, never silently reshaped", which was measured false. Six controlled ablations, each reverted, redden a named arm: the censoring bound back to wall uptime, the skew detector off, the precondition W arm back to wall uptime, degeneracy back to bucket-0-only, the injection literal removed, and the injection moved after nd.SetLedger. Deliberation: docs/thinking/2026-09-04-r29a-bbootstrap-histogram.md ("Round 2").

Changed

R2.9a — the B_bootstrap instrument is a FULL-CENSUS COUNT HISTOGRAM, not a row export (research-CERTIFIED 2026-09-04, verdict GATED on the shipped shape; a replacement of the shape and the clock, not a patch). The instrument D-R2.9-DIRECTION sentence 4 makes a precondition of pinning grant/r now publishes one int64 counter per (age bucket × log2-bytes bin): 8 age buckets × 164 quarter-log2 byte bins = 1,312 counters, measured at one 10,496-byte allocation per snapshot and a 3.1 KiB payload at both R = 10 and R = 20,000 — against the PE's measured 124 ms / 114 MiB for the row export at R = 500,000. Every requester with fetchedBytes > 0 is counted exactly once; no cap, no truncation, no sampling, no salt, no hash, no sort, no per-row allocation, and no per-cell byte SUM (a cell sum with count 1 is that identity's exact byte total in disguise). Two mechanisms drove the replacement. (a) Retaining the top 4,096 rows BY BYTES selects on the response variable of the regression the series exists to fit: it removes, from every age cell, the identities below an unpublished threshold, so the YOUNG cells — where the fit reads — empty first and hardest, and past R = 4,096 the age-conditional quantile is unrecoverable. (b) The age axis was the consensus epoch, which is identically 0 on a non-validator (EnableChain sits inside if *validator {), so the export would have published a constant on exactly the machine it was built for. The age axis now rides the injected ports.Clock (SetObservabilityClock, the SetEpochSource pattern), stamped once at first touch in Register — the certification's cited firstSeenTick write fires only inside RecordBondChallenge, i.e. for bonded validators and never for a pure fetcher, so it was not a usable stamp for this population. firstSeenTick is now read for observability only and still by NO standing calculation (the T-axis note stands). Deleted: MaxRequesterFetchRows, SetExportSalt and the salt, the daemon's crypto/rand draw, truncated, the per-requester rows and the label hashing — the PE's two blockers (crypto/rand in core/, the unsalted embedder) disappear under the shape rather than being fixed. The block is DEFAULT OFF behind the new -bbootstrap flag and is ABSENT from /api/status unless asked for: reads need no token there, so anything published is world-readable wherever -ui is bound off loopback. Instrumentation only — no conservation rule, no economic rule and no standing calculation reads it (SetObservabilityClock and BBootstrapSnapshot are classified neutral under Invariant A, and TestR29aBBootstrapSnapshotWritesNothing deep-compares the account map across a snapshot, because the sibling defect in this family is a reader that goes through acctRegister and mints a 500,000 grant). W and q are NOT pinned — G-BB-1 makes that the owner's call, no reading rule is hard-coded anywhere, the age-edge table carries a comment saying an edge may need adding, and BBootstrapRunPrecondition takes W as a required argument with no default. The RUN stays blocked on G-BB-1. Gates: TestR29aDeadClockPublishesNoAgeCells and TestR29aWirePayloadSelfReportsADeadClock (BB-1: a ledger with no clock publishes an explicit clockSource and a NULL grid, never an all-zero age column); TestR29aAgeAxisLivenessAndBoundaries (BB-2, every edge driven at the edge and one ns below); TestR29aCensusIsCompleteAndUncapped (BB-3); TestR29aYoungCellsAreExactUnderASkewedPopulation (BB-4, the top-k refutation encoded); TestR29aPayloadIsBoundedInTheRequesterCount (BB-5); TestR29aSnapshotCostDoesNotGrowInR (BB-6, exactly one allocation at R = 1,000 and R = 100,000); TestR29aWirePayloadCarriesNoJoinKey (BB-7, a CLOSED wire key set plus a hex/byte-sum scan of the block's own bytes); TestR29aStatusOmitsTheBlockUnlessAsked + TestR29aDaemonDefaultsTheInstrumentOff (BB-8, runtime and source gate, the source gate split in two so the clock assertion carries its own honest UNGATED: residual plus an injection-ORDER check); TestR29aOccupiedAgeBucketsNeverExceedUptime (BB-9/G-BB-4, one production-path arm — an 8-day forward wall-clock step — and one foreign-tick arm); TestR29aRestartIsVisibleNotSilent (BB-10); TestR29aWallClockStepIsDetectedNotAbsorbed and TestR29aWirePayloadReportsAClockStep (BB-13, clean / forward / backward, pairwise distinguishable, at the ledger and on the wire); TestR29aBackwardClockStepClampsAndSaysSo (BB-13's clamping arms — the isolating one steps the clock back to after the ledger start but before the stamp); TestR29aRunPreconditionAcceptsOnlyAValidRun (BB-14); TestR29aByteBinMatchesTheClosedForm, TestR29aUnstampedRequestersAreCountedNotAged, TestR29aFirstFetchIsStampedOnceAtTheFirstFetch (renamed with its subject when G-BB-24 moved the stamp off Register; see the Unreleased entry), TestR29aNodeSnapshotIsTheHistogramWithNoIdentity, TestR29aNoLedgerYieldsNoExport and TestR29aEconomySelfFieldsAreUnchanged. BB-12 is already covered by TestCoreImportsNoAdaptersAndNoEffects (internal/depcheck) and is not duplicated. Deliberation: docs/thinking/2026-09-04-r29a-bbootstrap-histogram.md.

Testing

R3.1 V1/V2 — the SMT second-preimage / domain-separation scope invariants are pinned (test-only, 2026-09-04; design docs/thinking/2026-09-01-smt-domain-separation-close-design.md). V1 (core/statehash/smt_domain_separation_test.go) asserts the leaf/inner node-type prefix bytes differ (0x00 vs 0x01, reconstructed via fold.go's own foldLeafPreimage/foldInnerPreimage) and that swapping a genuine node's body onto the OTHER type's prefix never reproduces the real library-committed root — the operational form of "a shortened proof cannot substitute one node type for the other." Ablation: with foldInnerPrefix forced equal to foldLeafPrefix, both tests go RED. V2 (internal/depcheck/smt_domain_separation_test.go) is an AST-walked (not grepped, not hand-listed) inventory over core/, cmd/, adapters/, internal/ pinning the three scope conditions the argument needs: SI-1 exactly one smt.NewTrieSpec construction site and it is (sha256.New, false); SI-2 zero WithValueHasher references; SI-3 zero ProveClosest/VerifyClosestProof/SparseMerkleClosestProof/nilPathHasher/newNilPathHasher references. Each of the three violations was injected in a scratch file and confirmed to redden its gate independently. The Researcher certifies the disjoint-preimage argument itself separately; these gates hold the scope conditions, not the argument.

Security

R2.10 / F8 — the credit ledger owns a CHAIN-ANCHORED epoch (research-certified 2026-09-04). The paid-serial guard's sweep floor and every admission screen used to run against an epoch the CALLER passed on RedeemDeliveryCredit[Reason] and SpendRelayAnchors, so the ledger's clock was an unauthenticated port input (one call at 2^62 refused every honest redeem thereafter — F8). Now the ledger reads its epoch from ONE injected ports.EpochSource (credit.SetEpochSource; the daemon wires the node's chainEpoch — the same function that prunes the demand keyset, drives the receipt bank and verifies relay anchors — once, right after EnableChain, via wireLedgerEpochSource), and NO ports.CreditLedger method takes an epoch: the parameter is removed from RedeemDeliveryCredit, RedeemDeliveryCreditReason, the core/node deliveryReasoner twin and SpendRelayAnchors. The watermark is max(watermark, source) read once at the entry of every guarded redeem and anchor spend; the sweep and every screen run against the watermark, never the raw source (a port contract — a mock or embedder source may fall; after O3 Direction T the production clock cannot). The finalized-head epoch is refuted as a source (permanently 0 without BFT finality; one epoch behind the keyset at every boundary, refusing honest anchors as anchor-future-dated). cmd/silt refuses to start -accept-delivery-receipts or -accept-relay-payments when the effective -epoch-blocks is 0 (an explicit 0, or -objective=false with the flag unset): with no epoch clock nothing expires and both lanes brick at the guard cap — a liveness precondition, not a security parameter; core stays permissive at epoch 0. Gates: TestF8_NoPortMethodCarriesAnEpoch (source), TestF8_FallingSourceLowersNothingAndReadmitsNothing_Delivery/_Relay, TestF8_NilSourceReadsAsEpochZero, the re-driven TestEpochWatermark_LaggardRedeemerCannotRePay / TestEpochWatermark_IsMonotone, TestSerialGuard_SetIsBounded's new guard-full assertion (the brick), TestF8_LedgerEpochIsTheNodesChainEpochAtEveryBlock, TestF8_LedgerFollowsItsSourceNotTheCaller, TestF8_DaemonRefusesPaidLanesWithoutAnEpochClock_Source, TestF8_DaemonWiresTheLedgerEpochSourceToTheNode, TestF8_LedgerEpochFollowsTheChainThroughTheDaemonSeam, and e2e TestF8_PaidLanesRefuseToStartWithoutAnEpochClock. Fixture cost: three legacy e2e daemons gain -epoch-blocks 8; fifteen unit fixtures that used the dropped parameter as their clock now drive a mock source, assertions unchanged. Three of those were caught by the blind PE review, not by the migration (RULING-R2.10-F8-build-178ff3b-2026-09-04.md): the compaction fuzz kept a moving epoch bound only to issuedEpoch, so its watermark never advanced, the band sweep never ran, and its own invariant (d) went RED past 32,768 live serials — invisible under -short, which is the tier CI runs; two relay subtests (the epoch-2 re-spend, the in-window cap advance) passed for the wrong reason at a watermark of 0. All three now drive the ledger's source, assert the reason, and the fuzz carries a -short-visible tripwire: a scenario spanning more than the guard window whose sweep never ran is RED. Residual R-F8-RESTART-REWIND (durable guard, unpersisted watermark) is filed on FP-2's carry-list with its close R-F8-RESTORE.

Security

R4.3b — the DHT eclipse cap (H5-B) now keys on the OBSERVED contacted-at address, in SHADOW MODE by default; de-herd relay selection. Mechanism: the per-bucket cap keyed on the self-declared -domain label, so N Sybils with N free labels were N domains and an undeclared Sybil was exempt (R4.3a, the owned hole). Now the transport classifies every completed TLS conversation as an opaque per-process-salted (class, group) — DIRECT at the peer's own IPv4 /24 (IPv6 /32), RELAYED at the RELAY's prefix on the two spliced paths, one namespace per prefix across classes (cert C-1), DIRECT never downgraded (C-3) — and the table stores the class WITH the entry (C-4), caps per (class, group) per bucket, bounds ALL non-DIRECT entries at K − R (C-2, R ≥ K/2), charges reply-learned ids to their INTRODUCER's group until they answer (Bitcoin Core's srcgroup rule, re-checked at first classification as a narrowing), exempts -bootstrap seeds and -persistent-peers, and caps one prefix at 10 entries table-wide (geth). Loopback and link-local are exempt; RFC1918 and CGNAT are CLASSIFIED (a private-range exemption would make the cloudtest shadow run measure nothing). The group never leaves the process: no wire field, no peers-file field, no log line (TestR43b_G9_SaltIsPerProcessAndGroupNeverLeavesTheProcess, TestR43b_G9_NoLogLineCarriesAGroup). -dht-address-cap=off|shadow|on, default shadow: the rule is evaluated and every would-be refusal is counted per bucket × class × the R ∈ {4, 6, 8} × cap_relay ∈ {2, 4} grid, and NOTHING is refused — shadow admissions equal off admissions exactly (TestR43b_G6_ShadowChangesNoAdmissionAndCountsEveryRefusal, a lock-step differential over 12 seeds × 600 events). -dht-address-width (24), -dht-relay-cap (2), -dht-address-reserve (4, refused below K/2 — a security parameter). /api/status.addressCap carries series A (wouldRefuse), B (relayFanIn) and E (groupCensus) as aggregates. The R4.3a open-break gate TestR43b_OPENBREAK_LabelledSybilsDefeatTheDomainCap is flipped to assert the defence (eight labelled Sybils from one /24 hold ≤ 2 of a K=8 bucket under on; the inverse under off is pinned); TestR43b_G1_OneObservedGroupIsCappedUnderOn, TestR43b_G10_ThirtyPoniesBehindOneRelayStayDiscoverable (30/30 ponies behind one relay discoverable under on; presence at public tables ≥ the off baseline summed over ten seeds) and TestR43b_G11_RelaySelectionSpreadsAndFailsOver are the new gates. De-herd: a NATed node picks among the gossiped relays by min H(self ‖ relayID) with fail-over past a relay that refuses registration (pickRelay; was KnownRelays[0] for life). Enabling on is an owner call after a cloudtest shadow run (ROADMAP Decisions owed). The -domain label stays for the C2 metric and preferFreshDomain; its legacy per-bucket cap remains wired (inert against an adversary). Research-CERTIFIED (the RELAYED class and the reserve): silt-reviews/research/research-outcome/R4.3b-relayed-class-and-observed-address-keying-RESEARCH-CERTIFICATION-2026-09-04.md.

Security

R2.13b — the publish-credit double-spend guard (creditSpent) is now DURABLE; an issuer restart can no longer re-open every held credit for a second spend (PE finding F-4, confirmed by reproduction 2026-09-04). Mechanism: creditSpent was process memory, while a publish credit's validity is the persisted publish key's lifetime (no epoch in silt/blindcredit/fdh/v1), so each restart forgot every spent credit and honoured each again — one 50,000 burn, two demand tokens with distinct serials, two conserved payouts (43,750 + 43,750); the durable paid-serial guard cannot catch it because it keys on the token serial. Bound was credits held × restarts; balance only, never standing. Fix: a SECOND guardstore.Disk at <store>/creditspent.log behind the unchanged ports.PaidSerialStore (never a namespace in paidserials.log, whose Compact keeps only the ledger's live set and would evict every credit record at the first sweep); Node.SetCreditSpentStore / LoadCreditSpent mirror the paid-serial guard (attached then loaded before the node serves; a load error is refuse-to-start); tokenChargeFor refuses while unloaded (errCreditGuardUnloaded), refuses at the cap (maxCreditSpent = 65,536, errCreditGuardFull, never evicts; no sweep because credits do not expire — a disclosed liveness ceiling until the research-gated epoch-binding Rock), and Appends BEFORE the in-memory mark, so an Append error refuses the withdrawal (errCreditStore) with the credit unspent and no token signed. Issue already charges before SignBlinded, so a crash between the durable append and the signature is an under-issue (a lost fee), never a mint; the tokenrole.go comment claiming the opposite is corrected. Gates (RED-first): TestCreditSpentSurvivesIssuerRestart (the PE reproduction; dropping the Append → RED), TestCreditSpentStoreFailureRefusesTheWithdrawal, TestCreditSpentCapRefusesNeverEvicts, TestCreditSpentDiskStoreIsASecondFileBesidePaidSerials, TestF4_UnloadedCreditStoreRefusesCreditBearingRequests, TestF4_AppendLandsBeforeSignBlinded (core/node), and the cmd/silt source gate TestDaemonWiresTheCreditSpentStoreBesideThePaidSerialStore. Ruling: silt-reviews/principle-engineer/RULING-F4-creditSpent-durability-and-F3-fee-constancy-2026-09-04.md. PE review (RULING-R2.13b-creditspent-build-fa9f988-2026-09-04.md, MERGE-WITH-CONDITIONS, landed): the three guard-state refusals (full / store / unloaded) are now WARN-logged and counted on the ISSUER (they collapsed to a silent OK=false); the file is BOUND to the publish key it was written under (Server = SHA-256 of the issuer key; a foreign file refuses the boot by name), because the only recovery from a full guard is rotating the publish key AND clearing creditspent.log together — rotate alone keeps the dead records counted, clear alone re-opens F-4 for every credit under the still-valid key; TestCreditSpentLoadRefusesAboveTheCap pins the boot refusal the docs claimed (unpinned before). Capacity, sized by the PE: ~65,536 × ⌊N/k⌋ publishes per key lifetime (one credit per canonical-prefix issuer per publish) — months for a flixz-class publisher — so an honest fill is realistic and the cap is an OPERATOR-MANAGED ceiling until the epoch-binding Rock (R-CREDITSPENT-UNBOUNDED). With -require-tokens, every validator at cap means the chain accepts no publishes.

Security

A genesis seats only the attestations that verify over its hash (D-GENESIS-ATTS-SEATING, owner-ratified 2026-09-04). AppendGenesis seated genesis Atts unverified, and Atts sit outside the Hash preimage: a relaying peer could append an unsigned stub, have it SEATED into validatorsSeen (a phantom from zero key material) and diverge the era-3 committed root on a fresh-sync victim. Now b.Atts becomes exactly the entries with verifyAtt(a, b.Hash) before apply; the rest are stripped, never refused (refusing would let the same stub wedge fork-adopt and Reload). Production genesis carries no Atts (TestProductionGenesisCarriesNoAtts), so the filter is the identity on every honest history. "Strip all" (the earlier MG-C, briefly on a branch) was REFUTED by the bootstrap fixtures. Gates G1–G10 (core/chain/genesis_atts_seating_test.go, core/genesis/genesis_atts_test.go): G1–G6 RED before; strip-all and refuse-invalid each redden their own set. Certification: silt-reviews/research/research-outcome/genesis-atts-seating-rule-RESEARCH-CERTIFICATION-2026-09-04.md. Also recorded: O4's numbering RATIFIED as widened I5 (no I6).

Security

R2.14 — the relay-lane prepayment ANCHOR (the R0.7 fix; R2.9's prerequisite): the PayWord chain root is now bound to blind-signed prepayment credentials the relay itself issued, and a relay settles at most what those credentials burned on its own ledger. BUILT ≠ LIVE: the lane is DARK until era-4. The construction, certified 2026-09-04 (owner: "let's do both"): a FOURTH FDH domain silt/blindrelay/fdh/v1 over uint64BE(epoch) ‖ serial under the relay's own chain-committed per-epoch demand key (sound under one RSA key by domain separation — BNPS one-more-inversion bounds tokens across ALL domains by fees paid); RelayOpen v2 = {Root, S, Funding, Anchors[k ≤ 6], Fetcher, Sig} with the ephemeral's ed25519 signature over sha256("silt/relay/open/v1" ‖ relayID ‖ Root ‖ uint32BE(S) ‖ uint32BE(k) ‖ serials) — Rivest–Shamir's M with C_U the blind credentials; MaxAnchorsPerSession = ⌈S_max / fee⌉ = 6 DERIVED in source with decode bounds (serial ≤ 32 B, sig ≤ 1,024 B, Fetcher == 32, Sig == 64); the relay verifies in the certified cheap-first ORDER (free guards → k bounds → sha256(Fetcher) == from → ed25519 → RSA under the SELF keyset only, newest epoch first, stop at the first failure) and then SPENDS all k anchors all-or-nothing into the ledger's bounded, durable (epoch, serial) guard — the R0.4b paid-serial guard SHARED, not a third in-memory twin: refuse at cap, never evict; expiry sweep on the demand-key window; restart is not an eviction — BEFORE admission, with budget = Σ face (S × increment deleted). Settlement pays min(count, budget) into acct(relay) only, never the ephemeral; the pump ceiling is min(count, budget) × B; the unconsumed remainder is BURNED. Conservation on the paying ledger: settled ≤ Σ face (INV-RELAY-CONS) and per session Δ Σ_L = settled − Σ face ≤ 0, equality iff fully consumed (correction C-1: the 2026-09-03 "unchanged" corollary is withdrawn). The fetcher's DURABLE identity buys anchors from the relay over the existing withdrawal wire (AcquireRelayAnchorswithdrawBlind, the demand withdrawal generalised by lane; NO issuer-side change: the issuer signs opaque blinded bytes and charges ChargePublish, refusable). The daemon schedules demand keys and loads the guard store under -accept-delivery-receipts || -accept-relay-payments (finding E); -accept-relay-payments' help is truthful ("built; dark until era-4; pays min(count, Σ face)"). Corrections folded in from the cert: C-1 (above); C-2 — "record face at spend" does NOT close fee drift; fee_E per epoch key is the close, an FP-2 / R2.10 precondition (F-3); C-3 — "present all k, spend lazily" is REFUTED (an unspent serial re-presented under a new ephemeral links sessions; only a top-up with FRESH anchors is sound); C-4 — the D3 private-purchase path is NOT used for anchors until creditSpent is durable (F-4, R2.13b). The fourteen gates, all GREEN (RED-first, Tester 2026-09-04): T-1 TestRelaySettlementRefusesUnanchoredSession / TestRelayOpenRefusesUnanchoredSession, T-2 TestRelayLaneConservesTotalSupplyOnOnePerNodeLedger / TestRelayAnchorsAreBoughtOnTheRelaysOwnLedger, T-3 TestRelayCredentialIsSpentOncePerLedger (incl. the two-relay G-A5 variant), T-4 TestRelaySettlementIgnoresForwardedBytesIsBoundedByAnchor, T-5 TestRelaySettlementNeverLeavesAnAccountNegative, T-6 TestRelayAnchorDomainIsNotADemandToken / TestRelayAnchorSignatureIsNotADemandSignature, T-7 TestRelayOpenRefusesCheaplyBeforeRSA (blindtoken.RelayAnchorVerifyRuns counter), T-8 TestRelayAnchorGuardSurvivesRestart, T-9 TestRelayCeilingNeverExceedsBudget, T-10 TestRelayOpenRefusalRecordsNoAnchor, T-11 TestRelayOpenCommitmentBindsRelayRootAndSerials, T-12 TestRelayAnchorGuardWindowMatchesKeysetWindow, T-13 TestRelayOpenRefusesWithoutSelfKeyset, T-14 TestRelayAnchorDomainIsPinnedByteExactly; wire TestRelayMaxAnchorsPerSessionCoversTheSessionCeiling, TestRelayOpenDecodeBoundsRefuseOversizedAnchors; SpendRelayAnchors classified neutral and pressed against an ANCHORED session in TestInvariantA_NoNonMintPressRaisesStanding; the paid > 0 precondition restored in TestRelayCreditNeverTouchesStanding; e2e TestPaidRelaySessionEndToEnd buys a real anchor over TCP under the durable identity and asserts wantCredit = min(S, k·face) with Δ Σ_L ≤ 0. The R0.7 interim is RETIRED: an unanchored open is REFUSED (errRelayNoAnchor) rather than admitted and paid 0 — TestSettleRelaySessionPaysZeroUntilAnchor / TestSettleRelaySessionLogCarriesNoAnchorReason are re-specified as the unanchored ablation guards, and the S5 settlement reason no-anchor is retired for anchored (recorded goalpost move, cmd/silt/observable_contract.go). TestRelayOpenFloodStaysBounded is re-specified: admission is PRICED, so a free flood admits nothing (the RT-RELAY-3 "sessions are free" half, closed); the cap and the sweep are pinned by direct insertion. Residuals (cert §10): R-ANCHOR-STALL ≡ R-ANCHOR-GRANULARITY (≤ 300,000 credits per 1 GiB session, burned; owner-accepted v1; follow-on R2.14b MsgRelayFund); R-RELAY-ANON-SET (the delivery lane's D3 channel, narrower yield); R-RELAY-WASH-ZERO-LOSS (collusion is a WASH — no v1 relay skim; owner call before R2.4); F-3 R-FEE-CONSTANCY; R-ANCHOR-REPRESENT-LINK; R-DARK-UNTIL-ERA4 (a v5 IssuerKeyReg is needed; a paid relay must be bonded). Cert: silt-reviews/research/research-outcome/R2.14-relay-prepayment-anchor-CONSTRUCTION-RESEARCH-CERTIFICATION-2026-09-04.md; advisory: silt-reviews/crypto-specialist/ADVISORY-R2.14-relay-prepayment-anchor-build-2026-09-04.md; record: docs/thinking/2026-09-04-r2.14-relay-prepayment-anchor-design.md. core/blindtoken/blindtoken.go, core/demand/keyset.go, ports/ports.go, core/credit/relayanchor.go, core/credit/relay.go, core/credit/delivery.go, core/relaypay/wire.go, core/node/relayrole.go, core/node/relaytransport.go, core/node/demandkeys.go, cmd/silt/daemon.go, cmd/silt/observable_contract.go, e2e/relay_paid_test.go, docs/design/pod.md §7.3.

Added

Floor box — the STRUCTURE round, Round 1A (R-STRUCTURE-REDERIVATION, owner-ratified 2026-09-03; call 16 main-only). ONE accept composition over a three-valued StateView: ValidateProposalV5 (P1…P13) and ValidateCommitV5 (= the proposal then C1…C5), dispatched from BOTH ValidateProposal and ValidateCommit on version — chain.go changes by exactly two dispatch hunks and one comment; era-1/era-2 legs untouched. StateView is sealed; HeadRef carries {Hash, NextHeight, ProposerID, StateRoot, LogRoot, Empty}; P13 is two conjuncts (StateRoot; LogRoot equality at k = 0, a named STALL at k ≥ 1 until tagRevLogSize); the legacy leg is a real leg (Rep on the view); the box owns a byte budget whose zero value stalls (UnlimitedBudget for the live view only); eight of nine box doors unexported and the package surface inventoried by AST with reasons; the recompute screens a slashed author; the witness parent-proposer slot deleted (the id is HeadRef.ProposerID). Gates, every one RED under a named ablation: the stage-cover gate (fixture non-vacuity, derived call cover, three-body digest, reverse cover), G-D13 per-row node-body digests (28), the nine-regime v4/v5 PARITY ORACLE (verdict + sentinel parity per mirrored stage), honest twins on all 26 gates, the two-sided pruned-carrier gate, the m = 1 right-spine control. Blind PE ruling (a 245-case differential against the pre-change node, 13 ablations): /Users/andrewedmond/Claude/claude/silt-reviews/principle-engineer/RULING-floorbox-structure-round-1a-869399e-2026-09-08.md; Researcher CERTIFIED: /Users/andrewedmond/Claude/claude/silt-reviews/research/research-outcome/FLOORBOX-STRUCTURE-ROUND-1A-COMPOSED-DIFF-869399e-RESEARCH-CERTIFICATION-2026-09-08.md. Build record: docs/thinking/2026-09-07-floorbox-structure-round-1a-build.md. Round 1B (the box-entry closers) follows.

Changed

O3 Direction T — the fork-choice weight term is RETIRED; heavier is height → head-hash among descendants of the finalized head (a consensus-rule change, owner-ratified 2026-09-03, research-CERTIFIED 2026-09-04). Weight, blockWeight, anchorWeight and Config.AnchorWeight are deleted (no CLI flag existed). Mechanism: blockWeight verified attestations against the BARE block hash (#558's third site) while every production attestation has signed consensusSigBytes since #432, so the term was identically 0 on every block any binary mints and shipped fork-choice was already height → head-hash; the term also read the evaluating replica's live qualification and bond state (the #357 oscillation mechanism), so it was unsafe to repair, and inside the finality gate any pure extension-monotone weight is provably redundant to height. The §1b height preference (#357) is promoted to the PRIMARY term. Identity transform on every v2+ chain. Four gates ship in the same commit (the cert's condition): (a) R-558-VERIFIER-INVENTORY — TestO3T_VerifierInventoryPin (+…HasTeeth, …AllowlistIsWellFormed: every ed25519.Verify in non-test core/chain against a classified allowlist, blockWeight tombstoned, signedBlock allowlisted as era-1-gated, the PR #720 carrierParentProposerFromWitness row added) and TestO3T_Era2CertificateAcceptedByEveryEra2Verifier; (b) R-INTERLOCK-GATE — TestO3T_HeavierReadsOnlyHeightAndHeadHash (+…PinHasTeeth: an AST purity pin, heavier reads only blocks[len-1].Height/.Hash), TestO3T_CertificateVariantNeverRanksHeavier, TestO3T_ForkChoiceIsCertificateIndependentWithoutFinality, TestO3T_FastSlowPathSameHead and core/node TestO3T_NodeFastSlowPathSameHead; (c) R-FORKCHOICE-RAMP-GUARD second half — TestO3T_NoWeightTermReferenceSurvives (+…HasTeeth) and the twin Weight <= 0 at modelcheck_i5_357_test.go deleted; (d) R-I5-TEXT-AND-CLAIMS-LEDGER — TestO3T_CanonI5TextMatchesCertification, TestO3T_NoWeightHeightHashOrderInDocs, TestO3T_ClaimsLedgerForkChoiceRowMatchesCertification. Fixture dispositions (never an equal-height head-hash coin flip): TestReconcileAdoptsHeavierForkTestReconcileAdoptsTallerFork and TestReconcileRejectsLighterForkTestReconcileRejectsShorterFork (the adopted fork is strictly taller; the refused one strictly shorter with MORE attesters); TestReconcileTieBreakDeterministic re-worded (height tie); TestWeakSubjectivityCheckpointRefusesLongRangeReorg both forks made taller; TestRedteamF6_ObjectiveWeightAgreesAcrossDivergentReplicas…ObjectiveForkChoiceAgreesAcrossDivergentReplicas (same head + no adoption on cross-reconcile); TestRedteamF6_LegacyWeightDivergesAcrossReplicas RETIRED with the term; the epoch drain monotone-weight check deleted (convergence kept). Canon: I5 widened (Statement/Assert/scars/Governs/ Literature per the cert; O4 hash-coverage placed inside I5, no I6); two closure-table rows; claims-ledger.md objective fork-choice row re-grounded on three objective-mode witnesses. signedBlock stays era-1-gated (O3-R13 follow-on, not folded into verifyAtt). Reopening condition (the only one): a shipping posture in which FinalizedHeight lags Head — a code change to that function, not a config posture. Sources: /Users/andrewedmond/Claude/claude/silt-reviews/research/research-outcome/O3-Direction-T-I5-restatement-and-divergence-RESEARCH-CERTIFICATION-2026-09-04.md, /Users/andrewedmond/Claude/claude/silt-reviews/principle-engineer/RULING-O3-fork-choice-weight-R-vs-T-2026-09-03.md, /Users/andrewedmond/Claude/claude/silt-reviews/research/research-outcome/O3-fork-choice-weight-R-vs-T-RESEARCH-RECOMMENDATION-2026-09-03.md; deliberation docs/thinking/2026-09-04-o3-direction-t-design.md.

Security

The LastCommit attestation carrier is REBASED onto main behind its hard merge gates — the R-BOX-ATTESTS fix (owner calls O1 and O2, ratified 2026-09-03) is now mergeable. The rule, as ratified: LastCommit []Attestation (cbor key 18, omitempty) republishes the PARENT's precommits and is FOLDED INTO Hash; validity is block-local (validateCarrier, one function, three callers: ValidateProposal, appendStructural, the floor box entry) — every entry a genuine PhasePrecommit signature over b.Prev at its own round, distinct ids, height 1 empty by rule, a pre-v5 block carrying the field invalid, a genesis carrying it refused (ErrGenesisLastCommit); the transition seats each carried signer with id != parent.ProposerID and attesterQualified against the child's PRE-state, folded BEFORE this block's bond regs / TTL / slashes (pinned like rotate-LAST), and a v5 block's own Atts write nothing (the frozen era-3 rule is untouched, era-gated). Merge resolutions, each decided by a gate rather than by eye: (1) CD-0 — the bodyHash literal names BOTH open-era additive fields, IssuerKeys (17) and LastCommit (18); Prune keeps LastCommit; TestHashLiteralPinsEveryHashCoveredField + runtime pair GREEN, the teeth fixture made order-independent. (2) CD-2 — CheckEquivocation's accept set is byte-identical (26-case golden corpus GREEN); the R0.6 SlashesBytesCap / pendingSlashes packing survive intact. (3) MG-C — the hash-covered half only: genesis LastCommit is refused; genesis Atts keep main's pre-carrier behaviour (neither stripped nor refused — the strip broke the anchor bootstrap; its disposal is research-gated, R-CARRIER-GENESIS-DISPOSAL). (4) R-V5-TAGSET-EQUALITY — issuerKeyCommit stays in the v5 tag set. Fixture debt exposed: c3Chain minted a v5 genesis whose Atts pre-seated the fixture attester; under O1 a v5 genesis's Atts write nothing, so its genesis version now follows its era (a sub-v5 genesis where the flip is later, like the production genesis). NOT in this change (also CD-4, the docs/design/m0.md C2 sentence that the seating measurement is proposer-gated — owed before the R4.4 brief); the readiness stamp (stays 3), an Era4ActivationHeight flag, the floor-box door exports (held), the R-CARRIER-BYTES value, R-CARRIER-PRUNED-HASH, R-CARRIER-MODELCHECK, HeadRef — each tracked on its Rock. Gates: lastcommit_carrier_pins_test.go, lastcommit_carrier_v5_test.go, redteam_carrier_boxsplit_gate_test.go, core/node/lastcommit_carrier_node_test.go, readset_v5_drift_test.go, floorbox_recompute_carrier_reflection_v5_test.go, genesis_stub_atts_test.go, r06_i5_evidence_recompute_test.go, r06_slashes_cap_packing_test.go; -short and -race -short GREEN on core/chain + core/node. Record: docs/thinking/2026-09-04-lastcommit-carrier-merge-design.md. core/chain/chain.go, core/chain/carrier.go, core/chain/genesis_stub_atts_test.go, core/chain/hash_literal_pin_test.go, core/node/r04b_c3_gates_test.go.

Security

Carrier merge gates on main. Landed GREEN on main with teeth by injection, so the LastCommit carrier rebase is held to them: TestHashLiteralPinsEveryHashCoveredField (the bodyHash literal names every exported Block field except the five deliberate exclusions — a dropped IssuerKeys or an unfolded LastCommit is RED; CD-0), TestV5TagSetEqualityAcrossStatehashAndBox (the 29 v5 committed tags in statehash.go equal the runtime tag sets and the box's references, one-entry allowlist), and the CD-2 CheckEquivocation golden corpus (core/chain/testdata/equivocation_golden.cbor, 26 cases; the accept set cannot move unnoticed across the merge); TestGenesisLastCommitIsRefused arms by reflection when the carrier lands. Withdrawn before merge: the MG-C "strip genesis Atts" fix — CI showed genesis attestations by the launch anchors are how anchors are seated into validatorsSeen (four bootstrap tests); the Atts half is research-gated (ROADMAP R-CARRIER-GENESIS-DISPOSAL). Sources: LASTCOMMIT-CARRIER-residuals-composed-direction-RESEARCH-CERTIFICATION-2026-09-03.md CD-0/CD-2; LASTCOMMIT-CARRIER-26977a4-DELTA-CERTIFICATION-2026-09-03.md §6.

Security

R4.3a — STRIPPED to the measure step: the DHT eclipse cap keeps its undeclared-domain exemption as a KNOWN HOLE; the A-axis metrics are printed; the red-team's findings ship as gates. The "unknown ⇒ capped" table change was built and then refuted by the red-team before merge: in the default (domainless) swarm every honest peer is also unknown, so two early undeclared Sybils lock a K=8 bucket (honest admitted 6 → 0, exclusion cost 8 → 2 identities), while N Sybils with N free labels defeat any declared-label cap at $0. Owner ruled "strip and merge" after four seats converged (no second declared label; the close is R4.3b observed-address keying, geth/Bitcoin Core schema, in shadow mode). Shipped: the daemon's C2 status line prints NakamotoDomains and DistinctDomains (the R4.2 "measure / publish" step); core/node/r43a_dht_domain0_test.goTestR43a_TwoUndeclaredSybilsDoNotLockADomainlessBucket (RED under the stripped rule), TestR43b_OPENBREAK_LabelledSybilsDefeatTheDomainCap (asserts the open residual; flips when R4.3b lands), TestR43a_HelloWritesOnlyTheSendersOwnDomain (the poisoning boundary that held); truthful doc comments and flag help naming the hole. Sources: the R4.2 direction certification §3; silt-reviews/red-team/RED-TEAM-R4.3b-dht-eclipse-keying-2026-09-03.md; the four R4.3b seat opinions cited in ROADMAP R4.3b.

Security

R0.6 — the I5 cross-height Pruned slash forgery (LIVE on main, every era) is closed: equivocation evidence is recomputed from the body, never read from Pruned; Slashes gets a per-block byte ceiling. VerifyEquivocation read the height from a struct field but the signed message from Hash, which short-circuits to the accuser-supplied Pruned for the two blocks inside Slashes[i]; two genuine signatures by an honest validator at two different heights, re-labelled with one height, convicted it through Append — and a Byzantine PEER sufficed, because an honest node queued the forgery on detection. Fix (owner-ratified, research-CERTIFIED direction, NO era gate): CheckEquivocation (the named form of VerifyEquivocation) refuses a pruned evidence block with ErrPrunedEvidence and hashes both bodies with bodyHash — no Pruned short-circuit, no memo — and FindEquivocations selects candidates with the SAME function, so proposer and validator close in one edit; the code now matches the rule its own doc comment always stated. Paired with SlashesBytesCap (16 MiB, PROVISIONAL — an immutable-#8 resource ceiling, not a security parameter; the owner ratifies the value), checked first on every write path in every era, at-cap accepted; the proposer packs pendingSlashes under it and carries the rest. Gates (RED-first, Append as oracle; six go RED again under a controlled revert): core/chain/r06_i5_evidence_recompute_test.go (T-1/T-2 era-1/era-2 forgery, T-3 genuine double-sign still convicts, T-6 cap over/at, G-2 honest detection never pairs a pruned block, G-4 memo bypass, G-6 one-hash-function source pin), TestPrunedEvidenceIsRefused (supersedes TestQ2_PrunedBlockStillSlashable, which pinned the removed behaviour), core/node/r06_i5_evidence_recompute_test.go (T-5, the honest-node vector, reproduced first), and the I5 model-check's three new axes (declared-vs-signed height; Pruned ∈ {unset, real, forged}; era ∈ {1, 2}; 9,792 cases). G-1: every persisted chain fixture scanned (core/chain/testdata/archival/*.cbor, both predicates: pruned evidence and Slashes over the cap; they carry no Slashes at all, so both pass vacuously; no field chain.cbor exists locally; accepted explicitly in D-F2-EVIDENCE-RECOMPUTE) — the fork set is empty. Packing gate TestProposerPacksPendingSlashesUnderTheBytesCap (core/node): a backlog over the cap is carried, the proposal never exceeds it, and a proof that alone exceeds it is dropped, never queued or embedded (PE F-1: embedding one would doom every later proposal by that node). Detection scans only the served suffix (PE F-3: candidate selection body-hashes both sides, and the genesis-rooted reconstruction re-hashed the whole chain per sweep, 228 ms at n=600). G-3 harness TestSlashesBytesCapWorstCaseCost (5 reg-laden proofs fill the cap; 15.0 MiB resident; 11.5 ms to validate). Accepted cost R-LATE-REVEAL (a double-sign whose evidence was already pruned is unslashable; safety unaffected). Residuals OPEN by name: R-EVIDENCE-BYTES, R-BIG-EVIDENCE-UNSLASHABLE, R-BOX (the floor box's un-verified b.Slashes read), R-MEMO, R-RELOAD-RE-VERIFY. Canon: docs/decisions.md D-F2-EVIDENCE-RECOMPUTE; consensus-invariants I5 scar; retention.go premise re-worded. Researcher delta cert on the cap value: the "not a security parameter" wording is REFUTED as stated — the cap is DUAL-FACE (resource ceiling on the honest axis; evidence-size completeness bound on the deterrent axis, which no value closes and only the v5 two-level block hash (d-3) removes, now on the R3.4 carry-list and the R4.4 brief); 16 MiB stays ratifiable with the face disclosed. Its V-1 gate TestOverCapProofDoesNotSilenceLaterProofsByTheSameCulprit caught and closed a defect the F-1 fix had introduced (the once-per-culprit local latch silenced later small proofs after a fat one); the over-cap WARN is pinned as an S5 line (V-6): silt-reviews/research/research-outcome/R0.6-SlashesBytesCap-value-security-face-DELTA-CERTIFICATION-2026-09-03.md. PE: MERGE-WITH-CONDITIONS (F-1, F-4 landed; F-2 the cap value is also the eviction-escape threshold — routed to the Researcher before the owner ratifies the number; F-5 owed: silt-reviews/principle-engineer/RULING-R0.6-i5-evidence-recompute-3131d5a-2026-09-03.md). Certification: silt-reviews/research/research-outcome/I5-cross-height-pruned-slash-forgery-FIX-DIRECTION-RESEARCH-CERTIFICATION-2026-09-03.md; deliberation docs/thinking/2026-09-03-r0.6-i5-evidence-recompute-design.md.

Security

R0.4b C3 merge-gate close — the CI job that did not parse, and the C-3 hardness checks on an unauthenticated hot path. Inputs: the delta certification R0.4b-C3-01bf8e9-merge-prep-DELTA-CERTIFICATION-2026-09-03 (gates G-F, G-G, G-H) and the crypto as-built advisory ADVISORY-R0.4b-C3-crypto-items-as-built-01bf8e9-2026-09-03 (items R1, R2, R4, R6). Full write-up: docs/thinking/2026-09-02-r0.4b-c3-close-design.md §12.

  • G-F — unresolved git conflict markers in .github/workflows/ci.yml silently DELETED a CI job. A rebase resolution missed the file. The markers made it invalid YAML, so the Website — changelog + links job stopped parsing and took out BOTH check_cited_tests.py and check_source_gates.py. Nothing was red — the job did not fail, it did not exist, and two shipped documents described the source-gate lint as CI-enforced while nothing in that job ran. Resolved as the UNION of both steps. New scripts/check_conflict_markers.py is the scar's gate: repo-wide, stdlib-only, excluding .git/ and generated website/; it flags a seven-< or seven-> header line anywhere, and a bare seven-= separator only in a file that also carries a header (a row of = is a legal Markdown setext underline). Zero false positives across 1,153 text files. Ablation: reinstating the markers reddens the lint and breaks the YAML parse.
  • G-G / G-H — three false or incomplete claims on shipped surfaces. (i) The two "(in CI)" claims are now TRUE and name the job. (ii) The FP-2 crash-window fix directions were recorded as co-equal; Direction 2 (pay-then-append) is REFUTED — it converts a residual under-pay into a possible MINT (a crash between payout and append leaves a payout with no guard entry, so the receipt re-pays on restart) and it removes the RT-DELIV-1/1b/2 bound. Direction 1 (write-ahead spanning the guard file and a ledger store) is the only sound direction. (iii) The FP-2 precondition list is THREE, not two: F8, arm D, and R-COMPACT-ORPHAN — re-priced, because C-7 moved compaction from ~never to once per epoch and its failure direction is an over-pay.
  • Crypto R1 — one unauthenticated inbound frame bought ~28.6 ms of RSA work on the node loop. handleDeliveryReceipt calls DemandIssuerKeyset as its first action on any MsgDeliveryReceipt, before the parse and before the sender screen; that call re-pins every held epoch; and demand.Keyset.Put ran the full ValidatePub — hardness included, ~3.3 ms — unconditionally, over a 9-epoch band. The C-3 split was correct inside core/blindtoken and re-entered through another door. A memo could not live in the held map, because Prune drops every future epoch on every read. Fixed with an admission memo in Keyset.Put keyed on KeyFingerprint — the same sha256 the consensus E ↦ key_E binding commits to — skipping the HARDNESS half only: blindtoken.ValidateShape (newly exported) still runs on every Put, so the F4 refusals stay on every path. Memo bounded at 64 entries against a working set of 9. Gate: core/node TestC3_InboundReceiptsCostOHardnessChecksNotOPerMessage COUNTS hardness executions via the new blindtoken.ValidatePubHardnessRuns() (a timing test cannot see this class), asserting the band's first admission costs exactly 5 and every one of 50 subsequent messages costs 0, plus TestC3_ADifferentCommittedKeyStillPaysFullAdmission. Measured BenchmarkC3InboundDeliveryReceipt at a 5-epoch band: 16.41 ms/msg before, 4.78 µs/msg after (3,436×). Ablation: remove the memo lookup → RED at "message 1: 5 hardness runs".
  • Three crypto divergences DECLARED, not changed (code comment + design record §12). (1) Minimal integer encoding is deliberate — RFC 8017/9474 require fixed-length modulus_len, and silt's choice preserves the s.Bytes() wire format that committed publish tokens re-verify against; the price is that it forecloses drop-in RFC 9474 interop, at ~1 in 256 honest blind_sig values. (2) The blinding factor is drawn by mod-reduction with 64 bits of slack, against RFC 9474 §4.2's rejection-sampling MUST — met within 2^-64, filed as ROADMAP Rock R0.4b-BLIND-SAMPLING. (3) math/big's fixed-window power-table lookup leaks the private exponent to a LOCAL cache attacker — unfixable in Go (no exported raw RSA private operation) and shared with Cloudflare CIRCL's RFC 9474 implementation — so it is carried as a deployment assumption, written for operators as docs/network-durability.md §9: do not run a demand issuer on a host with untrusted co-tenants sharing its CPU cache.
  • The ValidatePub cost gate is now a RATIO, not a wall-clock budget. The 5 ms in TestC3_ValidatePubCostBudget was a builder round-up of one M4 measurement (3.3 ms), and the CI runner measured 10.5 ms best — so the gate reddened on hardware, not on a regression. The ruling R0.4b-C3-ValidatePub-cost-gate-RULING-2026-09-03 REFUTES that 5 ms is a security parameter (nothing in ValidatePub branches on it; the crypto advisory's design quantity is the RATIO and the per-message denominator, not a millisecond ceiling) and certifies the replacement: best <= K × perVerify with K = 1000, both sides 2048-bit big.Int work measured in the same process, so a uniform hardware slowdown cancels. Measured 154× on an M4, 159× in the advisory, 170× on this box. The milliseconds are still LOGGED so a human reads the real cost. This is the UPPER bound on admission cost; TestC3_HardnessRunsAtAdmissionNotOnEveryModexp already asserts the LOWER bound (perVerify*10 <= admission), which trips if the hardness half moves back onto the modexp path. Neither is calibrated to a machine.
  • The inbound-receipt cost gate asserts its wall-clock half only without -race. TestC3_InboundReceiptsCostOHardnessChecksNotOPerMessage pairs a COUNT gate (zero hardness runs per inbound MsgDeliveryReceipt after admission — the property) with a 100 µs/message wall-clock budget. The count runs under both builds. The race detector inflates the measurement ~10× (4.8 µs uninstrumented, 45 µs under -race on the same box, 108 µs on the CI runner's -race job) so the budget reddened on the detector, not on the path. A raceEnabled constant pair (core/node/race_{enabled,disabled}_test.go, mirroring core/chain) gates the wall-clock assertion; the cost is still logged under both builds.
Security

R0.4b C3 final pre-ratification round — the G-8 dark-lane disposition (iii), the swarm receipt S5 legibility break, and the PE's final-review items. Inputs: the G-8 convergence R0.4b-C3-G8-dark-lane-CONVERGENCE-2026-09-03, the PE final ruling RULING-R0.4b-C3-close-271ab81-final-2026-09-03, and the Tester's 271ab81 verification. Full write-up: docs/thinking/2026-09-02-r0.4b-c3-close-design.md §11.

  • silt swarm receipt stopped saying "NOT banked" on the one refusal a user hits (S5). The R0.4b withdrawal lane added a key-resolution step ABOVE the submit, so the announced marker — which lives below the submit — became unreachable on the lane-off path: a daemon without -accept-delivery-receipts refused correctly and the client reported something else. That is the announced-observable class the freeload: ON rename scarred. Fix: the lane-off case is now routed to the marker. node.ErrNoIssuerKey is EXPORTED (was errNoIssuerKey) so the client can tell "that server does not run the lane" from "that server's chain has no era-4 binding yet" — a lane-off daemon opens no demand key store, runs no rotation and arms no bank, so it serves no issuer key at all. The marker is a single const notBankedMarker shared by both emitting sites; the lane-off text deliberately does not claim the token was spent, because nothing was withdrawn. Gates: cmd/silt TestLaneOffRefusalCarriesTheAnnouncedNotBankedMarker (RED under ablation with the exact text the Tester recorded) and e2e TestDeliveryReceiptRefusedWhenLaneOff, now GREEN on the shipped topology. Residual R-SWARM-NOTBANKED-DEAD is closed by a reachability argument written at the branch, not by deletion.
  • rotateDemandKeys stayed armed on a LANE OFF branch (PE H-2). On a failed boot key rotation the daemon printed LANE OFF and broke out with the scheduler still assigned, so the OnCommit hook kept rotating: the node went on holding demand keys, staging IssuerKeyReg commitments into consensus, serving MsgGetDemandIssuerKeys and blind-signing withdrawals — charging the withdrawal fee — while demandBank stayed nil and denied every receipt those tokens bought. Fixed by ORDER: the single assignment now sits below the single failure exit, so no branch is left that can arm a lane it has just declared off. Gate: cmd/silt TestDaemonArmsTheRotatorOnlyAfterABootInstall. Runtime observation of the property is filed as R-LANEOFF-ROTATION-RUNTIME.
  • A corrupt paid-serial guard bricked nodes that have no delivery lane (PE H-3). guardstore.Open + LoadPaidSerials ran outside any flag branch and are a refuse-to-start, so one bad byte in paidserials.log stopped a pure storage node that could never have written the file — the F7 blast-radius lesson, applied in the same commit to demandkeys.cbor and not to the file that commit adds. Both are now inside if *acceptReceipts. The asymmetry is deliberate: inside the lane the guard is load-bearing and a failure MUST stop the daemon. Gate: e2e TestCorruptGuardStoreStopsOnlyTheDaemonThatUsesIt, both arms.
  • The compaction fuzz redeemed with a NIL serial, so it covered none of the guard it was cited for (PE §4). RedeemDeliveryCreditReason short-circuits its whole R0.4b guard on if len(serial) > 0. core/credit TestCompactionTombstoneFuzz now mints a unique 32-byte serial per redeem from the seeded rng on a moving epoch clock, exercising paid-serial admission, the watermark advance and the per-epoch expiry sweep, and adds invariant (d): an honest unique in-window serial must always pay, the guard must stay far under its cap, and re-presenting a paid serial returns serial-already-paid while moving zero value. Measured 73.35 s against a 73.78 s baseline — no added budget.
  • The era-4 tally cannot latch on a non-objective chain — the trace behind the e2e re-scope, now a test. epochsEnabled() is EpochBlocks > 0 && objective(), apply() calls rotateEpoch only under it, and the era-4 readiness tally lives inside rotateEpoch. New core/chain TestGateF_NonObjectiveTopologyCanNeverLatchEra4 injects a readiness stamp of BlockVersionWitnessable on two chains differing only in objectivity and asserts the objective CONTROL latches while the non-objective arm never does and MintVersion stays below 5 at every height. Cross-referenced from gate F's clause (c) so a stamp-raising release reads it at the edit.
Fixed

R2.13 R-COMPACT-ORPHAN — a failed re-open inside the paid-serial guard store's Compact left the append handle on an unlinked inode. adapters/guardstore Disk.Compact renamed the temp file onto the live path and only then re-opened its append handle; if that open failed, Compact returned the error but d.f still pointed at the inode the rename had just unlinked. Write and fsync through that handle succeed, so every later Append returned nil for a record no Load could ever see, and the sole caller (core/credit sweepExpiredSerials) discarded the error unconditionally. Direction: an OVER-pay, once per epoch since C-7 moved compaction onto the band advance; latent today only because the ledger resets at the same restart (MEDIUM, PE ruling). Fix: open-before-rename — the new append handle is opened on the temp file BEFORE os.Rename, so a failure leaves the store unchanged with the old handle valid and nothing fallible runs between the rename and the handle swap. Backstop: a sticky guardstore.ErrStoreBroken, keyed on the ONE reachability signal (after the swap the handle's inode must be the inode at the path, os.SameFile) and checked first by Append and Compact, so a broken store fails loudly and never returns nil for a record Load cannot see; a retired handle's Close failure is NOT a broken store (PE ruling RULING-R2.13-compact-orphan-11396f1-2026-09-03.md finding 1: keying on it refused every payout on a healthy store). Port contract: ports.PaidSerialStore.Compact now carries the handle clause (after a failed Compact the store MUST either remain appendable-and-reachable or MUST fail every subsequent Append; it MUST NOT return nil from an Append a later Load cannot see). Ledger: the sweep no longer discards the Compact error; it is recorded (Ledger.CompactFailures, LastCompactError) and never refuses a payout by itself — the two error classes stay split, a broken store is observed through Append failing and the existing ReasonGuardStore path (blanket fail-closed at the sweep was REFUSED by the ruling as a self-inflicted liveness break). Gates (G-CO-1 RED-first with controlled revert; G-CO-2 green by design; G-CO-3 a regression pin whose RED→GREEN came from the store double honouring the port clause, not from source; the backstop pins are positive tests): TestG_CO1_PostRenameOpenFailureOrphansTheAppendHandle, TestG_CO2_BenignCompactionFailureDoesNotRefusePayouts (anti-over-correction, stays green), TestG_CO3_BrokenStoreMustBeObservableByTheLedger, TestR213_BackstopFiresWhenTheHandleDoesNotReachThePath (+ TestR213_RetiredHandleCloseFailureDoesNotBreakTheStore), TestR213_PreRenameOpenFailureLeavesTheStoreHealthy, TestR213_BenignCompactionFailureIsRecordedNotDiscarded. Also corrects the ROADMAP FP-1 parenthetical (the mirror crash window is not a pure under-pay, does not self-heal, and has a shipped gate — ruling §4). Ruling: /Users/andrewedmond/Claude/claude/silt-reviews/principle-engineer/RULING-ledger-durability-family-FP2-R2.13-R2.10-2026-09-03.md §1 / §6. Deliberation: docs/thinking/2026-09-03-r2.13-compact-orphan-design.md.

Fixed

R0.4b C3 merge-prep — Tester defect F6, the LANE OFF gate's missing teeth, the G-E comment gate, and crypto-specialist advisory items C-1 … C-8. Inputs: the Tester's 271ab81 verification, the delta certification R0.4b-C3-271ab81-G3-G4-GD-DELTA-CERTIFICATION-2026-09-03, and ADVISORY-R0.4b-C3-blind-RSA-epoch-binding-2026-09-03. Full item-by-item write-up with root causes, gates and measurements: docs/thinking/2026-09-02-r0.4b-c3-close-design.md §10.

  • F6 (defect, permanent liveness cliff) — an EARLIER rotation pruned a LATER rotation's already-staged pre-published key. EpochStore.ensureBand pruned on the caller's own band (e > cur+w). The daemon launches each epoch turn as a bare go rotateDemandKeys(cur), so turns complete out of order; a turn for an earlier cur deleted keys a later turn had generated AND handed to install(...), which stages their fingerprints on chain. applyIssuerKeys is first-write-wins, so once the staged fingerprint commits the regenerated key can never be registered — the demand lane is dead for that epoch, for that issuer, permanently, and nothing detects it. Measured with no concurrency: RotateWindow(11) then RotateWindow(10) loses epoch 15. Fix: the retained band's UPPER EDGE is max(genTo, highest epoch on disk) — monotone, so no rotation can shrink another's pre-publication; bounded, because the only writer above the edge is the generation loop, which stops at cur+w. Gates: TestRTC3_AnEarlierRotationDoesNotPruneALaterRotationsPrePublishedKey (deterministic, RED with LOST=[15]) and TestRTC3_EpochStoreRotationsNeverLoseAnInstalledKey (the daemon's four overlapping turns ×80, RED with LOST=[17]). The adopted gate's if e < 9 || e > 14 { continue } filter is REMOVED — its rationale had the direction backwards and it carved out the only epoch ever lost. A false comment in cmd/silt/daemon.go ("one rotation is in flight at a time") is corrected: bumping demandEpoch prevents a duplicate rotation for the SAME epoch and nothing else.
  • The LANE OFF gate had no teeth; it now has a runtime twin. The Tester reintroduced the F7 daemon-death with every asserted string intact, using a different early return: source gate GREEN, go vet clean. New e2e/laneoff_corrupt_store_test.go TestDaemonSurvivesACorruptDemandKeyStore drives a real silt daemon over real TCP with a corrupt demandkeys.cbor and asserts the process survives (registry, peer and bootstrap lines all print), the lane never arms, both operator lines are exact and name the store path, and the file is byte-unchanged. Ablation: reinstating the F7 death reddens it while the source gate stays green.
  • Third-time rule closed as a LINT. scripts/check_source_gates.py, run by the Website — changelog + links job in .github/workflows/ci.yml (the claim was FALSE when first written — see the G-F entry below — and is true as of this round): any _test.go reading a non-testdata .go file is a SOURCE gate; its failure messages must begin SOURCE GATE: and describe what was checked, and it must name a RUNTIME GATE: or declare the behaviour UNGATED:. Three sites brought into compliance. Recorded as rule 8 in docs/build-process.md.
  • C-1 (RFC 9474 §4.4 Finalize, the advisory's top item) — the unblind step now VERIFIES. A malicious issuer could return a garbage blind signature: the fetcher paid the withdrawal fee, fetched, signed a receipt, and the server's Bank.Redeem then refused it — so handleDeliveryReceipt never called the ledger and the serve's eager unwitnessed self-mint was NEVER REVERSED. An issuer handing out duds drove its whole cohort onto the self-mint path at no cost to itself and with no detection. Unblind / UnblindCredit / UnblindDemand verify under the domain (and for demand under (key_E, E)) before returning; the refusal is legible at the client.
  • C-2 — the issuer's private-key operation is blinded and verified after signing. SignBlinded was a bare Exp(b, D, N) on attacker-chosen input over the network, which is the Brumley–Boneh remote-timing setting; client-side blinding does not help the issuer, which is why Go's own crypto/rsa blinds every private-key op. Now: random blinding (rng INJECTED — crypto/rand is banned in core) plus s^e == b mod N before release, the Boneh–DeMillo–Lipton countermeasure. Wire format unchanged — the blinding cancels, and the gate asserts byte-equality with the plain modexp, which is what keeps the A2 retry-dedup honest.
  • C-3 — ValidatePub gains hardness checks, split from the hot path. The advisory's spike showed four moduli passing the shape-only bound set, two of them universal forgery by any observer (a single 2048-bit prime; 122 seventeen-bit primes), plus and e = 3. Added: the FIPS 186-5 exponent floor e > 2^16; no prime factor below SmallFactorBound = 2^20 (one gcd against a lazily-built primorial); not a perfect power; not prime. All four shapes now REFUSED. The hardness half runs only at ADMISSION (ParsePub, demand.Keyset.Put); the cheap validateShape stays the last line before every modexp. Measured on Apple M4: admission 3.43 ms (budget 5 ms, at most W+1 = 5 pins per issuer per window), one-time primorial build ~42 ms (prewarmed off the loop at daemon boot), verify 19.4 µs. Leaving the hardness checks on the modexp path would have been a ~180x CPU amplifier on the single-threaded node loop — a new DoS bought while closing a forgery. Honest residual, stated: the bound is a BAR, not a proof; blindness against a malicious issuer is bounded by key-correctness assumptions, not proven, and the on-chain commitment bounds EQUIVOCATION, not soundness.
  • C-5 — RSA representatives are canonical, and that closes an issuance-dedup bypass. SignBlinded opened with b.Mod(b, N), so blinded, blinded + N and any zero-padded spelling signed identically — while demandDedupKey is keyed on the RAW blinded bytes, so a re-encoding was a fresh cache key for an issuance already settled and the requester was charged twice for one signature. Now RFC 8017 §5.2.2 range plus minimal encoding, applied to the blinded value at the signer and to the signature at verify; Issuer.Issue checks the input BEFORE the charge, so a refused spelling cannot take the fee.
  • C-6 — the RFC 9474 §4.2 is_coprime(m, n) check is present (a shared factor with a deliberately smooth modulus is a linkability tag the issuer can compute).
  • C-7 — expired guard entries are retired on the epoch-band advance, not only at the cap. Both credit.paidSerial and demand.Bank.spent swept only at the 65,536 cap, so on any node below it — every node most of the time — expired entries were retained ON DISK indefinitely, past the W-epoch window that is their whole justification. Soundness is unaffected (refuse-not-evict is unchanged) and the new trigger is CHEAPER: O(cap) per epoch instead of O(cap) per refused redeem. Prior art: Brands' epoch-partitioned spent list.
  • C-4 / C-8 — NOT built, recorded as ROADMAP Rocks with their reasons. R0.4b-PoP: no proof-of-possession of the RSA key in validateIssuerKeys, so a bonded issuer can register another issuer's fingerprint (Duplicate-Signature Key Selection). Latent, not live — one configured issuer, per-node ledgers — and the close is a validity-rule change, so it is research-gated and owner-ratified before the stamp raise. R0.4b-FDH: length-prefixing the FDH domain tag and widening the reduction slack both alter the FDH output, and the publish and credit domains are byte-frozen against chain replay. The two code comments claiming RFC 9578 fidelity are corrected: RFC 9578 signs SHA256(SPKI); silt signs an epoch index, which identifies neither issuer nor key.
  • G-E (delta-cert, comment-only) — the G-4 ordering invariant is restated correctly. "A witnessed receipt reverses the self-mint exactly once" is false on a re-served lane; what is bounded is the LANE INSTANCE. Also corrected everywhere: "the 64 MiB production chunk" is the tree's stated MINIMUM production chunk, and the number that matters is that the break-even is B = 50,000 bytes against a SHIPPED default chunk of 65,536 — the lever was live at the default configuration, not only at an aspirational size.
  • FP-2, measured, NOT fixed. The other half of the supersede/append crash window: when the guard append LANDS and the pay is lost, re-presenting the receipt returns serial-already-paid above the supersede, so the server keeps the full +58,720,256 self-mint at a 64 MiB chunk with no witnessed reversal and no conserved payout, and the receipt is burnt (Σ unmoved; no double-pay). Vacuous on the shipped in-memory ledger; live the moment the ledger is persisted, because there are then two durable stores and no shared transaction. Not fixed because neither candidate direction is ledger-local: a write-ahead needs a cross-store transaction, and pay-then-append needs a guard-record wire change plus moving ReasonAlreadyPaid below the supersede — an economic-mechanism change. Recorded as a second precondition on the FP-2 flip, gated by TestFP2_CrashBetweenTheGuardAppendAndThePayBurnsTheReceipt.
Fixed

R0.4b C3 certification fold-in — three merge-blocking gates closed (G-8, the dark paid lane, is the owner's call and is NOT closed here): the research certification R0.4b-C3-composed-close-bc062d0-RESEARCH-CERTIFICATION-2026-09-03 returned GATED.

  • G-3 — a torn tail was not realigned, so the next append LOST an acknowledged paid serial. guardstore.Open used O_APPEND with no size check. Load drops a trailing partial record, which is sound only while it stays the tail: O_APPEND writes at the file's unaligned size, so the next append shifted every record boundary after it and the following Load spliced the orphan fragment onto the head of the real record. A plausible spliced length byte (~1 in 8) makes that a SILENT wrong load with a paid serial no longer guarded — the F2 double-pay through the adapter written to close F2. Open now truncates back to the last complete record boundary and fsyncs before any append handle exists; dropping the fragment is safe because Append had not returned, so nothing was paid against it. Gated by TestTornTailIsRealignedBeforeTheNextAppend (RED before the fix and under ablation).
  • G-4 — every guard refusal returned BEFORE the supersede, so a refusing server kept the unfunded self-mint. RecordServeToObject self-credits 0.875xB; the conserved leg pays a flat fee - skim = 43,750. Above 50,000 bytes refusing therefore beat being paid, and at the tree's stated MINIMUM production chunk of 64 MiB it beat it by 1,342x (+58,676,506), and by +13,594 at the SHIPPED 64 KiB default — a profitable, OPERATOR-TRIGGERABLE supersede-disable on Boulder 0's conservation rule, since an operator can fill its own guard with junk serials at a net cost of zero. The refusals now run BELOW the supersede, under one stated invariant: a witnessed receipt reverses the self-mint exactly once, whether or not it is paid. serial-already-paid deliberately stays ABOVE it — the guard's own record is what bounds double-reversal on a re-served lane — and an unwitnessed receipt never reaches the ledger at all, so the self-mint survives until a valid receipt arrives. The root cause (a flat fee against a byte-proportional mint, residual R-FLAT-FEE) is a D-POD-KNOBS re-pricing needing its own certification and is NOT closed here. Gated by core/credit/r04b_c3_g4_supersede_test.go (server nets 0 on a refusal at 1 kB / 64 kB / 64 MiB; the 1,342x arithmetic pinned; one receipt reverses once) and core/node.TestG4_UnwitnessedReceiptLeavesTheSelfMintAlone.
  • G-D — three false code-doc claims on money/consensus surfaces corrected (behaviour unchanged): issuerKeyCommit is pruned only on a REGISTRATION-CARRYING apply, not every apply; epochWatermark is an honest-skew correctness device, NOT the "purely subtractive" Byzantine defence its comment claimed (one caller-supplied 2^62 denies the ledger forever); and the paid-serial store is appended before the ledger PAYS, not before it moves any credit — the supersede's subtractive reversal runs first.
  • Flip preconditions recorded (no build owed yet). FP-2/F8: the ledger must OWN a chain-anchored epoch before any shared-ledger, third-operator-settlement or persisted-ledger deployment; a clamp is not a close, and the faucet rate limiter must not be keyed on the watermark. FP-1: demand.Bank.spent must be persisted before witnessed demand confers any value — the Tester's scar criterion is NARROWED (a guard needs memory as durable as the thing it guards), not waived, and re-arms in full the moment Bank.Demand / Node.WitnessedDemand acquires a non-observability consumer.
Fixed

R0.4b C3 re-break round — ten confirmed red-team breaks closed (DO NOT MERGE unratified; F1 is a consensus-rule change awaiting certification): a second blind pass on the C3 close (RED-TEAM-R0.4b-C3-close-RE-BREAK-2026-09-03) confirmed ten breaks and two refutations. The (b1) epoch binding held under every attack; the breaks were in the machinery around it. Every probe ships as a regression gate (rt_r04b_c3_*), run RED against the pre-fix build first.

  • F1 (CRITICAL, latent) — the floor box and the full node disagreed about the same block at every epoch turn. applyIssuerKeys pruned issuerKeyCommit by BLOCK HEIGHT on every v5 apply, so a block carrying ZERO registrations deleted committed leaves; the box's scope gate stalls only on len(b.IssuerKeys) > 0 and its O(payload) fold has no op for the prune. Both directions were measured: the box AGREED with a forged root a full node rejects, and read an HONEST zero-registration block as a forged root. Safe today only because WitnessValidateV5 never Accepts — a wrong-Accept the moment R1.8 flips it. Closed at the source: the prune is now PAYLOAD-DRIVEN, running only inside the registration-carrying branch, so "no registrations ⇒ no issuerKeyCommit write" is a property of apply() instead of an assumption of the box. The keyspace stays bounded — every ADD is in-band by validity, so pruning at each add re-establishes the 2W+2-bucket bound on every block that can grow it. Reproducing the prune in the fold is NOT available: core/statehash has point membership/non-membership only, and issuerKeyCommit carries no set-completeness digest (unlike dueBucket's MTH), so a witness-supplied member list would be omission-forgeable. Gated by rt_r04b_c3_split_test.go, which drives the scenario through the box recompute, the live validateEra3Roots, and WitnessValidateV5 cold-auditor and live-follower, and asserts the tiers never disagree.
  • F2 — a restart evicted every guarded token and the same wire receipt paid twice. Both serial guards were process memory with no persistence and no restore — the one eviction mode the design forbids, performed by every node at every boot. New ports.PaidSerialStore + adapters/guardstore: an append-only log of fixed-width records, fsync per append, atomic temp+fsync+rename+dir-fsync on compaction. The ledger persists the entry BEFORE any credit moves (the SignMarkStore ordering), a redeem before the load completes is refused (paid-serial-guard-unloaded) rather than paid, and a store that cannot write refuses the payout (paid-serial-store-write-failed).
  • F3 — "evicted ⇒ expired" was false in-process. The guard was keyed by the serial alone while its expiry epoch was the FIRST redeem's — the MINIMUM over the tokens sharing a serial, both of which the withdrawer picks. Both guards are now keyed by the TOKEN (uint64BE(issueEpoch) ‖ serial), so an entry is removed only once ITS OWN issue epoch is outside the band.
  • F4 — a committed RSA key was never validated. The commitment attests 32 bytes, which binds WHICH BYTES an issuer serves and nothing about whether they are a key: N = 0 panicked every verifier inside big.Int.Mod (a bonded Byzantine issuer crashing every fetcher that transacted with it), N = 1 verified every (serial, sig) pair, E = 1 made the signature the message. New blindtoken.ValidatePub (N odd, positive, 2048–8192 bits; E odd, > 1, ≤ 2^32−1) is enforced at ParsePub, at Keyset.Put, and before every modexp.
  • F5 — demand.Bank.spent was unbounded, and no guard bounded the serial's SIZE. spent is now capped and expiry-swept (refuse, never evict, at the cap); demand/credited are capped by object count; and the serial, the token signature and the receipt's ed25519 fields are bounded at the wire decode and again at Bank.Redeem, so no attacker-chosen byte count on a 132 MiB frame can become a long-lived map key.
  • F6 — EpochStore had no lock while the daemon launched every epoch turn as a bare goroutine, so two overlapping rotations lost a key whose fingerprint was already staged for commitment — and, because registration is first-write-wins, that epoch's lane is dead forever. Serialized behind a mutex spanning the whole load→generate→save cycle. -race cannot see this class (each goroutine loads its own map), so the gate asserts the outcome.
  • F7 — one corrupt byte in a demand key file bricked the whole validator. The daemon now reads the store before arming the lane and degrades to LANE-OFF, loudly; chain, storage and serving continue, and the file is never rewritten or regenerated.
  • F9 — gate F never failed on the stamp. Clause (c) was a t.Logf, suppressed under a plain go test, so raising the mint stamp passed a fully green tree. It is a hard failure now, with a teeth-proof for the tripwire itself, plus a driven gate for the Era4ActivationHeight route to v5 that bypasses the readiness tally entirely.
  • F8 (open, deliberately) — the monotone epoch watermark is a one-call permanent denial. One unvalidated currentEpoch = 2^62 refuses every subsequent honest redeem for the ledger's life. Bounding it is a mechanism choice about what the ledger may trust of a redeemer's clock, at a boundary that is not authenticated at all — routed to the Researcher with the question of whether R0.4b-5 buys anything in its own threat model. Unreachable on the shipped per-node topology.
  • F10 (disclosed) — cap griefing costs only the skim, which lands in the escrow of the griefer's own root. Quantified and pinned so the disclosure cannot go stale; production grant = 0 remains the close.
Fixed

Floor-box wrong-accept on the LastCommit carrier — the box now runs the SAME validity rule the full node runs (one function, three callers): the trustless floor box reproduced applyCarrier's TRANSITION (class A derived its write-set straight off b.LastCommit[i].AttesterID) but never validateCarrier's VALIDITY rule, which was wired only onto the node's two disk-write paths. So a v5 block whose carrier named the PUBLIC keys of real qualified validators with 64 zero bytes for a signature — no key material — was rejected by every full node (ErrCarrierBadSignature) and AGREED with by the box, against the attacker's own apply-computed root. Escalation: validatorsSeen is the sole input to C2MetricMatureCoefficientmatureNow → the one-way everMature latch and to the box's own RecomputeMatureNow, so the same forged carrier flipped the maturity latch — the MEASURED decentralization quantity the maturity shed gates on. Inert today (WitnessValidateV5 never returns Accept), so this was an R1.8 accept-flip entry blocker, not a live exposure. Fixed at the root, per the PE structure ruling: assembleStateRootRecomputeOps now calls the shared validateCarrier(&b) unconditionally before any class dispatches, and validateCarrier lost its *Chain receiver so the COMPILER (not the AST allowlist pin, whose glob is holed) forbids a live-state read on that path. apply was deliberately NOT made to verify. An invalid carrier is a STALL (ErrRecomputeCarrierInvalid); never-Accept is unchanged. Also corrected: the class-A "No wrong-accept" doc claim (true of the SCREEN, false of the INPUT) and the backwards pruned-block hash claim at Hash — a pruned block's Pruned field is a linkage token, not a content commitment, so no consensus decision may depend on re-reading a pruned block's body (Hash/Prune unchanged; the property is documented, and the follow-ups are filed as Rocks under Boulder 1). Six class-A test fixtures that minted an invalid HEIGHT-1 carrier block were repaired to height 2. Gates: core/chain/redteam_carrier_boxsplit_gate_test.go — five gates, each run on the warm AND cold box tiers, oracled on the full node's accept path (ValidateCommit, never a single predicate) and asserting the implication box agrees ⇒ node accepts, never the biconditional; all RED at 3bd13e2, GREEN after. (core/chain/carrier.go, core/chain/chain.go, core/chain/floorbox_recompute_stateroot_v5.go, core/chain/floorbox_recompute_stateroot_atts_v5.go; red-team RED-TEAM-lastcommit-carrier-3bd13e2-2026-09-03.md; PE ruling RULING-floorbox-predicate-rederivation-structure-2026-09-03.md; design record docs/thinking/2026-09-03-lastcommit-carrier-round-A-design.md addendum.)

Fixed

LastCommit attestation carrier — the R-BOX-ATTESTS fix, era-4 (v5) only (owner call O1, ratified 2026-09-03; the frozen era-3 format and transition are untouched): apply wrote validatorsSeen from b.Atts, but Hash excludes Atts and the committed-root predicate re-runs the real apply over the ATTACHED certificate. A proposer populates its roots BEFORE it gathers precommits, so ANY certificate that would seat a new attester made the recomputed root differ from the signed one and every replica — including the proposer's own — rejected that block ("commit rejected by own replica"). Two HIGH consequences: the decentralization MEASUREMENT froze (only blocks seating nobody could commit, so validatorsSeen was constant from the first root-checked block, permanently ceilinging MatureCoefficient and reading zero arrivals forever), and the chain stalled intermittently for any round whose first-to-quorum prefix carried a qualified never-seen attester (connected, all-honest, unbounded in expectation). The fix moves the seating input into HASH-COVERED content: a v5 block carries LastCommit []Attestation (additive cbor key 18, omitempty, folded into Hash) republishing the PARENT's precommits, and the v5 transition seats each carried signer that is not the parent's proposer and is attesterQualified against the child's pre-state — folded BEFORE the block's bond registrations, TTL expiries and slashes (structurally pinned). A v5 block's own Atts now write nothing. Validity: every entry verifies over b.Prev at PhasePrecommit at its own round (deliberately NOT bound to CommitRound, which Hash does not cover); distinct ids; a sub-v5 block carrying the field is invalid; height 1's carrier is empty BY RULE and a genesis LastCommit is refused BY RULE (genesis Atts are filtered by D-GENESIS-ATTS-SEATING, ratified and built the next day — see the entry above) (previously a convention, not a rule). The floor-box class-A recompute and the v5 read-set model are re-pointed to the carrier, with the parent-proposer exclusion anchored by the parent's own proposer signature over the hash-covered b.Prev. Disclosed: the seat lands ONE BLOCK LATE (monotone, benign); a proposer can DELAY a seating by omitting a signer but can never FORGE one (downward-only, unenforceable by rule). Every carrier-free block hashes BYTE-IDENTICALLY to pre-carrier code (drift guard), so the era-3 freeze (#632) is untouched, the readiness stamp is NOT raised in this round, and fork-choice weight (owner call O3) is deliberately untouched. Gates G1-G6b, G8, G9 of the converged verdict ship with it, each driven RED first. (core/chain/carrier.go, core/chain/chain.go, core/node/chainrole.go, core/chain/floorbox_recompute_stateroot_atts_v5.go, core/chain/readset_v5.go; docs/thinking/2026-09-03-lastcommit-carrier-round-A-design.md)

Fixed

LastCommit carrier — research-certification merge gates closed (test, comments and ROADMAP only; NO design change, no rule change, and the box still never-Accepts): the build certification (LASTCOMMIT-CARRIER-round-A-...-2026-09-03) returned GATED on three items. (1) Gate G2's GREEN assertion was under-specified — it asserted only validatorsSeen != 0, which the three pre-existing anchors already satisfy, so a regression that silently dropped a signer from the carrier passed. Each arm now pins the EXACT carrier membership and seated count (the fifth node FIRST: 4 carried, 3 seated, the fifth included; LAST: 3 carried, 2 seated, the fifth structurally NOT seated because its reply landed after the first-to-quorum prefix closed). Ablation: under-carrying one genuine signer is now RED and was green before. (2) The R-CARRIER-PARENTPROPOSER residual text was one-sided. Dropping the forger's OWN seat is bounded, but the ADD direction is NOT bounded by key ownership — a freshly minted keypair verifies against b.Prev, matches no carrier entry, so nothing is skipped and the parent's TRUE proposer self-seats (one id per block, wrong-accept direction). The claim that a verifying witness never falls through to "no exclusion" is WITHDRAWN. Inert today; filed as an R1.8 flip precondition with its certified fix direction (a tagLastProposer committed scalar, owed BEFORE the era-4 freeze). (3) The claim that carrier bytes are bounded by R-membership is WITHDRAWN — qualification is applied only at the transition, so any fresh keypair passes validateCarrier and ~1.3M entries fit maxFrame; filed as R-CARRIER-BYTES on the stamp-raise carry-list with its two candidate bounds, both of which are v5 validity-rule changes needing certification. Also corrected: a producer comment that claimed HeadCarrier carries "everything you hold" when it carries the parent's first-to-quorum prefix, and a stale cited test name in ValidateProposal. (core/node/lastcommit_carrier_node_test.go, core/chain/carrier.go, core/chain/chain.go, core/chain/readset_v5.go, core/chain/floorbox_recompute_stateroot_v5.go, core/chain/floorbox_recompute_stateroot_atts_v5.go, core/node/chainrole.go, ROADMAP.md)

Fixed

Three false claims corrected against the shipped tree (PE H-4, §4, §6). docs/thinking/2026-09-02-r0.4b-c3-close-design.md §7 said "no consensus-validity change" when the change adds a validity predicate on both receive paths, a genesis door, a relaxed empty-block rule and a changed Block.Hash preimage; §9/F10 stated production grant = 0 as a close when the shipped daemon grants 500,000 and it is an open owner call. Both are retracted in place. R-COMPACT-ORPHAN's description is corrected to the measured behaviour — the store SILENTLY REPORTS DURABILITY IT DOES NOT HAVE (Append returns nil after a failed post-rename re-open, and Load then sees nothing) — and ReasonGuardUnloaded is annotated at its declaration as unreachable on the shipped daemon, so the record stops counting it as an operator signal. The R-FLAT-FEE comment in core/credit/delivery.go now names the ACCUMULATED LANE rather than a chunk size: trackProvisional accumulates and the serve call site fires per chunk, so every object above 50 KB is already past break-even.

Fixed

R0.4b C3 close — the issue epoch is now inside the blind-signed message, and the demand lane is a scheduled, separately-keyed lane (DO NOT MERGE unratified; gates the R0.4b merge): a blind red-team pass on the R0.4b build confirmed six breaks, and the converged research verdict (R0.4b-red-team-reconciliation-CONVERGED-RESEARCH-VERDICT-2026-09-02) withdrew the prior "pump closed for every K" claim. The pump re-opened whenever ONE RSA key was bound to more than one epoch — which an ordinary RESTART does, since the persisted key was re-registered for the new boot epoch. VerifyInWindow returned the NEWEST held epoch that verified, so an epoch-0 token was re-dated to epoch 3; the credit guard swept its epoch-0 entry at E + W while the token still verified, and a second server on the same ledger collected a second full payout per serial. Fixed:

  • (b1) The issue epoch is bound into the demand FDH inputH(domain ‖ ctr ‖ epoch(8B BE) ‖ serial) under a new silt/blinddemand/fdh/v2 domain, restoring RFC 9578's token_key_id-in-the-signed-message schema the first import dropped. A signature verifies under exactly the pair (key_E, E), so issuedEpoch(token) is a PURE FUNCTION OF THE TOKEN for any key schedule — the coupling condition "evicted ⇒ expired ⇒ un-redeemable" needs. Token{Serial,Sig} and receiptMsg stay byte-identical (no new receipt quasi-identifier; the epoch is a consensus epoch index, certified Q1-neutral), and the only wire change is that the request NAMES E in the existing Message.Height. The publish and credit FDH domains are unchanged byte-for-byte — committed publish tokens re-verify on every replay. The proposed distinctness VALIDITY RULE was REFUTED and is not built: the committed band is pruned, so it would lengthen the pump's period to 2W+1 rather than close it, and remembering every fingerprint forever is unbounded committed state.
  • The publish key never enters the demand keyset, and rotation is SCHEDULED. The daemon installed the persisted publish key as key_{boot} once; from boot+1 the demand lane refused to issue and from boot+W+1 the bank rejected everything, while the fee-charging withdrawal path kept charging. New diskissuer.EpochStore persists a per-epoch band (one CBOR file, atomic rewrite, [cur−W, cur+W] retained, [cur, cur+W] pre-published) and the daemon rotates it off the node loop on every epoch turn.
  • A stale key registration no longer mutes the proposer forever. A reg that missed its own epoch rode pendingIssuerKeys indefinitely and failed the node's OWN local pre-check with ErrIssuerKeyEpoch on every later proposal — a permanent, restart-only mute triggered by an ordinary missed epoch. The proposer fold now DROPS it (policy, never validity) and the schedule re-stages; SetDemandIssuerKey refuses to stage a backdated reg and prunes the queue, which also closes an unbounded-growth path.
  • The pin FOLLOWS the chain. "Once pinned, never re-pointed" was the wrong invariant for a CACHE of a committed binding: after a reorg the redeemer kept verifying against the abandoned fork's key_E and refused the canonical one for W+1 epochs. Held keys are re-validated against the current commitment on every keyset read and re-pointed on mismatch.
  • Every shipped withdrawal path is on the pinned lane. swarm receipt uses FetchDemandIssuerKeys + AcquireDemandTokenInWindow; the D3 client (WithdrawDemandTokenPrivately) carries a (key, epoch) its DURABLE parent resolved against the committed binding and refuses a reply naming another epoch. The unpinned AcquireDemandToken is deleted. A per-cohort key is a DENIAL again, not an accepted tagged token.
  • Rollout rule encoded (TestReadinessStampImpliesIssuerKeyCoverage): a binary may stamp readiness v5 only if Block.Hash covers cbor 17 and stateRootTagsV5 includes issuerKeyCommit. The stamp is NOT raised here. Design: docs/thinking/2026-09-02-r0.4b-c3-close-design.md.
Security

R0.4b — the cross-server double-redeem money pump CLOSED by per-epoch issuer-key expiry (D-DEMAND economic mechanism + a committed-format addition; DO NOT MERGE unratified): RedeemDeliveryCredit paid fee − skim on every call and never saw the token serial, while demand.Bank.spent is per-SERVER. So K colluding servers sharing ONE demand token (one blind withdrawal, one ChargePublish, one serial) each fired the conserved leg and minted exactly (K−1)·fee — the banned per-receipt subsidy. A bounded FIFO paid-serial guard alone was REFUTED by the red-team: each flood serial used to evict a victim is itself a paid delivery the colluding operator collects, so advancing the FIFO costs nothing and frees a whole window of already-paid serials for re-collection — the pump is self-financing. The certified close (research certification 2026-09-02, crypto advisory R-ECON-4, economist advisory R0.4b) is Privacy Pass key rotation (RFC 9578) imported as a schema:

  • The epoch lives in the KEY, never in the token. Token{Serial,Sig} and the signed receiptMsg are byte-identical; demand.Keyset holds {key_E : current−W ≤ E ≤ current} and Bank.Redeem accepts iff some held key verifies (≤ W+1 RSA verifies). The held keyset IS the window, so there is no per-token expiry field to forge. W = 4 epochs, an Evolving-tier knob denominated in DerivedEpochBlocks and resolved from the consensus epoch clock (head_height/EpochBlocks) — never wall-clock.
  • A CONSENSUS-ATTESTED E ↦ key_E binding (new committed keyspace issuerKeyCommit, new additive Block.IssuerKeys, new v5-only leaf issuerKeyCommit\x00). Without it, per-epoch keys are WORSE for privacy than no epoch: an issuer serving a distinct key_E to a small cohort turns "which key verified you" into a fingerprint, and a pinned/published keyset does not close it (an issuer that equivocates on keys equivocates on its published list too). A redeemer REFUSES a key whose fingerprint is not the committed one, refuses an epoch with no commitment, and gets no self-issuance exception. Append-only, no backdating, bonded-gated in objective mode, pruned to a bounded epoch band, and v5-only — a v4 block carrying a registration is rejected, so the frozen era-3 leaf set and every v4 root stay byte-identical.
  • Credit-layer expiry visibility (R0.4b-3): RedeemDeliveryCredit now takes the serial, its issuing epoch, and the current epoch. The paid-serial guard evicts BY EXPIRY ONLY, so a forgotten serial is always one no in-window key can still validate ("evicted ⇒ expired ⇒ un-redeemable"). At a cap full of still-live serials it REFUSES TO PAY rather than forget one — an under-pay, never an over-pay and never a mint. The cap is DERIVED (W × EpochBlocks × maxServeTrackedPerBlock, floored at 65,536), not a bare 8192, so it dominates the honest live set. Gates: TestOpenBreak_CrossServerDoubleRedeemMoneyPump FLIPPED from asserting the (K−1)·fee mint to asserting Σ(balances+escrow) conserved at K∈{2,3,5}; the red-team's TestSerialGuard_EvictThenReRedeemMintsZero and TestSerialGuard_EvictionPumpIsNotSelfFinancing land as permanent gates and hold TOGETHER WITH TestSerialGuard_SetIsBounded — the triple no FIFO-alone design satisfies (ablation: restoring FIFO eviction keeps SetIsBounded GREEN and turns both eviction gates RED). New mandatory equivocation gate TestIssuerKey_OffCommitmentKeyIsRefused (+ uncommitted-epoch, append-only-pin, no-chain and no-self-exception siblings). TestAbortLeavesTokenReusable and the TestInvariantA_* firewall guards stay GREEN; the new committed field gets its stateClass row, its stateRootTagsV5 tag, a real leave-one-out probe (issuerKeyRootProbe), and an EARNED leaf-diff exclusion whose scope-gate stall is itself asserted. core/demand/keyset.go, core/chain/issuerkey.go, core/node/demandkeys.go, core/credit/delivery.go. Design: docs/thinking/2026-09-02-r0.4b-per-epoch-key-expiry-design.md. Disclosed deviation: the routing named MsgGetIssuerKey/answerIssuerKey/peerIssuerKeys as the path to make epoch-plural. Verified against source, those serve the PUBLISH-token issuer key, which is the chain's issuerKey lookup and is re-verified against committed publish tokens on every replay — rotating it per epoch would be a consensus break. The per-epoch keyset is therefore a SEPARATE demand lane (MsgGetDemandIssuerKeys / MsgDemandTokenRequest); the publish lane is untouched. Scope: the shared-ledger case only; K truly distinct-owner ledgers share no paid-serial set, so the cross-owner variant remains the standing Douceur / demand-authenticity limit, neutralized today only by the γ→1/N firewall. Automatic per-epoch keygen scheduling (cert residual R5) and the W-value measurement (R0.4b-2, the Tester's sim) are NOT in this change.
Security

R0.4b fix-up — the two build-verification merge conditions, plus the epoch-skew close (D-DEMAND; mechanism-neutral, no consensus rule changed): the build-verification research certification CERTIFIED the composed mechanism and GATED the artifact on two proposer/handler-local defects. Both are closed here, with the residual the same verification named.

  • C1 — remote nil-receiver panic. answerDemandTokenRequest gates on the DEMAND issuer for the current epoch, not on the PUBLISH issuer, and routes an attached prepaid credit to tokenChargeFor, which verified it against n.tokenIssuer.Public(). (*blindtoken.Issuer).Public dereferences i.key unconditionally, so ONE crafted MsgDemandTokenRequest crashed any node running the demand lane without a publish issuer. tokenChargeFor now returns a typed refusal (errNoTokenIssuer / errCreditRefused) instead of dereferencing: a credit cannot be honoured when there is no key to verify it against. The credit-FREE path is untouched, so such a node still serves ordinary withdrawals.
  • C2 — proposer self-wedge on a current-era network. validateIssuerKeys reads the bond ledger PRE-apply, while proposeBlock folded the proposer's own first BondReg AND every staged pendingIssuerKeys entry into the SAME block — so the local pre-check failed with ErrIssuerKeyUnbonded, and because a staged registration rides and stays queued, every later proposal failed identically. A fresh -accept-delivery-receipts validator could never propose again. Closed as proposer POLICY (chain.IssuerKeyRegAdmissible, mirroring the validity clause beside it): defer a registration whose issuer is not bonded in the pre-state. No validity rule changes, so an attester still ACCEPTS a block carrying a deferred registration and a mixed swarm cannot fork on it — the same shape as the IsSlashed filter on pending bond regs (#503 Q1(a)).
  • R0.4b-5 — shared-ledger epoch skew. The sweep and the admission check ran against the CALLER's currentEpoch, so two redeemers sharing one ledger whose heads straddle a boundary could re-pay one token: A at current 10 sweeps a serial issued at epoch 5; B, still at current 9, holds key_5 and its own demand layer accepts. The ledger now keeps a monotone epochWatermark — the highest epoch any redeemer has presented — and both the sweep and a new backdate refusal run against it. Purely subtractive: the worst case is an under-pay during a skew, never an over-pay and never a mint. Gates, each RED before and GREEN after: TestDemandTokenRequestWithCreditAndNoPublishIssuerRefuses (drives the crafted message over the handler AND the wire dispatch; RED = SIGSEGV at core/blindtoken/issuer.go:58) and TestTokenChargeForRefusalsAreTyped; TestIssuerKeyRegDoesNotWedgeTheProposer (a fresh validator on an already-current-era network proposes, then lands its registration within a bounded number of blocks; RED = ErrIssuerKeyUnbonded on every proposal) and TestIssuerKeyRegAdmissibleMirrorsTheValidityClause; TestEpochWatermark_LaggardRedeemerCannotRePay (carrying its own no-skew control, so the refusal is measured against the mint it replaced), _IsMonotone and _UnguardedRedeemIsUnaffected. R0.4b-8, the composed boundary, is also closed here. The per-layer triple never crossed the expiry boundary (both eviction gates run at epoch 0 and pass because the CAP refuses) and every node/sim fixture ran at EpochBlocks = 0, where the consensus epoch is 0 forever. TestComposedExpiryBoundary_EvictedSerialIsRefusedUpstream is the first node-layer fixture with a REAL epoch clock: two servers on one ledger and one chain, a receipt paid at epoch E, the chain advanced past E+W, and the same token refused upstream at the demand window on a second server whose own spent set is empty. Ablation: a no-op Keyset.Prune plus an unbounded VerifyInWindow scan turns it RED on exactly the "server B banked it" line. TestComposedExpiryBoundary_EvictionIsClosedAtBothLayers then drives the red-team's eviction pump across the boundary with the window bypassed and shows the credit layer refuses too. Two ordering facts recorded because they narrow the exposure the certification reasoned about: RedeemDeliveryCredit tests paidSerial membership BEFORE reservePaidSerial (which is what sweeps), and reservePaidSerial returns early while under the cap. So the credit layer forgets a serial only under CAP PRESSURE — eviction is not merely expiry-only, it is expiry-and-cap-gated. core/node/tokenrole.go, core/node/demandkeys.go, core/node/chainrole.go, core/chain/issuerkey.go, core/credit/delivery.go, core/credit/credit.go.
Fixed

Floor-box class-A screen no longer reads LIVE box state — Direction A, an R1.8 flip precondition (Boulder 1; recompute-side only, no committed/wire format change, never-Accept unchanged): the class-A attestation screen picked its qualification branch from c.matureEpoch (core/chain/floorbox_recompute_stateroot_atts_v5.go) and its anchor eligibility from c.launchAnchorc.handedOff (core/chain/chain.go) — box-own fields written ONLY by apply→rotateEpoch and adopt. The recompute's deployment target "holds NO registry and replays NO apply", so on a COLD box those fields are never set and the mature-epoch branch is unreachable: every mature-epoch block was screened under the PRE-maturity rule. Both failure directions were live with every witness proof passing — a mid-epoch joiner (bonded ≥ MinBond, ∉ the frozen epochSet, the I3 identity) folded into an attacker's committed root (wrong-accept, and the class-M poisoning entry), and the same attestation inside an HONEST block stalling the box (false stall). The fix Resolves the already-committed tagMatureEpoch leaf present against prevStateRoot, UNCONDITIONALLY, before the branch — homed on the class-M carrier StateRootMaturityWitness (required every block) rather than the boundary-only rotate witness — and evaluates the launch-anchor rule through ONE shared predicate launchAnchorGiven(id, handedOff) that the live node and the box both call (the #402 non-fork rule). apply's attestation loop runs BEFORE rotateEpoch (rotate-LAST, #620/PR #703), so the committed PRE-value is exactly the oracle, including on the handoff block itself. Adds the certified matureEpoch ⇒ everMature pre-state cross-check and a LOUD entry assertion that the injected bond verifier is wired (ErrRecomputeBoxWiring) so the #572 replay shape fails at the box entry, not as a fold mismatch. Stall-adding only; the one-way latch WRITE, the #402 tally arithmetic, and every committed format are untouched. Research cert: floorbox-R-FOLD-LIVE-STATE-READS-RESEARCH-CERTIFICATION-2026-09-02.md. Design: docs/thinking/2026-09-02-floorbox-fold-live-state-reads-fix-design.md.

Changed

Floor-box class-M maturity recompute — streaming proof verifier, cut resident witness O(N·depth)→O(depth) (2026-09-02; Boulder 1; box-side only, consensus-inert, never-Accept unchanged, cert-touching → blind re-cert/re-measure before merge): RecomputeMatureNow materialized ALL N validatorsSeen members' SMT proofs resident (SeenSetWitness.Members, ~20 KB/member) before the fold, so the fold cost was GC over a multi-GB resident set, not compute (measured box-side resident witness: ~311 MiB at N=100k, extrapolating multi-GB toward N=1M). Adds RecomputeMatureNowStreaming + SeenSetStreamWitness, which PULLS each member's proof on demand, verifies it against the committed root, folds the scalar, and drops that member's proof heap before the next — resident witness drops to O(depth) (id-list + one in-flight member, a few KiB per member; the id-list is the only O(N) box term and is small, ~32 MiB at N=1M). RecomputeMatureNow becomes a thin resident-map adapter over the streaming core, so the fold logic lives in ONE place and the two paths cannot drift. This is a pure MEMORY/PROCESSING refactor: the committedStateRoot anchor, the completeness MTH over the FULL id-list, the per-member predicate, and the own-config screens are all byte-identical — no consensus/validity rule, no metric, and NO WITNESS WIRE/FORMAT change (the witness types are in-memory Go structs; the streaming provider is a Go function parameter). Ships the R-M-STREAM-COMPLETENESS RED ablation (a short/truncated id-list still stalls ErrRecomputeSeenSetIncomplete), a forged-member-value ablation (still stalls ErrRecomputeMemberStateUnproven under streaming), a streamed==resident==full-node equivalence suite, and a box-side re-measurement of resident-vs-streaming witness + fold time at N∈{1e4,1e5,5e5,1e6} that derives the pony fold ceiling (the M_seen cap value; the remaining ceiling is TIME, the O(N·log N) compute floor streaming does not remove, not RSS). The three TestMeasureRecomputeMatureNow* measurement tests are pure reporting — they carry no t.Error/t.Fatal — and now skip in their ENTIRETY under -short rather than skipping only their largest rung, which is what CI's go test -short and go test -race -short lanes run: measured -race -short cost for the group falls 63.2 s → 1.7 s. The property gates (..._Structural, N=200 and N=2000, including the O(depth) streaming-witness accounting assertion) and all five streaming soundness ablations stay ALWAYS-ON — 0.34 s and 0.25 s respectively — so no assertion is gated behind -short (core/chain/floorbox_recompute_maturity_v5.go, core/chain/floorbox_recompute_maturity_streaming_v5_test.go, core/chain/floorbox_recompute_maturity_fold_cost_test.go, docs/thinking/2026-09-02-classM-maturity-streaming-verifier-design.md).

Testing

scripts/check_cited_tests.py — a cited test that does not exist now fails the build (scar:cited-test-does-not-exist-2026-09-02; tooling only, no production logic changed): encodes a fired third-time rule — a green check that does not verify the property it claims. The instance: a core/credit/delivery.go comment cited TestPaidSerialWindowMatchesDemandWindow as pinning the paid-serial window to the demand window, and a research certification then repeated the claim. No func TestPaidSerialWindowMatchesDemandWindow( has ever existed. Both texts read as "this property is verified"; nothing verified it. The lint resolves every TestXxx cited in a Go COMMENT (under cmd/ core/ adapters/ ports/ sim/ integration/ e2e/), in CHANGELOG.md, ROADMAP.md and docs/**, against the func TestXxx( declarations in the tree, and fails listing each phantom as path:line TestName (no such test). This widens check_claims.py, which enforced the same linkage for docs/design/claims-ledger.md ALONE — the narrow scope is exactly why the delivery.go comment got through.

  • A real Go scanner masks code and string literals, so t.Run("TestFoo") and a name inside a string are not citations. Family citations (TestOpenBreak_*Locked…, TestFoo_{A,B}) resolve by prefix, and identifiers soft-wrapped across comment lines resolve joined.
  • .claude/ is excluded and this is load-bearing: it holds agent worktrees, full copies of the repo on other branches, and scanning them would let a phantom "resolve" against a test that exists only on an unmerged branch. Frozen history (/archive/, docs/thinking|reviews|buildlog/) is excluded for the same reason check_status_headers.py excludes it.
  • The external certification/ruling trees are ADVISORY (--strict-external to enforce): they are outside the repo and not version-locked to it. They are absent in CI, so CI stays hermetic.
  • scripts/cited_tests_allowlist.txt ships as a LEDGER of the 9 known-unbacked citations left on the tree, each labelled frozen-HISTORY or OWED. An OWED entry is a debt whose repayment is DELETING the line, and this branch pays two of them down on arrival: the guards written in PR #707 — TestStateRootV5CoversExactlyTheV5Fields and TestEveryDiskWritePathRunsTheEra4VersionCheck — now exist, so the two production comments that claimed a binding no test enforced (core/chain/statehash.go:128 on stateRootTagsV5 drift, core/chain/era3validity.go:93 on the era-4 disk-write-path leg) are backed and their allowlist entries are removed rather than carried.
  • Wired into the website CI job and documented in the new scripts/README.md.
Testing

Floor-box R1.6 — per-field Resolve-path oracle probes for the 23-field carrier table + a confirmed recompute wrong-accept, ROUTED (Boulder 1; test-only, never-Accept unchanged): hardens the R1.4 witness-soundness cert, which left residual R-CARRIER-REFLECTION (the coverage walk pinned only 3 of the 4 value/predicate carriers and gave the "already-anchored" fields no driven teeth). Adds a driven per-field probe for each already-anchored carrier obligation (forge the field → assert the box STALLS), pins the StateRootRotateScalar carrier into the reflection walk, and extends probeUncovered to name the class-M poisoning stages A1/A2/A3 explicitly (each pointing at its driven gate). The probes bite: ablating the class-A Slashed, class-B OwnerProof, and class-P Weight Resolve anchors each drives its probe RED (core/chain/floorbox_recompute_perfield_oracle_v5_test.go, coverage table in floorbox_recompute_adversarialroot_v5_test.go, A1/A2/A3 in modelcheck_snapshot_equivalence_test.go). The probe discipline surfaced a real break: the class-P activation-lock OldValue predicates (GateLockedIn/Era3LockedIn/Era4LockedIn .OldValue, read at rotate_v5.go:442,:450,:458) are UNANCHORED branch reads — a forged OldValue=true suppresses the lock-in emission so the value is never folded, and the attacker commits a lock-free root → wrong-accept (confirmed for all three). This partially refutes the R1.4 Q1 "scalar pairs are already-anchored" classification. Encoded as three RED-on-current-code OPEN-BREAK gates (TestOpenBreak_*LockedInOldValuePredicate, classified FIX-OPEN in the coverage table) that assert the current wrong-accept and must be flipped to assert-stall by the anchoring fix. ROUTED to the Researcher/PE as a consensus-adjacent recompute-soundness break (research gate — the fix is a source change to the certified class-P reconstruction). Design: docs/thinking/2026-09-02-floorbox-R1.6-per-field-oracle-probes-design.md.

Docs

R-ROTATE-EPOCH-LAST — drift guard for the rotate-LAST coupling #620 leans on (core/chain/rotate_epoch_last_drift_test.go, 2026-09-02). Test only — NO production code, consensus rule, validity predicate, or committed format changed (chain.go diff is zero lines). #620's epochSet order-invariance holds ONLY because rotateEpoch runs LAST in apply (reading the block's post-apply bonded/slashed) and liveQualifiedSet reads ONLY those two final maps — a load-bearing coupling with no guard until now (RULING-620 "Couplings the consult should carry forward"). Four guards, each ablated RED before trust: an AST check that rotateEpoch is the last statement of apply; an AST read-set check that liveQualifiedSet touches only {bonded, slashed, cfg} (no history); a behavioral purity check that a blocks mutation does not move liveQualifiedSet's output while a slashed mutation does; and a boundary-slash fixture where the frozen epochSet equals the post-apply set and DIFFERS from the pre-block set (the distinguishing ordering #620's fixture lacked). Moving rotate before the slash loop, or making liveQualifiedSet read c.blocks, reddens the guards.

Fixed

Floor-box class-P scalar-anchoring — Direction A + B (Boulder 1; cert-touching, re-opens R1.4; box still never-Accepts, blind re-cert + blind Tester run pending): closes the confirmed class-P recompute wrong-accept (×4) and the fresh-in-block-bond false-stall the R1.6/R1.5 OPEN-BREAK gates drove. Root cause: scalarFoldOp (core/chain/floorbox_recompute_stateroot_rotate_v5.go) verifies a scalar's pre-state proof ONLY on emit; on the suppression path (OldValue==newValue) the pre-value is never fold-verified, so a forged OldValue that suppresses the emit (or gates a branch) is never checked and the box agrees with a lock-free / latch-free committed root. Direction A adds an unconditional pre-state Resolve(...).IsProvenPresent anchor (anchorRotateScalar) BEFORE the emit/branch decision for the seven suppressible scalars (MatureEpoch + the three lock-in bools and their ridden height scalars) and — cross-class — everMature (floorbox_recompute_stateroot_maturitylatch_v5.go); every scalar leaf is committed unconditionally, so a forged value fails IsProvenPresentNoWitness ⇒ STALL (stall-adding only; the box never gains an Accept path). Direction B surfaces regVerWrites (the fold-anchored post-write regVersion) from class B and adds the in-block RegVersion cross-check anchorRotateMember lacked, mirroring the existing Weight in-block treatment — so a boundary block carrying a decisive fresh bond now agrees with apply instead of under-counting and stalling. The #402 tally arithmetic (3ready > 2total) and every committed v5 format field are UNTOUCHED. The R1.6 TestOpenBreak_LockedInOldValuePredicate and R1.5 TestScheduleOracle_OpenBreak_A/_B gates are flipped from assert-wrong-accept to assert-stall; new suppress-path gates cover MatureEpoch and everMature; a split coverage walk (scalarSuppressObligations, R-COVERAGE-SCALAR-SPLIT) classifies every scalar emit-anchored vs suppress-anchored so a future suppressible scalar cannot be wholesale-classified "anchored"; and the two R-ROTATE drift-guard brittleness riders are folded in (allowlist the sanctioned idQualifies predicate; assert rotate-is-last-in-gate-body, not exactly-one-statement). Box still never-Accepts (TestWitnessValidateV5_NeverAccepts green). Design: docs/thinking/2026-09-02-floorbox-classP-scalar-anchoring-design.md.

Tests

Open-break regression gate: cross-server double-redeem money pump (confirmed break, blind red-team run on origin/main = abe2d35, 2026-09-02): core/credit/open_break_cross_server_double_redeem_test.go TestOpenBreak_CrossServerDoubleRedeemMoneyPump. K colluding servers share ONE demand-withdrawal token; with ONE ChargePublish (one fee), K calls to RedeemDeliveryCredit for the same (fetcher, object) each fire the conserved leg unconditionally (delivery.go:229-231), because the per-server double-spend guard (Bank.spent[serial], core/node/demandrole.go:108) lives in the node-handler layer and is NOT consulted by the ledger. Result: Σbalances + Σescrow climbs by exactly (K−1)·fee (K=2 → +50000, K=3 → +100000, K=5 → +200000). Gate asserts the current broken behavior (delta == (K−1)·fee) so CI stays GREEN; subtests named openBreakDeltaK=N will FAIL when the cross-server redeem gate is added, which is the signal to flip to a conservation-pass assertion. This is the second confirmed delivery-credit money pump (first: A4 provisional-eviction, fixed Boulder 0 R0.4a); distinct axis — K servers, one fee, K payouts. ENCODE ONLY — do NOT merge (economy/M0-adjacent; held for owner ratification). Scar: .claude/agent-memory/tester/scar-cross-server-double-redeem.md.

Added

Floor-box Resolve-path scheduling oracle (Boulder 1, R1.5 — model-check tier, test-only) (core/chain/modelcheck_floorbox_schedule_oracle_v5_test.go, core/chain/modelcheck_floorbox_schedule_oracle_tests_v5_test.go, 2026-09-02). The consensus model-check had ZERO coverage of the floor-box Resolve path because WitnessValidateV5 short-circuits at the never-Accept STOP boundary (floorbox_v5.go:244) before the recompute. This oracle exercises the recompute DIRECTLY (RecomputeStateRootEntriesRevocations), pre-flip. Two additions: (1) a box-as-I1-participant scheduling oracle — an adversarial scheduler delivers honest and forged witnesses to disjoint boxes under adversarial delivery order, asserting I1 (no two honest boxes Accept conflicting roots at one height) and I5 (an honest box is never wrongly refused); (2) a multi-block Resolve schedule asserting each box's verdict is stable under reorder (Resolve is pure over prevStateRoot + block) and that a forged witness at height h does not poison prevStateRoot for h+1 (I3-adjacent). Drives two known compound-shape breaks RED as documented-open gates: (a) the class-P activation-lock LockedIn.OldValue tally-suppression wrong-accept (rotate_v5.go:442,:450,:458 × scalarFoldOp:473), and (b) the RegVersion in-block cross-check gap (apply's tally reads the just-written in-block regVersion at chain.go:3444 while the box anchors regVersion against pre-state, absent for a fresh in-block bond). Changes no production consensus path; the box still never-Accepts.

Docs

BACKLOG.md folded into ROADMAP.md (SSOT) and archived (owner-ratified 2026-09-01): the repo-root scratch backlog is retired so forward work has one source of truth. Its genuinely-still-open captured ideas move into ROADMAP.md's Residual backlog under a new "Polish & latent wins" sub-section (low-priority, gate nothing on the Boulder lattice): demand-dispersion pull half, domain-aware placement gaps, direct-IPv6-before-relay, relay selection + failover, the docs/-staleness CI tightening, the relay-in-the-middle e2e variant, and the deferred 3 GB shape test. Items verified already shipped were dropped, not folded: the local chunk read-cache (adapters/cachestore, wired at cmd/silt/daemon.go), the paginated roots list (#45), the buildlog pipeline (docs/buildlog/website/buildlog.html), and the kill-a-node erasure e2e (e2e/economy_repair_test.go); #115 (UPnP/NAT-PMP) was dropped as stale (closed 2026-09-01, optimization-only, off the V1 path). The last state of the file is frozen at archive/BACKLOG-2026-09-01.md with the full fold-map; the one doc link (docs/design/column-placement.md) is repointed at ROADMAP. No code changed.

Docs

docs/TENETS.md restructured into pure guiding principles (owner-ratified 2026-09-01, no logic change): the tenets now state what "good" looks like as an outcome, abstracted away from mechanism, ship-status, proof machinery, format specifics, and issue/PR war-stories — the build looks up to the tenets, they never look down at the build. Mechanism/history relocated to companions (design/m0.md, decisions.md, network-durability.md, build-process.md, network-protection.md, design/owned-residuals.md) and to the new docs/tenets-history.md (the extracted amendment log + build-immutable war-stories). Adds an Immutable Register index and the tenet-tier T-AR (no permanent center of reward) principle. A fix-cycle restored four principles a blind review found wrongly dropped: the maturation bet (mature vs. young regime + the named capture race, immutable #3), the design-target framing of the Sybil composition (Part 0), and two permanent Douceur residuals (pre-farmable bare-age; wash is re-priced, not proven away). No immutable was traded — a presentation restructure. Relocation audit trail: docs/thinking/2026-09-01-tenets-principles-purge-ledger.md.

Docs

Session-17 PACE deliberations + Rock/Boulder roadmap (planning, no logic change): four build-day design docs land in docs/thinking/ ahead of the upcoming build — the floor-box witness-soundness fix (Resolve-anchor every witness value, 2026-09-01-floorbox-witness-soundness-fix-design.md), the A4 provisional-eviction money-pump fix (2026-09-01-a4-provisional-eviction-fix-design.md), the economy observability MVP (2026-09-01-economy-observability-design.md), and the SMT domain-separation close (2026-09-01-smt-domain-separation-close-design.md). ROADMAP.md reorganized to the ratified 5-Boulder plan (0 A4 money-pump · 1 accept-flip witness-soundness spine · 2 economy-ON + observability · 3 era-4/v5 freeze · 4 standing gates + M0 endgame), with the old 6-Rock overlay preserved as history and mapped forward. Folds in the ratified decisions (R0.2 (b)-prunable, R1.7 external-pass-is-a-hard-precondition, R2.3 A4-first/separate, R2.5 G2 = 1024 MiB measured, R2.6 hold/G2-gate, R3.4 freeze deferred to RC and decoupled from the flip).

Fixed

A4 provisional-eviction money-pump — close the double-pay at FIFO eviction (Boulder 0, R0.4a; economic-mechanism change, B3 conservation — Researcher re-cert pending): an object-aware serve eagerly self-mints bytes − skim to the server and routes skim to the object's escrow, tracking the lane as provisional so a later witnessed receipt supersedes it (RedeemDeliveryCredit reverses the mint, then pays the conserved fee − skim). When the bounded provisional map (maxProvisional, build-immutable #8) FIFO-evicted the oldest lane, it forgot the lane but LEFT the mint on the server's balance; a receipt redeemed after eviction then paid the conserved leg on top — one delivery paid twice, a network-minted per-receipt subsidy (the banned dual). Fix (the (b)-minimal claw-back, core/credit/delivery.go): eviction now REVERSES the evicted lane's self-mint before forgetting it, via the same floored reversal the redeem uses (reverseProvisional, shared verbatim so the escrow floor is identical at both sites — a repair bounty paid between serve and eviction is never clawed back). An evicted lane is thereby left in the same accounting state as "never served", so an evicted-then-redeemed lane equals a never-existed redeem: conserved leg only, mints nothing. The give is the unwitnessed bilateral fallback — an evicted, never-redeemed serve loses its self-record (an under-pay at the >maxProvisional tail, never an over-pay, never a denial). New gate core/credit/money_pump_test.go TestA4MoneyPumpConservation pins the closed-system invariant Σbalances + Σescrow == grant + legitimate transfers (RED at delta=+1024 before, GREEN after); TestProvisionalCapIsBoundedAndDeterministic flipped from encoding the buggy rule (c) to the correct rule (b); new TestPaidBountyIsNotRecoverableByEviction guards the escrow floor at the new reversal site. Design: docs/thinking/2026-09-01-a4-provisional-eviction-fix-design.md.

Fixed

A4 follow-on — provOrder desync fix (Boulder 0; conservation-shape-neutral, red-team 2026-09-01): the A4 fix leaned on provOrder, the FIFO order slice, which RedeemDeliveryCredit never kept in sync — it deleted the redeemed lane from the provisional map but left the key in provOrder. On the redeem-heavy path (map stays small, eviction loop never fires) the slice grew one entry per witnessed delivery, forever: unbounded state on the floor box (build-immutable #8, HIGH), an attacker-buildable single-serve stall on the serialized consensus loop when the dead prefix is finally scanned (MEDIUM-HIGH), and a stale front entry that reversed a LIVE re-served lane's mint before any redeem (MEDIUM, grief). Not a mint — the red-team's conservation fuzz refuted the double-pay. Fix (core/credit/delivery.go, credit.go): provOrder becomes []provKey with a companion provIndex position map; a redeem tombstones the lane's slot in O(1) (no slice scan), eviction skips tombstones and pops the oldest live lane, and an amortized-O(1) compactProvOrder caps the slice at 2maxProvisional. Conservation is untouched — no change to what is minted or reversed, only WHEN/HOW the order-slice key is removed; the conserved-lane key shape is unchanged, so the R0.4 conservation cert stays valid (the provKey-server shape change, RT-DELIV-3, is routed separately as cert-gated). New gates core/credit/delivery_provorder_test.go TestProvOrderStaysBoundedAcrossRedeems / TestRedeemDoesNotLeaveDuplicateOrderEntry (RED at 9d50437, GREEN after); TestA4MoneyPumpConservation and TestProvisionalCapIsBoundedAndDeterministic stay GREEN. Design: docs/thinking/2026-09-01-provorder-desync-fix-design.md.

Fixed

provOrder eviction front-drop desync — the cursor fix (Boulder 0, 2026-09-01; conservation-shape-neutral): the Tester's compaction fuzz (TestCompactionTombstoneFuzz/eviction-dominant, seed 0xdeadbeef0002) failed deterministically at step 13154 (full ops, not -short). The eviction loop dropped the FIFO front by re-slicing (provOrder = provOrder[1:]), which shifts every surviving entry down one physical position but left provIndex holding the STALE positions. removeFromProvOrder then tombstoned the WRONG live slot, compaction dropped a live lane's order entry, and a ghost index entry could reverse a live re-served lane's self-mint (grief, not a mint). A real product defect the R0.4/R0.4a fixes left on the eviction side — it fires only when the eviction loop runs repeatedly (poolSize > maxProvisional), which the -short path never reaches. Fix (core/credit/delivery.go, credit.go): a logical head cursor provHead replaces the front re-slice — eviction advances the cursor and nils the dropped slot, so provIndex holds ABSOLUTE positions a front-drop never invalidates. Amortized O(1) per op, no survivor-index rewrite (the RT-DELIV-1b stall is not reintroduced — the product path drives 100K/150K full ops in ~145 ms under -race); compactProvOrder resets the cursor on rebuild and still caps the slice at 2*maxProvisional (build-immutable #8). Conservation is untouched — no change to reverseProvisional, the fee/skim arithmetic, the terminal-reversal sites, or the provKey shape (RT-DELIV-3 stays separately cert-gated), so the R0.4 conservation cert holds. Also fixed a harness gap the desync had masked: the fuzz's redeem-branch fallback-serve did not predict its eviction, so expectedTotal under-counted — the identical prediction block from the other serve branches was added (test-only, no product accounting change). All three fuzz scenarios GREEN at full op counts; TestA4MoneyPumpConservation and the prior Boulder-0 gates stay GREEN. Ablation reddens the fuzz at step 13154 again. Design: docs/thinking/2026-09-01-provorder-eviction-cursor-fix-design.md.

Fixed

v5 trustless floor box — R1.2 witness-soundness fix: re-anchor every class-P/A/B screen predicate against prevStateRoot (still NEVER-ACCEPT) (core/chain/floorbox_recompute_stateroot_atts_v5.go, ..._rotate_v5.go, ..._bondreg_v5.go; gates + coverage meta in core/chain/floorbox_recompute_adversarialroot_v5_test.go). The recompute classes P (rotation), A (attestations→validatorsSeen), and B (bond regs) read per-member VALUES and SCREEN PREDICATES from the untrusted witness structs and used them as fold NewValues or branch predicates WITHOUT resolving them against prevStateRoot — a wrong-accept-by-recompute the existing ablations were blind to (they forged against an honest committed root; the attack moves the committed root to match). The fix threads prevStateRoot into the screen/tally/write-set functions and requires each untrusted read to statehash.Resolve present/absent against prevStateRoot before it is trusted (NoWitness⇒stall, never a false read — C-7 §104): class A anchors Slashed/InEpochSet/ BondedSize/BondedPresent AT SOURCE in attesterQualifiedFromScreen (which reaches class M — a forged screen can no longer inflate validatorsSeenRoot, PE ruling Q2); class P anchors the frozen Weight (against qualified||id, or the class-B qualWrites for an in-block bond) and RegVersion/RegVersionKnown (against regVersion||id) WITHOUT touching the 3ready>2total tally arithmetic (the #402 non-fork rule, TestActivationQuorumNonFork stays green); class B anchors PriorOwner/Claimed/PriorProven (against bondRootOwner||root / bondRootProven||root). The box STILL never-Accepts — the fix ADDS stall paths only; the STOP boundary and TestWitnessValidateV5_NeverAcceptsWhileRecomputeGated are unchanged. Proven by the 11 adversarial- committed-root gates (RED-on-main → green-after, each ablated red-then-green — every anchor is load-bearing) plus a reflection-pinned coverage meta-assertion over the three witness carrier structs (with a teeth companion). Design: docs/thinking/2026-09-01-floorbox-witness-soundness-fix-design.md + ...-BUILD.md; PE pins: silt-reviews/principle-engineer/RULING-floorbox-R1.2-invariant-pins-2026-09-01.md.

Added

Economy observability MVP — the four local-exact SELF panels (Boulder 2, R2.1 slice 6a) (GET /api/economy/self in cmd/silt/ui.go; per-node repair counter in core/credit, 2026-09-01). An operator now reads their OWN economy health from ONE node — no aggregator, no gossip, no network estimation — every field read from this node's own Ledger/care state and stamped local-exact. Ships cert-free and economy-OFF (the auto-skim still fills escrows with the economy off, so the accounting is real; bountyOn reports whether repair actually disburses). Four panels: (1) my-solvency — per cared object a funded-horizon cliff flag (a measurable, finite horizon inside a 30-day warning window; a not-yet-measurable object renders finite=false, NEVER faked as perpetual); (2) am-I-profitable — revenue split into serve vs bounty, margin = balance − an OPTIONAL operator-supplied ?cost=N (never a persisted flag; absent, margin is flagged not-given, so it is never asserted as fact); (3) is-durability-self-funding — pooled + per-object skim-in (funded) vs bounty-out (paid) with the drain signal net<0; (4) wash self-check — a serve/fetch symmetry SHAPE self-check conjoined with non-positive balance, labeled "suspected" NEVER "detected" (authenticity is not-knowable — Douceur — so it is never a slashing input). Adds the one new state the design flagged: a per-node RepairsDone/BountyEarned counter incremented in PayBounty (the per-object repairs count cannot attribute work to a node) — pure observability, classified neutral by the Invariant-A guard (it feeds no standing/conservation/disbursement rule). Extends the existing /api/status durability block with finite/cliff. Every check is ablation-proven: the repair counter, the wash conjunction's balance clause, and the pooled net each redden when their logic is inverted. Deliberation: docs/thinking/2026-09-01-economy-observability-design.md.

Docs

Gate-4 whole-set bonded witness size — measured, scar closed (core/chain/floorbox_wholeset_witness_size_measure_test.go, docs/thinking/2026-08-31-floorbox-wholeset-witness-size-measurement.md, 2026-08-31). Test/measurement only — NO production code, consensus rule, validity predicate, or committed format changed. A _test.go measurement that sizes the whole-set bonded-member witness the heavy floor box must carry, closing the gate-4 unfiled-measurement scar with a citable number instead of an estimate.

Fixed

v5 trustless floor box — close the last Path-1 completeness gap: the OFF-boundary everMature maturity latch (class M), + a permanent emission-keyed leaf-diff guard (core/chain/floorbox_recompute_stateroot_maturitylatch_v5.go, core/chain/floorbox_recompute_stateroot_v5.go, core/chain/floorbox_recompute_stateroot_rotate_v5.go, docs/thinking/2026-08-31-floorbox-classM-everMature-offboundary.md, 2026-08-31). apply latches everMature false→true at the TOP of EVERY block where !everMature && Mature (chain.go:3303-3305), BEFORE the boundary gate. #678 reproduced that leaf write ONLY inside class P (boundary-gated), so the GENERIC off-boundary maturity crossing (h % EpochBlocks != 0) had no reproducer ⇒ the recompute folded no tagEverMature op ⇒ recomputed root != committed StateRoot ⇒ STALL. SAFE (never a wrong-accept — the box still never-Accepts) but a LIVENESS gap fatal to the #657 accept-flip: off-boundary is the generic crossing ((EpochBlocks-1)/EpochBlocks). Fix: a boundary-independent class-M reproducer in the recompute entry, the SINGLE OWNER of the tagEverMature write. It fires on ANY block whose latch flips, REUSING RecomputeMatureNow (#668, not a rebuild) over the committed post-apply state, and threads the post-latch everMature into class P for its freeze gate (P's tagEverMature emission REMOVED — no double-emit at a boundary-coincident crossing). NO apply/consensus change; the box STILL never-Accepts. PERMANENT GUARD (the real deliverable — end the one-at-a-time discovery of unreproduced writes): an emission-keyed differential leaf-diff test that, on a real apply over a dry-run clone, diffs stateRootLeavesV5 PRE vs POST and asserts the committed-leaf key-diff equals the recompute's folded key-set — keyed on the LIVE marshaller output, so a FUTURE committed tag is caught with zero guard edits. Driven by a reachability generator whose schedule INCLUDES an off-boundary maturity crossing; ABLATED RED against the pre-fix recompute (class M removed) reddening naming exactly everMature, then GREEN with the fix. Plus the two #678 Tester-flagged coverage tests via the real entry on BOTH off- and on-boundary crossings: (b) an omitted tagEverMature write stalls; (c) a forged maturity screen (per-member bonded/slashed) stalls. #678's thinking-doc trued up (the everMature LATCH is any-height; the epoch FREEZE handoff is boundary-only — the two are separable). FOLLOW-UP (PE write-obligation ledger acceptance bar): the leaf-diff generator now drives ALL 28 committed-leaf tags through the diff assertion — E/R/A/P/M plus class-S (slash), class-B (bond-reg, incl. displacement) and class-T (TTL sweep), which the first cut left un-driven (16 of 28). A SELF-CHECKING coverage meta-assertion (TestLeafDiffGuardCoversEveryEmittableTag) asserts the UNION of tags the scenarios exercise EQUALS the FULL emittable tag set of the live stateRootLeavesV5 marshaller (derived from populateCommitted, itself reflection-pinned) — so a FUTURE format tag with no scenario reddens THIS test by name, closing the generator's own blind spot permanently. Its teeth are demonstrated (TestLeafDiffCoverageMetaHasTeeth: drop a scenario ⇒ the comparison names the uncovered tags), and the diff-minus-fold NAMING path is exercised per-class for S/B/T via real recompute ops with one emission dropped (TestLeafDiffNamingPathPerClassSBT, red-before-green). Test-only; no apply / class-M / consensus change; the box still never-Accepts.

Fixed

v5 trustless floor box — close the class-P young→mature HANDOFF-boundary completeness gap (core/chain/floorbox_recompute_stateroot_rotate_v5.go, core/chain/floorbox_recompute_stateroot_v5.go, docs/thinking/2026-08-31-floorbox-handoff-boundary-completeness.md, 2026-08-31). apply latches everMature (the maturity latch, class M) BEFORE rotateEpoch (chain.go:3303-3316), so on the ONE boundary that flips everMature false→true — the young→mature handoff — real apply freezes the epochSet but the class-P recompute read the PRE-latch everMature and took the !everMature early-return, freezing NOTHING ⇒ recomputed root != committed StateRoot ⇒ STALL. SAFE (fold-caught, never a wrong-accept) but a LIVENESS gap: every real objective chain crosses the handoff exactly once, so the box could never validate that block. Every class-P positive test used MatureValidators=0 (mature-from-genesis), so the path was unexercised. The fix consumes the POST-latch value: post_everMature = pre_everMature || matureNow(thisBlock), computed by REUSING the existing maturity recompute (RecomputeMatureNow, #668) over the committed post-apply state, and gates the freeze on it. Because everMature is a committed v5 leaf (statehash.go:196), the box also reconstructs the everMature false→true WRITE on the handoff (there is no separate class-M recompute; P is the boundary class). Same class as R-P-sameblock-order, but for the everMature scalar. O(registry)-honest; NO apply/consensus change; the box STILL never-Accepts. Regression: a handoff-boundary POSITIVE test (young at genesis, matures at the boundary) that AGREES with real apply + StateRootForVersion(5) and FAILS against the pre-fix code, plus a pre-everMature ablation (RED). Test cleanups (blind Tester on #677): renamed the mislabeled ...AblationFlippedTally to ...AblationForgedFreezeWeight (it forges a freeze weight, not a tally; the load-bearing tally test is ...AblationLiveTallyForgedRegVersion); added a standalone epochSetRoot byte-exact check analogous to class A's ...AttDigestByteExact.

Added

v5 trustless floor box — three pre-Accept hardening ablations for the era-4 v5 recompute (7a/7e/7f) (core/statehash/fold_test.go, core/chain/floorbox_recompute_stateroot_bondreg_v5_test.go, core/chain/floorbox_recompute_stateroot_ttl_v5_test.go, 2026-08-31). Test-only; the box STILL never-Accepts, no apply/consensus change. Each is red-before-green (inject the defect, watch the REAL code path stall, restore). 7a — a DIRECT adversarial ablation for the R-fold's delete off-path siblings. MEASURED against pokt-network/smt@v1.0.0: a delete-sibling's authenticity rides its DIGEST (proof-anchored to the verified SideNodes), NOT its preimage content — parseTrieNode stamps a resolved node with the lookup-key digest and digestNode returns that CACHED digest without re-hashing, so corrupting a sibling PREIMAGE is a ~13% no-op while corrupting its DIGEST un-seeds the honest referenced node and stalls ~96%. The ablation corrupts the digest and requires the real fold to go RED; the delete cross-product byte-exact pin stays load-bearing. 7e — class-B bonded-but-NOT-qualified: a fresh reg with MinBondBytes <= Size < MinBond writes bonded/bondDomain but does NOT enter qualified (a previously untested delta branch); a positive test reproduces it byte-exact vs real apply + StateRootForVersion(5), and an ablation that forges the qualification stalls. 7f — class-T multi-block contiguity: the dueBucket[b.Height] scope-gate is sound only under chain height-contiguity (apply's sweep is >); a multi-block schedule of consecutive sweep heights firing distinct buckets is reproduced byte-exact, and an ablation that SKIPS a height (so apply's > vacuums a bucket the == gate never witnesses) stalls — so a future contiguity break reddens.

Added

v5 trustless floor box — Path-1 state-root recompute P1-e, CLASSES A (attestations → validatorsSeen) and P (epoch rotation → epochSet + boundary scalars): the LAST two Path-1 classes (core/chain/floorbox_recompute_stateroot_atts_v5.go, core/chain/floorbox_recompute_stateroot_rotate_v5.go, core/chain/floorbox_recompute_stateroot_v5.go, docs/thinking/2026-08-31-floorbox-recompute-AP-options.md, 2026-08-31). Completes the O(payload) + O(registry-per-digest) state-root recompute: E/R/S/B/T/A/P is now the full apply transition set. The exact write-sets were MEASURED off real apply + stateRootLeavesV5 (not guessed). Class A screens each non-proposer attester from OWN cfg over per-attester point witnesses (slashed F2 gate, then FROZEN epochSet[id] membership in a mature epoch — R-A-membership-source, NOT live bonded — else pre-maturity bonded>=MinBond || launchAnchor), derives the validatorsSeen||id ADDs, and reconstructs validatorsSeenRoot. Legacy mode (rep(id), not a committed leaf) STALLS (R-A-legacy; a v5 block is objective by construction). Class P reproduces EVERY rotateEpoch write (R-P-boundary-scalars): epochStart (always), matureEpoch, the epochSet freeze (per-member leaves + epochSetRoot), and the three activation-tally lock-in scalars (gateLockedIn/gateHeight, era3/era4). The freeze SOURCE is the POST-apply qualified set — the box replays this block's B/T/S qualified deltas in apply ORDER on the anchored pre-qualified set, THEN freezes (R-P-sameblock-order; a stale pre-delta freeze is the I3 hazard, fold-caught). The three tallies compute 3ready > 2total from per-member regVersion WITNESSES over the frozen set with OWN-cfg thresholds (3/4/5) + activation guards (R-P-tally-regversion). The #535 recovery boundary (liveQualifiedSet re-base) is NOT reconstructed from committed state — it STALLS (R-P-recovery, the ratified trust-the-directive carve-out C-2). The scope gate widens so A and P blocks are IN scope; E/R/S/B/T/A/P is now the complete set (no out-of-scope class remains). Box STILL never-Accepts (R-scope). No apply/consensus change. COST is HONEST: A is O(payload) screen + O(|validatorsSeen|) digest; P is O(|qualified|) freeze + tallies — both O(registry), riding R-membership (OPEN, for the #657 accept-flip). Ships with R3 execution-derived ablations against real apply + StateRootForVersion(5), each watched RED before green: (A) forged qualification screen, legacy-mode, omitted validatorsSeenRoot, proposer-only no-write, byte-exact digest; (P) steady-state + bond-reg-compound agreement, stale (pre-delta) freeze, short qualified-set witness, live-tally forged regVersion, forged freeze weight, missing epochStart scalar, #535-recovery stall. Cert: floorbox-recompute-classA-classP-wholeset-RESEARCH-CERTIFICATION-2026-08-31.

Added

v5 trustless floor box — Path-1 state-root recompute P1-a, the O(payload) HYBRID: fold only the CHANGED paths (classes E + R), flat in total state (core/statehash/fold.go, core/chain/floorbox_recompute_stateroot_v5.go, docs/thinking/2026-08-31-floorbox-recompute-Rfold-options.md, 2026-08-31). Reproduces validateEra3Roots' StateRoot equality TRUSTLESSLY at O(payload) cost, superseding the O(whole-state) P1-a (which witnessed the entire pre-state). The CERTIFIED hybrid: (1) DERIVE the E/R write-set from the block payload itself (the box runs the generator, not the prover, so the changed-key set is complete by construction); (2) WITNESS each changed leaf's pre-state proof against prevStateRoot; (3) FOLD only the changed paths to compute the post-state root; (4) require the computed root == b.StateRoot. The R-fold primitive (statehash.FoldChangedPaths) DELEGATES all tree surgery to the audited pokt-network/smt@v1.0.0 — it reconstructs a partial trie over the changed paths and replays the library's own Update/Delete, so the prior naive fold's 36%-wrong-root trap (hand-rolling add-displacement, the three delete sibling-promotion cases, extension split/absorb) does not recur. Pinned BYTE-EXACT against statehash.Root over a randomized structural cross-product: 18,000 trials across 3 seeds, 0 failures (add-disjoint / add-displacing / overwrite / delete leaf-promote / extension-absorb / inner-fold / shared-prefix interactions / extension present-absent), each structural rule ablated red-before-green. The scope gate is RE-ANCHORED off the whole-bondRegHeight scan onto the O(1) dueBucket[h] non-membership accelerator (or the O(payload) claim would be false). Cost proven FLAT: the same small E/R block against a 100-entry vs a 10,000-entry pre-state uses 3 changed-leaf witnesses at both sizes (payload-fixed) and 28 → 53 sidenodes (log N, not 100×). The box STILL never-Accepts (WitnessValidateV5 not flipped — research cert R-scope). Additive: NO consensus rule, validity predicate, or apply change. Ablations ship red-before-green: tampered StateRoot, un-named extra committed write (the cert's wrong-accept), forged / omitted changed-leaf proof, out-of-scope class, non-absent dueBucket TTL witness — plus the R3 execution-derived drift guard against real apply + StateRootForVersion(5), watched red on a mis-derived write-set. Certs: floorbox-recompute-P1a-Opayload-multileaf-RESEARCH-CERTIFICATION-2026-08-31, RULING-floorbox-recompute-P1a-Opayload-multileaf-2026-08-31.

Added

v5 trustless floor box — Path-1 state-root recompute P1-b, CLASS S (slashes): the changed-whole-set-digest write-set primitive, the first delta-derivable digest class (core/chain/floorbox_recompute_stateroot_slash_v5.go, core/chain/floorbox_recompute_stateroot_v5.go, docs/thinking/2026-08-31-floorbox-recompute-P1b-S-digest-writeset-options.md, 2026-08-31). Widens the O(payload) state-root recompute to reproduce validateEra3Roots' StateRoot equality for a block that carries on-chain equivocation slashes. A slash of culprit changes THREE whole-set DIGEST scalars (slashedRoot/bondedRoot/qualifiedRoot) — each an MTH over the whole post-state id-set — on top of three per-member leaves. The certified changed-digest primitive reconstructs each: (1) anchor the witnessed PRE-set id-list against the pre-digest committed under prevStateRoot (NOT StateRoot — that is circular; the FoldOp's OldValue = nodeSetMTH(preIDs) is verified against prevStateRoot by FoldChangedPaths); (2) apply the payload-DERIVED membership delta (slashed ADD culprit, bonded DELETE, qualified DELETE — the qualified delete derived from the anchored pre-set, C-1, NOT trusted from a witness scalar); (3) NewValue = nodeSetMTH(postIDs); (4) fold the digest scalar as one changed leaf, require postRoot == b.StateRoot. Reuses FoldChangedPaths/nodeSetMTH; no apply/consensus change. The scope gate widens so S blocks are IN scope; A/B/T/P/M stay out-of-scope-stalling (a slash block that also carries a bond reg / non-proposer att / firing TTL / boundary stalls). Box STILL never-Accepts (R-scope). COST is HONEST: NOT O(payload) — reconstructing a digest is a whole-list MTH fold with no incremental update, so class S is O(payload leaves) + O(|keyspace|) per touched digest ≈ O(registry) per digest, riding directly on R-membership (OPEN, for the #657 accept-flip). Ships with R3 execution-derived ablations against real apply + StateRootForVersion(5), each watched RED before green: forged qualified-screen pre-set, slash-doesn't-delete-bonded, wrong culprit, omitted touched-digest reconstruction, the circular StateRoot-anchor swap, and a byte-exact digest check. Certs: floorbox-Rboundary-writeset-digest-reconstruction-RESEARCH-CERTIFICATION-2026-08-31, RULING-floorbox-recompute-P1b-SA-digest-scope-2026-08-31.

Added

v5 trustless floor box — Path-1 state-root recompute P1-c + P1-d, CLASSES T (TTL sweep) and B (bond registrations): two more delta-derivable digest classes on the changed-digest primitive (core/chain/floorbox_recompute_stateroot_ttl_v5.go, core/chain/floorbox_recompute_stateroot_bondreg_v5.go, core/chain/floorbox_recompute_stateroot_v5.go, docs/thinking/2026-08-31-floorbox-recompute-BT-options.md, 2026-08-31). Widens the O(payload) state-root recompute to reproduce validateEra3Roots' StateRoot equality for a TTL-expiry block and a bond-registration block, reusing the P1-b changed-digest primitive. The exact write-sets were MEASURED off real apply + stateRootLeavesV5 (not guessed). Class T derives the expired set from the dueBucket[b.Height] accelerator (the O(1)/O(bucket) witness, NOT a whole bondRegHeight scan): each expired id deletes bonded/bondRegHeight/regVersion/qualified leaves and the whole bucket empties, changing bondedRoot/qualifiedRoot. The bucket MTH is anchored via the CRUX closure — the box reconstructs dueBucketMTH(members) and the scope gate requires it PROVE PRESENT against prevStateRoot, so a forged/short expired set stalls. Class B reproduces apply's bond-reg loop exactly: canonicalBondRegs same-id canonicalization, the MinBond/malformed/slashed screens from OWN cfg, the per-root PROOF-BEATS-DECLARATION displacement (the displaced squatter is read from committed bondRootOwner[Root], an id NOT in the payload — the R-B-displacement residual, fold-caught), and the dueBucketMoveOnReg old-bucket delete + new-bucket insert (each an MTH leaf reconstructed from its witnessed pre-set). A pure same-id renew touches NO whole-set digest (the id-set is unchanged). The scope gate widens so B and T blocks are IN scope; the epoch-boundary (P) check moves FIRST so a boundary stalls as a scope stall regardless of TTL; A/P/M stay out-of-scope-stalling. Box STILL never-Accepts (R-scope). No apply/consensus change. COST is HONEST: NOT O(payload) — reconstructing bondedRoot/ qualifiedRoot is a whole-list MTH fold, so B and T are O(payload) + O(|bonded|) + O(|qualified|) + O(|touched buckets|) ≈ O(registry) per touched digest, riding R-membership (OPEN, for the #657 accept-flip). Ships with R3 execution-derived ablations against real apply + StateRootForVersion(5), each watched RED before green: (T) forged expired set, bonded-not-deleted, omitted digest, out-of-scope compound, byte-exact digest; (B) fresh/renew/displacement agreement, displacement-not-applied, forged screen, boundary-out-of-scope. Cert: floorbox-Rboundary-writeset-digest-reconstruction-RESEARCH-CERTIFICATION-2026-08-31.

Added

v5 trustless floor box — recompute increment 4: the QUALIFIED-COUNT predicate reproduced from witnesses (the slashed-over-bonded whole-set read) (core/chain/floorbox_recompute_qualifiedCount_v5.go, docs/thinking/2026-08-31-floorbox-recompute-increment4-qualified-slashed-options.md, 2026-08-31). Reproduces qualifiedCount (the distinct-qualified validator COUNT N that sizes the count-quorum floor, chain.go:1479) TRUSTLESSLY, from the committed StateRoot + witnesses alone, replicating increments 1-3's C-1 pattern over the WHOLE bonded map. This closes the slashed-over-bonded quorum-stack whole-set read the #664 enumeration named as the keyspace the earlier hand-lists OMITTED. Additive: NO consensus rule, validity predicate, or apply change — a full node still counts bonded[id] >= MinBond && !slashed[id] from its own maps; this is a SEPARATE root-only path. The four-part proof: (1) SET-COMPLETENESS over bonded — reconstruct nodeSetMTH(whole-bonded id-list) and require it equals the committed bondedRoot digest (reuses the root increment 3 already reads); (2) PER-MEMBER BONDED WEIGHT (C-1) — Resolve each bonded weight leaf (the >= MinBond screen operand), so a forged weight fails ⇒ stall; (3) PER-MEMBER SLASHED BIT (C-1) — Resolve each slashed[id] present/absent, so a prover can neither drop a slash (inflate N) nor inject one (deflate N); (4) OWN CONFIG (C-6) — MinBond read from own cfg, never the witness. The box STILL never-Accepts (WitnessValidateV5 not flipped, the #657 accept flip waits). Six hard ablations ship red-before-green (each injected into production and watched to flip the verdict): forged bonded weight (_ForgedBondedWeightRejects, N inflated to 4), dropped slash (_DroppedSlashRejects, N inflated to 4), injected slash (_InjectedSlashRejects, N deflated to 2), omitted/injected member (completeness, _{Omitted,Injected}MemberRejects), and config-from-witness MinBond (C-6, failing-first, _MinBondFromConfig, N inflated to 4). Equivalence to the full node's qualifiedCount is asserted by test across the >= MinBond and slashed screens. TWO STOP-AND-REPORT findings correct the task's premise: (a) qualified is an APPLY-channel whole-set read (the rotateEpoch freeze epochSet := clone(qualified)), NOT a quorum-stack fold — its only validity gate is the Path-1 state-root recompute (validateEra3RootspostApplyRootsapply), a large separate piece that warrants its own increment, so qualifiedRoot STAYS inert; (b) slashedRoot the DIGEST has no whole-set reader (qualifiedCount reads slashed[id] per-member over the bonded domain, anchored on bondedRoot), so it too STAYS inert — no producer change, no digest-root exclusion removed, TestInertDigestRootsAwaitRecompute unchanged (still skips both roots). NOT a consensus-rule change; the box never-Accepts.

Added

v5 trustless floor box — recompute increment 3: the DE-MATURE SUPER-QUORUM predicate reproduced from witnesses (core/chain/floorbox_recompute_dematureQuorum_v5.go, docs/thinking/2026-08-31-floorbox-recompute-increment3-dematureQuorum-options.md, 2026-08-31). Reproduces requireDeMatureSuperQuorum (the F-1 de-mature super-quorum over the WHOLE bonded map, chain.go:2947) TRUSTLESSLY, from the committed StateRoot + witnesses alone, replicating increments 1/2's structure over a DIFFERENT keyspace: the whole bonded id-list (the R-membership budget path), rather than the frozen epochSet. Additive: NO consensus rule, validity predicate, or apply change — a full node still folds Σ bonded from its own map; this is a SEPARATE root-only path. The three-part proof: (1) SET-COMPLETENESS — reconstruct nodeSetMTH(whole-bonded id-list) and require it equals the committed bondedRoot digest (F1's inert root now READ); (2) PER-MEMBER WEIGHT (C-1) — Resolve each member's bonded weight leaf against the committed root, so a forged weight fails verification ⇒ stall; (3) THRESHOLD (C-6) — the ⅔ ratio is the fixed consensus constant, never read from the witness. The de-mature predicate fires only when !matureNow, so the recompute GATES on the REPRODUCED maturity state (reuses increment 2's RecomputeMatureNow): it folds the super-quorum only in the reproduced !matureNow state and is a no-op (met=true, matching the full node's skip) when mature. requireDeMatureSuperQuorum consults no epoch set, so the #535 recovery boundary does not change this fold (no boundary carve-out for this predicate). The box STILL never-Accepts (WitnessValidateV5 not flipped, the #657 accept flip waits). The producer (readset_v5.go) now emits the bondedRoot completeness leaf + per-member whole-bonded weight reads; the drift guard's bondedRoot inert-exclusion is REMOVED with a real red-on-drop ablation (TestBondedRootReadReddensOnDrop), and the two still-inert digest roots (qualifiedRoot, slashedRoot) keep their exclusion. Three hard ablations ship red-before-green: forged bonded weight (C-1, TestRecomputeDeMatureSuperQuorum_ForgedBondedWeightRejects), omitted/injected member (completeness, TestRecomputeDeMatureSuperQuorum_{Omitted,Injected}MemberRejects), and config-from-witness threshold (C-6, failing-first, TestRecomputeDeMatureSuperQuorum_ThresholdFromConstant). Equivalence to the full node's de-mature verdict is asserted by test for both a coalition that meets and one that misses the ⅔ super-quorum, plus the maturity-gate no-op. NOT a consensus-rule change. PARTIAL GATE (flagged for the #657 accept-flip assembler): the full-node caller gate is everMature && objective && !matureNow (chain.go:2827), but this recompute reproduces ONLY !matureNow. The everMature/objective conditions are DEFERRED to the accept-flip assembler (#657), which MUST re-add them before flipping to Accept — else the reachable !everMature && !matureNow state (a young chain below the bar, which a full node does NOT gate on the de-mature predicate) would wrongly fold the de-mature bar. The everMature witness (tagEverMature) is already in the v5 read-set. Also fixed a pre-existing decoration in TestWitnessReadSetV5BoundednessAblation: it asserted on raw len(readset), which already scales with the registry via the legitimate cert-blessed frozen-set weight reads, so neutering its injected bondRegHeight scan left it GREEN (the check did not depend on its defect). It now asserts on sizeExcludingFrozenSet (the same exclusion TestWitnessReadSetV5BoundedNotRegistrySized uses), so the injected scan is the only thing that can diverge it (red-before-green restored). (Both per PE ruling RULING-floorbox-recompute-increment3-dematureQuorum-2026-08-31.)

Added

v5 trustless floor box — recompute increment 2: the MATURITY-LATCH predicate reproduced from witnesses (the C-6 teeth) (core/chain/floorbox_recompute_maturity_v5.go, docs/thinking/2026-08-31-floorbox-recompute-increment2-maturity-latch-options.md, 2026-08-31). Reproduces matureNow (the maturity-latch metric via C2Metric, chain.go:2178) TRUSTLESSLY, from the committed StateRoot + witnesses alone, replicating increment 1's structure. matureNow gates the de-mature super-quorum (everMature && objective && !matureNow, chain.go:2827) and is the FIRST predicate whose fold READS GENESIS CONFIG (MinBond / Anchors / OperatorMargin / MatureValidators), so it is where the C-6 obligation finally has TEETH (increment 1's predicate read only the fixed ⅔ constant). Additive: NO consensus rule, validity predicate, or apply change — a full node still folds C2Metric from its own maps; this is a SEPARATE root-only path. The three-part proof: (1) SET-COMPLETENESS — reconstruct nodeSetMTH(validatorsSeen id-list) and require it equals the committed validatorsSeenRoot digest (F1's inert root now READ); (2) PER-MEMBER VALUES (C-1) — Resolve each member's slashed / bonded / bondDomain leaf against the committed root, so a forged weight or domain fails verification ⇒ stall; (3) GENESIS CONFIG (C-6) — MinBond / Anchors / OperatorMargin / MatureValidators read from OWN cfg, never the witness. The box STILL never-Accepts (WitnessValidateV5 not flipped, the #657 accept flip waits). The producer (readset_v5.go) now emits the validatorsSeenRoot completeness leaf + per-member slashed/bonded/bondDomain reads; the drift guard's validatorsSeenRoot inert-exclusion is REMOVED with a real red-on-drop ablation (TestValidatorsSeenRootReadReddensOnDrop), and the three still-inert digest roots keep their exclusion. Six hard ablations ship red-before-green: forged bonded weight (C-1), forged bondDomain (C-1), forged slashed bit (C-1, TestRecomputeMatureNow_ForgedSlashedRejects), omitted/injected member (completeness), and the mandatory config-from-witness (C-6, TestRecomputeMatureNow_ConfigFromOwnConfig). Equivalence to the full node's matureNow is now asserted by test on the SLASHED-SKIP fold path (a fixture seats a committed slashed member) and the UNSET-DOMAIN zeroDomainWeights fold path (a mixed set/unset-domain fixture), closing the two coverage gaps the blind PE review flagged (both branches were previously exercised only in their always-false direction). NOT a consensus-rule change.

Added

v5 trustless floor box — recompute increment 1: one weighted predicate reproduced from witnesses (the C-1 pattern) (core/chain/floorbox_recompute_v5.go, core/statehash/prover.go, docs/thinking/2026-08-31-floorbox-recompute-increment1-options.md, 2026-08-31). Reproduces requireEpochWeightQuorum (the mature-phase >⅔ frozen-WEIGHT super-quorum, Σ epochSet) TRUSTLESSLY, from the committed StateRoot + witnesses alone, proving the C-1 weight-composition pattern the v5-wholeset-digest-root cert names as the load-bearing gap. Additive: NO consensus rule, validity predicate, or apply change — a full node still folds the quorum from its own epochSet; this is a SEPARATE root-only path (the floorbox_v5.go posture). The recompute is the three-part proof the cert requires: (1) SET-COMPLETENESS — reconstruct nodeSetMTH(id-list) and require it equals the committed epochSetRoot digest (F1's inert root now READ), so an omitted/injected member ⇒ a different MTH ⇒ stall; (2) PER-MEMBER WEIGHT (C-1) — Resolve each epochSet[id] weight leaf against the committed root, so a forged weight fails verification ⇒ stall (the digest bound membership; the tally was forgeable without this); (3) GENESIS CONFIG (C-6) — the fold reads own consensus params, never a witnessed threshold. The box STILL never-Accepts: WitnessValidateV5 is NOT flipped to Accept (the #657 accept flip waits until ALL predicates are reproduced). The producer (readset_v5.go) now emits the epochSetRoot completeness leaf + the per-member epochSet weight reads; the drift guard's epochSetRoot inert-exclusion is REMOVED with a real red-on-drop ablation (TestEpochSetRootReadReddensOnDrop), and the four still-inert digest roots keep their exclusion with a skip-guarded remove-on-recompute placeholder (TestInertDigestRootsAwaitRecompute). Three hard ablations ship red-before-green: forged-weight (C-1), omitted/injected member (completeness), config-from-witness (C-6). core/statehash.Prover is the provider-side complement to Resolve (builds the SMT inclusion/non-inclusion proofs a box verifies). NOT a consensus-rule change.

Added

v5 floor box — the QUORUM-STACK whole-set read enumeration + blind-spot closure (core/chain/readset_v5_quorum_wholeset_test.go, docs/thinking/2026-08-31-Rboundary-mechanical-wholeset-enumeration-options.md, 2026-08-31). Test-infra + enumeration only; NO consensus rule, validity predicate, or apply change, and NO committed digest-root leaf added (that is the gated format change). The merged execution-derived read-set guard (readset_v5_drift_test.go) derives ground truth from apply(b) + a validity source that runs validateTakedowns + per-entry ValidateEntry — it NEVER runs the QUORUM STACK, so it was structurally blind to every committed map the stack reads AS A WHOLE SET (a SUM/COUNT over the entire map). That blind spot is the root cause of three hand-enumeration misses this session. This increment fixes it by EXECUTION-DERIVATION, not hand-listing: an untouched-member perturbation oracle runs the FULL contract (collectQuorumSigs + requireQuorumStack, plus the apply channel) over poised worlds and flags a keyspace as whole-set iff perturbing a member the block does NOT touch flips the accept/reject verdict or the recomputed root. The EXHAUSTIVE full-contract whole-set-read set is {bonded, epochSet, qualified, slashed, validatorsSeen}: the quorum-stack folds are {bonded, epochSet, slashed, validatorsSeen} (slashed via qualifiedCount's !slashed[id] fold — the keyspace the ≥4 starting list OMITS; validatorsSeen via the de-mature gate's matureNowMatureCoefficient fold), and qualified is an APPLY-channel whole-set read (the boundary freeze), not a quorum-stack fold (the ≥4 list's mis-attribution). The blind-spot-closed red→green ablation runs per keyspace: the PRE-extension guard is GREEN (blind), the EXTENDED guard is RED (the per-key producer omits the whole-map completeness read), and a positive control (augmenting the producer with the completeness leaf → GREEN) proves the RED is the missing whole-map read, not a tautology.

Added

v5 state root — the five whole-set DIGEST-root leaves, increment F1 (format-only) (core/chain/statehash.go, core/chain/modelcheck_stateroot_digestroots_test.go, docs/thinking/2026-08-31-v5-five-digest-roots-F1-options.md, 2026-08-31). Additive, v5-ONLY, INERT. Adds bondedRoot, epochSetRoot, qualifiedRoot, slashedRoot, validatorsSeenRoot — one scalar leaf each whose value is the RFC-6962 MTH over the CANONICAL sorted id-list of that keyspace's member set (membership-only; weights stay in the per-member leaves). They commit SET COMPLETENESS for the five whole-set committed reads so a root-only floor box can reconstruct-and-compare, closing the gap that an SMT inclusion proof certifies only the members you were given. Certified 2026-08-31 (v5-wholeset-digest-root-addition-RESEARCH-CERTIFICATION, PE cross-check RULING-v5-wholeset-digest-root-cert-crosscheck). Emitted by stateRootLeavesV5 ONLY, after the untouched era-3 leaves, so a v4/era-3 root stays BYTE-IDENTICAL (immutable #632). C-4 always-emit: an empty keyspace commits translog.MTH(nil), the fixed empty-MTH constant, with NO absent-vs-empty shortcut. C-7 prefix-safe: each tag is \x00-terminated and collision-free under Key = tag||rawKey, bound to a dedicated emit guard (stateRootDigestTagsV5). NOTHING reads the roots this increment — no validity predicate, no recompute; F3 wires the root-only recompute (with the C-1 per-member value proofs and C-6 genesis config). Ablated red-before-green: v4 byte-identical (a digest root emitted into the era-3 path reddens the immutable gate); each root load-bearing (add/drop a member moves its root); empty keyspace commits the empty-MTH constant; the emit guard forces all five tags. Reconciled the R3 execution-derived read-set completeness guard and the quorum-stack whole-set enumeration (#664) with the new digest-root leaves (core/chain/readset_v5_drift_test.go, core/chain/readset_v5_quorum_wholeset_test.go): the five digest roots are DERIVED output commitments the box recomputes, not witnessed reads, so the ground-truth derivation excludes them from the write-diff and the cross-leaf perturbation (isDigestRootLeaf) — the leaf-set analogue of the existing validateEra3Roots root-recompute exclusion. Without the exclusion, perturbing any member of the five keyspaces flips that keyspace's digest root and the guard falsely reports every member as read, and the apply-channel enumeration mis-attributes quorum-stack folds (e.g. slashed) to the apply channel. Pure reconciliation: no guard-model change — the ablations still redden on a dropped per-member read (the member's own leaf, not its digest root, carries that signal), and the TestGroundTruthPerturbationCovers keyspace-coverage invariant keeps its teeth (the digest-root exemption is narrow and asserts each digest-root leaf is present).

Docs

Record the RATIFIED O(payload) HYBRID state-root recompute for the trustless floor box (docs/decisions.md, 2026-08-31). Documentation only — NO code, consensus, economic, or security content changed; records a decision certified/ruled elsewhere. Under D-TIERING: Andrew ratified the design that lets the tree-free pony (1 CPU / 2 GB) fully validate a block's committed StateRoot in O(payload), not O(whole-state) — preserving the heavy / fully-trustless posture without a hold-tree box, a light posture, or a new cryptographic primitive; grounds the node-tier ratio vision (ponies : horses : archival ≈ 10000 : 100 : 1). The HYBRID design is class-partitioned: the box DERIVES the write-set from payload + era-4 accelerators (running the generator, not the prover), collects each changed leaf's pre-state proof against prevStateRoot, FOLDS only those paths to compute the post-root, and requires it == b.StateRoot — derivation closes completeness, the fold catches discrepancy, neither closes alone. Payload/dueBucket classes (E/R/T) certified-in-direction now; whole-map classes (B/P/M) inherit the open R-boundary write-set completeness. GATED-on-build residuals: R-fold (multi-leaf SMT fold pinned byte-exact + ablated per case), scope-gate re-anchored on dueBucket[h], R-writeset (B/P/M), R-scope (box stays never-Accept until R-fold pinned), R3 (execution-derived drift guard vs real apply); standing R1 (RegCap) + R2 (#535).

Docs

Record two owner-ratified era-4 floor-box decisions in the ledger (docs/decisions.md, 2026-08-31). Documentation only — NO code, consensus, economic, or security content changed; records decisions certified/reconciled elsewhere. Under D-TIERING: (1) the v5 floor-box recompute DIRECTION — completeness closed by MTH-reconstruction (the dueBucket pattern), NO new crypto primitive and NO dueBucket format change (Andrew, 2026-08-30); (2) the R-boundary POSTURE RATIFIED HEAVY — the floor box reproduces EVERY validity predicate (Option B, fully-trustless), not merely re-derive-root-and-trust-finality (Andrew, 2026-08-31). Consequence: the v5 committed format needs an MTH digest-root leaf per WHOLE-SET committed read (≥4: qualifiedRoot, epochSetRoot, validatorsSeenRoot, bondedRoot), the complete set to be established by execution-derived enumeration across apply ∪ ValidateCommit then certified; the #657 WitnessValidateV5 seam widens to "reproduce every validity predicate"; the v5 format freeze stays DEFERRED until certified + ratified.

Docs

Record the RATIFIED v5 five-root whole-set digest-root format addition (D-V5-WHOLESET-ROOTS) (docs/decisions.md, 2026-08-31). Owner-ratified addition of five v5-only committed MTH digest-root leaves — bondedRoot, epochSetRoot, qualifiedRoot, slashedRoot, validatorsSeenRoot — membership-only digests (always-emit, mirroring the certified dueBucket MTH) that let the heavy-posture floor box prove SET-completeness for the five whole-set committed reads by reconstruct-and-compare, closing the whole-set wrong-accept gap. Additive: appends to stateRootLeavesV5 only, the 18 era-3 leaves untouched, so a v4 root stays byte-identical (era-3 frozen, #632) — NOT an immutable trade. Ships with seven binding, non-desk-liftable build / model-check conditions (C-1 per-member weight verification is load-bearing; C-2 #535 directive-trust preserved; C-3 corpus-poise caveat; C-4 always-emit; C-5 ablation suite; C-6 genesis-pinned config; C-7 prefix-safe tags), and the v5 format freeze stays DEFERRED until built and model-checked. Cites the research certification, the PE cert cross-check, and the read-set enumeration (all 2026-08-31, silt-reviews/).

Docs

Phase 5 operational-floor scoping — the operational floor decomposed into sized, ordered increments (docs/thinking/2026-08-31-phase5-operational-floor-scoping.md, 2026-08-31). Scoping/definition only — NO code, no consensus/economic/security content changed. Decomposes ROADMAP.md Phase 5 into three groups: (a) pure-ops packaging / installers / service integration (launchd, systemd, Windows service) — UNGATED; (b) the two S6 scaling kills — O(delta) proof-maturation cold start (kill the O(store) restart scan, core/node/node.go:980-1028, the named fast-follow at :990-991) and reprovide dirty-tracking (kill the O(held) per-interval re-sign, core/node/repair.go:99-162), each assessed UNGATED (perf, PE to confirm the classification before build); and (c) R4 signed self-update — a SECURITY/TRUST surface, GATED (research certification + owner ratification required before build). Grounds the S6 kills in the real hot paths and the flixz F3 incident (~8m45s restart scan on a 14 GB / 381K-file store); gives a build order and the two-half exit-gate demo.

Added

consensus model-check — the unified step-oracle assertInvariants(replicas) (#406) (core/node/modelcheck_unified_oracle_test.go, docs/thinking/2026-08-30-406-consensus-modelcheck-harness-options.md, 2026-08-30). Test-infra only; no consensus rule, validity predicate, or apply change. The existing per-invariant oracles each drive their own scenario and assert their own invariant; the spec (docs/design/consensus-model-check.md) also calls for a single cross-cutting oracle "called each step" — absent until now (grep assertInvariants returned nothing). This adds assertInvariants(replicas) plus a deterministic stepDriver that delivers one message at a time over the real mature 4+4 node world (matureWorld) and asserts the oracle after EVERY delivery. Covers the two invariants expressible as pure functions of observable cross-replica state: I1 (no two replicas finalize different block hashes at one height, via FinalizedHeight+Blocks) and I5 (no honest replica slashed, via OnSlash). Ablated to the hard bar: the oracle goes RED on an injected I1 disagreement and an injected I5 honest-slash, GREEN once removed; the honest 3-round schedule stays GREEN over 126 deliveries to finalized height 11 with all 8 replicas agreeing. I2/I3/I4 stay owned by their dedicated exhaustive oracles — this monitor does not reimplement them. Also trues up the oracle-coverage maps in both core/node/modelcheck_unified_oracle_test.go and core/chain/modelcheck_test.go: the docstrings now cite the sibling per-invariant oracles by their real repo-root paths (the I1/I3/I5-enumeration oracles are core/chain files, not in-package), and the stale "NOT YET BUILT" list in core/chain/modelcheck_test.go is corrected — those I1-mature/I3/I5/I2 oracles are now built (per PE ruling RULING-406-unified-modelcheck-oracle-2026-08-30.md).

Added

era-4 (v5) trustless floor-box validation — lane-1 Part B increment B1 (the SOUND, ADDITIVE slice: the #535 cold-auditor recovery-boundary policy + the additive entry point; the accept-core recompute is research-gated and NOT built) (core/chain/floorbox_v5.go, core/chain/floorbox_v5_test.go, docs/thinking/2026-08-30-lane1-partB-witness-validation-options.md, 2026-08-30). Chain.WitnessValidateV5(block, parentStateRoot, recoveryDirective) is the ADDITIVE floor-box validation MODE — a SEPARATE path a root-only client calls instead of holding the tree. It changes NO full-node accept path: apply, validateEra3Roots, postApplyRoots, every validity predicate, and I1–I5 are untouched.

  • Ships the RATIFIED #535 policy (decisions.md 2026-08-30 item 3): the recovery directive is sourced ONLY from the box's own local -ws-checkpoint-class config (RecoveryDirective), NEVER the proposer or the block. A directive PRESENT for an ambiguous recovery boundary ⇒ proceed to trustless validation; ABSENT (cold-auditor default) ⇒ a LOUD IndeterminateTrustlessly (do NOT accept, never trust the proposer); LiveFollower is an OPT-IN flip. RecoveryBoundaryDecision is the standalone, unit-tested policy; isAmbiguousRecoveryBoundary mirrors the full-node effectiveEpochSet recovery-branch gate exactly (chain.go:1466-1468). The #535 residual is NOT claimed closed — closure is gated on the #603 bonded/epochSet keystone probes (noted in-code).
  • The accept-core recompute is ROUTED TO THE RESEARCH GATE, not guessed. The bounded witnessable recompute that would decide Accept/Reject does not yet exist (PE ruling silt-reviews/principle-engineer/RULING-lane1-partA-readset-v5-producer-2026-08-30.md, premise 1: "a DIFFERENT, bounded witnessable recompute that does not yet exist in the tree (Part B)"). Building it soundly is blocked on two verified obstructions: apply() iterates WHOLE committed maps (the bondRegHeight TTL sweep, chain.go:3272) the BOUNDED read-set does not witness in full, so a witness-seeded-clone replay computes the WRONG write-set; and some read-set leaves are DIGESTS (dueBucket[h] is an MTH over its id set, statehash.go:224), not the typed data apply iterates. A correct bounded recompute is a NEW, soundness-critical, consensus-equivalent computation — a research-gated surface. This increment REFUSES to guess it: WitnessValidateV5 NEVER returns Accept (proven by TestWitnessValidateV5_NeverAcceptsWhileRecomputeGated); the safe-default IndeterminateTrustlessly (ErrRecomputeGated) holds until a certified recompute lands, at which point its verdict slots into the marked seam.
  • Every proof case is ABLATED (defect injected, watched red): the cold-auditor stall, the directive-present proceed, the live-follower opt-in, the non-boundary non-ambiguity, the epoch-boundary gate match, the v5-only version gate, and the never-Accept safety invariant. go test ./core/... ./internal/... green; new tests green under -race.
  • B1/B2 boundary: B1 = validate a witness bundle in hand (pure-core, witnesses fed in tests) + the #535 policy. B2 (later) = any-of-N witness delivery / networking / daemon mode, and the gated accept-core recompute.
Added

era-4 (v5) witness read-set PRODUCER — lane-1 Part A, REBUILT against the AMENDED cert (the COMPLETE read-set + an execution-derived completeness guard) (core/chain/readset_v5.go, core/chain/readset_v5_drift_test.go, docs/thinking/2026-08-30-lane1-partA-readset-producer-options.md, 2026-08-30). Chain.WitnessReadSetV5(block) emits, for a v5 block, the WITNESS read-set as a []statehash.ReadEntry: the committed-state keys the v5 witnessable recompute reads to trustlessly re-derive the post-state root. Now the COMPLETE 23-keyspace read-set per the amended cert (silt-reviews/research/research-outcome/era4-witness-floor-box-readset-v5-AMENDED-RESEARCH-CERTIFICATION-2026-08-30.md, PE ruling silt-reviews/principle-engineer/RULING-lane1-partA-readset-v5-producer-2026-08-30.md).

  • The completeness gap, closed. The prior build (dbeccf1, SHIP-WITH-FIXES) omitted the attestation-loop reads (per attester: slashed[id] + the qualification-set membership + the validatorsSeen[id] write-target), the maturity-latch reads (everMature + the Mature()C2Metric inputs over validatorsSeen, each member's bonded/bondDomain/slashed), and the committed scalar leaves (epochStart/ era4LockedIn/era4Height/matureEpoch/gateLockedIn/gateHeight/era3LockedIn/ era3Height). Those leaves are consensus-load-bearing (validatorsSeen/everMature → maturity → anchor gating, F-1); a floor box witnessing only the prior subset could be made to accept a forged block on any of them. The rebuilt producer emits all of them, payload-driven and O(payload) for ordinary/TTL, O(RegCap) at a boundary.
  • The execution-derived completeness guard (replaces the hand-written mirror). TestWitnessReadSetV5ExecutionDerivedGuard derives the "expected" read-set from the RECORDED leaf-touch of the REAL v5 recompute (postApplyRoots/stateRootLeavesV5 on a cloneForDryRun clone), by two ground-truth sources — a pre/post leaf write-diff and a leaf-sensitivity perturbation (a leaf whose pre-state perturbation changes some OTHER committed leaf is one the recompute reads) — and asserts the producer COVERS (⊇) it. It is NOT a second hand-written table (the prior guard was, so both sides shared the same blind spot and stayed green over the gap — the session-7 "green while covering nothing" scar). Corpus adds an attested block (real b.Atts), a maturity-latch transition (everMature false→true), and a standalone slash at a non-boundary height; the vacuity guard requires attested/maturity-latch/slash coverage.
  • Regression-proven RED on the escaped defects. Dropping the attestation-loop reads reddens the guard on validatorsSeen (the exact prior-build gap); dropping the slash-path qualified[culprit] read reddens on qualified (TestWitnessReadSetV5DriftGuardAblation). The certified boundedness ablation stays: an injected O(registry) bondRegHeight scan scales the read-set with the registry and TestWitnessReadSetV5BoundedNotRegistrySized / ...BoundednessAblation redden.
  • Boundary bound relabelled O(RegCap). The three activation tallies read regVersion + weight over the WHOLE frozen set, so the boundary READ-set is O(frozen-set) = O(RegCap) (not O(boundary-delta), which is the WRITE-set); box-fits at RegCap=256 unchanged. The certified hazard is still obeyed — the producer targets the BOUNDED witnessable recompute, never apply()'s O(registry) literal reads; the TTL completeness collapses to one dueBucket[h] leaf. Scope: the read-set PRODUCER + guard/corpus only — NOT the #535 boundary policy and NOT wiring IngestBlockWitnesses into acceptance (both Part B); no consensus rule or validity predicate changed, no production hook (the guard's leaf-perturbation setter is test-only).
  • The VALIDITY-READ guard gap, closed (test-only follow-on, PR #656) (core/chain/readset_v5_drift_test.go, docs/thinking/2026-08-30-lane1-partA-validity-read-guard-gap.md, 2026-08-30). Blind review found the guard's ground truth was apply()-shaped (write-diff ∪ apply-recompute perturbation), but a floor box needs validity ∪ apply-recompute. spent[serial] (chain.go:2617) and revoked[root] (chain.go:2643) are read ONLY in the validity predicate, never in apply(), so both guard sources were structurally blind to them — dropping either from the producer stayed GREEN (21 of 23 keyspaces reddened on drop; two did not). Fix: a THIRD execution-derived ground-truth source, the VALIDITY-READ perturbation — for each committed leaf, perturb its pre-state and run the REAL validity read-predicates (validateTakedownsrevoked/byRoot; per-entry ValidateEntryspent/byRoot); a leaf whose perturbation FLIPS the accept/reject verdict is a validity read. It excludes validateEra3Roots (the root recompute would flip every leaf; that channel is Source 1's write-diff), mirroring Source 2's own-key exclusion — so it isolates the pure validity-gate reads. A dedicated validity corpus (buildV5ValidityReadCorpus) carries a token spend and a revoke→unrevoke so the reads fire; the maintenance corpus is untouched. TestWitnessReadSetV5AllKeyspacesRedOnDrop is the completeness proof: drop EACH of the 23 committed keyspaces one at a time and all 23 redden (spent/revoked now included). TestWitnessReadSetV5ValidityReadsCovered is the coverage half. The producer's certified read-set is UNCHANGED (spent/revoked stay emitted); no consensus/validity/statehash production change (the perturbation is test-only).
Added

PoD §7.3 transport BATCH 3 — the daemon control-frame binding: the paid relay pump now runs on a LIVE node, failing-first (docs/decisions.md, adapters/relay/wire.go, adapters/relay/server.go, adapters/relay/client.go, core/node/relayrole.go, core/node/relaytransport.go, cmd/silt/daemon.go, 2026-08-30). Binds the Batch-2 paid pump to the live daemon so a paid relay session runs end-to-end: open → paid connect → pay-as-you-go → settle. Policy: D-POD-RELAY-COEXIST — paid relay is ADDITIVE (Option B, RATIFIED 2026-08-30); free swarm relay is UNCHANGED and shares the same transport caps (docs/thinking/2026-08-30-pod-7.3-batch3-daemon-binding-design.md; cert silt-reviews/research/research-outcome/PoD-7.3-free-vs-paid-relay-coexistence-RESEARCH-CERTIFICATION-2026-08-30.md).

  • The paid marker. One optional Paid uint64 field on the relay ctrl connect frame carrying the node's session handle (omitempty; zero = free, byte-for-byte today's path). A nonzero marker routes the connect to the paid splice; an old client (no field) decodes to free.
  • The adapter/node seam. The daemon installs a PaidResolver on the relay Server (SetPaidResolverNode.ResolveRelayAuthorizer) resolving (fetcher, handle) to the node-owned authorizer via the SAME ephID-ownership check handleRelayPay enforces, and a PaidSettler firing settle-at-close from the live pump's return. The verifier + M0 guards stay in core/node; the adapter stays a dumb byte pump. The resolver is called OFF the node loop and marshals the lookup onto it (clock.AfterFunc(0)), so the loop-only session table keeps its single-threaded invariant — -race clean across the seam.
  • Refuse-never-downgrade (certified residual #2). A paid connect whose handle does not resolve to a live, owned session is REFUSED, never spliced free — a free downgrade would hand a non-payer an unfunded, uncapped forward. Failing-first.
  • Reaper teardown. sweepRelaySeen now calls sess.closeSession() on each reaped session so a reaped session's now-live pump drains and exits (no leaked goroutine); single-settle is preserved (the reaper's delete makes a later pump-completion settle a no-op). Closes the Batch-2 TODO(Batch-3), failing-first.
  • e2e proof. e2e/relay_paid_test.go: three real nodes over real TCP with the seam wired, asserting forward integrity, the live pay-gate, conserved settle with Reputation() unchanged (Invariant-A firewall), free relay still working with payments on (the Option-B witness), and the M0 settlement-log audit.
Added

Keystone leave-one-out coverage for the era-4 (v5) qualified and dueBucket committed fields (core/chain/modelcheck_snapshot_equivalence_test.go, docs/thinking/2026-08-30-keystone-era4-loo-qualified-duebucket.md, 2026-08-30). Both fields were deferred by probeUncovered to 4c/4d because in 4b no v5 verdict read them; the committed-root predicate (4c #640) and activation (4d #641) landed, so a snapshot leave-one-out now flips a real validity verdict. v5RootWorld + qualifiedRootProbe/dueBucketRootProbe drive validateEra3Roots against a v5 block whose forged StateRoot omits the field's leaves: a full snapshot rejects it (ErrEra3StateRootMismatch), a snapshot that lost the field recomputes the same field-less root and wrongly ACCEPTS it (a claim-succeeded wrong-accept, not a panic). A vacuity guard reddens if the field's leaves do not move the root. Removed both fields from probeUncovered; no existing oracle weakened. Oracle/test coverage only — no consensus rule, validity predicate, or committed format changed.

Docs

Map the third-operator committed-settlement design space (DEFINITION only — the next PoD frontier) (docs/thinking/2026-08-30-third-operator-committed-settlement-options.md, 2026-08-30). A design-options strawman for a GATED economic mechanism: cross-operator settlement into COMMITTED chain state, where an operator that is NOT the fetcher's direct counterparty redeems value. No code, no mechanism decision, no recommendation. Frames the hard constraints (the γ→1/N firewall / Invariant A; conservation / the banned per-receipt subsidy; M0 Don't-#3; the keystone bounded-state rule; the STILL-SETTLING v5 format), lays out four options for what-is-committed (log-only, net-balance leaf, per-receipt leaf, settlement root) and three block-entry mechanisms, each with its own firewall + money-pump analysis, and enumerates the gated surfaces (economic-mechanism, committed-format, consensus-rule, security-parameter, M0-privacy) and the sharp open questions for Research / PE. Flags that the heavy options couple to the unfrozen v5 format and the ungated v5 accept-core recompute, so they cannot be soundly specified yet.

Docs

Add the "Rocks" big-step tracker overlay to the roadmap (ROADMAP.md, website/roadmap.html, 2026-08-30). Documentation only — no code, consensus, economic, or security content changed. Adds one section under The ordered path that maps the six big rocks between here and V1 to the existing canon (the phases + the D-TIERING keystone track): (1) trustless floor box — IN PROGRESS (read-set identity + RegCap=256 ratified, accept-core recompute research-gated, R-boundary digest leaves IN DESIGN/CERT); (2) third-operator committed settlement — DEFINITION (#658, gated economic mechanism under blind review); (3) era-4/v5 format freeze — DEFERRED BY DESIGN; (4) #406 consensus model-check — IN PROGRESS; (5) Phase 5 operational floor — NOT STARTED; (6) external red team (#183) → R1 → V1 — GATED. An overlay, not a replacement: no existing roadmap content was deleted or contradicted; in-flight items are marked, never claimed decided.

Docs

R-boundary DESIGN: the additive v5 digest-root format for the trustless floor box (docs/thinking/2026-08-30-lane1-Rboundary-format-addition-design.md, 2026-08-30). Design only — NO production code, NO consensus/validity rule changed. Specifies the R-boundary close the owner-ratified recompute direction owes: the v5 witnessable recompute reads three committed keyspaces as WHOLE SETS (qualified, epochSet, validatorsSeen), each committed one leaf PER MEMBER with no aggregate digest, so a root-only box cannot prove it holds ALL members → an omitted frozen member flips the 3ready>2total activation tally or the maturity coefficient (a wrong-accept). The fix, additive and mirroring the dueBucket MTH: commit three v5-only MTH digest-root leaves (qualifiedRoot/epochSetRoot/ validatorsSeenRoot) over each keyspace's canonical sorted id-list, gated on BlockVersionWitnessable (v5) so era-3/v4 roots stay byte-identical; the box reconstructs each MTH over the witnessed set and compares to the SMT-proven committed digest, closing completeness by collision-resistance. Enumerates all 23 committed v5 keyspaces (whole-set vs witnessed-per-key), states the dueBucketRemove no-empty-bucket invariant, and lists the model-check obligations + the surfaces needing Research certification and owner ratification (this is a gated v5 committed-format change). Cites the CRUX certification and the PE cross-check (silt-reviews/research/research-outcome/era4-v5-floorbox-bounded-recompute-CRUX-RESEARCH-CERTIFICATION-2026-08-30.md, silt-reviews/principle-engineer/RULING-era4-v5-floorbox-recompute-crux-CROSS-CHECK-2026-08-30.md).

Docs

Record the owner-ratified lane-1 (trustless floor box, increment 3) decisions (docs/decisions.md, 2026-08-30). Decision record only — no code, consensus, economic, or security content changed. Adds one dated entry under the #600 floor-box thread capturing three ratified items: (1) the lane-1 v5 witness read-set identity (AMENDED / complete form) — the sound read-set is the 23-keyspace committed read-set with its per-leaf table; the prior incomplete identity omitted validatorsSeen, everMature, and eight scalar leaves (a wrong-accept) and is SUPERSEDED; it is O(payload) with an O(RegCap) boundary read-set, payload-driven, with an EXECUTION-DERIVED completeness guard (never a hand-written mirror), while the full-node recompute stays O(registry); (2) R1 — RegCap=256 measured safe (255× honest headroom, 32 MiB boundary witness, no value change, re-derive only on a seven-determinant change, E6 on #299) — boundary read-set relabeled O(RegCap), box-fit and security parameters UNCHANGED; (3) R2 — #535 recovery-boundary disposition is one local-only -ws-checkpoint policy flag (default cold-auditor / stall-loud; live-follower opt-in), closure gated on the #603 bonded/epochSet keystone probes. Cites the amended certification (silt-reviews/research/research-outcome/era4-witness-floor-box-readset-v5-AMENDED-RESEARCH-CERTIFICATION-2026-08-30.md) and the two #535 reconciliation docs.

Docs

True-up: era-3 format FROZEN + era-4 spine BUILT, roadmap/design drift corrected (ROADMAP.md, docs/design/block-format-by-era.md, docs/decisions.md, 2026-08-30). Documentation reconciliation only — no code, consensus, economic, or security content changed. Reflects facts already on main: the era-3 committed state-root format (BlockVersion = 4) is BUILT + FROZEN (#632, build 3af40bc); the era-4 witnessable-transitions spine (BlockVersion = 5) is BUILT + merged across 4a–4d (#637/#639/#640/#641); RegCap is the per-block TOTAL BondReg count cap (fresh + renewal, after same-id fold) = 256, the "fresh-only" reading REFUTED. Records the owner-ratified sequencing decision: era-4/v5 is kept OPEN-ENDED and its freeze is deferred to the end of Proof-of-Delivery, to run as a second practiced era freeze.

Fixed

PoD §7.3 BATCH 3 review fold-in — the paid e2e made -race clean and CI-caught (e2e/relay_paid_test.go, .github/workflows/ci.yml, 2026-08-30). Two follow-ups from the Batch-3 reviews, no guard weakened.

  • Resolver stopped-loop hardening — DEFERRED (gate #651). The off-loop ResolveRelayAuthorizer marshals the session lookup onto the node loop and blocks on the reply — safe today because the production loop never stops (PE-verified). A future graceful-shutdown (loop.Stop() while the relay Server still accepts) would leak one blocked resolver goroutine per paid connect. The naive fix (time.After in a select) violates build-immutable #5 (no time in core) and a Clock-based timeout is loop-bound (dropped when the loop stops), so bounding this read needs a port/placement decision. Reverted to the PE-SHIP'd bare marshal-and-read (86990af) and tracked as gate #651; whoever adds graceful shutdown owns bounding it.
  • e2e harness data race (Tester, failing-first). TestPaidRelaySessionEndToEnd polled ledger.Balance / ledger.Reputation directly from the test goroutine while the event loop wrote the same field at settle (RedeemRelayCredit). The Ledger is loop-only by design (no mutex). Both reads are now MARSHALED onto the relay node's loop and returned over a cap-1 reply channel. TEST-ONLY — the production Ledger stays mutex-free (loop-only invariant not weakened). The race is now real under -race (was: test read vs core/credit/relay.go:73 write) and passes with the fix.
  • CI now runs the in-process paid-relay e2e under -race. The -race job uses -short, which the e2e skips; the plain e2e job ran without -race. So the harness race would have shipped green. The e2e job gains a dedicated -race step pinning TestPaidRelaySessionEndToEnd, with a === RUN assertion so a stale -run filter (which go test treats as a silent 0-exit "no tests to run") fails the step loudly.
Fixed

PoD §7.3 transport BATCH 2 — the wire protocol (step 3) + the paid forwarding pump (step 4), failing-first, the live-networking batch (ports/net.go, ports/ports.go, core/relaypay/wire.go, core/node/relaytransport.go, core/node/relayrole.go, core/node/node.go, adapters/relay/paid.go, 2026-08-30). Wires the Batch-1 machinery to live transport: a fetcher opens a paid relay session over the wire, pays increments, and the relay settles at close. REUSES the Batch-1 S-clamp, M0 guards, and epoch-tied eviction — they are not re-implemented. See docs/thinking/2026-08-30-pod-7.3-transport-design.md §1, §2, §6, §7.

  • The wire protocol (step 3). Three request/reply kinds on the existing ports.Message dispatch, mirroring the delivery-receipt lane: MsgRelayOpen (fetcher commits chain root + funding + S), MsgRelayPay (a preimage reveal that authorizes the next increment(s)), and their acks. Settlement is LOCAL at close — NO wire message (the fetcher already paid at blind withdrawal; the relay redeems its highest preimage via RedeemRelayCredit). handleRelayOpen routes through OpenRelaySession, so the Batch-1 S-clamp / M0 guards / eviction all fire on the live path; handleRelayPay drives the carried-S Verifier (bounded advance). Sender side: OpenRelaySessionRemote + SubmitRelayPay. A live session table on the relay keyed by handle; single settlement at close via SettleRelaySession (the verifier's monotonic count is the accumulator). RedeemRelayCredit is now on the ports.CreditLedger interface.
  • The paid forwarding pump (step 4). adapters/relay/paid.go: a payment-gated long-stream pump (paidPump / paidSession / Server.SplicePaid) that forwards the paid direction up to the node-owned authorizer's ceiling (raised on each verified pay), pay-as-you-go — if the fetcher stops revealing, the relay stops forwarding and the stiff is bounded to ONE increment. THE SPLICE-EOF GOTCHA: the existing splice closes BOTH conns on the first EOF (swarm exchanges are short-lived); a paid ≤1 GiB session is not. The paid pump does NOT tear down the paid forward stream on a reverse-direction EOF — the forward stream ends on its OWN completion, never on the reverse direction closing. The verifier and M0 guards stay in core/node (the tested layer); the adapter is a dumb byte pump that only knows how many bytes it is cleared to forward (design §2 Option A).
  • Failing-first tests (each RED before its code, verified by ablation): adapters/relay/paid_test.goTestPaidPumpForwardsOnlyAuthorizedBytes (pay-then-forward gate; RED when the gate is removed), TestPaidPumpSurvivesReverseEOF (RED against the naive both-close-on-EOF splice), TestSplicePaidFullSessionOverTCP (a real paid session over TCP, reverse-EOF mid-stream, settlement basis exact). core/node/relaytransport_test.goTestRelayFullSessionConservedSettlement (open → pay N → settle redeems exactly N × increment, conserved; RED on a double-settle), TestRelaySettlementLogCarriesNoDurableField (the M0 residual: the settlement log line carries no durable/cross-session-stable field; RED when a field leaks), TestRelayWireGuardsFireOnLivePath (the Batch-1 M0 guards + the #644 clamp fire when driven through MsgRelayOpen; RED when the wire bypasses OpenRelaySession).
  • Residual for the reviewer. The daemon control-frame binding — carrying a "paid" marker on the relay adapter's connect/accept frames and handing the node-owned Authorizer into SplicePaid on the live TCP path — is the remaining integration seam. The pump primitive and the wire lane are proven in isolation and over TCP; wiring the two across the daemon's adapter/node boundary is the follow-on.
Fixed

PoD §7.3 transport BATCH 1 — carry S (#644 DoS clamp) + epoch-tied seen-map eviction (#645), failing-first, pure core (no wire) (core/relaypay/payword.go, core/node/relayrole.go, core/node/node.go, core/chain/chain.go, 2026-08-30). Closes gate issues #644 and #645, the two transport IOUs the PE ruling required BEFORE any wire. No wire protocol and no forwarding loop (steps 3–4 of the decomposition are untouched). See docs/thinking/2026-08-30-pod-7.3-transport-design.md §3–§5, §7.

  • #644 — carry S, clamp the walk. relaypay.NewVerifier(root, S) now carries the committed chain length S. AdvanceTo REJECTS claimedCount > S before the hash walk, bounding the worst case from the ~5M-hash spin the PE measured to at most S hashes. OpenRelaySession clamps the accepted S <= relaypay.MaxChainLength = MaxSessionBytes / RelayIncrementBytes = 262,144, derived RELAY-SIDE from the relay's own config and the protocol increment — never trusted from the fetcher. Verifier.Advance rejects a reveal once count == S (a fetcher revealing the raw tip would otherwise push count to S+1, an unfunded increment past the committed budget). count is the monotonic single-settlement accumulator: count increment <= S increment <= the fetcher's paid-in blind credit. Failing-first tests: TestAdvanceToClampsToChainLength (a per-Verifier walkSteps field proves the walk stays <= S; RED shows 5,000,000 steps unclamped), TestOpenRelaySessionClampsChainLength, TestRelaySessionPayCannotExceedChainLength.
  • #645 — epoch-tied eviction with a monotonic floor. relaySeenEph/relaySeenRoot now tag each admitted ephemeral identity / chain root with its admit-EPOCH and are swept lazily on OpenRelaySession. Eviction is epoch-TTL (retention = current + previous epoch), NOT raw FIFO — a re-admitted root would be a guard-(ii) longitudinal regression. The eviction floor is MONOTONIC: it never lowers on a reorg (epochStart is reorg-swapped), so a backward epoch move cannot un-evict. The epoch is read from the chain (Chain.EpochBlocks(), a new read-only getter; head height / EpochBlocks). Failing-first TestRelaySeenMapEvictsOnEpoch ablates all three properties to RED: unbounded growth (no sweep), too-eager eviction (previous-epoch reuse slips through), and a lowered floor on reorg.
Fixed

PoD §7.3 relay compensation — the certified PayWord mechanism, failing-first (core/relaypay/payword.go, core/credit/relay.go, core/node/relayrole.go, cmd/silt/daemon.go, 2026-08-30). A relay/gateway forwards content-blind bytes and cannot sign a completed-delivery receipt, so it is paid as-it-goes by a sender-funded PayWord hash chain (docs/design/pod.md §7.3, certified 2026-08-30): the fetcher commits a chain root once, reveals one preimage per forwarded increment, and the relay verifies each with one SHA-256 (H(x_k) = x_{k-1}) and redeems the highest into its operator BALANCE. New core/relaypay primitive (chain build + relay-side incremental verify, SHA-256 only, no new dependency). RedeemRelayCredit is the conserved-transfer sibling of RedeemDeliveryCredit: it moves balance only, drawn from the fetcher's already-paid blind credit, never mints, never touches Reputation — classified neutral in the Invariant-A reflection guard (the γ→1/N firewall; a PayWord chain is fundable with zero object bytes by design). Two M0 access-privacy guards (immutable Don't-#3) are enforced by construction in OpenRelaySession and ship failing-first: (i) a chain funded by a DURABLE-account credit is REJECTED (funding must be an ephemeral blind credit, client.WithdrawDemandTokenPrivately); (ii) a reused ephemeral identity or chain root across sessions is REJECTED (no longitudinal linkage). Consumer gate --accept-relay-payments mirrors --accept-delivery-receipts, OFF by default. The increment size B is a single named constant relaypay.RelayIncrementBytes = 4096 (4 KiB), derived analytically from the floor-box-equivalent measurement (build-immutable #8, no billable run): the binding constraint is the chain-state memory bound — 1 GiB max object / 4 KiB = 262,144 increments, chain state 8 MB (MB-scale, fetcher-side); verify overhead (order ~100 ns SHA-256 on arm64) is slack at any real relay speed. BenchmarkPayWordVerify times the real Verifier.Advance. See docs/thinking/2026-08-30-pod-7.3-relay-compensation-design.md §5.

Fixed

PoD §7.3 relay compensation — blind-review fixes (PE SHIP-WITH-FIX + Tester PROMOTED) (core/relaypay/payword.go, core/credit/relay.go, core/credit/relay_test.go, 2026-08-30). RedeemRelayCredit now takes the fetcher's paid-in budget as a REQUIRED parameter and REJECTS any chainValue > budget (the conservation cap — bakes the B3-close bound into the settlement contract so a future transport caller cannot over-redeem past what the fetcher funded; ships failing-first as TestRelayRedeemCannotExceedPaidInBudget, RED without the cap). Corrected the false safety comment on Verifier.AdvanceTo — the walk is NOT bounded (it runs claimedCount - count unbounded hashes before rejecting; the caller MUST bound it), and the 8 MB chain state is FETCHER-side, not the relay's (the relay holds one 32-B preimage). The AdvanceTo S-clamp (gate #644) and the relaySeenEph/relaySeenRoot epoch-tied eviction (gate #645) are tracked as blockers for the transport increment, not fixed here. TestRelayCreditNeverTouchesStanding now iterates past bondUnit (65,537 > 64<<10) so a sub-threshold per-call standing leak cannot hide. Verify-cost figure corrected to order ~100 ns on arm64 (measured ~110–270 ns/op on M4; the B-derivation conclusion is unchanged — constraint (a) is still slack by ~100×).

Fixed

PoD §7.3 transport BATCH 2 — bound the live relay-session table (leak fix, failing-first) (core/node/relayrole.go, core/node/relaytransport_test.go, core/node/relayrole_test.go, 2026-08-30). Closes the MEDIUM the blind PE ruling surfaced (RULING-pod-7.3-transport-batch2-2026-08-30.md): handleRelayOpen inserted a relaySessions entry on every guard-passing MsgRelayOpen, but the only removal (SettleRelaySession) is not wired on the live path, so the daemon flag --accept-relay-payments grew the table without bound from cheap fresh-identity opens. Two bounds, mirroring the #645 seen-map fix: (1) an epoch/TTL SWEEP — each session is stamped with its admit epoch and reaped by sweepRelaySeen on the SAME monotonic floor as the seen maps once it ages past the retention window; (2) a hard per-node CAP (relayMaxLiveSessions = 4096, tunable) — an open past the ceiling is refused (OK=false), bounding growth BETWEEN sweeps. TestRelayOpenFloodStaysBounded is RED under either ablation (cap removed → table exceeds 4096; sweep removed → stale sessions survive the epoch advance); TestRelaySettledSessionRemovedPromptly pins prompt removal on settlement. No Batch-1/Batch-2 guard, M0 guard, or conservation cap was weakened. TODO(Batch-3): when the daemon control-frame binding wires SplicePaid, the session sweep must tear down a reaped session's pump.

Fixed

era-4 increment 4d — height-gated activation + mint-flip to v5 (the go-live gate) (core/chain/chain.go, core/chain/era3validity.go, core/chain/statehash.go, core/node/chainrole.go, core/chain/modelcheck_era4_activation_test.go, 2026-08-29). Flips MINTING to BlockVersionWitnessable (v5) at/above the era-4 activation height, the exact mirror of era-3 step 2c one era up: an era4LockedIn/era4Height activation latch (the era-3 tally reused with the readiness bar at regVersion >= 5), an era4Active height gate, a MintVersion extension (v5 at/above H_era4), PopulateEra4Roots, and a validateEra4Version version-boundary rule enforced on EVERY disk-write path (commit + own-disk Reload). The two activation scalars are committed as V5-ONLY leaves (tagEra4LockedIn/tagEra4Height in stateRootLeavesV5), so the frozen era-3 (v4) root stays byte-identical (#632). era-4 layers on era-3 (a v5 block commits a superset of the v4 leaves): New PANICS if a genesis-declared Era4ActivationHeight is set below Era3ActivationHeight. The mainnet activation height is a consensus value the HUMAN ratifiesEra4ActivationHeight ships with NO default; the post-latch readiness tally derives H_era4 from committed history. Does NOT re-touch the v5 predicate, RegCap, or the maintenance spine (4c/4b own those). Every activation gate ships with a demonstrated RED (the six ablations in the approach doc). See docs/thinking/2026-08-29-era4-4d-activation-mintflip-approach.md.

Fixed

Hardened the era-3 freeze guard TestEra3RootByteIdenticalWithV5KeyspacesPresent (core/chain/modelcheck_stateroot_determinism_test.go, 2026-08-29). The withoutV5 baseline now zeroes the era-4 latch scalars (era4LockedIn/era4Height) too. Before this, populateCommitted set those scalars in BOTH baselines, so an era-4 scalar leaked into the v4 (era-3) marshaller under a FRESH UNREGISTERED tag appeared identically in both and CANCELLED — the guard stayed green on a real leak (the disjointness guard catches only registered-tag reuse). With the scalars zeroed, an unregistered-tag leak now diverges the two v4 roots and reddens the guard, closing the coverage gap against the era-3 byte-identical FREEZE (#632).

Fixed

era-4 increment 4c — the v5 validity predicate + RegCap + version-widen (PREDICATE-FIRST) (core/chain/chain.go, core/chain/modelcheck_era4_regcap_test.go, 2026-08-29). Widens versionSupported to <= BlockVersionWitnessable (v5) so a v5 block DECODES, atomically with the era-4 validity rules — closing the era-3 interim window (a version decode-accepted before its validity rule existed). Two enforced rules: (1) the RegCap = 256 per-block TOTAL BondReg count validity rule (fresh AND renewal, counted AFTER canonicalBondRegs) — a v5 block with more than 256 registrations is INVALID, which bounds any single TTL due-bucket's inflow to a registry-INDEPENDENT constant (the O(registry) TTL-firing witness read-set era-4 exists to remove; Research CERTIFIED the total-count rule and the value 256 for it). (2) the v5 committed-root predicate, which needs NO new code: validateEra3Roots recomputes via StateRootForVersion(b.Version), so a v5 block flows through it on EVERY disk-write path (commit path via ValidateProposal, own-disk Reload via appendStructural) the instant the ceiling widens. v5-GATED: v4 (era-3) stays byte- and behaviour-identical — RegCap does not apply to v4 and the frozen era-3 format (#632) is untouched. Does NOT mint v5 (BlockVersion / MintVersion stay v4), add an activation height (4d), or touch MaxBondRegBytesPerBlock (proposer policy) or the 4b spine. Records the re-derivation gate: RegCap is a function of all SEVEN determinants (B, k, Samples, BlockSize, BondVDFDelay, MinBond, proof scheme), re-derived at the next BlockVersion mint. Ships the RegCap regression suite (over-cap reject for all-fresh / all-renewal / mixed; at-ceiling accept; count-after-canonical-fold; v4-unaffected) and the v5 Reload wrong-root rejection — each demonstrated RED before green. See docs/thinking/2026-08-29-era4-4c-v5-predicate-regcap-approach.md.

Fixed

era-4 4c test hardening — pin the ratified RegCap value from BOTH sides (core/chain/modelcheck_era4_regcap_test.go, core/chain/modelcheck_era3_schema_test.go, 2026-08-29, test/comment-only). Replaces the self-referential fixture sizes (derived from the RegCap constant, so a wrong cap moved both sides together and stayed green) with the LITERAL ratified value: the at-ceiling accept test pins 256, and the three over-cap reject tests (all-fresh / all-renewal / mixed) pin 257. The value is now pinned against a too-LOW mistake (const RegCap = 255 reddens the at-ceiling test) AND a too-HIGH mistake (const RegCap = 257 reddens the over-cap tests), both demonstrated. Also fixes a dangling comment reference to a non-existent TestV5DecodesAndIsAccepted.

Fixed

era-4 increment 4b — the maintenance spine (v5-gated, inert on the live chain) (core/chain/chain.go, core/chain/statehash.go, core/chain/era3validity.go, core/translog/translog.go, 2026-08-29). Adds the two live-maintained derived committed maps qualified (E-2, the boundary-computation accelerator) and the due-height index dueBucket (T-3), and promotes epochStart to committed (O-1). Wires the five qualified maintenance hooks at the bonded/slashed mutation sites, the due-bucket insert/move/delete at the TTL machinery, and the boundary copy epochSet := qualified (rotate-LAST; the #535 recovery boundary freezes from the liveQualifiedSet recompute, the Q5 coupling). Commits all three under the state root as v5-ONLY leaves via an era-gated marshaller (stateRootLeavesV5 / StateRootForVersion), so a v4 block's committed root stays byte-identical to era-3 (the era-3 freeze is preserved). The due-bucket value is an RFC-6962 MTH over the CANONICAL (sorted-ascending / dedup / unpadded) id list, reusing translog.MTH. Does NOT widen versionSupported, add the v5 predicate or the RegCap rule (4c), or activate (4d) — inert on the live v4 chain. Ships two drift guards (the qualified per-site guard reddens on the displacement site specifically; the T-3 dual-source guard reddens on a missed renew old-bucket delete), the byte-identical era-3 replay guard, the rotate-LAST stale-capture ordering ablation, and the Q5 recovery-agreement guard — each demonstrated RED before green. Does NOT touch MaxBondRegBytesPerBlock (proposer policy). See docs/thinking/2026-08-29-era4-4b-maintenance-spine-approach.md.

Fixed

era-4 increment 4a — schema + classification (inert) (core/chain/chain.go, core/chain/statehash.go, 2026-08-29). The first code increment of the ratified era-4 build. Mints BlockVersionWitnessable = 5 and defines the three era-4 field tags tagDueBucket, tagQualified, tagEpochStart reserving their on-wire byte layout. INERT on the live v4 chain: the tags carry no leaves and are NOT yet in stateRootTags or classified, so no v4 block's committed state root changes (the era-3 byte-identical/coverage guards stay green). Does NOT widen versionSupported (held to 4c, predicate-first), add maintenance maps (4b), add the v5 predicate (4c), or activate (4d). See docs/thinking/2026-08-29-era4-build-decomposition-options.md, increment 4a.

Fixed

witness floor-box validation — R3 DoS bound (byte caps + shape gate) (core/statehash/witness_bound.go, 2026-08-29). The pre-verify resource gate that guards the R4 accessor. At witness ingest, before any proof is parsed or verified, three gates decide which witnesses are admissible: (1) a per-proof byte cap S_proof_max = 16 KiB, enforced on the ENCODED size BEFORE unmarshal (a byte cap, not a side-node count — the pokt library leaves NonMembershipLeafData byte-unbounded, so a count cap ships and lies); (2) a per-block byte ceiling C_block = len(read-set) · S_proof_max, derived per block from the exact read-set (not a flat constant — it scales with the block, needs no per-block transition cap, and is not a consensus change); (3) a shape gate — the witness bundle must carry a proof for EXACTLY the block's read-set (no unread key, no duplicate, no missing read key). The safety-critical wiring: EVERY rejection — over per-proof cap, over per-block ceiling, shape mismatch, malformed/unparseable — resolves to the R4 accessor's NoWitness outcome, NEVER ProvenAbsent. A rejected witness read as a proven exclusion is the one banned move of C-7 (§104); the ingest builds only NoWitness Results on rejection, and ProvenAbsent is reachable only through the R4 Resolve admit branch. Security parameters (S_proof_max = 16 KiB, the C_block derivation) certified by witness-floor-box-dos-bound-RESEARCH-CERTIFICATION-2026-08-29 and ratified by Andrew; mechanism by RULING-witness-floor-box-mechanism-2026-08-29 (R3). Ablation tests (core/statehash/witness_bound_test.go) each watched RED-then-GREEN, including the conflation guard (an over-budget/malformed/shape-violating witness for an ABSENCE-query key must resolve to NoWitness, never ProvenAbsent). Scope: the byte caps + shape gate only; D-2 on-demand delivery and the A-serve slow-loris read deadline (a TIME attack the byte ceiling does not close) are increment 3. No consensus-rule (I1–I5) or frozen era-3 format change — a validation-layer bound (precedent: MaxBondRegBytesPerBlock is proposer-policy-only).

Fixed

witness floor-box R3 — Kind/Value disagreement + a weak gate-1 test (core/statehash/witness_bound.go, core/statehash/witness_bound_test.go, 2026-08-29). Two follow-up fixes on the R3 DoS bound. (1) Safety (blind PE review): a ReadEntry{Kind: QueryPresent, Value: nil} (or empty Value) silently resolved to ProvenAbsentResolve routes a length-0 value to the non-membership branch, so a presence query with no value verified against an absence proof read as a proven absence (Kind and Value disagree, Value won; the same class as the R4 empty-value finding). IngestBlockWitnesses now rejects a QueryPresent entry with an empty Value to NoWitness before ResolveKind is authoritative; a presence query with no value is never a proven absence. New ablation TestPresenceQueryEmptyValueNeverProvenAbsent (watched RED-then-GREEN). (2) Coverage (both Testers): the per-proof-cap ablation TestOverProofCapRejectedPreParse did NOT go RED when only gate 1 (the per-proof cap) was disabled — its single-key fixture (C_block = 1·S_proof_max) was then caught by gate 2 (the per-block ceiling), so the test stayed green and its name lied. It now uses a two-key read-set (C_block = 2·S_proof_max) with a small honest second proof, so the over-cap blob stays under the block ceiling and ONLY gate 1 can catch it — a genuine gate-1 discriminator (goes RED when gate 1 alone is removed). No consensus-rule (I1–I5) or frozen era-3 format change.

Fixed

witness floor-box validation — R4-a three-valued accessor spine (core/statehash/witness.go, 2026-08-29). The safety spine of the semi-stateless floor box: an accessor that resolves a committed-set key against the committed StateRoot with a supplied SMT witness into one of THREE outcomes — ProvenPresent(value) (verified membership), ProvenAbsent (verified non-membership), or NoWitness (no proof, or a proof that failed to verify). The hard construction invariant: ProvenAbsent is constructible ONLY from a verified non-membership proof — the Result.outcome field is unexported and has exactly one ProvenAbsent construction site, guarded by the verified non-membership branch. NoWitness is the zero value, so the safe state is the default: every non-verified path (missing witness, failed verify, and — in later increments — an R3 over-budget or D-2 failed-fetch verdict) resolves to NoWitness, which the caller MUST treat as stall, never absent. This makes the one banned move of the C-7 certification (§104: "no witness supplied → accept") a type-level impossibility, not a code-review catch. Certified by C-7 and RULING-witness-floor-box-mechanism-2026-08-29 (R4). Ablation tests (core/statehash/witness_test.go) each watched RED-then-GREEN: a missing witness never yields ProvenAbsent; a wrong-root/tampered/membership-as-absence proof never yields ProvenAbsent; verified proofs classify correctly (not a vacuous all-NoWitness accessor); an empty-but-non-nil []byte{} value query against a valid absence proof resolves to ProvenAbsent, NEVER ProvenPresent (the MIRROR of the banned move: the pokt library selects membership vs non-membership on bytes.Equal(value, defaultEmptyValue) with defaultEmptyValue == nil, so Resolve keys on len(value) == 0 to match it and an empty value can never read present); and a source-scan asserts exactly one construction site EACH for ProvenAbsent and ProvenPresent, so a second (banned or mirror-banned) one REDs before it can ship. This increment builds ONLY the accessor; the R3 per-block byte ceiling and D-2 on-demand delivery are separate increments that feed its NoWitness arm. No consensus-rule (I1–I5) or frozen era-3 format change — a validation-layer accessor.

Fixed

era-3 version-boundary rule enforced on the own-disk Reload path (step 2c defense-in-depth symmetry). Closes the asymmetry a blind PE ruled (RULING-era3-step2c-activation-mint-flip-2026-08-29): 2b duplicated the era-3 ROOT check onto appendStructural (the Reload/own-disk path), but the 2c VERSION-boundary rule (ErrEra3VersionRequired — a v2 block at/above H_era3 is invalid) lived only on the commit path (ValidateProposal/ValidateCommit). The rule is extracted into a named validateEra3Version and now run on BOTH consensus-entry paths, BEFORE apply (a rejected block is never left applied — the longest-valid-prefix contract). Not exploitable today (a valid quorum-signed v2 block cannot commit at/above H_era3), but a future disk-write path (fast-sync/import) is where the gap could turn into a hole. The write-set guard TestEveryDiskWritePathRunsTheEra3RootCheck now requires every disk-write path to run BOTH the root check AND the version rule — a path enforcing only the root check REDs (a v2 block carries no roots, so the root check alone is era-gated off for it). New model-check tests (TestReloadRejectsV2AtEra3Boundary with the signature-valid-cause ablation, TestReloadV2BoundaryRuleDoesNotOverReject). No change to the activation condition, mint-flip, weight tally, epochSet freeze, ⌈A/2⌉, or value encoding. I5 preserved (validateEra3Version is a pure header check against committed state).

Fixed

era-3 committed state-root activation + mint-flip to v4 (build step 2c, the FINAL step of the certified sequence). Height-gates the era-3 (v4) committed state-root format on a frozen-epoch-weight supermajority signalling regVersion >= 4 (BlockVersionStateRoot), the #506 lock-in machinery reused one readiness level up (research cert era3-committed-state-root-format-RESEARCH-CERTIFICATION-2026-08-28 Q5/Q7). New Config.Era3ActivationHeight (the pre-latch trusted-fleet override, mirroring RegGateActivationHeight) and derived committed state era3LockedIn/ era3Height: rotateEpoch tallies era-3-aware frozen weight and locks in one-way at the same >⅔ super-quorum, enforcing from the NEXT boundary (H_era3). New Chain.era3Active(h), Chain.MintVersion(h), and Chain.PopulateEra3Roots(b). At/above H_era3 the propose path (core/node/chainrole.go) mints v4 with the committed roots populated over the block's post-apply state; below it mints v2, UNCHANGED. ValidateProposal rejects a sub-v4 block at/above H_era3 (ErrEra3VersionRequired), and the 2b root predicate rejects a wrong-rooted v4 block — so once era-3 activates, a block must be v4 AND carry valid roots. Activation is derived from committed history and keys on finalized epoch boundaries (#357 Condition A), so it is replay-identical and reorg-stable. era3LockedIn/era3Height are committedSet (under the state root), carried in adopt/cloneForDryRun and exercised by the completeness, order-independence (new era3SwingOrderings same-id two-version swing), and leave-one-out (new era3World/era3Probes) oracles. New model-check tests (modelcheck_era3_activation_test.go) assert the mint-flip, the boundary rejection, the weight-counted readiness gate, monotonicity, and reorg-stability, each with a demonstrated ablation RED. Invariants: I1/I2/I4 untouched; I3 relied on (the rule change integrates only at a finalized boundary, by weight); I5 preserved (pure functions of committed state).

Fixed

era-3 committed state-root SCHEMA + HASH (build step 2a of the certified sequence) (2026-08-29; format ratified, mint-v4 correction ratified, this step is model-check-tier only). Commits the two roots INTO the block schema so attesters sign them, and makes a v4 block decodable — WITHOUT flipping what nodes mint. New Block fields StateRoot and LogRoot (*ports.Hash, cbor tags 15/16, omitempty), both folded into the Hash unsigned body (unlike Atts/PrepareQC/CommitRound/Pruned, which are excluded) so a forged root cannot ride a valid signature. New BlockVersionStateRoot = 4; versionSupported widened to <= 4 so a v4 block DECODES and is accepted (research cert Q7 mint-v4 requirement — a v4 block must not be silently mis-validated under era-2 rules); a version beyond 4 is still refused loudly with ErrBlockVersion. THE LOAD-BEARING COMPAT DECISION: an era-2 block hashes and validates BYTE-IDENTICALLY after this change (committed history is never re-interpreted). The roots are POINTERS because omitempty does NOT omit a zero fixed-size array — a plain ports.Hash would emit 32 zero bytes for every era-2 block and change its hash; the byte-identity oracle caught exactly that. A nil pointer is omitted (era-2 unchanged); a set pointer is emitted (era-3 always carries a definite, non-zero root — the empty-log root is sha256("") and the empty-state root is the SMT over the four always-present scalar leaves, both fixed non-zero constants). New modelcheck_era3_schema_test.go: a golden-hash byte-identity oracle (a v2 block hashes to its pre-2a value, verified against origin/main), a tamper oracle (a modified StateRoot/LogRoot fails the signature check), a decode oracle (v4 accepted, v5 rejected), and a population oracle (a v4 block carries the chain's StateRoot/LogRoot) — each with its defect injected and watched go RED. STOP boundary honored: no validity predicate rejects on a root mismatch (step 2b), no mint-version flip and no activation height (step 2c) — production minting stays BlockVersionRounds, so no v4 block is minted before its predicate exists. Compat deliberation in docs/thinking/2026-08-29-era3-step2a-commit-roots-schema.md.

Fixed

era-3 committed state-root VALIDITY PREDICATE (build step 2b of the certified sequence) (2026-08-29; format ratified). Closes the 2a→2b window: 2a made a v4 block decodable and signed the roots, but NO predicate read them, so a v4 block validated on its era-2 merits alone. This step adds the v4-gated validity predicate (core/chain/era3validity.go, validateEra3Roots), hooked into ValidateProposal (the one root-check site — an honest attester runs it before signing, and ValidateCommit invokes it first, so both consensus-entry paths carry the check). For a v4 block it enforces: (1) both roots present — a nil StateRoot/LogRoot is rejected explicitly with ErrEra3RootMissing (the check 2a's omitempty schema deferred here, per the 2a ruling); (2) the committed StateRoot equals the SMT recomputed over the POST-APPLY committedSet (ErrEra3StateRootMismatch); (3) the committed LogRoot equals the POST-APPLY RFC-6962 revocation-log root (ErrEra3LogRootMismatch). Because the StateRoot leaves encode the bonded/epochSet WEIGHTS summed in the three super-quorum finality predicates, a wrong-value leaf is a consensus SAFETY attack, not a read bug — enforcing root == recompute makes the value encoding load-bearing, relying on the step-1 determinism oracle for cross-node byte-identity. The post-apply recompute runs on a throwaway clone (cloneForDryRun + the real apply), so live chain state is never mutated during validation; the clone uses the one authoritative state-transition function, so a value-encoding or apply bug surfaces identically on the proposer's and validator's side. New translog.Log.Clone deep-copies the transparency log for the dry run. ERA-GATING: the predicate fires ONLY for Version >= BlockVersionStateRoot (v4); a v2/v3 block validates under era-2 rules UNCHANGED (additive, strict-superset rejection). New modelcheck_era3_validity_test.go: accept (roots = independent post-apply recompute), wrong-StateRoot/wrong-LogRoot reject, nil-root reject, the v2-unaffected era-gate, the commit-path carry-through, and a drift guard (TestDryRunCloneCopiesEveryAppliedField, the #558 class — the clone must copy every history-derived field, distinct-backed) — each with its defect injected and watched go RED. Invariants: preserves I1–I5, alters none; it ADDS a v4-gated validity rejection and is a pure function of (block, committed state), so every honest replica computes the same verdict (I5). STOP boundary honored: no mint-version flip and no activation height (step 2c) — the predicate is inert in production until 2c because no v4 block is minted yet, but it is correct now so 2c cannot land before it. Deliberation in docs/thinking/2026-08-29-era3-step2b-validity-predicate.md.

Fixed

era-3 committed-root check on the OWN-DISK Reload path (A-bare — closes the 2b reload gap) (2026-08-29; blind-PE ruled, no research gate). 2b enforced validateEra3Roots on the commit paths but the ReloadappendStructural own-disk replay path SKIPPED it, so a v4 block with a WRONG StateRoot re-signed with the proposer key was accepted at load. 2b's Decision-1 argument — "the signature covers the root, so validateStructural's signature verify rejects a bad root" — was WRONG: integrity ≠ root-correctness; a signature over a wrong root is still a valid signature. appendStructural now calls validateEra3Roots(&b) — the SAME clone-recompute predicate the commit path uses — BEFORE c.apply(b), so a rejection never leaves a bad block applied (preserving Reload's load-bearing "keep the longest VALID prefix" contract the daemon relies on). Reuses the SAME named errors (ErrEra3RootMissing/ErrEra3StateRootMismatch/ErrEra3LogRootMismatch) so proposer, validator, and reload agree or all fail; a v2/v3 block skips it (era-gated), UNCHANGED. New regression + guard in core/chain/reload_era3_boundary_test.go: TestReloadRejectsResignedWrongStateRootV4 (RED on a0f8839: Reload ACCEPTS the re-signed wrong-root block; GREEN with ErrEra3StateRootMismatch), an ABLATION subtest proving validateStructural accepts the re-signed block so the reject is the ROOT check not the signature, the LogRoot sibling, and a STRUCTURAL write-set guard (TestEveryDiskWritePathRunsTheEra3RootCheck) that enumerates every c.apply(b) caller and fails a FUTURE unguarded disk-write path (fast-sync/import) rather than a hand-maintained list. Invariants: preserves I1–I5, alters none — it extends an existing v4-gated validity rejection to a second disk-write path. Residual (named as a reopening item): A-bare re-validates each v4 root on boot at O(state)/block ⇒ O(depth²) over a full Reload — the hold-the-tree bridge until the incremental-SMT / ports.NodeStore keystone-store workstream (#600) makes StateRoot linear on every path, at which point the SAME call site becomes O(depth) with no change. Correction + reopening item recorded in docs/thinking/2026-08-29-era3-step2b-validity-predicate.md (Decision 1); PE ruling RULING-era3-reload-root-check-2026-08-29.md.

Docs

RATIFIED freeze of the era-3 committed state-root format recorded in canon. Andrew ratified the composed re-certification and authorized the freeze: the era-3 (BlockVersion = 4) two-root committed format is now IMMUTABLE as of build 3af40bc. Recorded a FROZEN 2026-08-29 entry in docs/decisions.md (D-TIERING) pinning exactly what is frozen — the block schema (StateRoot/LogRoot in Hash, versionSupported <= 4), the 18 committedSet fields under the state SMT root with the per-field canonical value encoding, the separate RFC-6962 log root over revLog, the height-gated v4 hard-fork activation, and the "no witness → reject/stall" verifier posture on every disk-write path — plus what is NOT frozen (the C-7/#600 witness floor-box and the incremental-SMT/ports.NodeStore follow-on). Added a frozen consensus formats marker to the Immutable tier of docs/TENETS.md Part IX: changing a frozen consensus format requires a NEW ERA (a new BlockVersion), never an in-place edit. Certified by era3-committed-state-root-format-BUILT-RECERTIFICATION-2026-08-29.md.

Docs

Corrected stale "16 committedSet fields" / "four scalar leaves" comments to the actual 18 fields / 6 scalars. The two era-3 activation scalars (era3LockedIn/era3Height) are now committed under the state root, so the marshaller commits 18 committedSet fields (16 + 2) with 6 scalar leaves. Comment-accuracy only in core/chain/chain.go, core/chain/modelcheck_stateroot_determinism_test.go, and core/chain/modelcheck_order_independence_test.go — coverage is reflection-bound (TestStateRootCoversExactlyTheCommittedSetFields), so no logic changed. Flagged non-blocking by the Researcher in the re-certification.

Changed

test-only: harden the era-3 disk-write guard to match a CALL, not a symbol name. TestEveryDiskWritePathRunsTheEra3RootCheck decided coverage with strings.Contains(methodBody, "validateEra3Roots"), which matched COMMENT text: a method with // validateEra3Roots skipped plus a bare c.apply(b) scored "guarded" while running no check, re-opening the A-bare hole the guard exists to close (Tester finding). New callsFn helper strips line/block comments (stripComments) then requires the validator name followed by (, so only a real call counts; applied at both the transitive-reachability check and the main coverage loop. TestGuardMatchesCallsNotCommentText is the ablation: comment-only and block-comment mentions RED, a real validateEra3Roots( call (with or without whitespace before () stays GREEN, no-mention stays RED. No production code changed; the guarded set and genesis allowlist are unchanged.

Fixed

era-3 committed state-root computation (build step 1 of the certified sequence) (2026-08-28; format ratified, this step is model-check-tier only). Computes the two era-3 roots and proves the per-field value encoding deterministic BEFORE the root becomes a signed block field. New core/statehash package: the pokt-network/smt v1.0.0 keystone promoted from internal/smtspike into product code, with the pinned canonical value encoders (8-byte big-endian for the int64/uint64 weights and heights, raw 32 bytes for identity values, one byte for bools/regVersion) — widths and endianness are CONSENSUS PARAMETERS (research cert Q6). New core/chain/statehash.go marshals the 16 committedSet fields into field-tagged, canonically-encoded leaves and computes StateRoot; LogRoot reuses the existing RFC-6962 RevocationLogRoot. New determinism oracle (modelcheck_stateroot_determinism_test.go, research cert residual R2): same logical committedSet ⇒ byte-identical leaves ⇒ identical root, order- and node-independent, with a wrong-value ablation (perturb a value-carrying field → root changes) and two coverage guards: a tag-list guard binding the committed tags to the live keystone classification, and an EMIT guard that populates every committedSet field and asserts stateRootLeaves actually emits a leaf for each — so dropping a leaf loop of ANY class (including a Class-A loop like spent) turns the suite RED instead of silently dropping the field from the root (PE coverage-gap finding, proven by ablation). The order-independence and snapshot-equivalence oracles now assert ROOT equality, closing the gap between "the 16 fields are equal" and "the computed StateRoot is equal." STOP boundary honored: no Block field, no Hash change, no validity predicate, no BlockVersion/versionSupported change — those are later certified steps. Decision + freeze conditions recorded in docs/decisions.md; encoding deliberation in docs/thinking/2026-08-28-era3-state-root-value-encoding.md.

Changed

CONSENSUS-RULE: canonicalize same-id intra-block bond registrations in apply (2026-08-28; certified + human-ratified). This is a state-transition consensus-rule change. apply (core/chain/chain.go) resolved multiple BondRegs for the SAME validator id in one block LAST-WRITER-WINS by slice position, committing an order-dependent regVersion/bondDomain/bonded → a different history-independent SMT root for the same admissible block (a #618-class latent fork). The same-id-twice guard (seenReg) is gate-gated, so pre-#506-gate this block is ADMISSIBLE (a legal F1 renew/resize, which the #618 seenRoot distinct-id guard does not catch), and regVersion feeds the #506 lock-in tally (rotateEpoch), so gateLockedIn/ gateHeight inherited the split when the two-version validator was the >⅔ swing. Fix: fold the block's BondRegs to ONE canonical winner per id by a TOTAL ORDER on content — largest Size, then Version, then Domain, then Sig — and apply ALL of that winner's fields (canonicalBondRegs/bondRegLess, chain.go). The commit is now a pure function of block content, identical across intra-block orderings. REJECT was refuted (it breaks the legal resize); CANONICALIZE is the certified direction (the resize's larger reg wins in both orders — the right renew/resize semantics). Scope is same-id ONLY: distinct-id same-root is untouched (#618 rejects it at height>0 validity; at genesis it is the named residual R-G premise). Coverage: TestRegVersionIntraBlockOrderIndependent (the same-id covering probe, RED without the fold), gateSwingOrderings/TestGateLockInSwingIsOrderIndependent (the #506 tally-swing fixture), and regVersion/bondDomain/gateLockedIn/gateHeight moved out of orderVacuous. Negative control TestSameRootSameIDRenewAdmitted stays green. Certification: silt-reviews/research/research-outcome/sameid-twoversion-intrablock-bondreg-contention-RESEARCH-CERTIFICATION-2026-08-28.md. Deliberation: docs/thinking/2026-08-28-sameid-twoversion-canonicalize-apply.md.

Changed

CONSENSUS-RULE: reject a block carrying two bond registrations from distinct identities on the same root (2026-08-28; certified + human-ratified). This is a validity-layer consensus-rule change. validateBondRegs (chain.go) deduped only per-ValidatorID (seenReg, gate-gated) and never per-root, so a block with two PROVEN registrations from DISTINCT identities on the SAME root was ADMITTED; apply (chain.go:2780-2790) then resolved the winner by intra-block SLICE ORDER. Two honest replicas applying the identical block in a different BondReg order committed a DIFFERENT bonded/bondRootOwner state — an order-dependent commit the era-3 SMT state root cannot tolerate. The fix adds a seenRoot dedup, a sibling of seenReg, that rejects such a block with ErrSharedRootInBlock. It runs UNCONDITIONALLY (NOT behind the #506 regGateActive gate — the freeze seam must close in every regime), and dedups on (root × distinct-ID) only: a validator re-registering its OWN root (same ID: renew/resize) is legal (F1) and stays admitted. A pure validity tightening — removes an admissible block class, never admits a new commit; I1–I5 and history-independence preserved; apply's tie-break, the weight sum, the epoch freeze, and the quorum threshold are untouched. Covered by a RED-then-GREEN model-check probe (redteam_verify_sameroot-intrablock_test.go): pre-fix the block is admitted and the committed state diverges across intra-block orderings; post-fix it is rejected in both orderings; a negative control confirms same-ID renew/resize is still admitted. Closes residual R2 of the certification. Research certification: same-root-intrablock-bondreg-contention-RESEARCH-CERTIFICATION-2026-08-28 (/Users/andrewedmond/Claude/claude/silt-reviews/research/research-outcome/same-root-intrablock-bondreg-contention-RESEARCH-CERTIFICATION-2026-08-28.md); PE ruling RULING-618-bond-registration-order-independence-2026-08-28. The prior #618 TestSharedRootDeniedViaValidatedBlock was updated: the validated path now REJECTS the shared-root block rather than admitting it and deduping in apply — strictly stronger, and order-free by construction.

Fixed

e2e flake (#583, third occurrence): derive the anchor-stop resume-observation window from the non-anchor catch-up cadence (test-harness only; the daemon and every consensus path untouched). TestAnchorStopHaltsBondedNonAnchors failed on CI a third time (val3 never observed a committed block after the anchors resumed, 76 s on CI vs 46 s local). Mechanism: after the driven publish commits on the resumed anchors, a bonded non-anchor that missed the live commit round heals on its next chainSyncTick, which reschedules every ChainSyncInterval (= 30 s) at an ARBITRARY phase. The old fixed 30 s observation window equalled EXACTLY one interval, so it caught ZERO catch-up sweeps in the worst phase — the same zero-overlap-margin defect #549-Q3 fixed. Derived the window per the #549-Q3 discipline: resumeObserveSweeps × ChainSyncInterval where 2 sweeps guarantee one fires regardless of phase and the 3rd is the measured ~30 s CI/local load stretch → 90 s. The window stays a POLL, so a genuine non-anchor halt still fails fast (proven RED by injection). An init guard fails the e2e package if the window is ever lowered below the two-sweep phase floor, so a fourth silent reroll is impossible. Derivation: docs/thinking/2026-08-28-583-anchorstop-resume-window.md. Measured 16/16 green locally (+13/13 in a prior run cut short by the default go-test timeout).

Added

Keystone leave-one-out — discharge all three shadowedProbes entries as in-scope fixture work; the debt list is now EMPTY (model-check/unit tier; no consensus rule touched — the probes OBSERVE existing predicates, move no rule). A blind PE ruling (RULING-bondregheight-probe-neuter-guard-2026-08-28, Q4) overrode the prior "research-gated" routing on the bondRootOwner/bondRootProven split: the coupling was a property of how the launch-world probes were built (both asked via c.bonded[claimant] in richHistory, where dropping EITHER field admits the challenger), not of the fields. The fix uses the field asymmetry: bondRootProven feeds ONE predicate (displacement, chain.go:2845), bondRootOwner feeds TWO (displacement AND restoresHeldStanding, chain.go:3054). bondRootProven gets provenDisplaceWorld — a PROVEN owner holds a root, a PROVEN challenger claims it; dropping bondRootProven ALONE (owner held constant) flips the displacement verdict proven-owner-helddisplaced-the-proven-owner. bondRootOwner gets restoreOwnerWorld — a LAPSED frozen-epoch member re-proving its OWN root within R is EXEMPTED by restoresHeldStanding (which reads bondRootOwner, never bondRootProven); dropping bondRootOwner ALONE flips acceptErrRegGate, while dropping bondRootProven in that world does nothing (uncoupled). The two shadowed launch probes are REMOVED; the mis-tagged revoking an unknown root probe (its verdict did not depend on the carried byRoot set) is REMOVED, byRoot still covered by dup-publish. Both new probes survive their own neuter — TestLeaveOneOutProvesEachFieldLoadBearing REDs naming the exact field, and TestNeuteringAnyProbeBreaksCompleteness is GREEN with shadowedProbes EMPTY. The displacement predicate, restoresHeldStanding, and the #506 R-rule are only OBSERVED; per-world genesis config selects the regime. Deliberation: docs/thinking/2026-08-28-keystone-shadowedprobes-discharge.md.

Added

Keystone leave-one-out — fix a shared-block SHADOWING decoration probe + add a structural neuter meta-guard (model-check/unit tier; no consensus rule touched). A blind review found bondRegHeightProbe (core/chain/modelcheck_snapshot_equivalence_test.go) was DECORATION in the running oracle: it shared its within-R re-reg block with lockedInProbe in the gate-lock world (gate armed by gateLockedIn), so the leave-one-out loop — which breaks on the first flipping probe — let lockedInProbe catch the bondRegHeight ablation first and bondRegHeightProbe never ran. Neuter it and the oracle stayed green: a probe that proved nothing. Part 1: give bondRegHeight its own sole-discriminator world bondRegHeightWorld, where the #506 gate is armed by cfg.RegGateActivationHeight > 0 (chain.go:3027) instead of gateLockedIn, so gateLockedIn/gateHeight are unset and bondRegHeight is the ONLY committed-field discriminator. A within-R re-reg past the activation boundary is ErrRegGate-rejected with the field, accepted without it; neutering the probe now turns TestLeaveOneOutProvesEachFieldLoadBearing RED (changed NO verdict in any world). RegGateActivationHeight is per-world genesis config selecting the regime (the certified pre-latch trusted-fleet mode, chain.go:201); the R-rule is untouched. Part 2: TestNeuteringAnyProbeBreaksCompleteness neuters EACH probe in turn (forces its ask constant, keeps its detect tag) and asserts the completeness guard goes RED — every probe must be the SOLE catcher of at least one field. buildLeaveOneOutWorlds/leaveOneOutFlipped are extracted so the guard and the oracle share one code path; neutering is non-destructive (fresh probe copies per iteration). Running the guard honestly surfaced three PRE-EXISTING shadowed probes (byRoot double-coverage; the bondRootOwner/bondRootProven displacement coupling, chain.go:2839) — quarantined in a declared, shrink-only shadowedProbes debt list with routing notes (the bond-root split is research-gated), not suppressed. Deliberation: docs/thinking/2026-08-28-keystone-bondregheight-sole-discriminator.md.

Added

Keystone weight-bytes — the bonded sibling probe (#603 era-3 freeze gate) (model-check/unit tier; no consensus rule touched — the probe READS the weight predicate, moves no threshold). TestBondedWeightBytesAreLoadBearing (core/chain/modelcheck_snapshot_equivalence_test.go) proves the committed per-member live bonded WEIGHT bytes — not merely membership — flip a de-maturation verdict through requireDeMatureSuperQuorum (chain.go:2591), the sibling of the epochSet probe (TestEpochWeightBytesAreLoadBearing, #606) named owed by the blind PE ruling RULING-603-weight-bytes-discharge-2026-08-28. bondedWeightBytesWorld latches everMature, then realizes UNEQUAL live weights plus one SHARED declared domain so !matureNow holds INDEPENDENT of the weight flatten (via NakamotoDomains=1, so the path-entry gate cannot masquerade as the weight rule). Membership held byte-identical, weights flattened to a constant: full (true weights, coalition 10 MiB of 12, need ≥8) → accept; ablated (flattened) → ErrDeMatureQuorum (2 MiB of 4, need ≥2), seen=1 clearing the Quorum=1 count floor. Non-vacuous by both ablations: no-flatten → accepts (ErrDeMatureQuorum assertion RED); empty membership → rejects via ErrNoQuorum (the assertion correctly refuses the membership-omission). Deliberation: docs/thinking/2026-08-28-keystone-bonded-weight-bytes-probe.md.

Added

Keystone leave-one-out — the latch/gate/domain tranche proven load-bearing (model-check/unit tier; no consensus rule touched — probes a threshold, moves none). TestLeaveOneOutProvesEachFieldLoadBearing (core/chain/modelcheck_snapshot_equivalence_test.go) now covers six more committed fields, each moved out of probeUncovered with a real verdict-flipping probe on the world where the field is load-bearing:

  • everMature — the de-mature bar (requireQuorumStack, chain.go:2471). deMatureWorld latches maturity while decentralized, then live weight concentrates (whale) so !matureNow(); a sub-⅔ real-bond coalition that clears the count floor must be refused. Full → ErrDeMatureQuorum (5 MiB of 105, need ≥70); everMature-dropped → accept (bar skipped).
  • matureEpoch — the frozen-weight quorum (chain.go:2457). matureEpochWorld freezes unequal weights (silent whales) and narrows live bonded to the two coalition members; a below-⅔ commit that clears the count floor (bftThreshold(2)=1) must be refused. Full → ErrNoQuorumWeight (2 MiB of 23, need >15); matureEpoch-dropped → accept (weight rule skipped, qualification leaves the frozen branch).
  • gateLockedIn — the #506 R-rule (regGateActive, chain.go:3030). gateWorld locks the gate at a boundary; a within-R re-reg PAST H_act must be refused. Full → ErrRegGate; gateLockedIn-dropped → accept (gate never armed).
  • gateHeight — same predicate, OPPOSITE direction: a within-R re-reg BELOW H_act must be accepted. Full → accept; gateHeight-dropped (H_act collapses to 0) → ErrRegGate (gate active early).
  • regVersion — read at exactly ONE verdict-relevant site, the rotateEpoch #506 lock-in tally (chain.go:3007), so a mutating probe APPLIES the boundary block, trips the maturity latch, and runs the tally. Full (⅔-ready) locks the gate → a within-R reg is ErrRegGate; regVersion-dropped tallies zero ready weight → gate never locks → accept.
  • bondDomainoverturns the prior "metric, not a validity predicate" excuse. bondDomain feeds matureNow() via the A-axis Nakamoto coefficient (C2MetricMatureCoefficient), and matureNow() gates the maturity latch (chain.go:2893) and thereby the launch-anchor shed. domainWorld merges all bonds into ONE declared domain so the network stays immature and an anchor-only commit is accepted; a mutating probe applies a block. bondDomain-dropped counts the bonds as independent → the coefficient rises → the latch trips → the anchors shed → the same commit is REJECTED (0 qualified via the anchor-shed). Full → accept; dropped → reject. Each probe was ablation-proven: its defect was injected and watched go RED with the named error, and the oracle's "changed NO verdict in any world" guard was confirmed to FIRE end-to-end when a probe's discriminator is neutralized (a green with no demonstrated red is decoration). STOP boundaries held: every flip changes which identities are ADMITTED (qualification) or whether a reg is a valid PAYLOAD — never the weight-sum seam (chain.go:2450-2456), the epochSet freeze / rotateEpoch (I3), the ⌈A/2⌉ threshold, or #603's weight discriminator. Deliberation: docs/thinking/2026-08-28-keystone-leaveoneout-latch-gate-domain.md.
Added

Keystone leave-one-out — the last two committed fields closed; probeUncovered is now EMPTY (model-check/unit tier; no consensus rule touched — probes a threshold, moves none). TestLeaveOneOutProvesEachFieldLoadBearing (core/chain/modelcheck_snapshot_equivalence_test.go) now covers every committed field:

  • bondRegHeight — the #506 min-interval R-rule (chain.go:1497), which reads c.bondRegHeight[id] and fires only when present. gateWorld (the #623 gate-active world) carries bondRegHeight[x]=1; a within-R re-reg PAST H_act must be refused. Full → ErrRegGate ("re-registered 4 blocks after its last reg, R=10"); bondRegHeight- dropped → the rule never fires → accept (a reg-flood identity admitted). The prior "no gate-active world" reason was STALE. (A third probe on gateProbes' past block.)
  • validatorsSeenoverturns the prior "legacy mode only" reason, which was WRONG. C2Metric enumerates validatorsSeen in the OBJECTIVE regime (chain.go:1978) → MatureCoefficientmatureNow() (objective branch, chain.go:1867) → the maturity latch (chain.go:2893) → the launch-anchor shed — the same verdict path bondDomain rides. validatorsSeenWorld holds four anchors plus six equal real bonds each in a DISTINCT declared domain, all seen; a mutating probe applies a block. Full → validatorsSeen enumerates six participants → coefficient 3 ≥ MatureValidators 2 → matures → sheds the anchors → an anchor-only commit is ErrNoQuorum ("0 qualified, need 4"). validatorsSeen-dropped → the C2Metric loop sees zero participants → coefficient 0 → immature → anchors held → accept. Both ablation-proven: the defect was injected and watched go RED with the named error (ErrRegGate / ErrNoQuorum), flip reject→accept in each case. STOP boundaries held — bondRegHeight flips a VALIDITY verdict, validatorsSeen a QUALIFICATION verdict; neither touches the weight-sum seam (chain.go:2450-2456), the epochSet freeze / rotateEpoch (I3), the ⌈A/2⌉ threshold, or #603. probeUncovered is now EMPTY: every committed field has a leave-one-out probe with a demonstrated ablation RED. Deliberation: docs/thinking/2026-08-28-keystone-leaveoneout-bondregheight-validatorsseen.md.
Added

Named the genesis same-root premise (residual R-G) — no genesis validity change. PR #618's seenRoot per-root distinct-ID dedup lives in validateBondRegs, which AppendGenesis (chain.go) does NOT run — it goes straight to apply. Genesis apply IS order-dependent for two distinct-ID unproven same-root regs (confirmed by execution: slice [A,B]→owner=A, [B,A]→owner=B). It is safe TODAY only by an EXTERNAL invariant, not by the guard: the production genesis is a byte-identical shared constant carrying NO BondRegs (genesis.Build, core/genesis/genesis.go), so there is no per-node slice order to diverge on. The era-3 SMT freeze's unconditional order-independence claim silently leans on this premise. This change NAMES it as a pinned, executable fact so it cannot break silently: a named anchor at AppendGenesis plus two guard tests — TestGenesisSameRootApplyIsOrderDependent (core/chain, pins the un-guarded order-dependence; flips RED if genesis is made order-independent) and TestProductionGenesisCarriesNoBondRegs (core/genesis, pins byte-identity + zero BondRegs; flips RED if the production genesis ever carries BondRegs or goes per-node). Both ablations verified. No change to genesis validity rules — this is the record-it half of the PE's "close it OR record it," inside the already- certified envelope. Making genesis order-independent by rejection would be a consensus-rule change to genesis validity (research-gated); see docs/thinking/2026-08-28-genesis-sameroot-residual.md option (b). PE ruling RULING-618-updated-sameroot-dedup-fix-2026-08-28 (/Users/andrewedmond/Claude/claude/silt-reviews/principle-engineer/RULING-618-updated-sameroot-dedup-fix-2026-08-28.md), residual R-G.

Added

Order-independence coverage for the mature-epoch family — the orderVacuous debt, next increment. The order-independence model-check oracle (modelcheck_order_independence_test.go) declared the mature-epoch family (everMature, matureEpoch, epochSet) as orderVacuous: the launch-anchor twoOrderings world never matures (MatureValidators=99), so those fields were compared over ∅ and their order-independence was unproven. A new matureOrderings fixture brings an ANCHORLESS objective world (epochs on, MatureValidators=2) to maturity over two OPPOSITE-order histories: a bonded non-quorum victim is slashed at height 1 in one ordering and height 3 in the other, so the (bonded, slashed) maps are built by two genuinely different histories (per the #618 lesson that a commutative fixture is a decoration). Both freeze the SAME four-member epochSet at the height-4 boundary (liveQualifiedSet excludes the slashed victim), and both latch everMature / set matureEpoch. All three fields are non-empty and byte-identical across the two slash orderings. epochSet is order-INVARIANT BY CONSTRUCTION: rotateEpoch runs LAST in apply on the final post-block state, so the freeze is a deterministic read of the bonded/slashed maps whose own order-independence #617/#618 cover. This fixture therefore CONFIRMS invariance; it does not discover-or-refute a fork the way #618 did, because no admissible slash ordering in this world lets the intermediate bonded=5 state reach the freeze. Un-stressed residual, on the record: the latch/handoff HEIGHT dimensions are NOT varied — all validators bond at genesis, so the latch trips at the same height in both orderings; acceptable because everMature/matureEpoch are one-way final-state bools that cannot flip, but they are the honest residual (the consensus- correctness trip-wire did not trip; no rule touched). TestCommittedSetFieldsAreOrderIndependent now pairs each committed field with its populating world (the union-of-worlds pattern the snapshot oracle already uses); a dedicated TestMatureEpochFamilyIsOrderIndependent asserts the latch/handoff/freeze ACTUALLY FIRED (coverage is not vacuous) and the two orderings reached identical state. Three ablations verified RED-then-GREEN (drop a governor from one frozen epochSet; flip matureEpoch/everMature in one chain — each names the field). Three fields removed from orderVacuous. Two-list union: epochSet already had a leave-one-out snapshot probe (#604), so it now clears BOTH oracle lists (freeze-ready); everMature/matureEpoch clear the order-independence list only and remain probeUncovered-owed. The #506-gate family (gateLockedIn, gateHeight) is left for the next increment. Deliberation: docs/thinking/2026-08-28-orderVacuous-mature-epoch.md. Test/fixture only; no consensus rule changed.

Added

Order-independence coverage for the bond-registration family — the #617 debt, first increment. PR #617 declared six committed bond-registration fields (bonded, bondRootOwner, bondRootProven, bondRegHeight, regVersion, bondDomain) as orderVacuous: the order-independence model-check oracle compared them over ∅ in every ordering, so their order-independence was unproven. The twoOrderings fixture now commits a height-5 bond block whose BondReg slice order flips between the two orderings — including a G3 proof-beats-declaration displacement of a genesis squatter (chain.go:2780-2794), the one bond rule whose intra-block order could genuinely matter. All six fields are non-empty in both orderings and byte-identical across them for this DISJOINT-ROOT construction: G3/bond-root ownership is order-independent for admissible blocks (the consensus-correctness trip-wire did not trip). A dedicated TestBondRegG3DisplacementIsOrderIndependent asserts the displacement actually FIRED (coverage is not vacuous) and both orderings reached identical bond-root state. Scope correction (certified 2026-08-28, see the CONSENSUS-RULE entry under Changed): this disjoint-root fixture does NOT cover two distinct-ID proven claims on the SAME root in one block — that case IS order-dependent in apply and is now rejected at the validity layer. The order-independence claim holds for every ADMISSIBLE block precisely because that collision can no longer be admitted. Six fields removed from orderVacuous. For the two-list union rule, a covering leave-one-out probe was added for bondRootProven (a proven owner must not be displaced by a later proven claim; a snapshot that lost the field wrongly allows it) and it was removed from probeUncovered. Fields now clearing BOTH oracle lists: bonded, bondRootOwner, bondRootProven. Fields clearing the order-independence list only (their snapshot-equivalence coverage is #506-gated or metric-only, tracked in probeUncovered): bondRegHeight, regVersion, bondDomain. The mature-epoch and #506-gate orderVacuous families are left for later increments. Test/fixture only; no consensus rule changed.

Added

Fixed a state-aliasing hazard in the snapshot-equivalence oracle's snapshotBoot. snapshotBoot carried committed maps into a replica by REFERENCE, so a mutating leave-one-out probe (one that calls apply) wrote through the shared map header into src and every sibling replica. The leave-one-out loop ablates one field at a time off the same src, so a mutating probe on the k-th ablation silently poisoned the (k+1)-th — this masked bondRootProven's verdict flip entirely (the bondRootOwner F1 probe displaced src's shared bonded/owner maps before bondRootProven was ever ablated, so its ablation saw already-corrupted state and changed no verdict). snapshotBoot now deep-copies carried maps/slices so each replica owns its state. Test-only.

Added

Order-independence + leave-one-out coverage for the spent and slashed SMT leaves, and a permanent vacuous-∅ guard. The keystone order-independence oracle reported "16/16 committedSet fields identical" while its fixture left spent and slashed empty, so two of the sixteen comparisons were DeepEqual(∅, ∅) — vacuous (PE ruling silt-reviews/.../RULING-keystone-spent-slashed-classification-2026-08-28.md). twoOrderings now commits two blind-signed publish-token spends and two committed equivocation slashes across two opposite orderings, so both fields are NON-EMPTY and byte-identical across order. A new fixture-side guard fails the order-independence test if any committedSet field it compares is empty in both orderings and not declared in orderVacuous (a shrinking debt with reasons), so "all N identical" can never again read as coverage over an empty map. The snapshot-equivalence / leave-one-out oracle gains spent and slashed probes (each flips a real verdict on omission), and both leave probeUncovered. A new TestBondedOrderFreeUnderSlashInteraction traces the PE's flagged residual — apply's delete(c.bonded, culprit) paired with slashed[culprit]=true — and proves bonded byte-identical across two opposite slash orderings. Test-only; no consensus rule moved. Each new probe was ablated (injected order-dependence → RED, reverted → green).

Docs

#600 ratified — the floor box is a semi-stateless witness-validating full validator. Andrew ratified the direction: witness-validation is the floor box's primary validation posture; holding the whole registry tree is a bigger-box opt-in behind ports.NodeStore, never the 2 GB-floor default. Recorded in docs/decisions.md (D-TIERING, dated entry) with five consequences: (1) same security, narrower self-sufficiency; (2) a HARD REQUIREMENT that witness-serving stay open + multi-provider (TENETS.md:557 — a permissioned availability choke is the banned load-bearing-centralized dependency); (3) the ≥1-honest-provider liveness assumption promoted optional → load-bearing (safety unaffected — a witness-less floor box stalls, never accepts), a new #183-sibling seam; (4) bbolt NOT reopened (pebble ties its heap); (5) an HONEST evidence basis — the billable coexistence run captured zero rssMB rows (killed by -timeout 60m mid-build) and showed severe memory pressure, so the decision rests on C-7 soundness + no-owed-measurement + hold-tree-unproven-to-fit + the pressure signal, NOT a conclusive OOM. docs/VISION.md marks the witness floor RATIFIED and adds the decentralized-liveness posture; ROADMAP.md re-sequences the era-3 format freeze to critical-path (witness is vacuous until the Block commits both roots). PE ruling /Users/andrewedmond/Claude/claude/silt-reviews/principle-engineer/RULING-600-floor-box-direction-2026-08-28.md; research note /Users/andrewedmond/Claude/claude/silt-reviews/research/research-outcome/600-floor-box-direction-post-coexistence-RESEARCH-NOTE-2026-08-28.md.

Security

R0.7 interim — the paid relay lane pays 0 until the R2.14 prepayment anchor; RT-RELAY-3 walk budget enforced; relay-lane doc truth. The break (RT-RELAY-1, behind --accept-relay-payments, default OFF): SettleRelaySession settled on the RELAY's own ledger and RedeemRelayCredit debited the fetcher's fresh ephemeral, which on that ledger is a phantom auto-granted the faucet amount on first touch, so the relay's balance rose by chainValue with nothing binding the chain to a payment — a per-session mint the file's own comment called "never a mint". The anchor the 2026-08-27 certification's conservation verdict depends on (Q4(a)) was never built. The interim, a certified NARROWING (under-pay only, no economic cert needed): RedeemRelayCredit returns 0 and performs NO ledger mutation (a debit against a phantom grant is the same fiction); the relay session settled line carries reason=no-anchor (S5, registered in cmd/silt/observable_contract.go with relay session settled itself); the flag help and docs/design/pod.md §7.3 state that the lane pays 0 and why; the five false claims in core/credit/relay.go are rewritten to the certified facts. RT-RELAY-3 (a bogus claimedCount = S preimage replayable forever on one session, ~53 ms relay CPU per ~48-byte pay): Verifier.walkSteps is promoted from instrumentation to the enforced per-session walk budget S — a claim whose walk would exceed it is refused with ErrWalkBudgetExhausted BEFORE walking; the #644 per-call clamp is kept. Gates (RED-first, Tester): TestRelayRedeemPaysZeroUntilAnchor, TestRelayRedeemPaysZeroEvenWhenFetcherIsFunded, TestSettleRelaySessionPaysZeroUntilAnchor, TestSettleRelaySessionLogCarriesNoAnchorReason, TestRelayPayAdvanceToCumulativeWalkBudgetEnforced; re-specified to pay 0: TestRelayCreditIsConserved, TestRelayRedeemDrawsFromFetcherPaidCredit, TestRelayRedeemCannotExceedPaidInBudget, TestRelayWashLoopIsAWashNeverAGain, TestRelayFullSessionConservedSettlement, TestNoDoubleSettleReaperAndPump, e2e TestPaidRelaySessionEndToEnd (balance unchanged). Cert: silt-reviews/research/research-outcome/RELAY-LANE-per-node-ledger-mint-FIX-DIRECTION-RESEARCH-CERTIFICATION-2026-09-03.md §2, §8, §9 step 1; break report silt-reviews/red-team/RED-TEAM-relay-lane-session-grant-and-byte-price-2026-09-03.md; deliberation docs/thinking/2026-09-03-r0.7-relay-interim-design.md. R2.14 is the fix.

Added

The O(depth) CI gate — a standing pass/fail check for the memory-accumulation subset of the depth-war class. The lineage #528/#535/#549/#555/#556/ #558/#560/#561/#562/#563/#572 is one class: per-height cost that grows with chain depth. A unit test at a fixed height is always green for such a bug (canonical #555: AllEntries built an O(n) slice per block — green in every constant-height test, catastrophic at real depth). sim/TestPerHeightCostLinear turns the standing memory-growth diagnostic into an assertion: it drives the mature-epoch consensus network up a height ladder and fails if baseline-subtracted HeapObjects grows super-linearly. Scope: this gate is MEMORY-only (baseline-subtracted HeapObjects). It catches the allocation-shaped depth blow-ups (#555 AllEntries), the OOM-producing subset that crash-looped the field cohort. It does NOT catch a CPU-time O(depth) scan that allocates little (e.g. #528's per-height CPU burn); that dimension needs its own noise study and is tracked as a follow-on. The gate that protects main on each PR is the SHORT ladder — every PR and push runs go test -short (ci.yml:44) and go test -race -short (ci.yml:65), which drive the 250→500→1000 ladder, still spanning two doublings. The wide 500→1000→2000 ladder runs on release.yml only. The bound is a two-stage doubling test — growth(2H)/growth(H) < 2.6 (measured linear baseline 1.998; a super-linear O(n²) regression ≈ 4.0; 2.6 sits 30% above baseline and 35% below the defect signal). HeapObjects is deterministic across runs (seeded sim + forced GC), so the gate does not flake on legitimate linear growth or GC noise; HeapInuse is logged but not asserted on (its arena-granular steps are too noisy). Proven failing-first: a synthetic O(n)-per-block accumulator (SILT_ODEPTH_INJECT=1) drives both doublings red (ratios 3.03/3.36). Runs in the default go test job, ~6s to h=2000, no new CI job. Shares the drive-and-measure helper with the OOM diagnostic so the measurement has one source of truth. Derivation and false-positive analysis: docs/thinking/2026-08-27-o-depth-ci-gate.md.

Added

The coexistence balloon — the instrument that makes the floor-box RSS measurement mean something. The node-store profile (docs/thinking/2026-08-27-disk-backed-mapstore-options.md) recorded bbolt at 1M keys as heap 305 MB / RSS 1328 MB, but on an otherwise-idle box — so the 1328 MB over-counts the coexistence risk by exactly its kernel-evictable page cache, and the #600 question (does the cache shed toward the ~305 MB unevictable floor under a ~1 GB daemon's pressure, or does the box OOM?) went untested. This adds an env-gated (SILT_COEXIST_BALLOON_MB) memory balloon to internal/smtspike/TestStoreProfile: it allocates that many MiB of anonymous RAM, writes every page to fault it fully resident, and pins a live reference for the whole scale loop, so on a no-swap box the balloon genuinely competes for physical RAM against bbolt's page cache. Every rssMB row is then measured under that pressure and labelled rssMB(UNDER-PRESSURE). Unset/0 preserves the prior behavior byte-for-byte (rssMB(no-pressure)). TestBalloonResident proves the balloon pins real RSS: cross-platform via touched-page count + checksum, and on the Linux floor box via the residentMB jump (a malloc'd-but-untouched buffer creates no pressure and is caught RED). Pure test/measurement harness — no consensus, economic, or security code. See docs/thinking/2026-08-27-coexistence-balloon.md.

Added

λ_H arrival-rate instrumentation — the one measurement the CT-1 conditional theorem is owed. The C-1 lift to CERTIFIED-CONDITIONAL (silt-reviews/.../C1-maturity-before-capture-CONDITIONAL-THEOREM-LIFT-2026-08-27.md) proves maturity precedes capture under an honest-arrival floor λ_H > 0 (measured), an adversary budget W_A (declared), and P2 (M_req > W_A/(2·w_min)). §6 names exactly one input silt did not hold in code: the live honest-arrival rate at launch, plus a floor-exit alarm. This ships both. λ_H is defined as the operator/domain-distinct bonded-arrival rate per block-height — the arrival COUNT A(t) is the SAME shed metric min(NakamotoOperators, NakamotoDomains), exposed as the pure getter chain.MatureCoefficient so the floor and the shed cannot count different quantities (the theorem binds T_mature ≤ M_req/λ_H). The daemon's commit observer trails A(t) over a configurable window (-lambda-h-window, default 20 heights) and narrates λ_H = ΔA/Δheight to the log beside the C2 concentration line. -lambda-h-floor (distinct arrivals/height; default 0 = disabled, so sims and existing deployments are unaffected) sets the certified floor: when the measured rate falls below it while the network is still young (pre-maturity latch — after the one-way EverMature latch the floor is moot, P4), a LOUD λ_H FLOOR-EXIT marker surfaces that the launch has left CT-1's hypothesis H and maturity-precedes-capture is no longer proven. OBSERVABILITY ONLY: it reads the committed C2 metric and narrates — it changes no validity predicate, no consensus rule (I1–I5), no security parameter. It parameterizes the certification, not the code (cert §6). Design: docs/thinking/2026-08-27-lambda-h-arrival-rate-instrumentation.md.

Added

Keystone weight-discriminator probe — the committed per-member WEIGHT bytes of epochSet proven load-bearing (closes issue #603, the era-3 format-freeze gate). The membership probes prove epochSet MEMBERSHIP is load-bearing but would still pass if the field stored membership with all weights set to a constant, because omission empties frozen membership and rejects via the COUNT floor (ErrNoQuorum) — the ⅔-weight predicate never fires. TestEpochWeight BytesAreLoadBearing closes that gap. It builds a mature-epoch world with UNEQUAL frozen weights and a block whose support coalition (proposer + one attester, concentrated real weight) clears the count floor (Quorum: 1, seen=1) but whose verdict is carried by requireEpochWeightQuorum. Full case (true weights): the coalition holds 10 of 12 MiB → 3·10 > 2·12 → ACCEPT. Ablated case: membership held fixed, weights FLATTENED to a constant → support/total collapses to 2/4 = ½ < ⅔ for any constant → REJECT with ErrNoQuorumWeight, the weight predicate as the discriminator — not ErrNoQuorum. The rules are used as written (chain.go:2443-2464); no summation, freeze timing, or boundary was moved. Ablation-proven (the session scar): injecting no-blinding makes the ablated case ACCEPT (RED), and injecting the membership ablation (empty epochSet) flips via ErrNoQuorum: 0 qualified with seen=0 (RED) — so the probe rejects a membership-flip masquerading as a weight-flip. This is the load-bearing weight claim the era-3 committed-root format may now freeze on. Certified by C-7 (../silt-reviews/research/research-outcome/C7-witness-based-floor-box-validation-RESEARCH-CERTIFICATION-2026-08-27.md, the witness path needs per-field load-bearing state) and by the blind PE ruling's fix 2 (../silt-reviews/principle-engineer/RULING-keystone-probes-bonded-epochset-2026-08-27.md). Deliberation: docs/thinking/2026-08-27-keystone-weight-discriminator-probe.md.

Added

Keystone leave-one-out — bonded and epochSet MEMBERSHIP proven load-bearing (PE-gated on the era-3 format freeze). The snapshot-boot-equivalence oracle's sharp half now probes two more committed fields out of probeUncovered and into probes (coverage 3/16 → 5/16): omitting bonded from the snapshot rejects a commit its bonded quorum should accept, and omitting epochSet rejects a mature-epoch commit its frozen membership should accept. Both flips run the real qualification+quorum predicate (collectQuorumSigsrequireQuorumStack), using the rules as written — no rule was tuned. The flip in each case is carried by membership (qualification), NOT the ⅔-weight predicate: omitting bonded disqualifies the attesters in the objective regime, and omitting epochSet empties the frozen set so its members fail membership. In both cases the verified RED is ErrNoQuorum (the count floor); requireEpochWeightQuorum never fires (with epochSet empty its total <= 0 branch short-circuits). Because bonded gates a verdict only where qualification reads the live bonded map (a non-epoch objective regime) and epochSet only governs a mature epoch, the two are load-bearing in mutually exclusive regimes, so the leave-one-out harness now ablates each field on the world where it flips. Ablation-proven: the leave-one-out goes RED ("changed NO verdict") when the probe is made field-blind, and each rejection is the frozen-set/bonded qualification error, not an unrelated panic. Owed (issue #603, era-3 format-freeze gate): these probes prove MEMBERSHIP is load-bearing, not the committed per-member WEIGHT bytes — a leave-one-out that flips via requireEpochWeightQuorum specifically (a coalition clearing the count floor but below ⅔ of frozen weight → ErrNoQuorumWeight) is still owed before the era-3 format freezes. Do not freeze era-3 on the weight claim until #603 lands. Blind-PE-reviewed (../silt-reviews/principle-engineer/RULING-keystone-probes-bonded-epochset-2026-08-27.md), Tester-confirmed injected RED. Deliberation: docs/thinking/2026-08-27-keystone-probes-bonded-epochset.md.

Added

The disk-backed node-store spike — a batching bbolt MapStore, proven correct locally before any billable run (PE-ordered). PR #596 disqualified the in-memory SMT backend by kernel OOM, so the keystone needs a disk-backed store, and the certification's owed boot-rebuild measurement cannot re-run until one exists. internal/smtspike/ now carries boltStore — a batching kvstore.MapStore over bbolt, test-only, importable by nothing. The load-bearing design point is write batching: the SMT calls Set once per dirty node during Commit, so a naive one-transaction-per-Set adapter would fsync per node and make the measurement meaningless; instead Set buffers and Flush commits a whole block in one transaction. Correctness is proven before cost (the local-proof-before-billable rule): the disk-backed trie produces byte-identical roots to the in-memory reference across every block, survives close+reopen while still serving membership proofs, and handles the delete/tombstone path. The measurement harness (SILT_STORE_PROFILE=1) reports RSS, not just Go heap — bbolt is mmap'd, so its residency lives in the page cache outside the Go heap, which is exactly the OOM-relevant number the earlier heap-only draft would have missed. New evidence surfaced for the backend decision: the LSM candidate (pebble) pulls in 127 modules vs bbolt's ~1, so the recommended sequence is bbolt-alone on the floor box first, adding the LSM only if bbolt's write cost proves binding — evidence-driven rather than prior-driven. The floor-box run itself is billable and awaits explicit authorization. The floor-box run is now DONE (both backends, 1M keys, fair SSD box, no OOM): the unevictable Go heap ties at ~305 MB, so both fit the box; pebble is smaller on disk (234 vs 418 MB) and lower RSS, but that RSS gap is mostly kernel-evictable page cache, not the must-hold floor. Builder recommendation is bbolt — pebble's 127-module supply-chain surface outweighs its evictable-memory edge once heap and cache are separated — and Q6 resolves to persist (reopen is 7 ms vs an 18-min rebuild). The one owed follow-up is a memory-pressure coexistence test against a ~1 GB daemon. Reasoning: docs/thinking/2026-08-27-disk-backed-mapstore-options.md.

Added

The hexagonal guard now walks TRANSITIVE imports — and it found the gap was already live (PE-ruled). internal/depcheck inspected direct imports only, which was honest while core/ imported nothing third-party. The keystone node-store decision makes that untrue, so the PE ruled the gap closed in the same change that opens it: a third-party package reaching the filesystem, clock, network or ambient randomness could otherwise enter core without tripping the guard, because the forbidden import sits one hop away — green check, vanished property. The new guard walks the module-aware closure (go list -deps) and checks third-party purity, deliberately skipping stdlib (fmt imports os by design; flagging stdlib would make it unrunnable). It fired immediately on four pre-existing effects, so this was never hypothetical. Each is now a reviewed, falsifiable claim rather than a blanket pass — most usefully: cbor's math/rand is verified unreachable in silt's configuration (its only use is SortFastShuffle in encodeStruct, and silt encodes with CanonicalEncOptions), so the entry doubles as a tripwire — switching encode modes would make block encoding nondeterministic and break consensus. The cpuid/os entry records the assumption it rests on out loud: the SIMD and generic Reed-Solomon paths must produce identical bytes, or erasure output would vary by host. Proven by ablation: an unlisted third-party effect fails the build. This is a ratchet, not a proof — it establishes that the next effect cannot arrive silently, which is the property that was missing.

Added

Both certifications landed — canon amended, and the order-varying oracle that enforces the refinement. Research answered both open consults. #597 (revLog): the conflict was a category error, not a contradiction — the SMT choice stands, and revLog gets its own append-only root rather than becoming an SMT leaf. Canon's "one root over all committed state" is refined to one history-independent SMT over set-valued validity state PLUS a separate RFC-6962 root for any committed ordered log (the Ethereum shape: stateRoot + receiptsRoot + txRoot); the snapshot carries the full revLog entry list, preserving H9 proofs for snapshot-booted nodes at the cost of the smallest forever term. epochStart is reclassified as an observable — reorg-swapped but under no committed root. Relay (knob 2): amended from "dispute-only quorum-TTP" to no TTP at all — the relay leg is self-enforcing, so there is no adjudicable dispute; a quorum-TTP could not remedy the one-increment stiff anyway, because forwarding is unprovable by any mechanism. PayWord chains, ~1–64 KiB increments pinned by a floor-box measurement, relay credit = operator balance (no new keystone field), one Invariant-A firewall regime. The feared privacy vector (a public dispute naming a fetcher key) dissolves with the dispute itself. The classification now carries a three-way taxonomy (committedSet / committedLog / observable), and core/chain/modelcheck_order_independence_test.go enforces the certification's Q4 mandate that the oracle vary append order: classification alone cannot catch a purely order-derived value. Two histories reaching the same final state agree on all 16 set-valued fields and produce different log roots — the certified resolution, asserted. Proven by ablation: reclassifying revLog as set-valued makes the oracle name it, i.e. it reproduces #597 mechanically.

Added

The era-boundary Reload oracle — the keystone's RED home #3, shipped ahead of era-3. The certification requires this test to land before the change it governs, and forbids the shape of the mistake: extend the shared era-aware verification path (verifyAtt) — never fork a parallel era-3 path, and a failed replay must be a loud rebuild, never a genesis fallback. Era-3 blocks are not minted yet, so core/chain/reload_era3_boundary_test.go pins the two properties era-3 will need, in a form that extends by one block when it arrives: (1) a history spanning an era boundary replays through the single verifyAtt dispatcher — a forked path works fine on a single-era history and breaks exactly at a boundary, which is what every real chain is at an activation height; (2) a block from a future era — precisely what era-3 activation creates for every un-upgraded node — is rejected loudly and reports the honest count of what it restored, so no caller can mistake a truncated replay for a complete one. That second property is #558 carried forward: the damage there was never the rejection, it was the silent fallback that discarded finalized history while reporting health. Both are proven RED by ablation (drop the PhaseLegacy branch → (1) fails; make the default branch accept unknown eras → (2) fails), and a positive control asserts the rejection names a signature/attestation failure so it cannot pass because the forged block was malformed for an unrelated reason.

Added

The incremental-cost oracle — the keystone's RED home #2. The certification's Q4 gate: count actual hash computes per block; RED = O(state), GREEN = O(changed·log n) with an explicit budget — guarding the #555 scar (Hash re-marshaling the world on the hot path), which would surface not as a correctness failure but as a node quietly falling over on the floor box. internal/smtspike/incremental_cost_test.go counts digests, not wall-clock: shared cloud hardware carries ~2× timing variance, so a time budget would be either too loose to catch a regression or flaky enough to get disabled, whereas a digest count is exact and identical on a laptop and a 1 vCPU box. Measured: applying 64 changed keys costs 544 / 780 / 978 digests at 1k / 10k / 100k state — 1.80× growth for 100× the state, and 6.78× for 8× the changed keys, so cost tracks changed and not state size. budgetK is set from measurement (0.85–1.61 digests per changed key per log₂n, so 3 gives ~1.9× headroom) rather than guessed, and the budget constrains the shape, not just the constant. The RED case is demonstrated rather than asserted: a full-tree recompute costs 44,733 digests, 18× over budget — and that test fails if the budget is ever loosened enough to admit it, so the constant cannot be quietly inflated to hide a regression.

Added

Snapshot-boot equivalence — the keystone's RED home #1, both halves. Part 2 is the differential oracle (core/chain/modelcheck_snapshot_equivalence_test.go): a validator booted from committed state alone — never having replayed the history — must reach the same verdicts as one that replayed, which is the property the whole keystone rests on. The snapshot-booted replica is built by reflection over the classification, and one detail falls out for free: blocks is classified input rather than committed, so "copy every committed field" yields a replica with no history by construction. The leave-one-out half is the sharp one — omitting a committed field must change a verdict, and the passing output is evidence rather than an assertion: dropping byRoot turns dup-publish from reject into accept; dropping revoked breaks un-revocation; dropping bondRootOwner turns a second identity's claim on an already-owned bond root from claim-blocked into claim-succeeded — one plot backing two identities, a direct C1 no-discount break. The oracle also corrected its own probes: it first flagged three fields as not load-bearing, and attribution showed the probes were wrong — F1 dedup lives in apply, not in a validate predicate, and bondRegHeight's min-interval is gated behind regGateActive (#506). Probe coverage is 3 of 18 committed fields; the rest are declared in probeUncovered with what each would need, and the test fails if a committed field is neither probed nor declared, so the debt cannot grow silently. Consensus engine untouched; I1–I5 untouched.

Added

State-field completeness ratchet — the keystone's RED home #1, part 1. The state-root certification makes one obligation load-bearing: the field enumeration must be proven complete by an oracle, not by inspection, because inspection already missed fields. The tempting test — capture the listed fields, restore, compare — is inspection wearing a test costume: it can only test the list it was handed, so field #17 lands green and silent (the #558 class). Instead core/chain/modelcheck_state_completeness_test.go cross-binds three enumerations by reflecting over the live Chain struct: the classification of every field, the populate helper, and adopt — product code on the reorg path. A new field fails classification; once classified it fails populate; then it fails adopt. Proven failing-first by three ablations (add an unclassified field / drop a field from adopt / drop one from populate), each RED then reverted. It found a live disagreement: Chain has 25 fields, the certification enumerates 16, and adopt already copies 19 — the extra revLog and epochStart are written from block history but absent from the certified set. revLog is the sharp one: it is a history-dependent append-only transparency log backing the H9 non-globality proofs, so a snapshot-booted node cannot rebuild it from set-valued state — which the certification's history-independence argument for choosing the SMT does not address. Consensus engine untouched; I1–I5 untouched (the tests only read state). Reasoning, findings and the still-owed differential half: docs/thinking/2026-08-27-snapshot-boot-equivalence-oracle-design.md.

Changed

Quarantined the TestRepairBountyPaysOnTheWire e2e (#514). The repair-bounty wire proof carries a PREMISE-ARMING flake: the kill-selector's holders-view can diverge from byte-reality, so the stripe is not always armed the way the test assumes. The test is t.Skip-ped at the top to unblock the verified era-3 keystone probe work (#604/#606) whose e2e job was catching this unrelated flake. This is a TOP-PRIORITY fix, not an accepted state — un-skip when #514 is proven closed by stress.

Docs

VISION + canon honesty pass — C-1 lift to a conditional theorem, C-5 operator-economics true-up, and C-2/C-3 register fixes. One coherent pass re-anchoring the north star to ratified canon. C-1 (maturity before capture) lifts GATED → CERTIFIED-CONDITIONAL (conditional-theorem lift): maturity provably precedes capture as Theorem CT-1 under an honest-arrival floor (H), a declared adversary budget (B), and a parameter constraint (P), with the falsifiable crossing inequality W_A < 2·w_min·M_req — still not unconditional (the weak-subjectivity wall). docs/VISION.md, docs/decisions.md (supersedes the prior GATED entry), docs/design/m0.md §10 (CT-1), and docs/design/owned-residuals.md E3 trued up; the #183 brief re-prices R1 to the inequality, names R5's attack region W_A ≥ 2·w_min·M_req, and opens R6 — the H⊥B independence break (an adversary's staged bonds count in both the honest-arrival floor and the capture weight, so λ_H must be measured as address-diverse arrival). C-5 (honest operator composed economics) ratified GATED: the γ→1/N firewall and conservation hold under the composition and no defense prices out the small operator; a FACTUAL VISION correction — the repair bounty pays the new holder of a rebuilt shard (custody rent), not the reconstructor (unpaid caretaker duty) — plus the hot/cold scope (repair self-funds hot objects; cold rides a funded horizon). The floor-box reconstruction RAM (G2) is owed before the economy-ON field run (owned-residuals.md D6). C-2/C-3 register fix: VISION's multiplicative-interlock paragraph now carries m0.md's own C_honest ≈ D-today and declaration-cheap-A-axis qualifiers (target-not-yet-live). Distinguishes factual errors (C-5 G1 — fixed) from register drift (C-1, C-2/C-3 — qualified); VISION stays a north star, not a status report. Certifications: silt-reviews/research/research-outcome/C1-maturity-before-capture-CONDITIONAL-THEOREM-LIFT-2026-08-27.md, silt-reviews/research/research-outcome/C5-honest-operator-economics-composition-RESEARCH-CERTIFICATION-2026-08-27.md.

Docs

Canon true-up recording two ratified research certifications. C-7 (witness-based floor-box validation) is CERTIFIED sound + complete: soundness no longer blocks the #600 direction, and the era-3 format now carries a HARD freeze prerequisite — the Block must commit both the state SMT root and the append-only transparency-log root over the completeness- and order-independence-proven field set, and the floor-box verifier must hold the invariant "no witness → never accept (stall)" (the block commits neither root today, core/chain/chain.go:311-405). C-1 ("maturity before capture") is ratified as a safe-parameterization, not a theorem, confirming the canon: the everMature latch is certified one-way — it bounds the consequence of a lost bet, not the reachability of pre-maturity capture. docs/VISION.md §108 trued up to carry the qualifier; two docs/decisions.md entries; C-7 residuals added to docs/design/owned-residuals.md (E6) and the C-1 R6 doc-register residual closed (E3); the #183 red-team brief sharpened (R1 is the live seam, R2 and R3-safety are CLOSED). Certifications: silt-reviews/research/research-outcome/C7-witness-based-floor-box-validation-RESEARCH-CERTIFICATION-2026-08-27.md, silt-reviews/research/research-outcome/C1-maturity-before-capture-RESEARCH-CERTIFICATION-2026-08-27.md.

Fixed

#514 ROOT CAUSE — the repair-bounty flake: the premise killed BEFORE DHT convergence, so publish-time lost-ack extra copies re-converged and healed the loss within slackTestRepairBountyPaysOnTheWire failed ~20% of runs with a premise defeat: the kill-selector killed a column's holders but the caretaker's byte-confirmed sweep saw missing ≤ slack and never armed repair. Mechanism, pinned by the caretaker's own sweep trace: the object carries publish-time lost-ack extra copies (#497 — -replication 1 does NOT mean one holder, a lost ack mints a silent extra copy) whose provider records converge a sweep or two AFTER the kill. The caretaker's first post-kill sweep DID see the loss over slack, but reachable then climbed as the hidden copies surfaced, the loss healed within slack, and the #517 two-sweep confirmation gate reset. Neither the record view nor a byte-confirmed selector view could see the hidden copies at kill time. Two parts. (1) HARNESS — the premise is now deterministic: STABILIZE (wait until the byte-confirmed swarm holders view stops changing, so every real byte-holder including the lost-ack copies is listed), SELECT within (slack, n−k] (killing a node removes every column it holds, so bound the loss so a stripe stays ≥ k and the bounty can pay), KILL ALL byte-holders of the target columns, then CONFIRM on the caretaker's OWN sweep (a stripe over slack), re-killing any surfaced copy (the caretaker's own DHT vantage can resolve a copy the selector could not) and re-publishing under a fresh root if placement concentrates all columns onto 2-3 nodes (the cloud grade records that as "economy UNTESTED, not failed" — the e2e re-rolls it instead). (2) PRODUCT — ColumnHolders (swarm holders) byte-confirms each column's provider records with MsgHasChunk (confirmColumnHolders), so the operator/selector view no longer reports phantom holders, corpse-gated exactly like probeShard (repair.go:479) so a stale record to a departed holder costs one HolderDialTimeout for the whole walk instead of one per shard — closing the dead-holder dial-storm PR #607's ungated all-shards walk re-introduced (the #226/#277/#501 class). RED-proven at the node tier (core/node/column_holders_bytes_514_test.go): a phantom record-holder with no bytes is dropped by the byte-confirmed view (ablate the confirm ⇒ lists the phantom); a dead record-holder is dialed exactly once, not once per shard (ablate the corpse-gate ⇒ 5 dials on a 5-shard column). The invariant the e2e proves (a verified reconstruction PAYS) is untouched — only the premise arming is made deterministic. Extends PR #607 (its byte-confirm direction was sound; its scoping and its selector-only premise were not). Evidence: 50/50 green serial iterations (docs/thinking/2026-08-27-514-repair-bounty-50x-evidence.txt), where the flake reproduced ~20% pre-#607 and ~5% post-#607.

Changed

The field-test publish bound re-derived downward, 360 → 300 s (owed Phase-3 gate clause). The 12-deep-heights deep drive measured ~48 s/height steady cadence (integration/cloudtest/results-fe2376a-deep.jsonl:29, was ~390 s at the depth-war start), so the publish retry budget in integration/cloudtest/scenarios.sh (PUBLISH_RETRY_S, and its sibling ECONOMY_PUBLISH_RETRY_S) re-derives from the measured number, not a guess (#549-Q3 discipline). The load-bearing finding: only the gather-leg term is cadence-free request-timeout arithmetic; the commit-wait leg is the #451 synchronizer 2-round escape FLOOR (dur(0)+dur(1) = 150 s, counted in fixed 30 s sweeps — a consensus-liveness parameter, left untouched). The 60 s shed is the historical escape-rounding cushion (220 → 184) plus stale slow-height straddle padding the cheap cadence retires. 300 s keeps the full 150 s escape window inside the bound (6.25× the measured cadence, 1.76× the 170 s per-height worst case at e2fab4b), above the too-tight 240 s scar. Config + docs only; no billable run. Derivation: docs/thinking/2026-08-27-publish-bound-rederivation.md.

Added

The keystone SMT spike — pokt-network/smt proven, not just read. The state-root library recommendation rested on quoted source rather than executed code, and was explicitly void if a spike disagreed. internal/smtspike/ is that spike, and it agrees. The assertion the gate turned on — an absence proof for a PRESENT key must FAIL — holds against all three adversary shapes, and the test fails if the library's "non-membership proof on related leaf" guard never fires, so the branch the soundness rests on is exercised rather than assumed. Measured cost is stable across 1k–1M keys: 2.24 nodes and 218 stored bytes per key, ~900-byte proofs, and applying 100 changed keys costs 2.37 → 3.01 → 3.80 ms across 1k → 100k state — 1.27× per 10× of state against the 1.25–1.33× log n predicts, so the certified O(changed·log n) shape holds and the #555 O(state) scar does not return. Measured on a real 1 vCPU / 2 GB floor box (dedicated e2-custom-1-2048, no swap), which confirmed the laptop projection to the digit: 751.6 MB heap at 1M vs 751.7 MB on the M4. The finding: the in-memory backend is disqualified. Residency is 752 B/key — 3.4× the 218 B stored payload — and the floor box OOM-killed the trie at 2M entries (anon-rss 1.68 GiB); even 1M would not fit alongside the flixz daemon's 1060 MB. The library is adopted, the reference in-memory backend is rejected: the trie needs a disk-backed MapStore (five methods), which is the only configuration that survives build-immutable #8. Boot rebuild measured at ~22 s per 1M, a lower bound pending the real disk store. The dependency adds zero new indirect dependencies and no product package imports it. Reasoning and numbers: docs/thinking/2026-08-26-keystone-smt-spike-results.md.

Added

PoD neutral lane runs in a real daemon — -accept-delivery-receipts + silt swarm receipt. This closes the e2e-tier gap #590 reported honestly: the lane was built and proven at unit and sim tiers, but no daemon ran it. Now both halves exist. Server: a validator started with -accept-delivery-receipts banks receipts against its own token-issuer key (the bilateral issuer==server shape the certification's settlement answer covers) and settles the conserved delivery credit. Fetcher: silt swarm receipt <root> -peers ID@ADDR blind-withdraws a retrieval token, signs the delivery receipt, and submits it — the fetcher half of the lane, which the CLI previously had no surface for. The e2e asserts the full path over real TCP and goes past "banked" to assert the settled credit is non-zero, so a lane that banked a neutral observable while paying nothing would fail rather than pass quietly. The lane is off by default; a daemon without the flag refuses the receipt and the client reports the refusal instead of a success it did not get. Delivery credit remains balance-only and can never reach standing.

Added

D-TIERING mode flags — -archive and -serve-content (the near-term build-gated items of the tier model). -archive is a genuinely new capability: an archival node retains every block's heavy space-time bond proof to genesis instead of shedding it below the rolling retention horizon, so it can serve the deep history a pruning swarm has already dropped — the answer a node stranded past the prune horizon needs (ErrNeedCheckpoint, #559's true-loss residual). It is retention-only, never validity: the trust floor and retention horizon are untouched, pinned by a test asserting an archival and a pruning chain agree on both, so the tiers cannot fork against each other. Costs O(all history) resident payload — off by default, and build-immutable #8 forbids it on the 1 vCPU / 2 GB box, which is the reason the tier model exists. -serve-content is the positive spelling of the content axis (default ON), so an edge profile composes as -serve-content -archive=false -validator=false rather than as a double negative; the legacy -freeload is unchanged and remains its inverse. A contradictory pair (-freeload -serve-content=true) is refused loudly rather than silently resolved (S3). The announced line carries BOTH spellings (serve-content: OFF (freeload: ON)) because freeload: ON is a stable marker the e2e harness and operator tooling grep — an announced line is an observable contract (S5), and the first cut of this change broke TestFreeloadRoleSeparation by renaming it.

Added

D-POD-KNOBS — the three Phase-4 economy/state knobs, DECIDED (owner ratification of the PE recommendations). (1) The delivery-credit skim routes to the object's durability escrow, not burn — the deciding reason is a cross-tier funding loop (edge delivery skim funds that content's durability on the persistent tier); conservation carries soundness independently, so the skim stays a deterrent knob and must not be raised for anti-wash reasons. (2) Relay compensation resolves disputes through a dispute-only quorum-TTP, under the load-bearing scope condition that a dispute adjudicates the payment chain only, never transit — which is what keeps the resolution signature-verifiable and keeps the verifiable-escrow unknown confined to strong-form PoD. (3) A bond root's ownership record follows current possession (TTL-lapse), required so the keystone's committed state stays bounded; lifetime provenance survives in the archival tier's chain history. Recorded in docs/decisions.md; (1) is shipped, (2) lands with relay compensation after its consult certifies the scope condition, (3) freezes into the keystone field set (live ledger behavior unchanged for now).

Added

Owner-knob guards + the relay dispute gate answered (per silt-reviews/principle-engineer/RULING-PoD-keystone-owner-knobs-2026-08-26.md, which concurs on all three knobs) — two regression locks the PE prescribed. TestPaidBountyIsNotRecoverableBySupersede: escrow skim-routing is sound because the supersede reversal floors at the remaining reserve, so a bounty already paid for real repair work is never clawed back — if that floor regressed, escrow would begin minting recoverable balance and burn-routing would become the correct choice instead. TestRootOwnerFeedsOnlyTheDedup: rootOwner feeds the F1 dedup and nothing else (both slash paths dock by identity regardless of root ownership), which is the property that makes the keystone's TTL-lapse option safe; the companion anti-griefing property is already pinned by core/bond TestRedteamG2_PlotBoundToClaimedIdentity. Recorded with them: the relay-dispute gate is signature-verifiable — the certification forbids adjudicating transit (no transit proof exists), so the only adjudicable quantity is the self-verifying payment chain, and the quorum-TTP direction does not reactivate the verifiable-escrow unknown so long as relay disputes stay scoped to payment. Builder evidence, routed to the relay consult; the three knobs remain the owner's calls.

Added

PoD neutral lane BUILT — the witnessed delivery credit, conserved (Phase 4 §7.1) (per the certified docs/design/pod.md) — a banked delivery receipt now settles a conserved balance credit: the fetcher's withdrawal fee, less the 1/8 durability skim routed to the object's escrow (Ledger.RedeemDeliveryCredit, wired in handleDeliveryReceipt). The certified supersede rule ships with it: every object-aware serve's self-credit is tracked provisionally per (requester, root) and a witnessed receipt REVERSES it before paying — a delivery is never paid twice (the banned subsidy). The receipt itself sheds its PoR leg (certification Q2: a public-seed proof deters no collusion and cost 128 SW samples/delivery on the floor box) — the neutral receipt is token + fetcher signature + the (serial‖object‖server) binding, domain-bumped to v2. Firewall pinned at both tiers: the Invariant-A guard classifies the new press neutral, a dedicated heavy-deliverer test asserts Reputation unchanged, and the sim closes the bilateral loop over the wire (pair net = exactly −skim; wash is a strict loss). Skim routing defaults to escrow pending the owner-knobs PE consult.

Added

PHASE 3 BANKED — the deep-heights exit gate is MET (run fe2376a-deep: 30 pass / 1 gap / 0 fail) — 12-deep-heights drove h78→h132 (target 128) at ~48 s/height, the #549 Q4 barrier stabilized in 215 s, the retention prune engaged on every validator at depth and the pruned chain converged (12b/12c), worst RSS 0.65 GiB, zero OOM, and the S7 economy closed on the wire for the third consecutive sheet. Field-confirms and closes the entire depth-stall lineage (#549, #560, #561, #572, #573); #183's close condition is met (issue held open by owner directive). ROADMAP updated: Phase 3 ✅, the publish-bound re-derivation carried as the owed gate clause, Phase 4 (PoD spec-first) is the next phase. Evidence: integration/cloudtest/report-fe2376a-deep.md (artifacts PR #585).

Added

#572: save-side regime line — restore/save PAIRS make the next under-latch self-locating — every chain persist (commit / catch-up / takedown) now prints the same regime snapshot as the restore line plus the head it went down with (chain: saved N block(s) [why] head=H:hash (everMature=… …)). Paired with the restore-time line, one diff decides the remaining #572 premises: last-save ≠ restore ⇒ store/replay layer; equal-but-wedged ⇒ downstream of restore; the head hash pins content. The legacy restored N block(s) from disk prefix is preserved (integration/consensus greps it).

Added

#572 round 3: chain replay proven pure; the divergence hunt moves to the daemon layer, instrumented — the write-site audit (every latch/regime map writes only in apply/adopt) plus a full FIELD-SHAPE oracle (TestLatchReplayFieldShape_572: organic 12-seat gather, renewal treadmill at the R-rule-legal cadence, TTL lapse + re-entry, latch + handoff + three rotations, wire-faithful Reload) are both GREEN — replaying identical blocks provably reproduces the latch at chain level. So 474718e-deep's under-latch must arise outside pure replay: the daemon now prints the full regime state at every restore (everMature/matureEpoch/seen/bonded/epochStart/epochSet via chain.Regime), so the next occurrence names the map that failed to rebuild. Also: ValidateProposal's objective rejection now names the ACTUAL disqualifying branch (slashed / not in the frozen epoch set / not a launch anchor / under-bonded) — the field's misleading "bonded 1048576, needs 1048576" was a frozen-set refusal wearing a bond-size costume. The #563 memory bench now skips under -race (shadow memory + pool suppression inflate the live-heap peak ~10×; the budget is only meaningful uninstrumented).

Added

#572 round 2: latch-replay determinism oracle — the 474718e-deep diagnostic named the stall branch (val-d refused every mature commit with ErrAnchorRequired need=3 at h32 after a drill restart restored the 32 blocks that had latched everMature live at ~h14): replay of a latch-producing history did not reproduce the latch. TestLatchSurvivesReplay_572 asserts latch-replay determinism (wire-faithful roundtrip + fresh Reload); GREEN on matureWorld12's shape, which bounds the remaining search to the field's latch dynamics (validatorsSeen/C2 accumulation under the renewal/TTL cadence). Full attribution on issue #572.

Added

#572 sync-stall repro guards + per-sweep catch-up diagnostic — the 027c354-deep val-c stall (100+ min of no-progress sweeps) was unattributable because every failure branch of the SyncChain walk logs at debug or below. The deterministic repro (core/node/syncstall_572_test.go, exact 12-seat topology, epoch rotation in the gap) EXONERATED the leading suspects: a healed behind seat catches up in one sweep, and the field's chain-behind-ahead-mark shape (chain h24 / mark h33 — the markstore is atomic, chain.cbor save cadence lagged) neither blocks adoption nor moves the I2 mark. Both stay as regression guards. Since the mechanism is still unnamed, SyncChain now emits ONE warn when a sweep ends with zero adopted blocks while demonstrably behind — our-next, max-peer-head, probe/window/append/reconcile counters, last branch error — so the next occurrence carries its mechanism (RED-proven oracle: TestSyncStall_572_NoProgressSweepWarns). No behavior change on any sync path. Record: docs/thinking/2026-08-26-572-sync-stall-attribution.md.

Fixed

#572 ROOT CAUSE — the restore under-latch: the daemon replayed history before wiring the bond verifierobjective is MinBond>0 AND verifyBond!=nil, and EnableObjectiveChain ran ~80 lines after chainstore.Replay, so every restore replayed under the LEGACY rep-gated qualification with an empty boot ledger: validatorsSeen rebuilt EMPTY, the everMature latch was silently lost, and the restored validator demanded launch-rule anchors for mature commits forever — the 474718e-deep/8a52aba-deep drill-restart wedge, proven by the save/restore regime pairs (saved seen=12 everMature=true → restored seen=0 everMature=false over identical blocks). Fix: the daemon wires node.SpaceTimeBondVerifier (factored from EnableObjectiveChain) BEFORE Replay, and Reload now REFUSES an objective-config replay with no verifier — the ordering can never regress silently. RED-proven (guard removed → the replay proceeds and, at the field's -min-rep, under-latches); the field-shape oracle asserts both the refusal and the with-verifier latch. Reconcile's tmp replica already inherited the verifier (chain.go:3089) — that doorway was never open.

Added

#570 archival-format golden-fixture suite — committed chains every future HEAD must replay — four write-once serialized fixtures (core/chain/testdata/archival/: era-1, era-2, era-2-pruned, mixed era-1→era-2, the exact bytes chainstore.Save persists) replayed at HEAD against pinned head-hash and derived-state constants. Asserts the property no HEAD-minted test can: bytes written by an older binary must replay today — the #558 class (era-2 replay silently falling to genesis from #432 until last week) is now caught locally, RED-proven by reinstating the #558 bare-hash verification (era-2/pruned/mixed fail at block 1; era-1 stays green). Head-hash pins also catch silent hash-computation changes over committed bytes. Era-3 (the D-TIERING state root) must ADD a fixture here — this suite is its standing RED home (consult Q5). Record: docs/thinking/2026-08-25-570-archival-fixture-suite.md.

Fixed

#563 cold-sync Reconcile OOM on the 2 GB box — the hypothesis was garbage, literally — the deep-run kernel-OOM (a434494-deep, val-d ×2) was attributed by a new deterministic memory oracle (core/chain/reconcile_mem_563_test.go, born RED): there is NO 2–3× resident fork copy (retained-after-GC is negative — adoption shares payload backing); the spike is ~1× fork-bytes of transient CBOR garbage from each decoded block's first Block.Hash materializing its full multi-MB body, on top of a measured 2.35× decode inflation and GOGC=100's heap-doubling headroom. Two-leg fix: Hash now marshals into a pooled buffer (UserBufferEncMode.MarshalToBuffer, byte-identity with the reference encoder asserted by TestHashPooledBufferIdentity_563 — same bytes, same hashes, no consensus surface), dropping the Reconcile peak extra 69→6 MiB; and the cloudtest fleet now DEFAULTS MEM_LIMIT=1500M (the a9cfc06-proven GOMEMLIMIT guard the OOM'd run had silently dropped — console-a434494-deep.log carries no -mem-limit). Deliberation + outcome: docs/thinking/2026-08-25-563-reconcile-memory-bench-deliberation.md.

Fixed

#562 renewal jitter grid clamped to the #506 R-rule — no more refused-renewal sweeps — the #555 phase-jitter's nearest-grid rounding reaches down to TTL/4 blocks after the last committed reg, but the reg-inclusion rate bound R = K+2 can exceed TTL/4 (10 vs 8 at the field TTL=32), so a colliding identity's renewal was refused every sweep ("re-registering 9 blocks after its last reg (R=10)", a434494-deep) until the chain outran R. renewalDueHeight (factored out of BondRenewalDue for direct testability) now clamps the grid point to the rate bound: at most one off-grid cycle (the next due point re-aligns to the grid), steady-state periods stay exactly TTL/2 (the #313/#556 property), and unlike jumping to the next grid point it can never overshoot the TTL at small TTLs. Client-side pacing only. RED-proven: TestRenewalDueClearsRegRateBound_562 (colliding phases 12/13 land at +8/+9 < R pre-clamp; on-grid steady state asserted inert).

Fixed

#561 round-escape decoupled from the chain-sync peer walk — dead peers can no longer stall the view-changemaybeAdvanceRound (the #432 escape counter) ran inside SyncChain's completion callback, which fires only after the sequential ask-walk over every peer completes; dead peers stretch that walk by their full retry budgets. In the a434494-deep 10a stall-drill (4 of 12 peers stopped), the honest cohort's first round-change came ~8 minutes after the stall against a 430 s bound built on 30 s sweeps — while renewals (tick-driven) flowed on schedule the whole time. The escape now runs on the TICK: it needs only local state (pending work + the sweep count), tick cadence is the #549 Q3 skew-bound premise, and the new-view proposal path was already message-driven at the designee. The drain keeps its freshest-head property (#338) by staying in the callback. RED-proven: core/node/roundescape_tick_561_test.go (held delivery models the never-completing walk; pre-fix the node never leaves round 0).

Fixed

#558 era-2 chain replay always failed — silent genesis fallback on every validator restart, exposed as a stranding at depthvalidateStructural (the Reload path a restarted daemon replays its own chain.cbor through) verified attester signatures over the bare block hash — the era-1 form. Era-2 (#432) attestations sign the domain-separated consensusSigBytes(phase, round, hash), so replay of any era-2 chain failed at its first non-genesis block with bad signature, and the daemon silently fell back to genesis. Invisible until now: peer catch-up re-fetched the whole chain after every restart (an expensive hidden full Reconcile — a #555-adjacent load source). In the a434494-deep run the retention prune removed that mask: OOM-restarted val-d could not re-sync below the prune horizon and was stranded at genesis with its intact h83 store on disk (not a torn write — chainstore.Save is atomic). Fix: validateStructural uses the shared era-aware verifyAtt (the live commit path's arithmetic); Reload keeps the longest valid prefix; the daemon names a replay failure loudly (prefix kept, prune-horizon consequence, #559 pointer) instead of a one-line stderr note. RED-proven: core/chain/reload_era2_558_test.go reproduces the exact field failure (era-2 + pruned block replay through the persisted representation) against the pre-fix code.

Fixed

#555 deep-drive crawl attributed and fixed — Block.Hash memoized; the crawl was hash-work saturation, not gather latency — The 95d39e8-deep field log produced the measurement the #555 certification held for: the intrinsic two-phase gather is ~10 s at 12-seat WAN (h74: new-view → commit in 9.6 s), well inside the 60 s round-0 base — the apparent 90–150 s "gather" was event-loop saturation. ChainReply processing blocked the single node thread 16–86 s per reply (cost growing 2.4 s → 42 s with depth), stretching the sweep timers (waited p50 18 s, p90 146 s) and starving the gather; the watchdog stacks pin the work to Reconcile → recentBondRegNonces → blockByHash → Block.Hash → sha256 — the hash re-marshaled the full block body (~1.5 MB per reg proof) on every call, recomputed per scan step, K=8 lookups per validated block: O(depth × window × scan) per full fetch, self-sustained by probe timeouts forcing more full fetches. Fix: memoize Block.Hash (decode-fresh on the wire; Sign invalidates; pruned branch keeps priority) — one hash per block per lifetime; no consensus rule, no wire change, no timing constant (roundAdvanceSweeps stays 2 — the #549 Q3 skew derivation remains the binding lower bound). RED-proven hash-work oracle core/chain/reconcile_hashwork_555_test.go: 798 → 25 computations for a 24-block cold-sync reconcile; Head now does zero hash work. Attribution: docs/thinking/2026-08-25-555-crawl-attribution.md.

Fixed

#555 fix (b): bond-renewal phase-jitter — spread renewals so ~1 reg lands per block (research-certified) — The deep-drive crawl (#555) was inflated by heavy blocks: validators that all registered near genesis hit the TTL/2 renewal point together, so 5–7 ~1.5 MB space-time proofs landed in one block, and each attester verifies every proof on the two-phase gather's critical path before signing (1 vCPU box) — inflating the gather latency that drives the crawl. BondRenewalDue now places each identity's renewal on a per-identity ABSOLUTE grid (period TTL/2, phase = a deterministic offset), rounded to the nearest grid point within ±TTL/4, so the genesis-aligned fleet's first renewal spreads across [TTL/4, 3·TTL/4) (≈1 reg/block) while the PERIOD stays exactly TTL/2 on every later cycle — keeping the #313 re-registration-frequency bound and the ≥TTL/4 renewal margin intact. It is client-side pacing (BondRenewalDue gates the node's own drain/submit, never block validation), so it changes WHEN an identity re-proves, never a consensus rule; the TTL denomination and its #503 couplings are untouched. This is the certified "lighter blocks first" half of #555; fix (a), sizing the round base to the (now reduced) gather latency, follows. Tests: core/chain/renewal_jitter_555_test.go (spread + ≥TTL/4 margin + determinism; RED-proven by neutering the offset → clustering).

Added

#549 in-process repro of the DEEP-run h68 stall — the field cause is not synchronizer logiccore/node/modelcheck_549_scatter_test.go models the field's distinguishing dimension (sybils active in the frozen set inflating the low-round round-change head-count while the heavy validator weight is mass-scattered across rounds). It is GREEN even under 50% sustained round-change loss (commits at round 2): because RoundCatchupMet is weight-based, sybil head-count cannot dilute convergence, so the h68 stall is not a synchronizer-logic defect but a real-WAN wall-clock timer-skew + scale dimension the untimed model cannot reproduce. Feeds the research consult (silt-reviews/research/549-h68-view-synchronization-stall-CONSULT.md); a RED here would be the home for any logic fix.

Added

#183 red-team coverage caveats C-1 + C-2 closed — I5/I2 exhaustive oracles + disk-backed I2 durability — The external red-team verdict (M0 HOLDS) noted two harness assurance gaps, not protocol defects; both are now closed. C-1: the I5 and I2 oracles, previously covered by scenario tests, are promoted into the exhaustive-enumeration tier — core/chain/modelcheck_i5_accountable_test.go drives the real VerifyEquivocation over the full 2^4×2^4×2 space of same-height signature schedules (honest-never-slashed AND completeness, both directions), sweeps fork-choice determinism over all 24 permutations of a fork set (up from 3 hand-picked orders), and pins the bare-ProposerSig-is-not-a-vote exemption; core/node/modelcheck_i2_exhaustive_test.go drives the real signAllowedAt watermark against a mark reloaded across a restart over every (signed slot)×(competitor slot) pair in the {height,round,phase,hash} space. C-2: the I2 fsync-before-the-wire durability, previously verified only by inspection (all restart tests used markstore.NewMem), now has a disk-backed home — adapters/markstore/markstore_test.go (the first tests for that package: Save/Load survives a fresh-instance restart, atomic overwrite leaves no torn file, missing-vs-corrupt is start-vs-refuse) and core/node/i2_disk_durability_183_test.go (a real Disk-backed node refuses a same-slot competitor across restart; recordSign withholds the signature when the store cannot persist the mark — the fail-safe against an honest self-slash). All RED-proven against reverted mechanisms.

Added

#535 fix (3): the operator-directed weak-subjectivity liveness-floor escape — The remaining layer of the certified recovery stack, built per the ratified design (PR #544). Config.LivenessRecoveryHeight (daemon -liveness-recovery-height, 0 = off) names ONE epoch-boundary height at which mature-epoch validation re-bases proposer/attester qualification and the >⅔ weight quorum against the LIVE qualified bonded set instead of the frozen epochSet — a single effectiveEpochSet(h) consulted by all three predicates (and the gather's SupportMeetsQuorum/solicitation, now height-threaded), so the set a quorum is sized over and the set it is filled from can never differ (the #402 law). After the recovered boundary commits, the normal rotation freezes the same live set and the chain resumes. NEVER automatic: a genuine >⅓-of-frozen-weight loss is outside the BFT liveness model (automatic re-basing was refuted — fix (2)), so a bled boundary stalls by default and the trust moves to a HUMAN who confirms the loss is a real outage and coordinates the SAME height on every honest node — the WSCheckpoint trust class, with the wrongly-invoked-recovery fork as the documented residual. Operator visibility (S5): Chain.BoundaryLivenessFloorLost diagnoses the wedge (live-bonded frozen weight ≤ ⅔ bar), the round-change path logs stalled-at-boundary naming the recovery, and chain-status flags a next-height-is-a-boundary head. Model-check (core/chain/modelcheck_535_fix3_recovery_test.go, RED-proven against the pre-fix rule): recovery-when-invoked commits the wedge topology and resumes; off/wrong/ non-boundary directives still stall (ErrNoQuorumWeight); replicas replaying the same directive reach the identical head. Detail: docs/thinking/2026-08-24-535-fix3-built.md.

Added

#535 fix (2) refuted by the proof-first model-check — the recovery stack is (4) + (3) — The certification adopted boundary-local quorum re-basing (old∩next) conditional on a model-checked #402 handoff-intersection proof for a bled set. core/chain/modelcheck_535_fix2_rebasing_test.go discharges that obligation and finds the naive form unsafe: a boundary block finalizes a fork iff Byzantine weight exceeds ⅓ of the re-based total, and excluding possibly-honest lapsed weight raises that fraction — at the field numbers, 171 MiB Byzantine is safe against the full 516 MiB frozen set but breaks I1 over the re-based 324 MiB. This is the same fault-tolerance wall that sank fix (1). Automatic denominator re-basing is not safely realizable; the certification pre-ruled the fallback (fix (3), the operator-signaled weak-subjectivity escape, is the guaranteed-safe recovery). The model-check stands as the permanent evidence + regression (it asserts the shipped no-re-basing behavior — a bled boundary STALLS rather than forks — is the safe one). Detail: docs/thinking/2026-08-24-535-fix2-refuted-stack-is-4-plus-3.md.

Fixed

cloudtest: 5-convergence grades with a bounded wait-for-convergence — The flow-5 convergence check took a single point-in-time sample immediately after the 6-fault-tolerance drill stops/restarts val-d, so it could read a spurious catch-up lag as a FAIL (observed on run eb510a7-deep: val-a=33 but val-d=27 right after the restart). It now polls until the validators converge (within 2 of tip + shared tip hash) or CONVERGE_WAIT_S (default 120s) expires — the same grade-after-stabilization lesson as the #549 Q4 deep-heights barrier, applied to flow 5. A genuine non-convergence or a persistent fork still FAILs; only a transient post-drill lag (or a fork fork-choice is still resolving) is given time to settle.

Fixed

#549 Q3 companion: round-duration base derived + guarded (no numeric change) — Assessed the certification's Q3 (round-duration base tune). Measurement: the cross-region sweep-timer skew is structurally bounded by ChainSyncInterval (each node sweeps once per interval at an arbitrary phase, so two nodes' round-change timeouts differ by < 30s; WAN delivery ~80ms is negligible). So roundAdvanceSweeps = 2 (base = 60s = 2× the skew) is already the smallest value that reliably outruns it — 1 (= 30s = the skew) has zero overlap margin, 3+ is "larger than necessary" (slower recovery + churn on the 2 GB box, against the cert's M1 guidance). No numeric change; instead the derivation is made explicit (the constant is now derived-not-magic, build-immutable #5) and pinned by TestRoundBaseOutrunsSkew, which fails if the base ever drops to the skew or drifts above the certified minimum. Deliberation: docs/thinking/2026-08-24-549-q3-round-duration.md. This closes the #549 certified companions; the finding now needs only a clean DEEP field-confirm.

Fixed

#549 Q4 companion: cloudtest deep-heights stabilization barrier — The Phase-3 deep-heights flow graded liveness immediately after the maturing drills (10a/10b/10c) mass-restart 8 of 12 seats, so it measured post-restart CHURN rather than steady state (the certification's Q4). The harness now requires the network to reach GST before the drive grades — all validators converged on ONE head AND one fresh commit under normal conditions — via a bounded barrier (STABILIZE_S, default two per-height worst-cases). A network that cannot re-stabilize after the drills is reported as a degraded PREMISE (GAP), never a deep FAIL. This is the harness half of the #549 fix: the catch-up-target change makes convergence sound, and this barrier stops the harness from grading before it completes.

Fixed

#549: the h68 view-synchronization stall — catch-up jumps to the highest qualifying round, not the smallest of the union (research-certified) — The DEEP-run Phase-3 exit gate stalled at h68 for ~26 minutes (r1-congestion, no prepare-QC) after the drill sequence mass-restarted 8 of 12 seats. Root cause (certification silt-reviews/research/research-outcome/549-h68-view-synchronization-stall-RESEARCH-CERTIFICATION-2026-08-24.md): maybeCatchUpRound unioned round-change senders across ALL rounds above the current one, checked the weight threshold on that union, then jumped to the SMALLEST such round — a round that may carry only a fraction of the union's weight (structurally unable to form a QC). Because duration(r)=base+r(r+1)/2 is keyed to the round number, targeting the smallest pinned the effective round low, so the increasing-duration ladder never outran 3-region WAN + 30s timer skew and the after-GST convergence guarantee never engaged. Fix: jump to the HIGHEST round that INDIVIDUALLY meets the catch-up weight threshold — coalesce the weight at the leading edge and let the ladder climb. Safety untouched (I1/locking): it changes only WHEN a node changes round, never which value it may sign, and is still gated on >⅓ weight (a Byzantine minority cannot drag the round forward), so the anti-overshoot property PBFT's "smallest view" rule sought is preserved — evaluated per round rather than on the union. Deterministic RED/GREEN home: core/node/modelcheck_549_catchup_test.go (drives the real maybeCatchUpRound over a low-weight-trailing + quorum-weight-leading smear — RED jumps to the sub-threshold trailing round, GREEN to the qualifying leading round). Companions (not in this change): a harness post-mass-restart stabilization barrier and a round-duration base tune sized to measured cross-region skew.

Fixed

#183 red-team F-1: the MsgSubmitEntry CPU-DoS gap — per-sender rate gate + cheap replay reject — The first external red-team engagement (verdict: M0 HOLDS at the shipped defaults) handed back one real, bounded liveness finding: under -require-tokens (publisher privacy, off by default), the entry-submit path had none of the #424/Phase-1.2 CPU hardening its structurally-identical sibling MsgSubmitBondReg has. A single peer could harvest a public committed token, pair it with a novel Root, and flood MsgSubmitEntryValidateEntry ran publishtoken.Verify (an RSA modexp per signature) to completion on the single consensus loop before a spent-serial check placed after it caught the replay. Two fixes, both admission-order, no consensus rule touched (build-immutable #3 intact): (1) allowEntrySubmit(from) — a per-sender window burst gate charged BEFORE decode+validate, mirroring allowBondSubmit; (2) ValidateEntry now checks c.spent[serial] BEFORE publishtoken.Verify, so a replayed (already-spent) token fails on an O(1) map lookup instead of N modexps. Failing-first regressions, both RED-proven against the pre-fix code: core/chain/entry_replay_cpu_183_test.go (a spent token with tampered sigs must fail ErrTokenSpent, proving the verify was skipped) and core/node/entrysubmit_gate_183_test.go (a 100-message single-sender flood queues at most entrySubmitBurst; a second sender is not starved; the window refills). Bounded/conditional — absent at the shipped default (tokens off), where the residual is the already-documented, shelved byte-flood E5.

Fixed

#535 fix (4): the R-gate restore exemption — a returning frozen member can re-bond to heal a stalled boundary (research-certified) — The h64 epoch-boundary wedge's non-recovery was compounded by #506: a member whose standing lapsed was R-refused when it tried to re-register (re-registering 1 block after its last reg, R=10), so it could not restore its weight to help the boundary commit. Fix: a re-registration that RESTORES standing the identity already held — a current frozen-epoch member (epochSet) re-proving a Root it already owns (bondRootOwner, which survives a lapse) while its standing has lapsed (bonded < MinBond) — is exempt from the R interval. Safe by construction: it can only restore weight the honest set already trusted for the epoch, never admit new weight, so it cannot cheapen capture (unlike shrinking the quorum denominator — the certification's rejected fix (1), which cheapened cost-to-corrupt 344→216 MiB). Narrow: a still-bonded member re-proving its root is the #506 storm and stays refused (flood protection intact). This is layer (4) of the certified recovery stack; (2) boundary-local re-basing and (3) the weak-subjectivity liveness escape are the remaining layers. Cert: silt-reviews/research/research-outcome/535-epoch-boundary-liveness-cliff-RESEARCH-CERTIFICATION-2026-08-23.md.

Fixed

#536 cloudtest: the escape fingerprint read round-changes from the wrong channel → a manufactured WEDGE FAILft_escape_progress counted round-change from journald (jlog_since), but that structured n.logf line is written to $STORE/debug.log only, never journald (cmd/silt/daemon.go openLog) — so rc read 0 on every sample regardless of ladder activity, and a LIVE ladder (114 round-change lines at h64 r1→r5 in run 45da13c-17686's captured debug.log) fingerprinted as FROZEN, grading the down-designee flow a WEDGE FAIL instead of the honest "advancing-but-uncommitted" out-of-model GAP. Fix: read round-changes from debug.log (dlog), time-scoped by the ISO-timestamp column ≥ the kill instant (new portable epoch_to_iso); and — the #525 lesson extended to the empty-read case — an UNREADABLE source now yields ?, never 0, and the WEDGE-FAIL branch requires a readable (no-?) frozen fingerprint. Shipped with an offline RED/GREEN self-test (integration/cloudtest/check_escape_fingerprint.sh, wired into CI) per the third-time rule.

Added

#535 deterministic repro: the epoch-boundary liveness cliff (consensus model-check)core/chain/modelcheck_535_boundary_wedge_test.go pins the mechanism the first Phase 3 deep field run (45da13c-17686) wedged on: the mature-epoch finality quorum needs signers holding > ⅔ of the FROZEN epoch weight, but a member that lapses or goes offline mid-epoch keeps its frozen weight in the denominator for the whole epoch — so once > ⅓ of frozen weight cannot sign, no block reaches the super-quorum, INCLUDING the boundary block whose commit is the only event that rotates the snapshot to a lighter set (a permanent stall that cannot self-heal; #506's R-gate compounds the non-recovery). The repro reproduces the field arithmetic exactly (a 9-of-12 live coalition at 324 MiB refused; a 10-of-12 control at 388 MiB commits). This is a consensus-rule question (whether the frozen denominator should exclude provably-lapsed members) — research-gated, consult filed at silt-reviews/research/535-epoch-boundary-liveness-cliff-CONSULT.md; no unilateral fix. Attribution: docs/thinking/2026-08-23-535-boundary-wedge-attribution.md.

Added

The DEEP=1 exit-gate flow + chain-status prune visibility (ROADMAP Phase 3)flow_deep_heights (opt-in DEEP=1, DEEP_TARGET=128 default) drives the chain past the maturing drills to depth with three graded rows: the honest ceiling reaches the target inside a wall bound with the #525 freeze early-exit (a crawl/stall at depth is itself the Phase 3 finding, reported with measured cadence); the retention prune is confirmed ENGAGED on every validator from real persisted state; and the flow-5 convergence probe re-runs on the pruned chain (the slice-5 suffix-sync-around-the-gap property at depth, on the #528 suffix-append path). Supporting product change: silt chain-status now prints the payload-stripped block count, so an operator (and the harness) confirms the prune from chain.cbor rather than a debug log line. Per-height bounds reuse the #451/#525 topology-aware arithmetic (610 s at 12 seats, verified against flow 10's certified value). Design deliberation: docs/thinking/2026-08-23-deep-heights-exit-gate-design.md.

Added

#299 measured: the 1.5 MB bond answer is a parameter question, not an encoding one — Committed measurements (core/bond/answer_size_measure_test.go, verify_cpu_measure_test.go) decompose the answer: label-open blocks are 1264 KiB of the 1513 KiB total (64 opens x 5 x 4 KiB raw plot bytes); cross-open duplicate leaves are 0.9% at 64 MiB (the issue's dedup interim is refuted at scale); the Merkle multiproof union floor saves ~6% of the total; verify CPU is 1.8 ms/answer (batch verification not a cost center). The 10x levers (DefaultLabelSamples, BlockSize) are soundness parameters -> research consult filed (silt-reviews/research/299-label-samples-answer-size-CONSULT.md); Phase 3's multiproof/batch-verify tiers are deliberately NOT built on this evidence. Deliberation: docs/thinking/2026-08-23-299-answer-size-evidence.md.

Fixed

#528 — the h≈56 liveness knee: catch-up sync validates only the new suffix, never the whole chain — The RC run 0de4b96-64567 wedged at h57: every catch-up reconcile re-validated the ENTIRE chain from genesis in a throwaway replica (~1s per 1.5 MB reg block, on the event loop), so at accumulated MATURING reg weight one reconcile outlasted the round durations, starved sweep and round-change processing, and h57 never committed (198 ChainReply watchdog HANGs; deterministic 2/2 with run 94ef1e8-36901). Fix: a served window that provably EXTENDS the local committed head (finality active; the first new block's Prev chains from our head hash, which transitively commits our entire already-validated history) is adopted per window through the normal Append commit path — O(delta) validation, loop occupancy bounded by the #466 window byte budget — instead of reconstructFork + Reconcile's O(height) genesis replay. Every other shape (divergent fork, equal-height fork, legacy no-finality config, pruned gap) keeps the unchanged slow path with all reorg / equivocation-scan / finality-gate / ErrNeedCheckpoint guarantees. No consensus rule changes: adoption without a heavier pass is sound exactly because the finality gate makes an extension the only adoptable shape and each appended block re-proves a super-quorum commit. Measured locally: near-head catch-up on a 60-block heavy-reg chain fell from 280 ms (full replay) to 4.7 ms (suffix append), 60×; at field bond-verify cost that is the 40–60 s loop pin removed. New cost gauges ChainSyncSuffixAppends / ChainSyncFullReconciles split the two routes; deliberation in docs/thinking/2026-08-23-528-suffix-append-catchup.md.

Added

The #506 version gate: the per-identity reg-inclusion rate bound ships as a validity rule behind a BFT-native activation — The #503-certified R-rule is now enforceable: past the activation boundary, a bond registration is a valid block payload only if its identity is unslashed (R∞ — the Defect-A commit path closed structurally, beyond the #508 proposer filter) and its last committed reg is ≥ R blocks old (R = max(TTL/4, K+2), derived; first registrations exempt; one identity cannot register twice in one block; ValidateBondRegErr pre-filters submissions so an honest proposer never mints a block its own rule rejects). Activation follows the research certification (BIP9 schema re-based on silt's primitives): each bond reg carries a signed readiness byte (BondReg.Version, conditionally signed like Domain, hash-committed, prune-surviving — deliberately NOT on the attestation, which Block.Hash does not commit and any re-serving peer could strip); at each mature epoch boundary the frozen set's rule-aware WEIGHT (never heads — a cheap-bond cohort cannot fake-signal an activation) is tallied against the same >⅔ super-quorum finality uses; the first boundary that clears it locks in one-way and enforcement begins at the NEXT boundary (H_act, chain-derived, replay-identical on every replica, reorg-immune per #357 finality). Monotonic: a later ready-weight collapse stalls, never forks. The trusted pre-latch fleet declares the boundary as genesis config instead (Config.RegGateActivationHeight). Deliberate deviation from the certification's packaging: no v3 block tag is minted — versionSupported on pre-gate binaries is an exact set, so a v3-tagged block would hard-fork them at decode; enforcement is height-keyed (the certification's own Q2 form) and BlockVersionRegGate serves as the readiness threshold. Stated residual: if the fleet never crosses ⅔ ready weight the rule never activates and the #503 interim remains the fallback — there is no safe force-activation. Regressions (core/chain/reggate_506_test.go, ablation-verified RED three ways): the three-clause R-rule at a pre-latch boundary, weight-not-heads lock-in, boundary-exact enforcement (storm accepted at H_act, refused at H_act+1), signature-binding of the signal, prune survival, replay-derived H_act, monotonicity. Build record + deviations: docs/thinking/2026-08-22-506-reg-gate-build.md.

Fixed

Cloudtest harness: the #525 trio — a false-wedge fingerprint read, base-topology bounds graded onto a 12-seat rotation, and the re-drive clobbering the sheet (all three evidenced by coverage run 94ef1e8-36901) — (1) The #509 escape fingerprint is now TIME-SCOPED (jlog_since, journalctl --since @kill-t0), never a last-600-line window: economy sweep narration scrolled the survivors' round-change lines out of the window, so both samples read rc=0 ("frozen") while the journals showed the ladder advancing h38 r1→r3 — a manufactured WEDGE FAIL (the run's true verdict was #509's out-of-model GAP). Same unscoped-read class audit #303 closed for matches. (2) The 6-fault-tolerance tiers and the maturing-handoff drive bound are TOPOLOGY-AWARE: the certified 260s/575s and 220s-per-height figures price the 4-seat base rotation, but pre-epoch the (h+r) mod N designee rotation spans every bonded seat. Policy: one extra #451-priced escape rung (dur(r) = 2 + r(r+1)/2 sweeps × 30s) per 4 rotation seats beyond the base, added to the base constants — an N=4 sheet computes exactly the certified figures; the 12-seat MATURING sheet computes 650s/1445s and 610s/height (the run missed h57 by ONE block inside 9×220s while the latch itself tripped). The handoff drive also exits early once the ceiling freezes for a full per-height bound, so a real stall grades without burning the whole window. (3) ./cloudtest.sh run no longer truncates results.jsonl: run is the documented re-drive entry, and the truncation destroyed every previously graded verdict (the run's archived sheet held only the 3-row re-drive pass; the 14 first-pass verdicts survived only in the console log). The sheet now clears where a NEW sheet begins (all and up); a re-drive appends, and the report shows each pass's verdict. Rider: the PERSIST_NET marker .persist_net is git-ignored.

Fixed

Chainless registry lookups no longer block the event loop (#473) — the async pass — The remaining face of the concurrent-publish 502 class: on a chainless node (client mode, or a daemon on a remote registry) six loop-driven sweeps — Care, repairRoot, netGet, Audit, repair-claim judging, ColumnHolders — called ports.Registry.Lookup inline on the event loop, and against httpregistry that is a blocking HTTP round-trip holding the node's single thread for up to the HTTP timeout, per call, per sweep. New optional capability ports.AsyncRegistry: httpregistry runs the round-trip on its own goroutine and the node marshals the continuation back through the loop (AfterFunc(0) — walltime posts, sim enqueues); in-memory registries keep their sync Lookup via a fallback that still defers completion (the #467 contract: a continuation never runs on the stack that initiated it, error paths included). Regressions (core/node/registry_async_473_test.go, ablation-verified RED): a chainless Audit against an async-capable registry makes zero blocking lookups, the loop stays live while the round-trip is in flight (a timer fires mid-lookup), and the sync fallback defers.

Fixed

Cloudtest harness: persistent VPC, canonical region octets, and the preflight counts the natgw's address (harness-hardening items from the 2026-08-19 audit) — (1) ./cloudtest.sh net-up creates a long-lived network (terraform/network: VPC, canonical subnets, firewalls, Cloud NAT — own state, $0 idle) and PERSIST_NET=1 runs attach to it instead of creating and destroying a per-run VPC, saving those minutes every run; the run's destroy never touches it. (2) topology.py's region→octet assignment is now CANONICAL — a function of the region alone, never of which regions a topology subset uses (the old subset-relative numbering gave us-east1 octet 21 in a SMOKE but 22 in a full sheet — fatal for persistent subnets; the full-sheet assignment is byte-identical). (3) The IP-quota preflight now counts the natgw — its interface holds the masquerade external IP but the counter skipped it, undercounting its region by one; at the full SYBILS+MATURING+ECONOMY sheet's exact 8/8 us-west1 fit, that hidden margin was the whole margin. Verified with generated topologies against live quotas: the full coverage sheet fits every region's default 8-IP quota as-is (us-west1 8, europe-west1 6, us-east1 5) — no quota increase needed.

Fixed

Chain serve is windowed — a validator no longer marshals its whole chain into one buffer to answer MsgGetChain (#466) — The serve-side OOM driver measured on the 2 GB box (chain.EncodeBlocks marshaling the full bond-reg-laden suffix: 144 MB live / 98 MB retained / ~310 MB per encode at cloud heights) is closed: the server now replies with the longest block prefix that fits maxChainReplyBytes (EncodeBlocksUpTo, always ≥ 1 block so an oversized block still moves), and the requester (SyncChain's fetchFull) loops windows — advancing from each window's last decoded height, releasing each raw reply buffer after decode, terminating on the head probe's height (or the first empty window against a pre-window peer) — then reconciles the reassembled suffix through the unchanged validation tail, so a spliced or truncated window fails closed exactly as before. Two rulings from the PE approach review (RULING-466, 2026-08-22) shaped the shipped form: the window is derived, not a literalRequestSizeFloorBytesPerSec × requestSizeExtensionCap × ½ = 3.75 MiB at daemon defaults — and the requester arms a reply-sized deadline for MsgGetChain (the 30 s size extension keyed off the outbound payload, so a windowed reply was otherwise bounded by the base 2 s RequestTimeout, which no window near the floor can meet). Rollout is atomic with no capability negotiation: an old requester against a windowed server converges sweep-by-sweep (head-match termination makes silent under-sync structurally impossible; measured in the mixed-fleet drill), and a new requester accepts an old server's whole-suffix reply. Regressions: core/node/chainsync_window_466_test.go (deadline coupling, window derivation, serve bound, multi-window convergence, both mixed-fleet directions, splice fail-closed — ablation-verified RED). Requester-side chain retention is explicitly NOT closed by this (the #299/pruning axis). Design + ruling trail: docs/thinking/2026-08-18-paginate-chain-sync-design.md.

Fixed

The #467 recursion audit — five sibling continuation chains bounded, closing the PE's audit extension — The flixz stack-overflow crash itself was fixed by #471's walk-terminal trampoline, but the ruled follow-up scan ("no unbounded recursion / no re-entrant cycle between subsystems") had never run. Run now, it found five chains that still advance INLINE when a fast path completes synchronously: the repairStripes healthy-stripe walk (O(stripes) frames + an O(stripes × refs) rescan monopolizing the loop each sweep), FetchChunk's already-held fast path and fetchFrom's no-provider exit (fetch chains recurse O(ids) over fully-held or unsettled lists), repairTick's root walk over synchronously-skipped roots, distribute's dedup member skip, and — the class enabler — request running its callback inline on a synchronous send failure, which re-armed every "safe because it crosses a request" chain against a dead transport. All six sites now post their continuation through the loop (AfterFunc(0), the #471 contract: completion never runs on the stack that initiated it), and the stripe walk groups refs by stripe once, taking a large file's sweep CPU from O(stripes × refs) to O(refs). Failing-first regressions: core/node/recursion_audit_test.go (all five RED before the fix). Audit record, with the bounded-chain inventory so the next audit doesn't re-derive it: docs/thinking/2026-08-22-467-recursion-audit.md.

Fixed

TestMeasure_StoreChunkDrainRate no longer races itself (#507) — local -race ./core/node/ runs clean with no skip — The measurement's ack counter was written by the tcpnet readLoop callback and read by the test body unsynchronized; every local race run needed -skip TestMeasure_StoreChunkDrainRate, masking real signal for consensus-touching changes (build-immutable #2). Now atomic.Int64; the full core/node race suite passes unskipped, and the E5 measurement still reads clean (268 MB/s, cap/drain 0.955s under the 2.0s bound — consistent with the shelve verdict).

Fixed

The cloudtest fault-tolerance bound models escapes that START under load (#509) — two computed tiers with a progress-graded extension, and the wedge signature is now a FAIL — Seed f35a0f9-76780 GAPped 6-fault-tolerance when a healthy, advancing round escape (already at r1 pre-kill, sweeps stretched by the economy triple) outran the flat 260s bound, which models a 2-round escape from idle. The bound is now two-tier, both from the #451 arithmetic: the 260s expected tier stays the first check; a miss extends — while the survivors' escape fingerprint (round-change count + max height) demonstrably advances — to the r≤3 hard cap (dur(0..3)=18 sweeps ≈ 575s). Grades sharpen both ways: a commit inside the cap PASSES with the slow escape narrated; a FROZEN fingerprint across the extension is the wedge signature and now FAILS (previously an unattributable GAP); only advancing-but-uncommitted-at-cap remains a GAP, marked out-of-model. The knob was not raised — the model was completed (build-process rule 7).

Fixed

A repair bounty can no longer starve for want of a judge — rebuilt shards prefer holders outside the caretaker-judge quorum, and every verdict is evidence-carrying (#518) — A repair claim excludes both the paramedic and the named holder from judging, so a two-caretaker deployment whose rebuilt shard landed ON the other caretaker had ZERO eligible judges: the claim died silently and the bounty never paid (captured with the #519 narration: all four of a repair's claims naming the other caretaker as holder, quorum=2 — the last e2e flake mode, made likelier by #517 synchronizing the two caretakers' confirming sweeps). Placement now stably prefers civilian holders (preferNonJudges — preference, never veto: a shard on a judge still beats a shard nowhere; the self-hold path is exempt since claimant==holder is excluded once and the other judges still judge). A second captured sub-mode is fixed with it: a claim arriving moments after the repair-time fetch storm found the judge's survivor fetches transiently short (survivors fetched=2..5 of k=10, live-but-slow holders freshly negative-cached) and was denied TERMINALLY — emission is one-shot, so a 30s condition silently cost the bounty forever. The judge now DEFERS a transiently unjudgeable claim and re-judges after HolderCooldown (the duration of the very transient being waited out), bounded at 3 attempts, denying with the reason only when they exhaust. The verdict path also narrates end to end: every repair claim denied carries a reason=, deferrals name themselves, the holder retrievability challenge logs its outcome, a release that pays nothing warns escrow empty on this judge, and a verified-but-not-released verdict names itself. The sim bounty test's shared-ledger endowment is recalibrated: with starvation fixed every judge settles claims that previously died, and on the rig's one-shared-ledger wiring that double-draws the escrow the 5M prepay sat knife-edge at storm exhaustion (production per-node ledgers each pay once, by design). Regressions: core/node/prefer_nonjudges_518_test.go + core/node/judge_defer_518_test.go (a staged transient — survivors down at claim time, revived mid-retry-schedule — must end in a paid bounty; RED with the defer disabled); the capture arc rides docs/thinking/2026-08-22-517-repair-confirmation-gate.md.

Fixed

The repair trigger is minimum-filtered — one noisy probe sample can no longer fire a false repair (#517; roots the #514 e2e flake) — A caretaker's FIRST sweep after arming races record propagation: the captured #514 run read three never-lost shards as missing (reachable=18 while every shard had a live holder), "repaired" them from parity, and placed the rebuilds at the daemon's replication 3 — persistent, record-backed duplicates nobody paid to place (a third source of the #497 extra-copies census, after #500/#502 closed the other two), which then defeated the e2e kill-selector's premise: the "doomed" columns survived their holder's death, the caretakers correctly watched missing ≤ slack, and no bounty could ever pay (#514, ~2/10 under load). Per network-durability.md §3 (minimum-filter a noisy signal — never trust one sample) the repair and dispersion-re-spread triggers now require the over-slack observation to persist across TWO consecutive sweeps (a clean sweep resets; a firing that fails below-k retries every sweep without re-confirming), narrated as stripe repair pending confirmation. Costs one repair interval on a true loss. probeShard gains shard confirmed by= debug narration so every reachable verdict names its confirmer, emitRepairClaim warns repair claim found no eligible judge when a claim has nowhere to go (the newly-filed #518 judge-starvation corner: a 2-caretaker quorum whose rebuilt shard landed on the other caretaker), and the e2e gains a premise fast-fail (no over-slack observation within 60s of the kill → loud premise-defeat failure, not a silent 180s timeout). Regressions: core/node/repair_confirm_517_test.go. Capture story + attribution: docs/thinking/2026-08-22-517-repair-confirmation-gate.md.

Fixed

A restart mid-repair no longer orphans the survivor working set — Care reconciles it at boot (#502) — A repairing caretaker (or judging caretaker-judge) pulls up to k×stripes survivor chunks and drops them only in the post-reconstruction cleanup continuation; a restart in that window (operator, crash, or the harness's relaunch_with/econ_restore) killed the chain and nothing at boot reconciled — the pulls sat in the store forever: record-less bytes counting against the pledge and read as local by the next sweep's includeLocal probe (the plausible source of the persistent ~3× disk census on re-driven fleets that motivated #497). Care's warm-start continuation now runs reconcileWorkingSet: a LEAF of the cared root, present in the store, with no proof in the persisted backing is definitively an orphan (every legitimate leaf holding carries a persisted proof — MsgStoreChunk refuses proof-less shards, the repair self-hold and NetGetRetain mint one, and plain NetGet drops its working set — which is why #502 was sequenced after #500) and is dropped, narrated as repair working set reconciled. Warm-start manifest copies (held bare by design) are exempt; legacy proof-less NetGet leftovers from pre-#500 fleets are cleaned by the same sweep at their next boot. Regression with a REAL injected crash — an actual sweep stepped to mid-window between fetch and drop, then a fresh Node incarnation booted on the same chunk+proof stores: core/node/repair_orphan_502_test.go. Deliberation: docs/thinking/2026-08-22-502-working-set-boot-reconciliation.md.

Fixed

NetGet's pulls are an explicit working set, and the UI consumer==provider promise is wired (#500) — a fetch either drops what it pulled or retains it as REAL, discoverable, audit-answerable hostingfetchFrom writes bytes with no provider record (deliberate), and two callers retained what they pulled: NetGet kept the whole object forever — undiscoverable bytes counting against the capacity pledge (the #497 records-vs-bytes divergence; 55 chunk pulled vs 30 chunk stored in one economy drive, none discoverable) — and the UI /api/fetch consumer==provider path retained on purpose but never announced, so no fetcher could ever find the "provider". Now: NetGet drops its working set after assembly, success or failure (the repair-path paramedic discipline; chunks the node already hosted are never touched), and NetGetRetain converts the pulls into full hosting — each shard's StorageProof + PoR tags are minted from the manifest tree and the link's layout key (the retainer can defend an audit exactly like a MsgStoreChunk recipient — never host what a later audit can't defend), registered under its placement key via the existing repair self-hold primitive, and ANNOUNCED to the nodes near the key. Retained copies then ride the normal reprovide lifecycle. The UI fetch uses retain: a daemon that consumes a link comes out the other side visible in swarm holders and able to serve the object after every original holder dies (both asserted: core/node/netget_retention_500_test.go sim tier incl. serve-after-holder-death, e2e/netget_retention_500_test.go on real daemons over TCP). swarm get and the netcheck self-test keep the drop default. includeLocal durability semantics are now honest: the only record-less local copies left are in-flight working sets. Deliberation: docs/thinking/2026-08-22-500-netget-retention-semantics.md.

Fixed

The repair sweep is BOUNDED under dead holders (#501) — sweep-scoped corpse gating + a decaying dead-peer cooldown; the measured 3–4 min sweep drops to ~72s worst-case first discovery and the wire repair cycle to ~28s — A sweep's discovery walks paid a full retry ladder (RequestTimeout × 4 attempts ≈ 22s at daemon defaults) to every freshly dead peer, per phase, re-paying it mid-sweep each time the flat 30s HolderCooldown lapsed — a deterministic sim reproduction (daemon-faithful transport, field-faithful replication-1 placement) measured 159s for the first post-kill sweep and a 45s full-re-discovery sweep recurring every ~30s FOREVER, even on a fully healed object (corpses re-enter lookups via other peers' FindNode replies). Two cache-scope changes, no transport deadline / retry / eviction semantics touched: (1) a peer whose ladder exhausts anywhere in the current repair tick is skipped by every gated leg (walk, probe, fetch, announce) for the tick's remainder — one sweep pays at most one discovery ladder per corpse; (2) each successive exhaustion doubles the corpse's cooldown (30s → 60 → 120 → 240 → capped 480s, under the reprovide period), so the recurring re-discovery tax decays geometrically — any inbound message still clears the entry instantly (proof of life, #69), and the sole-candidate guards keep a lone holder probeable. Sweeps now narrate their phase timings (repair sweep complete … manifest-heal-ms probe-ms, repair pass complete … repair-ms total-ms), making any future slow sweep self-attributing in a run journal. The e2e bounty window re-tightens 600s → 180s (the fixed cycle measures ~28s on the wire; the deadline is the #501 regression signal again). Regression + measurement: core/node/repair_sweep_duration_501_test.go; deliberation + measured tables: docs/thinking/2026-08-22-501-sweep-duration-bound.md.

Added

Chunk write-path debug narration (chunk stored / chunk pulled / place attempt) — the #497 records-vs-bytes attribution instrument — Every path that writes chunk bytes into a node's store now names itself at -log debug: the MsgStoreChunk receiver logs chunk stored (chunk, sender, placement key, lease), a fetch-pull logs chunk pulled (chunk, provider — these copies mint NO provider record, the exact records-vs-bytes signature), and the placement client logs each place attempt outcome (a delivered-but-unacked store would be a silent extra copy; SILT_SWARM_DEBUG=1 narrates the swarm add/get ephemeral client to stderr). With it a disk census is attributable line-for-line to its writers — the instrument that attributed #497 in one LOCAL run: the publish is clean (30 files for 30 acks, records==bytes) and the "extra copies" are the repair sweep's transient survivor fetches plus retained NetGet pulls, none of which announce. The economy drill's premise is fixed on the same evidence: the pay window now covers the MEASURED repair cycle (sweep duration under dead holders is ~3-4 min — -repair-interval 2s bounds only the idle gap between sweeps), sized by ECONOMY_REPAIR_WINDOW_S (default 600s) with one journal-driven grace extension (ECONOMY_REPAIR_GRACE_S, default 300s) when the cycle is visibly in flight at expiry, and every verdict carries the post-kill sweep/repair evidence — a premise defeat (dead shards still "reachable") is now named as such instead of GAPing as a timing miss. Trail: docs/thinking/2026-08-21-497-records-vs-bytes-attribution.md.

Fixed

An F2-evicted identity no longer re-registers forever — the bond-renewal storm behind the island OOM is suppressed at every honest layer (#503, Q1 of the research certification) — A slash deletes bonded[id], which made BondRenewalDue read true FOREVER for the evicted identity: its daemon re-broadcast the full ~1.5 MB space-time proof every ~30 s sweep, no layer consulted slashed, and honest proposers committed the banned identity's registration as a fresh block each time, unbounded — ~35 MB/min of chain growth on the cloud sheet's equivocation island until the 2 GB box OOM'd (build-immutable #8; the fa501cc-56689 sheet's one FAIL). Certified fix, zero block-validity change (a mixed-version swarm cannot fork on it): the client backs off permanently once it observes its own slash and logs "permanently evicted" once instead of silently retrying (B5); a receiver refuses a slashed identity's submitted reg at arrival, before decode, by a map lookup; and the proposer's fold re-filters the queue so a reg that raced in before the slash landed is dropped as policy, not validity. The honest renewal loop then decays to quiescence on its own (~0.19 renewals per block-of-aging < 1, certified) — the TTL's height denomination and all four coupled security parameters (WS period, safetyDepth = 2·TTL, EpochBlocks ≪ TTL, BondRegHeadWindow ≪ TTL) are deliberately untouched (Q2 certified contraindicated). The structural close against an adversarial re-registrant — a per-identity reg-inclusion VALIDITY rule — is #506 (version-gated). Certification: silt-reviews/research/research-outcome/503-bond-renewal-storm-RESEARCH-CERTIFICATION-2026-08-21.md; deliberation: docs/thinking/2026-08-21-503-q1-fix-deliberation.md.

Fixed

A prepare-only equivocator is now selected for slashing — equivocation candidate-selection enumerates every signing role the verifier checks (#496)signers, which feeds FindEquivocations its candidate set, read proposer + Atts (precommit) but never PrepareQC (prepare), while VerifyEquivocation scans both. An era-2 double-signer whose signature in the honest canonical block sat only in the prepare certificate — the objective-mode equivocator at the genesis child, where the culprit is reliably prepare-only — was therefore never even tested by the verifier that would have convicted it: a real I5-accountability hole an adversary could aim at any height by choosing to be prepare-only. Found by the randomized field sheet (a height-1 double-sign went unslashed for a whole drill window while height 2 was always caught, run 1642465-57233); mechanism research-certified by execution before the fix (candidate-selection asymmetry — not a height floor, not delivery, not fork-choice; widening selection cannot manufacture a false slash because VerifyEquivocation remains the gate with its honest exemptions intact). Invariants: I5 strengthened, I1–I4 untouched (no commit/quorum rule changes — local detection only). Regression pinned born-RED by core/chain/TestFindEquivocations_PrepareOnlyCulprit.

Added

Cloudtest harness: contained equivocation island (runs every sheet), seeded flow randomization, LOCAL/cloud state separation, empty-response honesty, idempotent economy retry — A batch of harness-trust fixes, several provoked by a real self-inflicted incident this session (a LOCAL verification clobbered a live cloud run's shared nodes.json — see below). Design: docs/thinking/2026-08-20-equivocation-island-design.md + the local-first doc.

  • Equivocation island (flow_equivocation_island, 184-equivocation-island): the one destructive drill (a proven double-sign is a permanent F2 eviction) now runs on EVERY sheet, in a fully-contained separate consensus universe — 4 island anchors naming only each other, own genesis, NO external IP (Cloud NAT egress → zero IN_USE_ADDRESSES quota). Its slash consumes only the island's fault tolerance, never the main sheet's (the PE 2026-08-17 zero-FT-tail objection made structurally impossible), so it closes the skip-is-a-blind-spot gap the ruling left. LOCAL-verified green (real slash on the wire, height 1); terraform validated.
  • Seeded flow randomization (RANDOMIZE=1 default, SEED= to replay): the order-independent flows run in a seeded-shuffled order so no flow can free-ride on state a fixed predecessor left behind (the hidden coupling that shared FT_LAST_LINK hid). takedown and restart-content now self-publish (like chaos/durability already did), so they're truly order-independent. Fixed points pinned: warm-up first, destructive flows (soak/maturing) last. First fully-green LOCAL sheet (20/0/0) ran on a random order that placed takedown and restart-content BEFORE publish-fetch — the proof.
  • LOCAL/cloud state-file separation: LOCAL writes nodes.local.json/topology.local.json (never the cloud's nodes.json/topology.json), so a LOCAL run can NEVER corrupt a live cloud run's node map. Verified: a LOCAL sheet leaves a planted cloud sentinel untouched. This is the root-cause fix for the incident where LOCAL island verifications overwrote a running cloud sheet's map, breaking its ssh_node on zone=local and masking the real verdicts.
  • Empty-response honesty: an ssh_node that returns NOTHING (node unreachable / wrong map) is now flagged LOUDLY as a PLUMBING failure and scored GAP, in both ft_publish and the economy flow — instead of an empty parenthetical that read as benign latency (which masked the clobber for a whole cloud run).
  • Idempotent economy retry: the economy setup publish generates its payload ONCE before the retry loop (mirroring ft_publish), so a retry re-publishes the SAME root and picks up an entry that committed server-side after the client's fixed 10s registry timeout gave up (#441). The fixed 10s httpregistry client timeout itself is an owned product finding (build-immutable #5), proposed not shipped — see the thinking doc. LOCAL_PROOF parity is linted; the sever race and chaos premise-grading are fixed; re-drive loop (TEARDOWN=0 / FLOWS=) + dual commit stamp; nightly netem CI** (2026-08-20) — The local-first package (design + attributions: docs/thinking/2026-08-20-harness-local-first.md). Motivation: 20 archived cloud runs, zero fully green, with the recurring red concentrated in harness-quality rows — and the 1,600-line graded drive logic never executed anywhere but against billable VMs.
  • LOCAL=1 backend: one container per topology node (shims provide the systemctl/journalctl/sudo surface; binary + shims COPIED, never bind-mounted — a host edit of a mounted shim tears the container's view, which LOCAL's own first run caught), same static IPs on a docker bridge, ssh_node → docker exec. The SAME scenarios.sh executes: first full local SMOKE sheet graded 10 pass / 1 gap / 0 fail in ~8 min for $0, RSS telemetry included.
  • 184-partition sever race attributed + fixed: the sever was already correctly widened to all validator-role peers; the surviving GAP class was the BASELINE being read before the sever relaunch lands (seconds) on a chain committing drain blocks continuously — val-c "advanced during the partition" by committing in the unsevered window (run 2323b09: h27→h29). The flow now confirms the post-restart PARTITION banner, then baselines.
  • chaos-fetch / durability-turnover premise classifier (roadmap 2a): a fetch failing with root not in registry means the publish premise broke upstream (#441-family) — now GAP (UNTESTED), never FAIL; real mismatches still FAIL. Run B's two chaos FAILs were this shape.
  • Per-flow # LOCAL_PROOF: annotations + check_local_proofs.sh in CI: every graded flow names the local test that proves the same property, or an explicit n/a — <WAN-only reason>; the n/a set (2 flows) IS the owned cloud-only residue. Extends the #490 per-run gate per-flow.
  • Re-drive loop: TEARDOWN=0 keeps the fleet standing; FLOWS="…" ./cloudtest.sh run re-runs a named subset; reports stamp product AND harness commits so a harness-only re-drive is attributable. Convergence aid only — a grade stays one clean uninterrupted sheet.
  • Nightly netem workflow (.github/workflows/nightly-netem.yml): the adversarial + flakynet tiers get a standing gate — merge CI is clean-network and the GCP fabric is cleaner than the adverse internet these suites inject (build-immutable #5).
  • The economy wire grade now covers S7's FULL sentence — prepay → SKIM → bounty + the g sample (owner-directed, same day): flow 11b-economy-skim arms one shard-holder as a zero-prepay caretaker (the skim lands on the SERVING holder's per-node ledger, and the UI surfaces only cared roots), drives fetches that must route through it (replication 1 → sole holder of its column), and asserts funded > 0 — pure skim, unmistakable. First wire PASS: funded=98310 with zero prepay (previously the skim leg was sim-only). Flow 11c-economy-horizon records the payer's reserve/horizonSec/cost-per-repair as an observational row per graded run — the S7 g instrumentation trail ("the one number to instrument"), a series no single run can grade.
  • e2e/anchorstop_test.go (TestAnchorStopHaltsBondedNonAnchors) — the local twin of the cloud 5-sybil-no-capture flow: 3 anchors + 2 bonded non-anchor validators; baseline commits; ALL anchors killed → the bonded survivors commit nothing (the launch anchor gate, #402); anchors restart → the chain resumes. Green in ~60 s. (Its own failing first cut re-proved the #402 arithmetic: with A=2, -quorum 2 leaves one counting non-proposer attester and the baseline can never commit.) The maturing-latch e2e twin is the named residual (needs a >⅔-weight maturer regime design — docs/thinking/2026-08-20-harness-local-first.md).
Added

The LOCAL proof of the full S7 economy loop (e2e/economy_repair_test.go) + -repair-intervalTestRepairBountyPaysOnTheWire runs the whole Phase 2 Slice 4 integration on real daemons over real TCP: publish erasure-coded (one k=10/n=16 stripe at the cloud's 256 KiB chunk) → swarm holders → kill the holders of 3 columns (> RepairSlack) → a caretaker reconstructs from parity → a peer caretaker-judge verifies both legs and the bounty draws the object's escrow (paid > 0) → the file still fetches bit-perfect. This is the enforced RUN_LOCAL_PROOF for the confirming ECONOMY=1 cloud run (the integration run 2323b09-20931 GAPed on was never proven locally — build-immutable #7). Building it found three latent cloud-scenario defects that would have GAPed the re-run even with the publish fix (#489): the scenario armed ONE caretaker, but the paramedic never judges its own claim and credit is per-node-local, so paid lands on the OTHER caretaker's ledger; it funded 2,000,000 against the 500,000 starter grant (FundEscrow refuses); and its relaunched caretaker had no -registry, which silently disabled the care loop entirely. flow_economy_repair now arms two caretakers (the judge is the relay — outside the killable role set), funds both within grant, and polls both. New daemon flag -repair-interval (default 60 s, unchanged) mirrors -bond-audit so a local swarm's repair sweep — and this proof — fires in seconds. Design + the run-1 attribution honesty note + the one-shot-claim residual: docs/thinking/2026-08-20-economy-local-loop-design.md.

Fixed

-care with no registry now refuses to start instead of silently never caretaking — The care loop requires a registry to resolve the cared entry; without -registry or -serve-registry the daemon came up looking healthy while -care did nothing (the #235 silent-skip shape, one layer up — and exactly the no-op caretaker the cloud economy scenario armed in run 2323b09-20931). Regression pinned by e2e/TestCareWithoutRegistryRefusesToStart.

Added

silt swarm holders <link>: object shard placement made observable — Prints, per erasure column, the NodeIDs that claim to hold that column's shards (their DHT provider records under colKey). An operator uses it to see where an object lives; a test harness uses it to force a controlled reconstruction — killing every holder of more than RepairSlack columns drops that many shards from every stripe, so the caretaker must rebuild from parity (the deterministic trigger the cloud economy grade needs, which the sim had via KillColumns but the cloud lacked). New read-only node accessor ColumnHolders (resolves each column's providers via the same DHT walk NetGet uses; plants and stores nothing); uncoded objects report their per-chunk holders under uncoded. Tested over real TCP (e2e/holders_test.go — 16 columns, 48 holder entries resolved).

Added

§0.1 repair-path memory footprint measured locally (core/erasure/reconstruct_mem_test.go) — The research cert's §0.1 gate ("measure repair RAM at production chunk size before the economy-ON grade") is a single-node property, so it is measured locally for $0 rather than with a billable cloud run (build-immutables #6/#7: reproduce locally first; the cloud harness publishes at the 64 KiB sim size and would hide the spike ~1000× anyway). Result: reconstructing one DefaultParams (k=10, n=16) stripe holds 1.0 MiB resident at 64 KiB vs 1.0 GiB at the 64 MiB production minimum. On a 2 GB floor box that leaves ~1 GB, and the daemon baseline is 0.5–1.25 GiB (measured in field run 6a38d7b-42691) — so production-scale repair + baseline can exceed 2 GB → OOM. Consequence: the economy-ON field grade (Slice 4) must run on a larger box or land a streaming/column-wise decode mitigation first (build-immutable #8). Plan: docs/thinking/2026-08-19-cloudtest-harness-improvement-plan.md.

Added

-economy: the S7 repair-bounty payout enable — the keystone (Phase 2, Slice 1) — Turns the half-open economy fully on: an opt-in -economy flag (default OFF) under which a verified repair PAYS for a rebuilt shard from the object's own escrow. Per the PE ruling + research certification:

  • Protocol price, never an operator amount (an operator-set base is a lottery, not a price — it undefines S7's equilibrium and opens a censorship-via-underfunding lever), and relative to the erasure geometry: base = c × (k × shardBytes) (credit.RepairBountyBase), so re-tuning Evolving-tier erasure params re-prices repair automatically. c = 1 is research-certified (decode <0.1% of the fetch cost so no upward pressure; g-neutral; smallest floor-honest value; self-funds hot data, cold stays prepay-dependent per D-S7). Config RepairBountyBase int64 (absolute, test-only) → RepairEconomy bool; the settle path threads the repaired shard's byte size and is a true no-op when off.
  • Payee: (a-domain-fresh) — the paramedic that reconstructs a shard KEEPS it (becoming the paid holder) iff its own failure domain is unused by the stripe, funding the node that bore the reconstruction cost + the ~640 MiB–1 GiB RAM peak WITHOUT reducing failure-domain diversity (node.selfHoldEligible/hostShardLocally); else it places remote exactly as before. Chosen over paying the cheap holder (mis-attributes the price) and over unconstrained self-hold (trades away S2 dispersal).
  • Invariant A holds — the bounty moves balance only, never standing — asserted by the failing-first merge gate (core/node: release-pays-holder-never-standing, economy-OFF-is-a-true-no-op, default-OFF), plus the S2-safety gate (selfHoldEligible: self-hold only when the economy is on and the domain is fresh).
  • Owned residual: the reconstruction step is unfunded in the non-fresh-domain fraction (fork (c) split-pay is the evidence-gated fast-follow); and the repair-path RAM at production chunk size must be measured before the economy-ON field grade (build-immutable #8 — the 64 KiB sim hides the spike ~1000×). Rulings + cert: docs/thinking/2026-08-19-phase2-economy-on-deliberation.md.
Added

POST /api/fund: the durability endowment path (Phase 2, Slice 3) — A publisher/operator can now prepay an object's repair reserve from the daemon's own earned credit balance, so content outlives churn before it is popular enough to self-fund via the serve auto-skim. Wires the built-but-uncallable FundDurability to a token-gated endpoint that accepts a silt:/siltcare: link or a bare root hash plus an amount (credits), and returns the object's new reserve and the node's remaining balance. Status contract: 200 endowed, 402 insufficient credit (a client-correctable condition, not a server fault), 400 bad input, 401 without the bearer token. Standing is untouched — the credits come from serving and fund durability only (Invariant A). Tests: cmd/silt/fund_test.go (parse link/hash, endow-debits-balance, 402/400/401). Decision- independent of the pending RepairBountyBase ruling that gates Slice 1 (the payout enable). Deliberation: docs/thinking/2026-08-19-phase2-economy-on-deliberation.md.

Added

Durability telemetry: the S7 repair economy made observable on /api/status (Phase 2, Slice 2) — The economy runs half-open on a live daemon today: the serve auto-skim (1/8) already fills each object's durability escrow (node.go records RecordServeToObject), but the funded reserve, lifetime skim/pay, the funded horizon, and whether bounties actually disburse were invisible (credit.G/Horizon were computed only for a local repair decision, never surfaced). /api/status now carries a durability block: the node's credit balance (what serving earned), a bountyOn flag (whether RepairBountyBase > 0 so verified repairs actually pay — false by default, the half-open state named honestly), and per cared object its reserve / lifetime funded / paid / repair-count / projected funded-horizon seconds. New node accessors CreditBalance, CaredDurability, RepairBountyEnabled (loop-owned, read-only). Standing is never in this block — Invariant A holds (credits fund durability, never consensus weight). This is the prerequisite for watching g once the economy is switched on (Slice 1). Tests: core/node/durability_telemetry_test.go. Deliberation + the full Phase 2 slicing (and the one open decision — is RepairBountyBase a protocol constant or an operator flag?): docs/thinking/2026-08-19-phase2-economy-on-deliberation.md.

Added

RSS/memory-envelope telemetry in integration/cloudtest (Phase 1.3, evidence hygiene) — The field harness detected memory only as a binary crash signal (scan_node_liveness greps journals for OOM-kill / Go-fatal) plus an on-demand heap profile; there was no continuous RSS series, so the MATURING OOM "return-to-2GB" headline rested on the absence of a crash, not a measured ceiling (the fresh-eyes audit's finding — no committed RSS artifact backed the claim). Now every run samples each node's cgroup memory (systemctl … MemoryCurrent — the exact quantity GOMEMLIMIT and the OOM-killer act against) every MEM_SAMPLE_INTERVAL (default 30 s) into a rss-<RUN_ID>.jsonl, and scan_node_memory records an infra-node-memory finding with per-node peak / final RSS. Strictly additive and failure-tolerant (a missed read never affects a verdict); purely observational (S5 — it reports the envelope, the crash verdict stays with infra-node-liveness). The series is git-ignored by default and force-committed for any run cited as evidence, matching the console-log convention. Sampler + summary logic unit-verified locally; the artifact itself lands on the next real run. Design: docs/thinking/2026-08-19-cloudtest-rss-telemetry.md.

Added

The MsgSubmitBondReg CPU gate — per-sender submit budget + sender binding (Phase 1.2, the pre-#183 DoS floor) — The bond-renewal submit path had no rate limit: every well-formed, self-signed registration forces up to one VerifySpaceTime on the node's single loop (measured ~2–3 ms valid / ~0.5 ms garbage-until-reject at the field config on an M4 core — core/bond/ verifycost_bench_test.go, the new sizing benchmark), so one authenticated identity holding a pipe could keep the loop at a permanent duty cycle for free — the #424 bond-challenge CPU-DoS, one message kind over. Two cheap gates now run first: (a) allowBondSubmit — submits examined per sender per ChainSyncInterval window (burst 8; honest cadence is one submit per sweep while BondRenewalDue, so the budget clears honest traffic with wide headroom), charged BEFORE decode so a refusal costs a map lookup and a flooder gains no amplification; (b) sender binding — a submit is always the sender's OWN renewal (SubmitBondRenewal self-submits), so a reg whose ValidatorID differs from the authenticated transport sender is refused before any signature/proof work, closing the third-party replay hole (queue dedup sat AFTER the expensive verify, so replaying one captured valid ~1.5 MB reg re-paid full verification per message). Refusals are logged, never silent (B5); a refused honest submit heals by the existing resubmit-next-sweep retry, the same recovery as a WAN-skew refusal. Failing-first regressions: core/node/bondsubmit_gate_test.go (flood bounded to the budget, second sender not starved, budget refills on window turnover, third-party relay refused pre-crypto). Deliberation: docs/thinking/2026-08-19-bondreg-submit-cpu-gate.md — which also records the verify-cost measurement correcting E5's drain folklore (the ~100 ms figure was the prover; the verify is ms-scale by design), making the E5 drain-rate measurement rider the next step.

Changed

-inbound-cap sizing made two-axis legible; the v2b consensus-priority lane sequenced behind the bond-reg CPU gate as owned residual E5 — A PE-mandated timed drill (parked RED on drill/v2b-gate-starvation) showed that under a within-share sybil-cohort bulk flood, consensus-frame starvation lives in the single loop's FIFO drain (latency ≈ cap/drain, measured within 1% of the analytic prediction), not in gate admission — so the planned admission-side reserve alone is insufficient, and the severe regime is exactly the bond-reg/VDF slow-drain that the Phase 1.2 CPU gate bounds. Per the PE drain ruling the lane is sequenced, not shelved: 1.2 first, then a measured-drain re-run of the drill as the go/no-go for a single two-class priority-drain mechanism (with a bounded-priority second oracle so bulk/repair never starves — I4's storage-plane face). Recorded as owned residual E5 (docs/design/owned-residuals.md) with the reach-recipe; the -inbound-cap flag help now states the two-axis trade (OOM headroom vs worst-case ~cap/drain latency — a saturated 256M draining at 2 MiB/s is ~128s) so operators size for their expected-worst drain. Drill design + verdict: docs/thinking/2026-08-19-v2b-gate-starvation-drill-design.md. Resolved 2026-08-19 (the E5 drain rider): the measured real single-loop drain for the cheapest bulk a flood rides (MsgStoreChunk, real hash-verify + store handler over a real TLS transport — core/node/draindrate_measure_test.go) is ~1227 MB/s on an M4 core, ~600× the drill's hypothetical 2 MiB/s, so cap/drain ≈ 0.21s at the shipped 256M — well under the 2s bound. Both legs of the RED drill's slow-drain premise are gone (the bond-reg/VDF flood is now rate-gated by the Phase 1.2 CPU gate, and its cost was ms-scale to begin with), so the two-class priority drain is shelved (owned residual E5 updated; the drill stays parked as its merge oracle, #183 has the named target). One caveat owed: measured on M4, not the ~1-vCPU floor box — SHA-256 is hardware-accelerated on cheap ARM so the shelve is expected to hold, flagged as expectation-not-measurement.

Fixed

The concurrent-publish 502: a Care/NetGet event-loop self-deadlock — NOT the inbound cap — Under concurrent UI ingest (the first production workload's 4-worker segment flood) a validator daemon hard-failed publishes with 502 while surviving. Local repro + a control run attributed it: the inbound cap was innocent (unbounded cap fails identically; the always-on loop-saturation telemetry stayed silent). The 15s hang watchdog caught the real mechanism: every successful publish's -care-published auto-caretake ran Node.Care ON the daemon's event loop, and Care's synchronous Registry.Lookup — chainhost on a validator — marshals BACK onto the same loop and blocks awaiting a task the wedged loop can never run: a reentrant post-and-wait self-deadlock, 30s per publish (the chainhost timeout), starving every queued message behind it (placements blew their 4×2s attempts → "placed on no node"; entries outlived the 30s commit poll). Five core doorways shared the class (Care, NetGet via apiFetch, the repair sweep, the audit sweep, repair-claim verification). Fix: node.lookupEntry — a node holding a chain replica answers these lookups from its own committed chain (the very read chainhost performs), so no loop-context registry read round-trips through the adapter; chainless nodes fall through to the registry port unchanged. Failing-first at two tiers: the loop-reentry integration pair (adapters/chainhost/loopreentry_test.go) and the concurrent-UI-publish e2e (e2e/publishflood_test.go), which also covers the drive-by fix that -inbound-cap 0 (the documented "unbounded" sentinel) was rejected by the size parser. Mechanism record: docs/thinking/2026-08-19-publish-502-attribution-care-self-deadlock.md. The roadmap's Phase 1.1 note that the fairness/priority-lane work "also fixes" this 502 is corrected — that work stands on the PE ruling's adversarial case alone.

Fixed

P2P robustness under real streaming load — DHT recursion crash, reprovide (#69), fetch/discovery, cold-start — Surfaced running silt under a real HTTP-streaming workload (a 14 GB / 381 K-file store on a 2 GB box; the #464/#465 OOM fix independently confirmed to hold there). DHT walk recursion → stack overflow: the provider-resolution/repair continuation chain (announceAll, resolveProviders⇄probeShard⇄sweepProviders) fired its terminal callback INLINE when a walk converged synchronously (no live peers), piling up O(keys) frames over a large held set → fatal error: stack overflow (watchdog kill, blocked large-file publish); the walk terminal is now trampolined through the loop → O(1) depth. Periodic full reprovide (#69): provider records lease out at ProviderRecordTTL and AnnounceHeld ran once at startup, so a holder went undiscoverable ~TTL after boot; StartReprovide re-announces on a TTL/2 timer (a full re-announce, stack-safe atop the trampoline). Fetch/discovery: apiFetch serves locally-held content before the swarm and pulls drawn content onto the main node (consumer==provider, no ephemeral-node loopback leak); a public node drops loopback peer addresses learned via gossip (selfPublic-gated) so its book/resolution don't rot. Cold-start: the relay + registry listeners bind before the O(store) proof maturation scan (they need no proofs), so a public node's resolver-facing services answer immediately instead of being connection-refused for the minutes the scan takes; the scan logs progress.

Added

-allow-web-origin: opt-in browser transport onto the network — An off-by-default, exact-match CORS + Private Network Access allowance so a hosted resolver page can draw content from a viewer's LOCAL silt node (a cross-origin HTTPS→localhost request the browser otherwise blocks). Secure by default (empty list ⇒ localhost-only); the operator opts a specific origin in. One browser-facing transport; more are expected.

Fixed

Storage-node cold start: the O(store) proof scan goes fully async — startup never waits on it — On a real 14 GB / 381K-file store the startup proof reload (LoadProofs) scanned the WHOLE store (~8m45s) to rebuild its resident proofMeta index, blocking the daemon's startup sequence — growing with store size (a TB-scale durability node → tens of minutes). This is an availability regression introduced by the O(hot) proof paging (#464/#465): paging traded resident RAM for a startup scan. The listener-bind reordering above removed the scan from in front of the relay/registry; this completes the fix: Node.StartProofReload schedules the scan onto the event loop in bounded batches (clock.AfterFunc, 128 proofs/task) instead of running it inline, so the WHOLE daemon starts and serves immediately while proofMeta matures lazily in the background; the full proofs already page on demand (#464), so serving never waits for the scan, and an announce that races the scan self-corrects on the next reprovide sweep (#69). Every proofMeta write stays on the single event loop (no new locking). Surfaced by flixz's public-node load test. A persisted proofMeta sidecar (cold start O(delta), not O(store)) is the tracked fast-follow.

Fixed

The MATURING daemon OOM — an unbounded inbound message queue (a resource-exhaustion DoS), fixed with read backpressure (-inbound-cap) — Heap-profiled on the wire (e03f80d-heapprof): consensus nodes at ~1 GB RSS held ~500 MB of live decoded CBOR on the path tcpnet.readLoop → eventloop.Post → node.handle (252 MB in cbor.fillByteString alone; only 35 goroutines — no goroutine leak). Mechanism: readLoop decodes every inbound frame and Posts a closure capturing the payload onto the event loop's unbounded queue (Post "never blocks"); under load (big bond-reg blocks, gather storms, 20 peers) inbound decode outruns the single serialized loop, the queue backs up, decoded messages pin RAM → OOM-crash-loop. This is availability-under-adversary (a flood OOMs an honest node — the memory twin of the #424 CPU-flood), a security floor, not efficiency: bounded-then-fast. Fix: a bounded inbound-bytes admission gate (adapters/tcpnet/inbound.go) — the per-connection reader acquires a frame's bytes before decoding and releases when the loop finishes the message; over the cap the reader stops draining the socket → TCP flow-control pushes back on the sender. A fatal OOM becomes a survivable throughput limit (alive > crashed); the loop only releases, never acquires, so no deadlock; a lone oversized-but-legal frame is still admitted. -inbound-cap (default 256M; 0 = unbounded/legacy). v1 is a global budget that stops the OOM; per-peer fairness + a consensus-priority lane are the pending hardening before the red team (a flood could otherwise stall consensus behind the cap) — PE ruling. Plan + ruling: docs/thinking/2026-08-17-inbound-backpressure-fix-plan.md.

Added

The MATURING OOM return-to-2GB: rolling retention horizon, payload-selective pruning, and suffix-sync (H2 slices 1–5 — now ENABLED) — A validator's chain grows O(all history) in the ~1.5 MB space-time bond proof (BondReg.Answer) carried by every registration, OOMing a 2 GB box (build-immutable #8, the hobbyist floor). The fix bounds the RESIDENT heavy payload to a recent finalized window. Slice 1: RetentionHorizon = finalizedHead − 2·BondTTL, epoch-floored (research-certified safetyDepth). Slice 2: Opt-1 pruned-block representation (Block.Prune drops the heavy Answer, keeps header + consensus sigs, stores the pre-prune hash so a pruned block still hash-links and stays valid late-reveal slashing evidence). Slice 3 (this change): the Q2 gate — a pruned (Answer-less) block is trusted, and its space-time re-verify skipped, ONLY strictly below the node's OWN finalized/checkpoint anchor (trustFloor); at/above it is rejected (ErrPrunedAboveHorizon), and a pruned block still carrying an Answer is rejected (ErrMalformedPruned). During a Reconcile replay the floor is pinned to the RECEIVER's anchor (threaded into the throwaway replica), never the peer's fork — so a peer cannot skip verification to forge standing (a C1/M0 no-discount break). The Reload/own-disk path needs no gate change (it never re-verifies bonds; the stored hash covers the sig). Consensus-invariants: preserves I5 (a pruned block is still slashable), reads I3/I4's finalized anchor; no quorum-sizing/signing/fork-choice change. Slice 4: pruneBelowHorizon — the actual in-place shed of BondReg.Answer from finalized blocks strictly below the prune floor (max(2·BondTTL, BondRegHeadWindow+margin) below the finalized head, epoch-aligned; 0 without finality or with a degenerate BondTTL, the PE-acked over-prune guard). Because the durable store and serve path both read c.blocks, one in-place shed bounds resident, on-disk, AND served heavy payload. Still DORMANT — no production path calls it: enabling it changes how nodes sync (mesh catch-up is a full-genesis Reconcile, which the Q2 gate rejects against a pruned peer), so the PE gated enablement on the safe sync redirect. Slice 5 (this change) is that redirect, which enables the prune: mesh catch-up now suffix-syncs from a node's OWN finalized head ({Height: FinalizedHeight} instead of {Height:0}), prepending its own verified prefix so the existing genesis-rooted Reconcile (with its slice-3 trustFloorOverride pinned to the node's own anchor) accepts its own pruned history but never trusts a peer-served head — the C1/long-range guard the PE ruled (peer-served-head trust rejected as a Sybil break). A node behind by less than the weak-subjectivity window catches up around the pruned gap; a deep-cold node beyond it gets ErrNeedCheckpoint (obtain a recent -ws-checkpoint out-of-band or use an archive node) — surfaced, never silent (I4/S5). pruneBelowHorizon is wired into the commit path, so a validator sheds heavy proofs below its floor as finality advances — the line that returns the MATURING box to 2 GB. Consensus-invariants: I4 (catch-up-or-signal, both asserted), I3 (trusted set from the node's own finalized snapshot), I1/I5 preserved (no quorum re-sizing; equivocation still caught in the suffix, at/above the finalized head where forks can exist). Ablation-verified load-bearing; full core suite green. Plans + rulings: slice3, slice4, slice5.

Added

Daemon memory controls: -mem-limit (soft heap ceiling) + -debug-addr (pprof) — The MATURING field cohort OOM-crash-loops on 2 GB nodes, and it is NOT the PoR proof map (#464 shipped without moving it; the crash-looping nodes hold ~no chunks). Local + in-process probes show no leak — the signature is a large-but-bounded working set colliding with Go's default 2×-heap GC target on a small box. -mem-limit <size> (e.g. 1500M) sets runtime/debug.SetMemoryLimit so the GC reclaims before the kernel OOM-kills (equivalent to GOMEMLIMIT; the flag wins) — a memory-bounded head, not a hard cap. -debug-addr <addr> serves Go pprof (heap/goroutine) + dumps a heap profile to <store>/heap-<pid>.pprof on SIGUSR1, so the true consensus-node footprint can finally be ATTRIBUTED (the daemon had no heap profiling, which is why the wrong structure was first blamed). Both off by default. integration/cloudtest gains DEBUG_PROFILE=1/MEM_LIMIT= knobs and a ./cloudtest.sh heap <node> capture. Deliberation: docs/thinking/2026-08-17-oom-not-the-proof-map-attribution.md.

Added

#184 adversarial drills made DRIVABLE on the wire under the objective BFT model — equivocation (slash-on-detection) and partition-heal (stall-then-catch-up) — Both marquee attacks GAPped on every field sheet because the drills imported legacy-mode (-objective=false, quorum 1) assumptions that objective 3-of-4 correctly forbids. Fixed per three PE rulings, mechanism proven at the code level first (core/node/modelcheck_184_equivocation_objective_test.go, failing-first). Equivocation: a fork can never be COMMITTED onto a target under a BFT quorum (2-attestation single-target commit is quorum-short; a minority fork is an I1 violation), so the crime is signing two conflicting blocks at one height, not committing two forks. New PlaceConflictingSigned adversary primitive: a consensus-set validator participates honestly (its era-2 (round, prepare) signature lands on-chain), then SERVES a conflicting signed block at that slot (crafted GetChain/head-probe response); an honest peer fetches it on sync and FindEquivocations slashes the same-slot cross-fork prepare pair unaided, pre-Reconcile, never adopting the quorum-short loser. It runs on its OWN dedicated ephemeral net (e2e/equivocation_test.go, objective 4-anchor, over real TCP; netem via integration/adversarial) — the one destructive drill (a proven double-sign is a permanent F2 eviction) is isolated from the shared sheet, whose mid-sheet eviction would pin the commit requirement at 3-of-4 against 3 live anchors (zero fault tolerance). Partition-heal: a severed sub-quorum minority cannot commit, so on heal it CATCHES UP (a forward sync, dropped=0), it does not reorg — a droppable reorg would require a minority to commit a conflicting fork (the I1 violation model B forbids; the absence of a reorg line IS the safety property). The drill (integration/cloudtest + e2e/partition_test.go) severs the minority from the whole > ⅔ majority, drives the majority to commit a heavier chain, asserts the minority STALLED (anti-vacuity), then reconverges to the majority head (height + hash) on heal. Deliberation + the three rulings: docs/thinking/2026-08-17-drill-drivability.md.

Added

Publish client: the accept→commit poll re-derived for the #451 round durations (180s→360s) and made self-healing — the poll loop now RE-SUBMITS the entry every 30s — Run 82bcd2b-39478's durability-turnover GAP ("accepted but not committed within 3m0s") was honestly unpinned between discovery (#351) and a #441 mature-quorum residual. The pin, per the PE work order, came from two new deterministic model-check oracles over the 12-member mature fixture (core/node/modelcheck_441_publish_bound_test.go): (A) under steady renewal contention with delivery intact, an accepted entry rides the VERY NEXT committed block — three consecutive publishes — refuting the fold-starvation residual at the model tier; (B) with the fire-and-forget submit burst dropped, the entry strands in the accepting validator's mempool for a MEASURED designee-rotation wait (8 chain heights in the oracle schedule ≈ tens of minutes at the field's 220s/height escape bound) — unreachable by any single-shot poll. So the failure class is CLIENT-side liveness, not a consensus defect: publishPollTimeout was still the genesis-era 180s, BELOW the in-spec per-height worst case the #451 synchronizer durations imply (H_ESCAPE 220s; the harness's own re-derived PUBLISH_RETRY_S is 360s — same derivation, now one number), and the certified #441 design's drop-recovery lever ("the client's retry loop re-sends") never fired inside the window because the client submitted once and only polled. The re-submit is a mempool-dedup no-op on the happy path and the recovery lever on the lossy one (failing-first regression: a stranded entry that only a re-submission lands). Harness: a failed ft_publish now captures the VALIDATOR journals with the GAP verdict (82bcd2b's capture had only client-side nodes — the accept→commit window was unattributable, #7's capture-first rule), and the verdict text decomposes by the captured error instead of the #351-or-#441 disjunction. M1 note: the 360s bound shrinks by shrinking round durations (batching, #299), never by the client under-reporting them.

Fixed

Daemon OOM: bound resident PoR-proof RAM to O(hot), not O(total held chunks) — A node kept the full StorageProof (Merkle Path + per-block PoR PorTags, ~5.4 KB each) resident for EVERY hosted chunk, never evicted — so a disk full of content pinned proof RAM at O(total held) and crash-looped the whole MATURING cohort (field-corroborated). The full proof now lives in the durable proof store; the node keeps only ~80–100 B of resident METADATA per chunk (Root, Index, Total, Column — everything the existence checks, iterate-all sweeps, re-announce and denylist sites read without paging), and a new bounded LRU (adapters/proofcache, mirroring cachestore: byte-budget, write-through-no-warm scan resistance) pages the big fields in only to SERVE or AUDIT a proof. Resident proof RAM is now O(hot). A daemon wires proofcache over diskproofs (new -proof-cache budget, default 64M); sims keep an in-core in-memory backing (hexagonal core takes no adapter dependency, B1). The audit answer is byte-identical whether the proof was resident or cold-paged — a where-it-LIVES change, not a where-it-VERIFIES change (PoR verification, I1–I5 untouched). ports.ProofStore gains Get/Keys (per-id paging + one-proof-at-a-time startup reload, so LoadProofs is O(N) I/O but O(hot) RAM). Design + the two build refinements: docs/thinking/2026-08-17-proof-map-oom-fix-plan.md, docs/thinking/2026-08-17-proof-map-oom-build-refinements.md.

Added

#456 concurrent gather: prepares and precommits are broadcast-and-collected — a dead epoch member no longer taxes every proposal its full retry timeout — The two-phase gather asked attesters strictly sequentially, so each unreachable member's full transport retry budget (~34s at field config) sat on the critical path before the next attester was even asked: with a third of the epoch silent, every proposal paid ~270s (the B2 drill's twice-field-reproduced stall — runs ce15a80/1eded27 — after the #453 synchronizer was confirmed working via the catch-up telemetry). Per the research certification (456-gather-serialization): BFT tolerates f faults BY CONSTRUCTION — a correct protocol never waits for the faulty — and broadcast-and-collect-until-quorum is the universal shape; the sequential chain inverted it into f×timeout on every proposal. Both phases now send to every attester at once and complete on the SAME quorum predicate ValidateCommit demands (SupportMeetsQuorum: count + anchor majority + frozen weight); assembled certificates are copied at capture so a late reply can never mutate them. No certified property is order-sensitive (one gatherer per proposal; the QC is carried in the block, never re-derived) — #432/#402/#397/#389 untouched, the full oracle set green. Per-peer patient retry (network-durability §2) is kept; only its SERIALIZATION across peers — the flat-aggregate anti-pattern one level up — is removed: patient AND concurrent. Failing-first via the model-check's first COARSELY-TIMED oracle (the sim clock as the cost model, the certification's method fix): dead-first ask order, RED sequential (4.02s simulated = 4 dead × timeout × 2 phases), GREEN concurrent (0 simulated).

Added

syncTargets returns a deterministic (ID-sorted) list — a B2-determinism leak caught by the #451 fixture flaking under -count=10 — The list that drives gather ask-order, round-change broadcast order, and (in the model-check) the entire schedule was returned in raw map-iteration order: every gather's QC composition was a per-call dice-roll, flaking the #451 freeze-frame fixture 2-in-10 (the sybil author's prepare-QC landed on order-lucky subsets) and adding silent run-to-run variance to every field gather. ID-sort is safe here — ask order is not an inclusion-fairness surface (any assembled quorum is valid), unlike the reg/entry queues which stay FIFO per #448/#441; EligibleProposers already sorts for the same reason. The #451 oracles now pass 10× under the race detector.

Added

#451 view synchronizer: increasing round duration + responsive catch-up — round-based liveness gets its missing second half — The clean re-run's B2 stall drill re-proposed a carried lock across rounds 4–15 for 370 s with 8 of 12 members live holding >99% of the weight: silt had #432's locking (safety under round disagreement) but no SYNCHRONIZER (convergence into a shared round) — a fixed roundAdvanceSweeps let independently-skewed sweep timers smear the members across rounds forever, and a round-change recorded at a receiver never pulled it forward. Per the research certification (451-view-synchronization, adopting Tendermint/PBFT whole): (a) round duration now grows dur(r)=dur(r−1)+r·k in deterministic sweep counts — after GST the round eventually outlasts any timer skew or adversarial round-change smearing (red-team seam #7, closed as a bounded residual), the load-bearing guarantee; (b) a straggler JUMPS to the smallest round proven ahead by f+1 anchors (launch) / >⅓ frozen weight (mature) of recorded round-changes, or by a valid higher-round new-view certificate — catch-up at message speed. Neither ingredient changes which value a node may sign: locking, the proposer-prepare rule, and the #402 arithmetic are untouched (the I1/S1/S2 oracle set stays green). Failing-first per the certification's merge gate: the staggered-sweep oracle (pre-GST chaos — skewed sweeps, per-target prepare delivery, driver-fired request timeouts scattering the sign marks; post-GST — the certified convergence bound) is RED against locking-without-a-synchronizer and GREEN with both ingredients. Method fix recorded in the model-check canon: per-node round-advance skew is a first-class adversarial schedule dimension.

Added

Bond-reg queue goes FIFO-by-arrival — the ID-sort starvation behind the confirm run's 22-minute maturity stall — Run 54003f7-91159's latch missed its computed bound because the 3rd maturer's first-time registration sat in the designees' queues for 22 minutes: the reg fold sorted pending regs by validator ID, and with the byte budget admitting ~one plot-sized reg per block, ID order is a strict priority — the highest-ID submitter loses to ANY lower-ID renewal, every block, for as long as renewal traffic flows (the census: 49 ahead-skew refusals were the visible symptom; the queue acceptance was fine, the fold order was the starvation). This is the exact class the #441 certification closed for entries with FIFO (Addition 2: no fees ⇒ no priority order that can defer indefinitely), still live on the reg side — #429 had named it ("ID-sorted packing makes order seed-luck"). The queue is now FIFO by arrival with replace-in-place renewal updates (a resubmission refreshes bytes but keeps its position: renewing cannot queue-jump, waiting cannot lose seniority), and an over-budget reg keeps its seniority for the next block instead of being dropped. Proposer-side inclusion policy only — no validity or quorum rule changes (block-byte determinism needs only the single designee's own order). Failing-first: TestBondRegFIFONoIDSortStarvation (RED under the ID-sort — the high-ID first-timer never banks across four one-reg blocks — GREEN under FIFO).

Added

#441 publish starvation FIXED: entries are mempool content the designee's block carries — the certified operation-liveness mechanism — The first mature-regime field run committed ZERO publish entries post-latch across 33+ heights while the chain stayed live on drain blocks, and the first launch soak stalled a publish-contended height 361s past the computed escape bound: one root, the #432 round machinery's new-view seat AND its escape arming belonged exclusively to the bond-reg drain path, so an entry proposal could win no round of any height. Per the research certification (441-publish-starvation, 2026-08-16, direction A): a publish now SUBMITS its entry (MsgSubmitEntry, the MsgSubmitBondReg mirror — validate-on-arrival with synchronous refusal reasons, FIFO mempool dedup'd by root) and polls for finality; the single (h, r) designee's block folds pending entries under a byte budget SEPARATE from the reg budget (-max-entry-bytes-per-block — neither stream can starve the other), and pending entries arm the round escape alongside regs. Entries are content, never a competing value: locks/POL, the proposer-prepare rule, and the #402 arithmetic are untouched, and a forced (lock-carrying) re-proposal never folds. The born-RED starvation oracle plus five certification siblings (launch-face entry-only arming, adversarial-designee drop within the O(f+1) fairness bound — an owned residual, no permanent minority censorship — FIFO no-internal-starvation, entry-flood-vs-renewal budget isolation, and S1-with-entries lock-never-displaced) are all RED under the recorded fold+arming revert and GREEN with the fix. Bonus, pinned by the certification §7 discriminator: drain-alone commits at r0, so the field's every-height r1 escape (~95–155s/height) was entry contention — the fix recovers the r0 happy path. Legacy (-objective=false) deployments keep the direct propose path (no rounds machinery exists there to drive a mempool, and no drain contention either). I4's full statement is now operation-liveness — no legitimately submitted operation is permanently starved — recorded in the invariant map.

Added

BlockVersion (the mint era) flips to 2 — the era-2 follow-through promised by the #432 change — The rounds era shipped with the propose path explicitly stamping BlockVersionRounds, deferring the const flip as behavior-neutral test churn. Landed: production minting is byte-identical (the propose path already stamped v2; no non-test site builds genesis from the const), the att/bond-reg wire wrappers now carry version 2 (accepted by every era-2-capable binary — a pre-#432 binary cannot participate in an era-2 network regardless), and the 63 test files that hand-build era-1 blocks now declare Version: 1 explicitly — the honest form, since they exercise the still-supported era-1 validation rules rather than tracking the mint const. Full suite + race green.

Added

Node-level mature-epoch model-check fixture + the dynamic mature S1/S2 oracles — Closes the #432 certification's named residual (1): the S1/S2 merge-gate oracles ran the round machinery over the real node loop only in the launch regime. matureWorld (core/node) now drives a real 8-node network — 4 launch anchors + 4 bonded 64 MiB distinct-domain maturers, the field re-split shape — to a governed mature epoch entirely over held delivery (drain banks the maturers, they qualify by attesting, the everMature latch trips, the epoch boundary freezes the maturer snapshot), verifies the premise first (latch on every replica, anchors shed from eligibility, an anchors-only proposal REFUSED in the governed epoch, a 3-of-4 weight-quorum commit tracked everywhere), and then runs the certification schedules dynamically: S1 (a delayed >⅔-weight round-0 quorum must be carried forward by the lock rule) and S2 (a Byzantine maturer's weight-short forged lock must die at round-change verification) — both anti-vacuity-pinned (CommitRound == 1) and both RED under the recorded lock-free revert. This world is also the deterministic home for the open mature-regime r0-contention observation from run 09fbe60-84613.

Added

Drain-curve observability: commit lines carry the bond-reg count, and a refused renewal submit is attributable to WAN head-skew instead of reading as forgery — The committed-block lines (daemon banner and the node's structured block committed) printed entries and attestations but NOT how many bond registrations the block banked, so a field journal could not answer the drain curve's decisive question — which blocks carried which regs (the interrupted confirm run 09fbe60-84613 was misread as "blocks committing empty" while regs were in fact landing). All three commit lines now print the reg count. And the census of that run's 54 bond-reg submit REFUSED lines — every one a bare "signature" error across ≥7 honest validators — is the AHEAD-skew face of the #427 K-head window: a renewal is signed over the submitter's head, a receiver accepts only nonces of its own last-K COMMITTED heads, so a reg arriving ahead of the receiver's commit fails every window nonce and heals on the next resubmit sweep. The refusal line now logs the receiver's next height, the submit side logs the height it signed over (bond renewal submitted), and TestBondRegAheadOfReceiverWindow_refusedThenHeals pins the mechanism — refused while the receiver trails, the same bytes validate once it commits the head — so the field signature-refusal census reads as commit-propagation skew, not attack.

Added

#432 rounds + locking: the two-phase (prepare→precommit) gather with a lock-carrying view-change — the I4 liveness escape, research-certified, era-gated as block version 2 — The #397 height-only never-sign-twice watermark made a crossed 2-2 proposer race a PERMANENT stall of a connected, all-honest launch network (the field wedge in runs 9c3777d/8ae8326; TestModelCheck_I4_WedgedHeightMustRecover, born RED). Per the research certification (432-rounds-locking-liveness, 2026-08-15): consensus signatures are now (height, ROUND, phase)-scoped; a commit carries TWO quorum certificates at one round (the prepare-QC that justified precommitting, and the precommit certificate), each held to the full commit threshold in both regimes (launch strict anchor majority / mature >⅔ frozen weight — the POL threshold IS the commit threshold); validators LOCK on the highest-round prepare-QC (durable, mark-before-sign, restart-rehydrated); round advance is a deterministic sweep count (never wall-clock), and the view-change quorum carries the highest lock forward, forcing the next proposer to re-propose any potentially-committed value. Equivocation is round-scoped (same-(h, r, phase) double-sign slashes; a POL-justified cross-round re-sign is honest), and a committed era-2 block REQUIRES its author's round-scoped prepare — the structural ProposerSig's era-2 analogue, which is what keeps a double-proposal attributable (I5) while staying count-neutral in every quorum (#402 arithmetic untouched, tested byte-identical). Merge-gate oracles per the certification: S1 (delayed lower-round quorum) and S2 (equivocate-then-misreport, forged-lock injection) both RED against a recorded lock-free revert and GREEN with the prepare phase, plus per-(h, r, phase) restart I2 and §5.3 lock re-presentation. Era 1 blocks keep validating under era-1 rules — committed history is never re-interpreted.

Added

Never refuse silently: the two consensus refusal sites that hid the #432 wedge now log their reason — A peer-submitted bond registration that fails validation was dropped on arrival with no line (MsgSubmitBondReg receipt), and a drain proposal blocked at the proposer's own #397 sign watermark returned silently — so the #432 wedged-height liveness defect (chain stalled, cohort regs arriving-verifying-vanishing) was mis-attributed across three field runs (discovery/#351, CPU, staleness). The receipt path now logs bond-reg submit REFUSED with the exact ValidateBondRegErr reason (decode failures too), and the drain logs bond-reg drain BLOCKED at own sign watermark — at most once per 30s sweep, only while pending work is actually blocked, which is precisely the #432 wedge signature. One field observation now names this class (B5; build-immutable #7's instrument-first). The wedge itself is #432 (rounds+locking, research certification in progress) — this ships the observability ahead of the rule change because it is not one.

Added

MATURING topology re-split: 4 honest maturers + 4 Sybils, with COMPUTED harness windows (per the PE concurrence on the latch-premise finding) — The MATURING=1 SYBILS=8 field topology re-splits its 8 cohort slots into 4 honest maturers (non-anchor validators, full 64M bond, UNSET -domain — each an independent address-diversity group) + 4 single-domain MinBond Sybils, making the drill the on-the-wire confirmation of the I3 oracle's certified shape: the bar-2 latch is now REACHABLE (min(NakamotoOperators, NakamotoDomains) = 2 at full drain) while the cheap cohort alone still cannot mature the network — both pinned deterministically by TestMaturingResplitTopologyLatchReachable. MATURING=0 topologies (the P1 launch gate) are byte-identical. The harness windows are now computed bounds, not arbitrary wall-clocks (PE cadence ruling §4): the latch window derives from worst-case drain order (ID-sorted packing ⇒ 9 reg-blocks × 64s worst-case block time + a submission leg ≈ 630s), the publish window from the per-leg retry budget (8s × 4 attempts + backoff ≈ 34s/leg × the fresh-publisher leg census ≈ 240s) — and a latch miss inside the computed bound with the maturer cohort live now records FAIL (a finding), never a re-graded GAP (maturers down ⇒ honest GAP, preemption-shaped). The flow also records the drain curve (val-a's per-commit C2 lines: 64MiB jumps = maturer bonds) so the bound is checked against the real drain order, and the B2 capture drill now stops the maturers with the anchors (leaving honest weight up would fake a capture). Also fixes a second latent premise bug the finding exposed: the cohort-seated gate required participants ≥ 4+n_syb (=12) but the C2 participant count is NON-anchor bonds only (max 8 here) — now n_mat + n_syb.

Added

MATURING-drill premise repro: the 10-maturing-handoff latch is unreachable in the field topology as parameterizedTestMaturingFieldTopologyLatchUnreachable (core/chain) constructs the exact MATURING=1 SYBILS=8 parameterization at FULL drain — every bond banked, every participant generously granted attester status — and measures min(NakamotoOperators=3, NakamotoDomains=1) = 1 < 2, so Mature is false no matter how many regs commit: C2Metric excludes anchors by design (counting the scaffolding's own bonds to shed the scaffolding would be circular, immutable #3) and the topology's 4 validators are all anchors, while the only non-anchor cohort — the 8 single--domain Sybils — aggregates to ONE address-diversity group, which is the certified C2 discount doing its job. The two field GAPs ("latch never tripped in 420s") therefore had two stacked causes: the bond-reg drain staleness race (fixed) and this premise defect — the drill asked the maturity metric to be tripped by exactly the cohort it exists to refuse. Core behavior is correct and now pinned; the fix is a harness topology re-split (honest non-anchor distinct-domain maturers, the I3 oracle's certified shape), routed through a PE concurrence note before the re-run. Found on a laptop while deriving the principled maturity bound — before the billable run, not after.

Added

-loop-budget — emit the event-loop goroutine-budget decomposition at INFO for a diagnostic run — The per-window per-handler breakdown (from the eventloop instrumentation) is debug-gated by default so it's silent at steady state; -loop-budget raises just that summary to INFO so a load/diagnostic run captures the full per-handler CPU breakdown without the -log debug firehose — which, logged synchronously on the loop goroutine, would itself skew the very measurement. Slow-task, queue-wait, and hang lines are always-on regardless. The cloudtest harness threads it via LOOP_BUDGET=1.

Fixed

Bond-reg drain staleness (factor ii of the MATURING cadence wall): accept a reg over the last K committed heads — A bond registration is signed over BondRegNonce(prev) and was validated only against the current head, so the instant the head advanced a reg in flight went stale and was refused. Over a real WAN (a proposer proposes on head-advance before the resubmission arrives) this starved the drain — blocks committed empty below the #286 byte cap and maturity never reached bar-2 in-window (the instrumented run measured the goroutine ≤7% busy, so it was never CPU; this staleness race was the cause). validateBondRegs/ValidateBondReg now accept a reg whose proof validates against any of the last BondRegHeadWindow committed heads (default 8, deterministic walk), removing the one-head brittleness while keeping freshness bounded (K ≪ BondTTLBlocks, and continuous bond-audit re-challenges possession — so a released-and-replayed old reg still decays out; pinned by a beyond-window-rejected test). Paired with a durable pending queue — the proposer now keeps valid regs that didn't fit the byte cap instead of wiping the whole pending set and relying on a re-broadcast. Failing-first + the #406 model-check tier (I1–I5) green. The K-vs-anti-release bound is a C1 parameter flagged for a research security-check.

Fixed

#424 — bond-audit answer path was a remote-triggered CPU-DoS; add a per-challenger rate-limit — Answering a bond challenge forces a fresh sequential VDF-eval (an unpredictable nonce, so it can't be precomputed) on the node's single event-loop goroutine, and answerBondChallenge served one on every incoming MsgBondChallenge with no per-challenger limit — so one peer flooding challenges could pin a validator's thread and starve its token-issue/commit/sync handlers (red-team seam #7). allowBondChallenge now caps evals served to a single challenger per BondAuditInterval window (bondChallengeBurst=8), refusing the excess before the costly eval so a flood gains no amplification. The cap is per-challenger (not global) so a flooder cannot starve honest challengers of their own budget; honest cadence is one challenge per peer per window, well under the cap. This also caps the O(n) audit fan-out that is a suspect for the MATURING single-goroutine saturation wall (PR #423 instrumentation will name the dominant term). The exact cap borders the audit path — flagged in #424 for a research/PE confirm.

Added

Event-loop latency instrumentation — name the slow (or hung) handler from real load (Andrew's timing-for-evidence idea) — The single event-loop goroutine (adapters/eventloop) is the one serialization point, so it is the one place to see where the node's thread actually goes. Each task now carries a label (inbound deliveries by msg.Kind; timers/commit/api by a constant) and the loop optionally reports: slow tasks (SlowThreshold/OnSlow — a single task blocking the thread), a hang watchdog (HangThreshold/OnHang — a task still in-flight past a deadline, reported once with an all-goroutine stack dump of exactly where it is stuck), queue-wait (QueueWaitThreshold/ OnQueueWait — the causal signal: a task that executes fast can still blow a downstream deadline by waiting behind a saturated thread, so this ties saturation to the 8s request-timeout that execution-time alone would miss), and a per-window budget summary (SummaryEvery/OnSummary — count/total/max execution AND queue-wait per label = the goroutine-budget decomposition, cause and effect in one window, so the dominant handler is named from real execution, not a reconstruction; the summary is debug-gated, slow/hang/queue-wait are always-on). Adapter-only, zero-value = off (the sim's own scheduler is untouched); wired in the daemon via the existing logger. This is the observability the starved MATURING field run lacked (its per-issuer gather legs were debug-gated).

Added

#406 — consensus model-check, tier 2: the I5/#397 honest-never-slashed oracle — Over the real node loop + held-delivery: two proposers cross-attest-race one height in a WEAK config where both forks can commit (objective, no anchors, ByzantineQuorum off, Quorum 1, N=4 → no finality gate, no anchor gate — the pre-#402 baseline), and no honest validator is slashed: the #397 propose-time never-sign-twice watermark makes each proposer refuse the rival, so no honest node double-signs. Proven failing-first by controlled revert (removing the propose-time recordSign lets each proposer cross-attest → both forks commit → sync slashes both honest proposers via OnSlash — RED; with the watermark — GREEN). Resolves the deferral from the substrate PR: #402's I1 shields the both-commit fork in the normal objective tree, so the weak config is what makes it reachable. core/node/modelcheck_tier2_test.go. Test-only. With this + the #357 launch replay, the launch tier now has all four scars (#357/B2/#397/#402) failing-first — the P1-run gate criterion.

Added

#406 — consensus model-check, tier 1: the #357 launch replay (no reorg of a finalized block + fork-choice determinism)core/chain/modelcheck_i5_357_test.go asserts the invariant that closes #357: over adversarial competing forks (bare-genesis, shorter, equal-height, and even taller conflicting), a super-quorum-finalized launch block is never reorged (D-1), and fork-choice is order-independent (reconciling the same fork set in any order yields the same head — a pure function, never the height-blind hash tiebreak that dropped committed blocks to height 0). Proven failing-first by a controlled revert: forcing finalityQuorumActive false (the pre-#357 no-gate state) lets a taller fork reorg the finalized chain — RED; with the shipped gate — GREEN. Also asserts the #357 weight face (a committed anchor-attested chain carries nonzero fork-choice weight). Test-only.

Added

#406 — consensus model-check, tier 2: the I2-across-restart oracle — On the held-delivery substrate: a validator signs a block at height h through a REAL gather, then "crashes and restarts" (a fresh Node+chain on the same endpoint), and must refuse a competitor at h — the never-sign-twice watermark must survive restart (#397 Q1b; Tendermint priv_validator_state). Failing-first by construction: the identical scenario run with a NON-persisted mark (the pre-#397 crash-wipe) attests the competitor, so the in-test control proves the persistence is load-bearing and the assertion is not vacuous. core/node/modelcheck_tier2_test.go. Test-only.

Added

#406 — consensus model-check, tier 2: the simnet held-delivery substrate (adversarial delivery over the REAL node loop)adapters/simnet gains a test-only held-delivery mode (EnableHeldDelivery + Pending/Deliver/DropPending): Send parks each message so a driver fires them in an order IT chooses — the "adversarial delivery scheduler" the design specifies — off by default so every existing sim/e2e path is untouched, conformance-tested (adapters/simnet/helddelivery_test.go). core/node/modelcheck_tier2_test.go proves the substrate drives the real loop: a real proposer gathers a real quorum and commits + broadcasts to every replica entirely over driver-controlled delivery (no clock advance; request timeouts sit on the clock, which the driver never advances, so a gather completes purely by delivered replies). The adversarial invariant oracles that build on it (I2-across-restart; the genuine I5/#397 catch) are the documented next increment — the first I5 draft was withheld because it could not be shown failing-first: #402's I1 structurally prevents the both-commit fork the #397 slash needs, so the #397 fix is shielded by a different fix in the current codebase, and a genuine I5 catch needs a pre-#402 baseline (docs/thinking/2026-08-15-406-tier2-substrate-and-the-i5-honesty-catch.md). The only production code is the adapters/simnet held-delivery mode, which is inert unless EnableHeldDelivery is called (no existing path calls it), so all shipping behavior is unchanged.

Added

#406 — consensus model-check, tier 1: the I3 mature weight-quorum oracle (the B2 catch)core/chain/modelcheck_i3_test.go drives the REAL Chain to a mature epoch whose frozen epochSet holds the B2 imbalance — 3 real validators (distinct domains, 20 MiB) + 8 sybils (one shared domain, 2 MiB) — and asserts I3: a coalition finalizes IFF it carries a >⅔ WEIGHT super-majority, never a head-count one. The 8-sybil cohort is a head-count quorum (7 non-proposer attesters = bftThreshold(11)) but a weight minority (16 ≪ ⅔·76), so it is refused. Exhaustive over the finality-relevant space by equivalence class (finality is a pure function of proposer-type + #honest/#sybil attesters, so ~72 representatives cover all 2^10·11 cases). The setup is verified first (TestModelCheck_I3_SetupReachesMatureWeightedEpoch asserts the mature-epoch state + exact frozen weights before any oracle trusts it — the anti-#303 discipline; it caught a real proposer-never-seen setup bug during the build). Proven failing-first by a controlled revert of requireEpochWeightQuorum to head-counting → RED. Test-only; no production change.

Added

#406 — consensus model-check, tier 1: the exhaustive I1 launch oracle — The first rung of the deterministic adversarial consensus harness (docs/design/consensus-model-check.md): core/chain/modelcheck_test.go drives the REAL Chain finality predicate over an exhaustive enumeration of adversarial anchor coalitions (N∈{3,4,5} launch regime, +8 sybils) and asserts I1 — no two disjoint anchor coalitions may both finalize a block at one height (the invariant all four scars #357/B2/#397/#402 violated at their core). Proven failing-first: reverting the launch rule to the pre-#402 AnchorQuorum=1 makes it go RED ("disjoint coalitions [0] and [1] both finalize" — the exact #402 defect) in milliseconds on a laptop, GREEN under the derived ⌊A/2⌋+1. This is the cheapest-tier catch the testing-tiers assessment calls for; it begins the gate that ends "discover a consensus invariant by billable field run" (D-CONSENSUS). WIP — the mature/I3/I5 oracles and the simnet tier-2 (I2-restart) layer are the documented next steps; scope is stated honestly in the file header (S5). Test-only; no production change.

Fixed

cloudtest: the C2 no-capture flow's PASS verdict crashed on an unbound variable (P1 run b525b0b-87478), dropping an otherwise-green resultscenarios.sh:832 wrote the no-quiet-capture PASS with ($h1→$h2) — an UNBRACED $h1 immediately before the multibyte . macOS /bin/bash 3.2.57 under set -u absorbs a byte of the multibyte char into the identifier (parsing h1<0xe2>), which is unbound → the record crashed BEFORE writing, so the C2 drill — which behaviorally passed (sybils couldn't advance with anchors down; a driven block committed + synced when they returned) — recorded no verdict and dropped out of results.jsonl. Root-caused by reproducing the exact bash: h1�: unbound variable on bash 3.2. Fix: brace the vars (${h1}→${h2}); swept + fixed the one other instance (${cp}… at the WS cold-sync line). Regression guard integration/cloudtest/check_shell_multibyte.sh (a STATIC lint — the bug is bash-3.2-specific, so a Linux/bash-5 runtime test can't catch it) is wired into CI. Harness-only; no product change (the C2 property held on WAN).

Added

#402 — chain-tier repro attributing the launch anchor-gate fork (the field "CAPTURE" was a fork, not a Sybil capture) — Field run 4faaee8-22913 graded a flow-5 "CAPTURE"; the captured evidence + core/chain/fork_anchor_gate_402_test.go show it was a fork. The wheels-engaged commit gate requires only AnchorQuorum=1 distinct anchor attester, and the honest side commits at the bare count quorum (proposer + 2), leaving one free anchor; a Sybil-proposed competitor attested by that one free anchor passes the gate — while a zero-anchor Sybil quorum is still refused (ErrAnchorRequired), so C2 holds (no quiet capture). The residual is a launch-phase fork-creation / liveness vector. A second test shows AnchorQuorum=2 closes the same fork (the honest commit then holds 2 non-proposer anchors, leaving <2 free), naming the fix direction AnchorQuorum ≥ ⌈#anchors/2⌉ and its cost (launch commits need 3-of-4 anchors up). The fix is a consensus-rule change, routed to research (docs/reviews/fork-anchor-gate-402-RESEARCH-CONSULT-2026-08-14.md); tests-only here, no product change.

Fixed

#402 — the launch anchor gate is now a DERIVED strict anchor majority ⌊A/2⌋+1, structural in objective mode (research-certified consensus fix; encoding B) — Closes the launch face of the intersecting-quorum invariant (I1). Two parts: (1) anchor-only launch proposing — during the young window only anchors propose; a bonded sybil drains via MsgSubmitBondReg (submit-don't-propose, #397), removing the sybil-proposed fork at its source. (2) The commit gate now requires a strict anchor majority ⌊A/2⌋+1 (=3 of 4) counting the proposer-if-anchor, derived from len(Anchors) in objective mode independent of the -anchor-quorum knob — so a missing/low config can never disable quorum intersection. Attribution correction that drove the structural choice: the field run (4faaee8-22913) left -anchor-quorum unset (flag default 0 → gate inert), so the fork needed no free anchor — a two-sybil-signature quorum committed it. The consult's proposed AnchorQuorum=⌈A/2⌉ (=2) was insufficient: a both-sybil-proposed 2-2 anchor split satisfies it and the finality gate then cements a permanent conflicting-finalization partition. Legacy (non-objective) mode is unchanged (configured AnchorQuorum capture-prevention floor, no finality gate). Fault tolerance is the same 3-of-4 (1-fault-tolerant), now uniform and intersecting. Failing-first repros in core/chain/fork_anchor_gate_402_test.go (the 2-2 split; sybil-can't-propose; derived-ignores-config; 3-of-4 liveness); seam-7 equivocation test re-expressed at A=3 to keep its lone-culprit property under the majority rule. Certification: silt-reviews/.../fork-anchor-gate-402-RESEARCH-CERTIFICATION-2026-08-14.md; deliberation: docs/thinking/2026-08-14-402-anchor-gate-encoding-and-derived-threshold.md. Invariants: I1 (launch intersection), I3 (set = membership).

Fixed

cloudtest flow 5: the C2 resume clincher now DRIVES a block after restoring the anchors instead of waiting for a spontaneous one — Three runs GAPed "chain did NOT resume within 180s" after the anchors returned. Attributed from the captured journals (run 9b2198e-67673, the #396 evidence harness): the restored anchors were fully healthy — bootstrapped in seconds, standing back, bond challenges passing — but the chain is reactive (B6): every due renewal was drained into blocks before the stop, and renewal-due is HEIGHT-based so a frozen chain mints no new ones — the restored network was legitimately QUIESCENT, and the clincher's wait mis-graded healthy idleness as a liveness gap. (The pre-#397 drain over-proposed own renewals — an accidental heartbeat that masked this.) The clincher now restores the anchors and then drives a publish until it commits and the Sybil syncs it (the same drive-then-verify pattern as flow 10's B2 drills), turning the verdict into a driven verification. Harness-only; no product change.

Fixed

#397 — an honest proposer can no longer be slashed for a protocol-manufactured double-sign (research-certified consensus signing fix + the certified race closures) — The first evidence-instrumented field run (b88245d-3496) wedged at a 2-2 fork at height 6 with BOTH racing anchors permanently slashed as equivocators: proposeBlock signed a proposal without recording it in the never-sign-twice ledger (only attestations were recorded), so when two renewal-clock-aligned anchors proposed the same height, each found an empty ledger and honestly attested the competitor's block — signing two different blocks at one height; the cross-fork scan then correctly slashed both honest anchors on both branches and the anchor-quorum chain died (publishes failed with validators 4/4 reachable; restarting the anchors could not undo the committed slash). Research certification (silt-reviews/…/honest-proposer-cross-attest-RESEARCH-CERTIFICATION-2026-08-14.md) established the launch finality quorum was already intersecting (support-3-of-4; the double-commit was the bug-manufactured >f break, not a quorum-design flaw) and certified this fix set, all shipped here: (Q1) a proposal now enters the same never-sign-twice ledger as an attestation at sign time — (Q1b) replaced by a persisted monotonic {height, hash} watermark fsync'd BEFORE any consensus signature is released (adapters/markstore, the Tendermint priv_validator_state pattern; wired refuse-to-start in the daemon), so a crash/restart can no longer wipe the mark and let a validator contradict a signature it already shipped — permanent slashing (Q3, unchanged) is sound only with this; (Q2b) the two certified liveness race-closures: the drain takeover fallback now readmits one proposer per sweep by rank (was: every eligible proposer at once after 3 idle sweeps), and a non-designated proposer whose only pending work is its own due renewal submits it (SubmitBondRenewal, already broadcast every sweep) instead of proposing — removing the genesis-aligned renewal-clock collision that drove the field race; (Q4) two detection bugs: the local ledger slash is now idempotent-once per culprit (it re-applied and re-logged every ~2s reconcile sweep for as long as the fork stayed live) and a pending on-chain slash record now requeues until a commit confirms it (the requeue list was built but never appended to, so a slash whose carrier proposal failed to gather quorum was silently dropped). Failing-first regressions at unit + sim tiers: proposer-refuses-competitor, restarted-validator-refuses (crash variant), slash-idempotency, slash-requeue, submit-don't-propose, staggered takeover; the sim training-wheels test's doomed no-anchor attempt now uses an expendable proposer, mirroring its throwaway-attester convention (its old shape had the proposer double-signing height 1 — the exact #397 hazard).

Added

cloudtest: a scenario-level FAIL/GAP now captures its evidence before teardown (build-immutable #7) — Run beb3628-95860 (the P1 all-corners run) ended with two FAILs (9-cross-nat, chaos-reprovide) and a new sybil-resume GAP that were unattributable: journals were only captured for nodes that never came ready, the scenario console died with the terminal, ft_publish's per-call gap signal was lost in its command-substitution subshell (so 9-cross-nat graded FAIL where the honest verdict may have been a #351 GAP, with no recorded error), and the next run overwrites results.jsonl/report.md. Now: record snapshots the flow's involved nodes' service state + journal + debug.log into flow-evidence-<run>.log the moment a non-green verdict lands (nodes stashed by require_nodes/require_live or explicit flow_evidence_nodes); ft_publish hands its gap signal and last captured error across the subshell boundary by file, so publish_verdict grades honestly and the report names the mechanism; 9-cross-nat attributes WHICH leg died (publish vs fetch); the scenario console is tee'd to console-<run>.log; and each run's results.jsonl + report.md are archived under archive/ so a regression is distinguishable from a never-passed flow. Harness-only; no product change.

Added

#390 — the cross-NAT restart re-fetch (integration/nat, #69 phase) no longer flakes on a loaded CI runner — The RESTART=1 cross-NAT job REDed bimodally (~1-in-4, on docs-only commits too) at "content undiscoverable after restart". Attributed: reprovide-after-restart is a real, working product path (proofs persist to the proof store, reload on restart — reloaded storage proofs count=N — and AnnounceHeld re-plants provider records after re-bootstrap), but the harness waited for it with a fixed sleep 6 and then re-fetched ONCE. That is the magic-constant anti-pattern (build-immutable #5: wait for the condition, never a constant): under a loaded runner the re-announce + DHT propagation sometimes took longer than 6s, so the single re-fetch found no provider and false-FAILed. Fixed harness-side by confirming the reload then RETRYING the re-fetch on a ~60s bounded deadline — a genuine reprovide gap still fails after it (never masks a real #69 break; it only rides out a slow re-announce), the same "retry, don't guess a timeout" discipline the product uses. No product change. Green 5/5 locally (was ~1-in-4).

Added

#378 — the -equivocate red-team drill is now deterministically drivable under WAN delay (the local adversarial gate's entry criterion for the external red team) — The netem adversarial gate (integration/adversarial, delay 80ms 20ms) REDed BIMODALLY on TestEquivocatorSlashedOverTCP — the drill that proves a double-sign is caught and slashed over real TCP (#184 accountability). The property itself always held (the slash fired); the drill driver wedged, so a required gate flapped ~1-in-4 — corrosive (it trains "re-run until green"). Root-caused to THREE distinct placement wedges under warm-up jitter, each closed and each guarded failing-first: (1) ErrDupRoot re-placement — the old driver rebuilt the conflicting blocks at the LIVE head every retry, so once a leg committed and the culprit synced it back (head advanced), later attempts re-proposed the same deterministic root at a new height and were refused forever; fixed by PINNING the fork base + blocks on the first attempt and LATCHING each placed leg (the #345 live-tip win is preserved — the base is the tip AT PIN TIME). (2) Attested-but-not- committed — a target attests on the proposer's standing but commits on its OWN attestation's qualification, which warms up later, so a leg could attest yet ack the commit not-OK; latching on the round-trip alone then stranded the next block chasing an uncommitted parent. Fixed by latching a leg only on a CONFIRMED commit (proposeAndCommitTo now reports committed) and retrying an attested-but-uncommitted one. (3) Detection disqualifies the proposer mid-drill (found via wire diagnostics) — the moment an honest node holds BOTH forks it slashes the culprit, removing it from that node's qualified-proposer set, so any REMAINING placement there is refused "not yet standing" forever (the property firing wedged the drill). Fixed by placing the COMPLETE heavier fork [Y,Z] FIRST and the conflicting X LAST, so no honest node can see both forks until every leg is down; the slash then propagates asynchronously (the detector reconciles the heavier fork), which is the property under test. Harness-only (core/node/adversary.go); no honest consensus path changes. Regressions: two failing-first unit repros (equivocation_resumable_test.go for the pin, equivocation_uncommitted_test.go for the commit-confirmed latch — both red-verified against the old code) plus the netem gate itself, now 10/10 consecutive PASS under the exact bimodal-RED condition (was ~1-in-4 FAIL). This clears the external red team's entry criterion.

Added

MATURING cloud topology + field flow 10: the handoff/post-shed regime is now field-exercisable (§4 of the PE ruling; gates the external red team) — The base cloud topology never matures BY DESIGN (4 equal validator bonds → Nakamoto coefficient 2 < -mature-validators 4), so every prior field run exercised only the YOUNG anchor-gated regime — while the red team's sharpest target (brief seam #8) is the handoff and what follows. MATURING=1 SYBILS=8 ./cloudtest.sh now runs the topology that hands off on the wire: the maturity bar is set to the coefficient the 4 distinct-operator validators actually reach (2, at an explicit -operator-margin 1 — deliberate and disclosed, uniform across every consensus role) and the Sybil cohort bonds the MINIMUM (1M vs the validators' 64M) so the B2 drills price per-head cheapness the way the research certification does. The new flow_maturing_handoff (flow 10) grades, outcome-first: (10) the everMature latch trips on the wire (wheels shed permanently), commits cross the epoch boundary into the governed mature snapshot, and no anchor-required refusal appears post-shed; (10a) the B2 stall drill — the cheap cohort declines to attest (stopped) and the honest >⅔-weight coalition must still commit (head-counted quorum left this exact network born-unable-to-commit); (10b) the B2 capture drill — the cohort alone must not advance past the honest ceiling (ceiling read from the honest validators before stopping them, the #383 catch-up lesson; a real capture also requires a fresh cohort commit log; the frozen-weight super-majority refusal corroborates), with the clincher that the chain resumes when honest weight returns; (10c) WS cold-sync under the latch — a validator restarts pinned to a peer-published checkpoint: H:HASH, catches up, and comes back with the wheels STILL shed (a restart must never re-arm the anchors, F-1). Preconditions grade as honest GAPs (latch never tripped, cohort never banked), never fake passes; flow 5 (anchor-gate no-capture) self-skips under MATURING=1 since its premise — a network that never sheds — is deliberately absent. Runs LAST (it stops validators). Both topology modes dry-run-validated; the base topology is unchanged byte-for-byte with MATURING unset.

Fixed

Mature-phase quorum is now WEIGHT-counted — closing a cheap-member stall/capture seam at the handoff (B2, research-certified consensus change) — The research certification of the token-gather consult (Item B2) refused to confirm the drafted mature-phase residual and instead found a real break: post-handoff, commit quorum was sized by member count (bftThreshold(len(epochSet)), ValidateCommit counting distinct attesters) while epoch admission is deliberately unfiltered (#357 Condition A seats every qualified bond). Every MinBond identity riding an honest handoff therefore weighed one head: 8 cheap members among 4 honest validators made the mature phase born unable to commit (stall at 8×MinBond, nothing slashable — the cohort just declines to attest), and 9 made a cohort-only commit valid with zero honest attestation (capture at 9×MinBond, persisting into full maturity) — a C1-discount + C2-quiet-capture break, caught by the PE addendum + research escalation rather than the external red team. The fix adopts the settled pattern (B8 — Tendermint voting power, Casper FFG ⅔-of-stake): a mature-epoch commit's coalition (proposer + distinct qualified attesters) must now carry strictly >⅔ of the frozen epoch's bonded weight (requireEpochWeightQuorum; new ErrNoQuorumWeight), replacing the head-count escalation (RequiredQuorum keeps only the Config.Quorum count floor there). Quorum weight and fork-choice weight are now the same frozen quantity (epochSet). The §3 finality gate stays engaged under the weight rule (finalityQuorumActive: two >⅔-weight coalitions intersect in >⅓ weight, hence honest bond), and the proposer's gather asks the chain "is this support enough" (SupportMeetsQuorum) instead of counting heads. Launch phase is UNTOUCHED (fixed anchor set; the §2 repro stays green). Failing-first drills verified red against the head-counted code (capture committed, honest 97% weight coalition refused "3 qualified, need 8"): core/chain/quorum_weight_test.go — cohort-only capture refused ErrNoQuorumWeight; honest weight commits through a declining cohort; strict-⅔ boundary; plus the epoch Condition-A suites re-expressed in weight (frozen denominator across mid-epoch join and slash). The E4 owned-residual (bonded-minority stall) is now priced truthfully — its ⅓/⅔ claims were only true under weight counting, which research held it on. The §4 maturing-topology field flow gains the stall + capture drills as its sharpest red-team target (seam #8).

Added

Parallel publish-token gather — transport concurrent, signer selection unchanged (research-stamped) — The flagship privacy flow (a fresh ephemeral client acquiring a -token-quorum k blind-signed publish token) was failing over real WAN under load: every leg was sequential — issuer-key fetches, per-issuer credit mints, canonical-set discovery, and the k blind-sign round-trips — ~2·V+k WAN round-trips end to end (ft_publish FAILED after 120s in both SYBILS=8 runs). All four legs now overlap, collapsing the gather to ~3 round-trip times. The privacy boundary is unchanged and research-certified (token-gather-privacy-and-fault-tolerance- RESEARCH-CERTIFICATION-2026-08-13.md, Item A1): requests fire concurrently at the fixed network-canonical top-k signers and wait for that exact set — acceptance is a function of canonical rank + liveness, with per-issuer failures falling forward in canonical order, never first-k-of-N-to-reply (the forbidden variant that would stamp the publisher's network position into the token's revealed signer set, re-opening R-3); token Sigs are assembled in canonical order so the token cannot leak the arrival permutation structurally. Canonical-set discovery (FetchCanonicalIssuersFromAny) races all validators for a deterministic answer (privacy-neutral: who answers first changes nothing about what is answered). Issuance is now idempotent under transport retries (certification Item A2): a retry re-presents the SAME blinded serial (deterministic RSA-FDH ⇒ identical signature) and the issuer dedups both the signature and the charge/credit-spend keyed on the blinded-serial hash within the transport retry window — before this, a lost reply double-charged the legacy fee and was refused as a credit double-spend on the prepaid-credit path, failing the whole gather. Instrumented per-leg (token gather leg debug logs with per-issuer elapsed). Failing-first regressions at unit (issuer dedup: single charge, identical sig, credit-retry accepted, TTL-bounded) and sim tier (canonical set accepted under heavy reply-order jitter across seeds; canonical fall-forward past a dead signer; round-trips proven to overlap on the virtual clock; 25% loss ridden out at exactly k fees). The named prediction for the next field run: ft_publish and the ~6 cascading flows flip to PASS, and 6-fault-tolerance recovers (research Item B1's conditional close — if it does not, the latency attribution reopens).

Added

Attribution repro for the SYBILS=8 6-fault-tolerance GAP: quorum sizing is correct; the GAP is gather-latency under load, not a consensus bugcore/chain/TestFaultToleranceBranch_SybilBondsDoNotInflateLaunchQuorum reproduces the exact committed state (4 anchors + 8 banked single-domain sybil bonds, pre-maturity, objective, epochs on) and names the branch behind the field GAP: validatorSetSize=4 (the launch anchor branch fires — NOT the qualifiedCount=12 fall-through), RequiredQuorum=2, and a 3-of-4 anchor commit with one validator down passes in-process. So banked sybil bonds do not inflate the launch quorum, and the cloud GAP is a gather-latency effect under the 8-sybil load — not a quorum-sizing bug, no consensus rule change. Test-only; routed to research for concurrence (archive/reviews/token-gather-privacy-and-fault-tolerance-RESEARCH-CONSULT-2026-08-13.md).

Changed

#382 (M1) — chain-sync elides the whole-chain re-fetch when peers already agree (cheap head probe) — The first M1 efficiency change under the standing rule "trust stays green while cost drops." SyncChain used to fetch and re-validate every peer's ENTIRE chain every 30s sweep — O(chain × peers) bytes and CPU even when the whole network agreed (the dominant cost behind the participating-Sybil slowness, #382). Now each peer is first sent a cheap head probe (MsgGetChainHead → height + head hash); an identical head hash proves an identical committed history (a block hash commits its whole ancestry), so the full fetch is elided. It runs only on a real head difference — catch-up, reorg, or an old peer that can't answer the probe — where the unchanged full fetch + Reconcile + equivocation scan still fire. Trust- neutral by construction: every validity, reorg-detection, and slashing guarantee is unchanged; the probe only skips provably-redundant work. Backward-compatible (a peer too old to answer the probe falls back to the full fetch). M1 cost gauges added (Stats.ChainSyncHeadMatches / ChainSyncFullFetches — the chain-bytes-per-sweep signal, near-0 in agreement). Failing-first regressions: 5 sweeps against an agreeing peer do zero full fetches; a head difference still triggers exactly one full fetch and catches up; heads re-agree → elision resumes. Full node/chain/sim/e2e consensus+equivocation suites + -race green (trust-neutrality verified). A genesis-to-head block diff inside Reconcile is a further follow-up; this closes the dominant no-op-sweep cost.

Fixed

#303 — test-honesty audit: closed the 7 still-live positive-control gaps / confounds before the blind field test — Most of the 27 audited items were already fixed (PRs #304–312, #339); this closes the 7 that were still live, all in the integration harnesses (shell/compose only — no product code, go test ./... unaffected). Each fix makes the test FAIL if the property it claims to check is actually broken: the upgrade #69 finding is now gated on real reload evidence (reloaded storage proofs count + V1-had-no-persisted-proofs) so a HEAD-reload regression or mesh/decode confound can no longer masquerade as the ancient-V1 format boundary; consensus P2 replaces a vacuous "no reorg" grep (which passes trivially when the local fork is already heavier) with a real positive control (valA's own committed head byte-unchanged) plus an honest SCOPE note that inbound-fork receipt is not CLI-observable; cloudtest chaos-reprovide and restart-standing are scoped to the post-crash/post-restart boot via a new waitfor_since (--since @t0) so a stale pre-crash log line can't satisfy them; bond PHASE 2 gates the low-bond reject on an honest node first ACCEPTING a well-bonded proposal (-goodpropose), so a reject-everything node can't false-pass; retrieval adds a baseline cold-fetch positive control so a seed/registry saturation isn't misattributed to the #43 routing finding. Every newly-asserted string was grep-verified as real product output.

Fixed

C2 field flow reported a FALSE capture — a lagging Sybil catching up read as an "advance"; the property itself HELD — The SYBILS=8 run FAILed 5-sybil-no-capture ("the Sybil cohort advanced the chain 26→37 with all anchors down"). Root-caused: NOT a capture. The flow anchored its "ceiling" to sybil-1's own head (h0), and under the run's load (see the participating-Sybil slowness, #382) sybil-1 lagged ~11 blocks behind the true committed tip; when the anchors stopped, sybil-1 caught up via normal SyncChain to the anchors' already-committed blocks (26→37) and the flow misread that as a Sybil advance — the same catch-up false-positive class the local harness already fixed, never ported to the cloud flow. Chain-level proof the property holds in this exact topology: core/chain/TestC2SingleDomainSybilsDoNotMature — 8 equal single-domain bonds cap NakamotoBonds at 3 (< MatureValidators 4) regardless of domain or margin, so the network cannot mature, the launch-anchor gate cannot shed, and a no-anchor Sybil quorum is refused with ErrAnchorRequired (verified). Fix: the flow now (a) anchors the ceiling to the true committed tip read from the anchors before stopping them (a catch-up caps at that tip, so it can't be misread as an advance), and (b) requires a fresh Sybil committed block log for a CAPTURE verdict (a catch-up logs chain reconciled, never committed block) — a height rise past the ceiling with no fresh Sybil commit is now correctly classed as catch-up (GAP: property held, drivability masked by lag), not a FAIL.

Fixed

#338 cloud follow-up — the Sybil cohort ran a divergent quorum FLOOR that stranded it at genesis, so the C2 capture drill still could not be driven — The SYBILS=8 field run confirmed the drain + static-tier sync fix works (base validators reached tip 8, up from 3; every product corner green), but 5-sybil-no-capture still GAPed with the same signature ("sybil-1 never synced a committed chain, head 0"). Root cause, reproduced deterministically in-process (TestDivergentQuorumFloorStrandsSyncingNode338): the harness gave the Sybils -quorum 5 (a "self-majority"), while the anchors commit blocks at quorum 2. Config.Quorum is a hard FLOOR on ValidateCommit (max(Quorum, bftThreshold)), so when a Sybil re-validates the anchors' honestly-committed 2-attestation blocks inside Reconcile under its own floor of 5, every block fails ErrNoQuorum, the whole fork is rejected, and the Sybil is stranded at genesis — regardless of correct transport, static peers, and sync targets. In OBJECTIVE mode the "self-majority capture" is sized by bftThreshold over committed bond, not a config knob, so the fix is a uniform quorum floor across the objective swarm (topology.py: the Sybil role now runs the network -quorum, not n_syb//2+1). This both lets the Sybils sync (capture precondition met) and makes their capture attempt reach the real anchor gate (ErrAnchorRequired) rather than dying on a quorum count. The objective-mode quorum-floor footgun (a per-node floor above the network's bftThreshold silently breaks replica sync — arguably objective mode should ignore the local floor when validating committed blocks) is filed separately as a consensus-rule question, not changed here.

Fixed

#338 — an idle young objective network never drained its deferred bond registrations; a non-attester validator had no path to the committed chain — Two structural gaps behind the local nakamoto 0 bonds state and the SYBILS=8 field GAP ("sybil-1 never synced a committed chain — anchors hadn't banked the Sybil bonds; capture precondition unmet"). (1) Reactive bond-registration drain. Pending registrations (#336-deferred off the lean genesis, peer-submitted via the H2 renewal path) were only ever folded into publish/revocation proposals — on a young network with no content traffic nothing proposed, so no validator (anchors included) ever earned committed standing and maturity was unreachable. Now a proposer-eligible validator holding pending registrations (or whose own is due) proposes a BondRegs-only block on the chain-sync sweep — reactive (B6: fires on pending state, quiesces empty), budget-bounded (#286 L2b), guarded by never-sign-twice-at-a-height (the failing-first repro caught two anchors drain-racing one height, cross-attesting, and equivocation-slashing EACH OTHER into a wedged chain) and a deterministic designated proposer per height derived from committed state (chain.EligibleProposers, ids[height % n], absent-proposer fallback after 3 sweeps). (2) The configured persistent-peer tier is now a chain-sync target (syncTargets): a validator whose attester seed holds no chain-carrying peer (the cloud sybil cohort — attesters are only other sybils) and whose bond gossip hasn't warmed otherwise had NO path to sync or submit (configure-not-discover, network-durability.md §8); the cloudtest sybil role now configures -persistent-peers over the validator set. The local integration/sybil harness graduates from its scoped-down standing-gate form to the REAL C2 property: it now asserts the autonomous drain commits, the sybil syncs, a bonded sybil publishes through its own registry with the anchors present (positive control), and the capture attempt without anchors is refused by the anchor co-sign gate on a genuinely-bonded Sybil set — previously cloud-scoped, now local. Failing-first regressions at the node tier (TestIdleYoungNetworkDrainsPendingBondRegs338, TestSyncTargetsIncludeStaticPeers338).

Added

#357 Conditions A+B — the mature phase epoch-snapshots its validator set, and the young→mature handoff lands at a finalized boundary (research-certified) — Completes the #357 arc: the research certification conditioned its C1/C2 soundness ruling on two pieces of mature-phase machinery, both now built. Condition A — finality is quorum-INTERSECTION safety, which only holds when every super-quorum is taken over the SAME set; recomputing validatorSetSize/qualification live from the churning bond ledger (joins, renewals, TTL expiry) could let two conflicting commits each "finalize" against two different sets. Post-handoff consensus (quorum size N, attester/proposer qualification, attester fork-choice weight) now reads a per-epoch FROZEN snapshot of the committed bonded set (Config.EpochBlocks), rotated only at epoch-boundary blocks — each itself super-quorum-final under the §3 gate. Churn integrates at the next rotation (bounded by one epoch); the one live mid-epoch disqualification is a proven slash, which is shrink-only against a frozen N (can only raise the effective bar). Condition B — the anchor→bond weight-meaning transition is now the FIRST mature rotation: after the everMature latch trips mid-epoch, the anchors keep governing (eligibility, weight, anchor sign-off) for at most one more epoch until the finalized boundary sheds them, so bond-weighted fork-choice is rooted at an immutable base and can never reach back across the boundary. One-way both ways (F-1: neither the latch nor the handoff ever re-arms). Daemon: -epoch-blocks (consensus-critical, set identically across the swarm), safe-by-default for an untrusted objective validator (DerivedEpochBlocks 8 — ≤ ¼ of the derived bond TTL, so a mid-epoch-lapsed bond's vote outlives its TTL by at most an epoch); explicit 0 opts a trusted/demo swarm out (pre-epoch live recompute, unchanged). Failing-first regressions at the chain tier: mid-epoch join/TTL-expiry cannot move N, RequiredQuorum, or qualification; the handoff waits for the boundary (anchor sign-off still required between latch and boundary); a mid-epoch slash disqualifies immediately with N frozen; plus the certification's repro-ladder step-2 drain-window ordering test (a lagging replica converges by catch-up; a conflicting, heavier drain ordering is refused without dropping committed height; weight strictly monotone across the drain). Full go test ./... + -race (chain/node/sim) green.

Fixed

Field-test harness: the 8-takedown probe tested the wrong surface; the equivocation drill's refusals were silent — Two test-harness observability fixes from the M0-candidate field run's GAPs. (1) flow_takedown GAP'd on every run (denied= served=1) because its denial leg ran swarm get ON store-1 and grepped for a refusal — but swarm get is a short-lived CLIENT node that never consults the daemon's denylist and fetches from any other holder, so the grep could never match (audit-#303 class). The denial leg now asserts the surface -denylist actually gates: the daemon's own enforcement narration ("denylist: N root(s) denied…"), with the no-global-switch leg (store-2 serves bit-perfect) unchanged. (2) The -equivocate drill's retry loop swallowed every refusal, hiding a real wedge (#345 family, caught while certifying under netem): after a PARTIAL placement (X committed on the first target while the second hadn't yet qualified the adversary), every later attempt rebuilds the same adversarial entry root, which the first target refuses forever (root already registered) — the drill starves permanently and bimodally under WAN-ish delay. Each refused attempt is now narrated so a wedge is distinguishable from warm-up; the drill-design fix (atomic or resumable placement) is tracked separately.

Fixed

#357 §3 — the quorum-finality gate: a super-quorum-committed objective block is irreversible (research-certified, owner D-1) — Completes the ratified bond-weighted BFT model (research certification 2026-08-13). §1+§2 stopped the oscillation; §3 adds the safety guarantee: Reconcile refuses any fork that would revert our committed head (ErrPreFinalityReorg), so heaviest-weight fork-choice only ever adjudicates among DESCENDANTS of the finalized head — "reorg to height 0" is structurally impossible, and under a >⅓ partition a node STALLS rather than reorg committed history (owner decision D-1; the storage plane keeps serving throughout, D-2, so durability is unaffected). Finality is quorum-INTERSECTION, never bare depth (a depth cap lets two partitions finalize conflicting blocks — worse than a reorg), so the gate is gated on a real super-quorum: it engages only when RequiredQuorum ≥ bftThreshold (always true with ByzantineQuorum, the untrusted default). A trusted weak config (Quorum=1) has no quorum intersection — a lone equivocator can split the honest set onto two committed forks — so it keeps heaviest-chain reorg and its equivocation slash heals by adopting the heavier fork (this is why the Quorum=1 live-tip equivocation-slash path is unaffected). Realized on the existing WS-checkpoint machinery (a rolling finalized floor). The red-team fork-choice tests that encoded the old Nakamoto healing are rewritten to the BFT model: F6 now proves convergence by CATCH-UP (a behind replica adopts a longer chain that EXTENDS its finalized prefix; a conflicting heal required equivocation, which B slashes); F7 proves a cross-height double-backer is neutralized by FINALITY (the finalized fork stands, the conflicting heavier fork is refused). Ramp repro extended to assert the gate. Full go test ./... + -race (chain/node) green. Staged next (mature-phase machinery, research Conditions A+B): epoch-snapshot the mature validator set + a finalized young→mature handoff; the drain-window sim (repro-ladder step 2) and a BFT e2e partition-heal rewrite (supermajority commits / minority catches up).

Fixed

Fork-choice oscillation during the anchor→bonded ramp — §1 convergent weight + §2 stable quorum (#357) — The blind multi-region field run committed blocks then reorged them back to height 0; the local consensus sim passed (it only ever tested the mature regime). Research root-caused it (silt-reviews/research/.../357-...-RESEARCH-RESPONSE.md) and the owner ratified the bond-weighted BFT model (B). Two of the three ranked defects are fixed here (the third, the finality floor, is a staged follow-up): §1 — during bootstrap anchors are attesterQualified but contribute bonded[id]=0, so every fork's Weight was ≈0 and heavier fell through to its height-blind head-hash tiebreak — a genesis fork whose hash sorted lower thus displaced a committed chain. Fixed by (1a) crediting a qualified launch anchor a fixed bootstrap weight (Config.AnchorWeight, default MinBond) so an anchor-attested chain carries real, height-growing weight from block 1, and (1b) making the tiebreak height-aware (equal weight ⇒ the taller chain wins; head-hash only breaks a weight+height tie). Both are C1/C2-neutral: the weight is the sanctioned immutable-#3 training-wheels trust, vanishes at maturity (launchAnchor ⇒ false once everMature), and the mature-regime quantity (summed committed bond) is unchanged. §2RequiredQuorum was sized against the live-moving qualifiedCount, so it shifted block-to-block as registrations drained in and no fork held a quorum of a consistent set. Fixed by sizing against a stable validator set (validatorSetSize): the fixed anchor set during the young window (seeded at genesis — bftThreshold(4)=2), transitioning to the committed bonded set at maturity. Deterministic bootstrap-ramp repro (core/chain TestForkChoiceRampCommittedChainOutweighsGenesis357, research Ask 4) that FAILS before §1 (Weight==0) and passes after; full go test ./... + -race on chain/node green. Staged follow-up (owner review): §3, the rolling BFT finality floor (refuse to reorg below a quorum-committed block), completes model B but rewrites objective fork-choice semantics (Nakamoto-heal → BFT-final) and the red-team tests that encode the old model — held for review.

Added

Regression guard for the #288 evict-on-one-miss anti-pattern (P0-4)core/node TestLivePeerIsRetriedNotEvictedOnOneMiss locks the build-immutable #5 rule that a live peer must be retried, not evicted, on a single slow/dropped packet: a dead peer given RequestRetries=2 must be dialed 3 times (initial + 2 retries) before eviction, so re-introducing evict-on-the-first-miss (the shape that starved consensus under loss) turns this test red. Counting dials over a run-to-completion is timing-independent, so it can't flake. The internal/wanguard scope note now records that retry/evict are guarded by routing + this behavior test (they are semantic policy, not an AST-lintable construct). No product behavior change.

Fixed

Canonical issuer-set discovery falls through an un-synced validator (#351, P0-2 residual) — A chainless publisher (silt swarm add) picks its publish-token signers by a canonical, ledger-ranked ordering it fetches from a validator, so the signer subset isn't a per-publisher quasi-identifier (R-3 / seam-4). It asked only validators[0], so a single un-synced or transiently-unreachable validator — e.g. one that just restarted mid-run (#351) — dropped the publisher into the -peers fallback, which narrows the publisher anonymity set. Added Node.FetchCanonicalIssuersFromAny, which tries each validator in order until one serves the set (cmd/silt/swarm.go now uses it). The canonical ranking is deterministic — every chain-holder computes the same bond-ranked order — so asking a different validator returns the same answer: pure liveness/anonymity robustness, no change to selection, consensus, or the privacy claim. Regression: core/node TestFetchCanonicalIssuers_ReturnsLedgerRankedSet now also asserts FromAny skips a chainless validator and returns the chain-holder's ranked set. Scope (V4): this closes the canonical-set half of #351 only; the token-acquisition-after-restart path (reaching enough signers for the token-quorum when one is down) is a privacy-sensitive residual that needs a deterministic repro to pin the failing stage — tracked, not addressed here.

Fixed

Provider-record lifecycle — age out departed holders so the repair/fetch loop stops re-dialing corpses (#277, P0-1) — The principal-engineer substrate plan (P0-1) targets the dominant durability/retrieval wound: the #277 dial-storm. Attribution first (build-immutable #6), which corrected the audit's hypothesis: the DHT walk is already deadUntil-gated (PR #355) and signed provider records are already expiry-filtered on read (acceptedProviderIDsVerify). The genuine residuals were three: (1) a confirmed-dead holder's provider record is never removed, so deadUntil only rate-limits the re-dial to one full RequestTimeout per HolderCooldown (30s) forever — the persistent dial-storm floor; (2) the re-serve path (MsgGetProviders) handed out stale records with a raw Get, propagating the corpse to whoever asked; (3) the announce/re-announce path was the last provider-record consumer not gated on deadUntil. The loop drowns in dials to departed holders because their records outlive them in every consumer; fixed by giving the store a lifecycle and gating the last consumer. Adds dht.Providers.Live (filter expired on read — used by the re-serve path), Evict (per-RepairInterval age-out sweep in repairTick), and RemoveIfNotSole (prune a confirmed-dead holder from every key with a live alternative, called when retries exhaust and the peer is negative-cached) — the last keeps a SOLE holder's record so its content stays discoverable and re-probeable (#69/#226) rather than orphaned. Also gates the announce path on deadUntil. M1 baseline instrumented now (the efficiency gate starts in P0): two gauges, Stats.HolderDialsSkipped (dials avoided by the negative cache — the dials-per-fetch/repair bound) and Stats.DeadProviderRecordsPruned (records aged out). Failing-first regressions: core/dht unit tests for Live/Evict/RemoveIfNotSole (incl. the sole-holder-kept case), and core/node TestConfirmedDeadHolderPrunedFromReplicatedKeptForSole (verified fail-before / pass-after). Full go test ./... + -race on core/node/core/dht green.

Changed

Corrected two materially-false public claims flagged by the principal-engineer rescue audit — A read-only fresh-eyes audit found the public site overclaiming two M0 corners beyond what the canon and code support. (1) marketing/anchor-launch.html claimed a published root is unlinkable to its authorizing identity "at any layer an observer can watch" — the exact transport/metadata layer the code does not cover (and it contradicted website/index.html, which already admits a transport IP+timing link remains until issuance-mixing ships, Pre-V1). Softened to the refuse-to-surveil / access-held-in-tension wording already ratified in TENETS immutable #4. (2) README.md, ROADMAP.md (→ generated website/roadmap.html), and website/index.html claimed the storage plane is "field-proven at scale" — but "at scale" is the deterministic in-process simulation; no warm multi-region cloud run has graded a full suite end-to-end. Changed to "sim-proven at scale, field-proven cross-network at small scale," keeping every true claim (the CI Docker NAT/hole-punch evidence is genuine). Docs-and-copy only; no behavior change.

Removed

Deleted a stale 13 MB prebuilt binary committed to git and a dead exportintegration/flakynet/silt (a committed aarch64 binary) is removed and gitignored — the flakynet harness already go builds the daemon from source at run time, so the checked-in binary was a supply-chain smell and a risk of "field-proven" silently certifying a stale build. Also dropped the fully-dead core/bond/bond.go PlotSeed export (zero callers). No behavior change.

Fixed

Provider diversity sweep honors the dead-peer negative cache — closes the last ungated resolve leg (#277) — The deadUntil negative cache that stops silt re-dialing a just-timed-out peer was consulted on the DHT distance walk (node.go), the fetch path (file.go), and the repair probe (repair.go) — but not on the domain-diversity sweep (sweepProviders, dht_diversity.go), the second leg resolveProviders runs whenever DHTDomainCap > 0. Since the daemon and client both set DHTDomainCap = 2, the sweep runs on every provider resolution, so under churn a departed holder still in the routing table was re-dialed at a full RequestTimeout on every resolve — a contributor to the #277 repair/retrieval dial-storm (a caretaker sweeping a churny swarm "drowns" and never finishes a sweep). Fixed by gating deadUntil in sweepProviders exactly as the other three paths do (a sweep is breadth discovery, so a cooled peer is simply skipped — no sole-holder concern like the fetch path's #69 anyLive guard). Surfaced by the 2026-08-12 blind field test, which also exposed a unit-test blind spot: the existing dead-cache tests set DHTDomainCap = 0, isolating away the exact leg the daemon always runs — so a new failing-first regression (TestProviderDiversitySweepSkipsCooledPeer) engages DHTDomainCap = 2 (FAILs "got 1 dials, want 0" before, passes after). Closes one named, verified leak on the resolve side; not the whole #277 envelope. Follow-up: gating the sweep would have blacklisted a recovered holder still inside the 30 s cooldown (breaking #69 cross-NAT restart survival — the deadUntil cache was only cleared on a successful bootstrap dial). So a message is now proof of life: receiving any message from a non-ephemeral peer clears its deadUntil entry, cleanly separating a departed holder (sends nothing → stays gated, dial-storm fix intact) from a recovered one (restart + reprovide, or a NATed peer now reachable via the relay → un-gated at once). Guarded by TestInboundMessageClearsDeadCache and a green RESTART=1 integration/nat.

Fixed

Equivocation red-team drill double-signs at the live tip, not a stale height 1 (#345) — The 184-equivocation field-test drill FAILed on the #286 GCP re-cert ("no slash line within 120s"), while the same property passes in-process (#204). Root cause: Node.Equivocate (the -equivocate red-team harness) hardcoded the double-sign at height 1. On a fresh chain that is the live tip and slashing fires; but the cloud drill runs after the warm-up has committed several blocks, so a height-1 double-sign is stale — an honest target refuses to attest a proposal that is not at head+1 (ValidateProposal), so proposeAndCommitTo fails, the conflicting forks are never placed, they never enter fork reconciliation, and nothing is slashed (the adversary never even logged "equivocation complete"). Fixed to double-sign at the current uncommitted tip via chain.Head — backward- compatible on a fresh chain (tip = genesis ⇒ height 1, so the in-process slash test is unchanged) and live on an advanced chain. Also pointed the cloud drill at the direct detector (val-b, which holds fork X and catches the double-sign the instant it syncs val-c's heavier fork) instead of val-a, which only sees the slash after on-chain propagation. Guard: TestEquivocateAtLiveTipSlashesOnAdvancedChain345 advances past height 1, runs the real Equivocate path, and asserts the culprit is detected + evicted — it FAILS on the old height-1 code ("target refused the proposal at height 1"). This is a red-team harness fix; the product's equivocation detection + on-chain eviction was already correct.

Fixed

Distribute/repair proofs are O(log n) too — same cached Merkle tree (#340) — The sibling of the bond fix, on the storage plane: distributeFrom (file.go) and repairStripe (repair.go) build a Merkle inclusion proof per shard so hosts can answer storage challenges — each via the standalone manifest.Prove, which is O(n) (it rehashes subtrees). Proving S shards over an n-leaf manifest was therefore O(S·n) ≈ O(n²) on the node loop — for a large file (thousands of leaves) that is seconds of on-loop proof work during distribution or a repair sweep, exactly the kind of loop stall the bond compute layer exposed (#286). Fixed by building one manifest.Tree per distribution/repair and drawing every per-shard proof from it (O(log n) each); it also replaces the redundant m.Root (a second full O(n) MerkleRoot) with tree.Root. Proofs are byte-identical (TestTreeMatchesStandaloneProve), so the storage-proof and PoR-tag paths are unchanged — verified by the full node suite (distribute + proof-verify + repair) staying green under -race.

Fixed

Bond proof answers are O(log n) again — cached Merkle tree (#286 compute layer, #340) — The confirming 3-region GCP run reopened #286: after the network layers (L1/L2a/L2b) genesis still wedged at height 0 because bond proof-of-space-time compute saturated the single consensus loop (B2) — decisive proof: bond 64M→2M dropped CPU ~90%→~3% and genesis committed instantly. A pprof isolation pass (build-immutable #6, root-cause before you patch) found that the per-challenge answer also scaled with plot size, contradicting the assumed size-independence — and pinned why: manifest.Prove was O(n), not O(log n), because auditPath recomputes merkleTreeHash over half the leaves on every call, and AnswerSpaceTime draws O(k) proofs, so each answer cost O(k·n) and grew with the bond. On a 64 MiB plot one answer took ~743 ms (and RegisterBondReg rebuilt proofs on every propose retry). Fixed with a precomputed manifest.Tree cached on the bond Commitment (built once in Seal/Reconstruct), so each inclusion proof reads cached subtree hashes in O(log n): the same construction, byte-identical root and proofs (guarded by TestTreeMatchesStandaloneProve across every leaf count) — no proof-param change, C1-neutral. Measured effect: a 64 MiB answer drops 743 ms → ~8 ms (~95×) and is now flat across plot sizes. This removes the recurring per-audit-epoch scaling; the remaining Ω(size) on-loop cost is the one-time Seal at onboarding, moved off-loop next (research Option A, #340).

Fixed

Genesis commits small — bond registrations spread across blocks (#286 Layer 2b) — A real 3-region GCP cert run (with the #331 persistent-peers fix + #327/#332 -log debug) proved address convergence was fixed but genesis still didn't commit, and pinned the true final blocker: gather: starting height=1 bytes=7866154 regs=5 — the proposer piled every founding validator's ~1.5 MB space-time bond registration into the single genesis block (~8 MB), which the quorum gather can't move + re-verify over a WAN before the round churns, and because it never commits the validators re-submit forever (308×), pinning the block huge. Root cause and fix confirmed by research against the code: the founding set are anchors (chain.launchAnchor), so genesis quorum bootstraps from anchor eligibility at zero committed bond (qualifiedCount=0 keeps RequiredQuorum=Quorum) — the block does not need the bonds in it. Fixed with a byte budget, Config.MaxBondRegBytesPerBlock (flag -max-bondreg-bytes-per-block, default ~2 MiB): the proposer embeds bond registrations per block up to that byte budget (chainrole.go), so genesis commits small on the anchor bootstrap and the deferred registrations drain over the next blocks — each validator still gains real bonded weight and the set reaches MatureValidators. A byte budget, not a count, is the right lever because the blocker is size, not number: at genesis one full ~1.5 MB proof fits per block, but small steady-state renewals pack many per block — so an attest-only validator's renewals are never starved under a tight TTL (a count cap lapsed them; sim/bond_renewal proved it). Anchors grant eligibility, never fork-choice weight, so nothing is fabricated. Reproduced deterministically in-process (no WAN needed) and guarded by TestGenesisBondRegsSpreadAcrossBlocks286L2b (4 anchor validators, quorum-2: genesis commits with one ~1.5 MB reg and all four bonds drain by block 4). The structural close remains #299 (succinct + aggregated bond proof).

Fixed

Registry client rides out transient loss on its reads (#329, durable-WAN audit) — The HTTP registry client's idempotent GET reads (Lookup, /publish-status, All) were single-shot: a single dropped packet or a transient 5xx failed a swarm get / root resolution outright. This was the one client path NOT behind the consensus layer's retry — the lone true violation of build-immutable #5 found in the durable-WAN audit (#329). Fixed with a bounded exponential-backoff retry (3 attempts, 200 ms base) on those GETs — ride out transient loss instead of deciding on a single sample (docs/network-durability.md §1 "modest initial + retry", §2). A definitive 4xx (e.g. a 404 not-found) is returned at once, never retried; POST /publish is deliberately NOT retried here (its commit durability comes from the async 202 + poll). Guarded by TestClientGetRetry_RecoversTransient / _BoundedGivesUp / _NoRetryOn404.

Added

Handshake attribution instrumentation (#286 Layer 2 Q3) — To attribute the cross-region inbound err=EOF before tuning any deadline (the research team's "instrument first" discipline), tcpnet now logs, on every failing handshake, the numbers that separate the candidate causes: the inbound side logs the concurrent in-flight handshake count (the hub-stampede gauge) and the elapsed time before failure; the dialer side logs the dial budget and the elapsed spent. On the next run, a clustered EOF with high concurrency and elapsed ≈ the dialer budget attributes it to the hub-stampede / dialer-deadline variant (which the -persistent-peers fix independently relieves by spreading load and letting the proposer dial out); an instant failure points instead at a pin-rejection / teardown. Logging-only; docs/network-durability.md §8.

Added

-persistent-peers: a static, never-evicted consensus-peer tier (#286 Layer 2, dominant fix) — The blind field tester root-caused the cross-region genesis stall (with the #327 -log debug): it is a mesh address-convergence bug, not a timeout. A proposer had send with no known address for the other validators, so it could not initiate the attestation gather → no quorum → no genesis. Cause: at genesis there is no chain (no discoverable validator registry), all validators bootstrapped to ONE seed, and silt's routing table holds bare NodeIDs (addresses live in the transport layer, learned only from inbound frames/gossip) — so hub-and-spoke never converges addresses across a fresh WAN, and the proposer cannot dial out. Fix (research-directed, docs/network-durability.md §8; the settled BFT/PoS practice — Tendermint persistent_peers, Ethereum static peers, libp2p peerstore): configure the validator/anchor set as -persistent-peers ID@HOST:PORT,… in every validator. Those peers are AddPeer'd at boot (their address is known up front, no dependence on inbound learning) and marked a never-evicted tier — a transient WAN miss is a retry, never an eviction, so a proposer can't lose an attester mid-formation and stall quorum (Q4). The cloudtest/awstest topologies now pass the whole validator set as persistent-peers. Guarded by TestStaticPeerSurvivesReachabilityEviction286 (a static peer survives retry-exhaustion eviction; a normal discovered peer does not). WAN-certified on the next GCP run.

Added

Gather-path debug logging for the #286 Layer-2 root-cause — The consensus propose→gather→attest path previously logged ONLY on a successful commit ("block committed"), so a quorum-2 genesis gather that starts and never completes over the WAN (the #286 Layer-2 blocker) was invisible — a silent ValidateProposal reject of the ~1.5 MB first block looked identical to "never received". Added -log debug lines on both sides (core/node/chainrole.go): the PROPOSER logs gather: starting (with block SIZE), each gather: requesting attestation, each collected/refused/ failed reply, and gather: NO QUORUM; the ATTESTER logs receive → REJECTED (ValidateProposal) with the reason / REFUSED (already attested) / ATTESTED. So the next multi-region GCP run with -log debug shows exactly where the round stalls. Debug-level only — no info-level noise, no behaviour change (all consensus tests unchanged).

Fixed

Publish no longer dies on a flat wall-clock deadline — async accept + poll (#286 Layer 1) — The binding failure in the quorum-2 genesis stall was on the HTTP publish path, not the validator↔validator RPC path #318 tuned: silt swarm add held the HTTP connection open for the whole consensus gather under three stacked flat deadlines (10 s http.Client.Timeout, 30 s server WriteTimeout, 30 s chainhost loop timeout), any of which guillotined the one-time ~1.5 MB genesis gather over a real WAN before quorum could form — a flat transport deadline is a category error (build-immutable #5; docs/network-durability.md). Fixed by making publish asynchronous: chainhost.PublishAsync runs the LOCAL validation synchronously — so every refusal (no publish token when required, a durable Publisher identity the refuse-to-surveil chain rejects, a double-spent token, a duplicate root) still surfaces at once — then kicks off the commit gather in the background and replies 202 Accepted; the client polls a new GET /publish-status until the entry commits or the gather reaches a terminal no-quorum. No connection is held open for the gather (no flat guillotine on the commit, no slowloris — #48), yet Publish still BLOCKS the caller until commit so sequencing (double-spend replay, dup) is preserved exactly as the sync path. /publish-status exposes the gather's terminal outcome, so a not-yet-committable publish (no quorum before mutual standing is earned) fast-fails instead of polling out the budget — preserving the publish-retry-until-standing semantics the bond earned-standing and on-chain revocation e2e flows depend on. Guarded by TestAsyncPublish_AcceptThenPollCommits / _RefusalIsSynchronous / _TerminalFailureFastFails and the full e2e suite over real TCP. This removes the Layer-1 transport guillotines; the deeper WAN-only genesis gather (Layer 2) is still open pending a -log debug cloud run (#327) and the structural ~1.5 MB → succinct bond proof (#299).

Fixed

Objective chain wedged after the first bond, cross-region (#313) — Found on the GCP cert field test: a 3-region objective validator set committed block 1, then every publish hung and the chain stalled at height 1. Root cause: F6 "proposing IS registering" re-embedded the proposer's full space-time bond proof in EVERY block (and the H2 non-proposer path re-submitted it every sweep). Once a validator's bond sealed, each subsequent block carried the ~1.5 MB proof (#299); over real cross-region links attestation could not carry the bloated block in time, so the proposer's quorum gather stalled and the chain wedged. Re-registering an already-committed bond bought nothing — the latest registration already stands. Fixed with Chain.BondRenewalDue(id): the proposer attaches (and a peer submits) a bond registration ONLY when not-yet-bonded or past the TTL renewal point (half the TTL, leaving margin), so ordinary blocks stay lean while the release-and-coast defense (RT-2/G4: a released plot still decays out on the TTL cadence) is preserved. Also lowers the per-block bandwidth residual (#299) and the participation floor (build-immutable #4). Reproduced and guarded in-process at the exact cert parameters (4 objective validators, quorum 2, Byzantine-ON, no genesis bonds): TestWedge313_ObjectiveByzantineMultiBlock (exactly one registration block, not every block) and TestWedge313_RenewalStillHappensUnderTTL (renewals still fire on the TTL cadence).

Fixed

Size-aware consensus transport deadline (network durability) — The per-attempt DHT/ consensus RPC deadline (Config.RequestSizeFloorBytesPerSec, default 256 KB/s) now scales with the OUTBOUND payload: a request gains len(payload)/floor of transfer headroom (capped 30 s), so a large one-time block (a validator's ~1.5 MB bond registration) gets WAN margin while lean blocks are unaffected and holder-fetch dials keep their tighter, non-extended deadline (#277). A textbook-correct application of the #289 tenet (transport deadlines must be generous; security lives in the proof, not the clock — docs/network-durability.md). Guards: TestRequestTimeoutFor_SizeAware286 + TestRequestTimeoutFor_ExtensionOptOut286. This is a genuine durability improvement — but it does NOT fix #286 (see Known issues); the GCP re-run proved it tuned a non-binding path.

Added

silt daemon -goodpropose <peerID> — a POSITIVE CONTROL for the -forge-block/-lowbond-propose rejections — The integration/redteam forged-block and low-bond scenarios asserted only that H3 replied OK:false, but a validator replies OK:false for many unrelated reasons (chain role disabled, ErrWrongParent, already-attested), so a target that refuses every proposal would false-pass both — "reject the good one too" is indistinguishable from "reject the bad one" (audit #303). New test-harness flag -goodpropose <peerID> (sibling to -forge-block/-lowbond-propose), backed by core/node.ProposeGoodBlock, sends a well-formed, properly-bonded proposal and asserts the target ACCEPTS it — logging goodpropose proposal ACCEPTED by <id> on attest, goodpropose proposal UNEXPECTEDLY REJECTED by <id> otherwise (it retries until its bond earns standing). The redteam harness now gates SCENARIO 2 & 3 on this positive control, so a rejection is attributed to the real defence, not a dead/wedged target. (#303)

Changed

-revoke now tells the operator it is waiting, instead of looking silently inert — Field-test finding #235. -revoke <root> does not act immediately: it polls until the named root is committed on-chain and this validator has earned standing to gather a takedown quorum. With no output, a bogus or not-yet-committed root left the daemon looking hung. It now prints, at start, revoke: target <root> — waiting until it is committed on-chain and this validator has standing to gather a takedown quorum, and a one-time revoke: <root> is committed — gathering a takedown quorum on the transition (then the existing takedown: proposed … on success). (#235's repair-sweep item was already covered by the repair sweep complete line added with the field tests; the pre-format-store item is folded into the migration-policy work, #237.)

Fixed

Holder-fetch dials fail fast again — no retry/backoff regression on the dial-storm — Follow-up to the adverse-network hardening above. That change applied the new 5 s timeout + retries to every RPC, including speculative holder-fetch dials (MsgFetchChunk/MsgHasChunk), so a fetch from a dead holder cost up to (retries+1)·5 s ≈ 20 s (was ~½ s) — deepening the dead-holder dial-storm (#277) exactly where content lives on churning holders. Fixed: holder-fetch RPCs are not retried (the fetch loop already retries at a higher level via FetchAttempts and skips known-dead holders via deadUntil) and use a tighter -holder-dial-timeout (default 2 s) so a dead holder fails fast, while mesh/ consensus RPCs keep the generous timeout + retries needed to ride out jitter. Restores the pre-hardening fetch responsiveness without giving up the jitter durability.

Fixed

Consensus now bootstraps under a jittery network (adverse-network durability) — New integration/flakynet harness (4 objective validators behind tc netem) reproduced a real durability collapse the clean local tests never showed: under a mild, realistic 80 ms ± 20 ms jitter the network committed in 6 s on a clean link but never committed jittered (the root of the flaky-GCP #286 symptom). Three causes, fixed: (1) RequestTimeout was 500 ms — LAN-tight for a global P2P RPC (TCP connect + query routinely exceeds it on a jittery path); the daemon default is now 5 s (-request-timeout). (2) A single timed-out RPC evicted the peer from the routing table (table.Remove), so one slow/dropped packet tore a good peer out of everyone's mesh; timed-out RPCs are now retried with exponential backoff (-request-retries, default 3; -request-backoff 250 ms) and the peer is only given up after retries are exhausted. (3) A BondChallenge reply carries a large, slow space-time proof; under adversity these time out in droves and (2) then evicted the peer — starving consensus of the very standing it was establishing. A bond-challenge timeout now never evicts from routing (it is a standing signal, not a reachability one — standing lapses and re-audits on its own). DefaultConfig keeps RequestRetries=0 so the deterministic sim/tests are unchanged; the daemon opts in. KNOWN HARDER GAP (filed): jitter + packet loss still does not reliably bootstrap — loss on the large proof/chunk replies plus the C1 reply-latency gate (-bond-answer-latency) reading network latency as a short-storage cheat; the latter is a security tradeoff needing a deliberate call.

Fixed

A node that joins before its bootstrap peer is listening now recovers on its own — Field-test finding #281, found on the first real 13-node cross-region GCP run. silt's Kademlia join (Node.Bootstrap) was one-shot: on a multi-node cold start with no ordering guarantee, three validators started their FIND_NODE before the boot validator's listener was up, landed with an empty routing table, and — with no re-bootstrap — never tried again, even though the target became reachable seconds later. The network never meshed, the chain stayed at height 0, and every publish timed out (reachability was fine; consensus never formed). Added a periodic self-heal (core/node/bootstrap.go, StartBootstrapRetry): while Table.Size == 0, re-run the join against the original -bootstrap seeds every BootstrapRetryInterval (new -bootstrap-retry flag, default 15s; 0 disables). The retry first clears the seeds from the deadUntil negative cache (their failed initial dial had marked them dead for HolderCooldown), so it re-dials immediately instead of waiting out the cooldown; it is a no-op once any peer is in the table. Recovery logs re-bootstrapped: recovered from an empty routing table (N table entries). Covered by unit tests (simnet Kill/Restart race) and a real-process e2e (TestBootstrapRetryRecoversColdStartRace).

Fixed

A sparse cold-start mesh now converges (periodic bucket refresh) — follow-up to #281. Recovering an EMPTY routing table is necessary but not sufficient: a node can re-bootstrap to just one or two peers and stall there, and the seedless boot validator (no -bootstrap) never re-looks-up at all — so it stays stuck at the single entry an incoming dial gave it and can't discover the rest of the validator set. The bootstrap-retry loop now also does a Kademlia self-lookup while the table is below BootstrapWellConnected (default 8), which discovers more peers and converges the mesh. This reaches the boot node too — it queries the peers that dialed IN. Covered by a simnet convergence unit test (a boot-like node with one entry converges via refresh). NOTE: this fixes the DHT-mesh convergence; a separate consensus-bootstrap gap remains where a fully-simultaneous cold start of an objective validator set does not establish anchor standing even once connectivity is fine (filed separately).

Changed

The C1 bond reply-latency gate is now SOFT, not a standing gate (build-immutable #3; #289) — A live bond challenge no longer denies standing for a slow reply. Reply-latency is transport (RTT + jitter + loss) plus compute, and gating security on the sum is unsound on the open internet — it read network jitter/loss as a partial-storage cheat and starved durability under adverse networks. Standing now rests on the sound signals only (anti-release floor + identity binding + the space/labeling proof VerifySpaceTime); a valid answer earns standing however slowly it arrives. The partial-storage timing deterrent becomes a soft, disclosed signal: the node tracks the windowed-MINIMUM (low quantile) of each peer's bond-challenge reply latencies — which filters the one-sided network noise — and raises a non-gating suspicion only when that floor is SUSTAINED above -bond-answer-latency (a partial-storage prover recomputes on every challenge so its floor stays elevated; an honest node on a bad path is only randomly slow). New read-only accessor Node.BondLatencyFloor. The old hard-gate regression test is inverted to assert the sound behavior (TestC1TimingIsSoftNotAHardGate). The hard structural close remains tight-PoS (H-track), owned as residual A5. Following the 2026-08-10 network-durability research opinion.

Changed

Anti-release bond floor decoupled from the transport timeout (build-immutables #3/#4) — Following the network-durability-vs-space-time research opinion, the anti-release floor MinBondBytes is now sized explicitly against a named compute window (AntiReleaseComputeWindow, ~2s) times the measured seal rate (bond.PlotSealThroughput, ~270 MB/s), not the transport -request-timeout. DerivedBondFloor is a derivation (2× margin over window×throughput ≈ 1 GiB) rather than a magic constant, and a regression test (TestAntiReleaseFloorIsComputeSourcedNotTransport) locks it to the compute arithmetic. This means raising -request-timeout for durability under adverse networks (#288) can never balloon the floor toward multi-GiB and price out small validators (immutable #4), and the anti-release argument no longer rests on a network reply deadline (immutable #3; enforcement is the floor + bond-audit statistics-over-history, since a full re-seal is a multi-second cost). No change to the default floor value; flag help and comments corrected.

Added

PoR audit seam on silt daemon-liar + -audit make the verify-without-fetch catch+slash wire-driveable — The silt sim run audit headline (a verify-WITHOUT-fetch PoR challenge catches a storage node that kept its proof tags but dropped the bytes, and slashes its standing) existed only in-process: Node.SetLiar and Node.Audit were unit/sim-tested, but nothing in cmd/silt toggled the liar or invoked the sweep, so over the wire a liar was caught only indirectly (it answers MsgHasChunk=false once its bytes are gone). Two new flags (siblings to the consensus red-team flags -equivocate/-forge-block/-lowbond-propose): -liar (keep the tags, advertise as a provider, drop the bytes, prove over data it no longer holds) and -audit <interval> (a -care-ing caretaker runs Node.Audit on every cared root — challenge each shard's providers, grade the proofs against the care-link key with no ground-truth fetch, and slash the liar). integration/audit now gates the literal claim over the wire (honest holders pass, the liar is caught and slashed) and demonstrates why it is needed — the liar's MsgHasChunk lie fools the availability probe but not the audit. (#232)

Added

swarm add token-replay seam (-save-token / -use-token) drives the double-spend rejection over the wire — The publish-token double-spend guard (core/chain ErrTokenSpent + the online issuer's spent-set) was real and unit-tested but had no CLI/wire seam: every swarm add -token-quorum N minted a fresh random serial, so two publishes never collided. swarm add now takes -save-token <file> (write the acquired token, CBOR) and -use-token <file> (publish carrying that saved token instead of minting a fresh one). Re-presenting the same token for a second file re-uses its already-committed serial, which the registry's pre-check refuses with the exact ErrTokenSpent reason and never commits. integration/economy now gates the double-spend over the wire (a fresh-token control still commits) and ties its unlinkability assertion to the tokened entry's own zero-NodeID Publisher. (#233)

Added

Cloud variants of the field-test series in integration/cloudtest — Four new GCP scenarios carry the local suites' properties onto real VMs / real regions, mapped onto the existing 13-node topology with no topology change: flow_publisher_unlinkability (privacy #3 — a durable-Publisher publish is refused by the default chain over the real wire), flow_durability_turnover (durability #2 — content survives a permanent storage-node departure, fetched bit-perfect from a survivor), flow_chaos_crash (chaos #7 — a SIGKILLed storage node re-announces its chunks (#69) via Restart=on-failure and content stays fetchable), and flow_web_ui_guard (client/UI #4 — the #89 web-UI guard holds on a real VM: no-token→401, DNS-rebinding→403, read→200). Wired into run_all_scenarios; shell/topology dry-validated (no billable run). C2-Sybil (#5) has no cloud flow yet — it needs non-anchor Sybil validator VMs (a topology.py addition) so the Sybils' bonds bank over a longer cloud run and the pure ErrAnchorRequired gate + ≥8-bond atomization note become assertable; recorded as a skip until then.

Added

New field test: chaos / crash-recovery (integration/chaos) — Tests whether the system survives hard crashes: a SIGKILL (abrupt process death, no graceful shutdown), then a restart of the same node — same identity, same IP, same on-disk store (docker start on an un-removed container, unlike durability's docker rm). WAVE 1 (default, the gate): SIGKILL every holder, restart, and assert each re-bootstraps AND logs re-announced N held chunks (#69) so content stays discoverable, then a fresh client cold-fetches it back bit-perfect — with no crash-loop. Validated locally (PASS: 6/6 holders re-announced, bit-perfect after a full holder crash). WAVE 2 (opt-in, WAVES=2): also SIGKILLs the sole seed/registry/bootstrap; this surfaces a discoverability gap — the content stays on disk but a fresh client can't rediscover the providers in the window, and a single holder re-announce doesn't restore it — recorded honestly as an observation to root-cause (entangled with the single-bootstrap SPOF a real deployment avoids; retest with a redundant-bootstrap topology + on the cloud test), not a verified product defect, which is why it is off by default. Wired into run-all.sh (gate tier).

Added

New field test: C2 "no quiet capture" under a Sybil validator set (integration/sybil) — Tests the M0 systemic C2 claim cynically, as an OUTCOME: can a bonded Sybil validator set — many identities, real bonds, its own quorum — quietly capture a young objective network? Two honest anchors (a1 proposer, a2 co-signer, since one anchor can't co-sign its own block under -anchor-quorum 1) plus a Sybil set. C2-a: with the anchors present the chain commits a real block and the daemon prints wheels engaged (young network — anchor quorum still required) — the chain is live and the training wheels are on. C2-b: stop both anchors and the Sybil set (s1 proposes, s2 attests) cannot advance the chain — no new block; the test reports which training-wheels layer refused it (locally the standing gate — a young Sybil set can't even earn committed bonded standing without an anchor-proposed block; behind it the anchor co-sign gate, ErrAnchorRequired). A chain advancing for the Sybils would be a hard FAIL. C2-c (bonus): with ≥8 committed equal bonds the C2 metric's atomization note fires (an equal-bond split reads as a fingerprint, not real decentralization). Honest scope (immutable #5): on one host the Sybils' bonds don't reliably bank on-chain, so the standing gate usually fires first (itself a faithful no-capture outcome); the pure anchor-co-sign gate with pre-banked bonds and the ≥8-bond atomization signal are exercised at scale on the cloud test. Validated locally (PASS). Wired into run-all.sh (gate tier).

Added

New field test: the client / web-UI path under an adversary (integration/client) — Tests the path a real user takes — run a daemon, open its web UI, drop a file in, get a link, someone fetches it back — over the daemon's HTTP API, not the silt swarm CLI; and the path a real attacker takes against that same local API. A ui node (storage + its own registry + -ui) plus N holders; the test drives the UI with curl from inside the operator's container (the realistic browser-on-the-same-box model, and the only way the guard's local-Host rule is met). U1–U3: POST /api/publish (multipart + the bearer token grabbed from the daemon's own ui: …?token= line) scatters the file, /api/roots reflects it, and GET /api/fetch?link=… returns the bytes bit-perfect — the real end-to-end round-trip over HTTP. U4–U8 attack the guard (#89): a no-token and a wrong-token POST are refused 401, a DNS-rebinding request (non-local Host) and a cross-origin drive-by (evil Origin) are refused 403, and a token-free localhost read still returns 200 (ergonomics preserved). Every assertion is a real HTTP status code / real SHA-256, never an echoed string; any failure is a hard FAIL (the user path and the local-security guard are both load-bearing). Validated locally (PASS, all eight). Wired into run-all.sh (gate tier).

Added

New field test: publisher unlinkability under an adversary (integration/privacy) — Tests immutable #4 (refuse-to-surveil) cynically, as an OUTCOME: can an adversary get the network to record who published a given file? A silt registry entry may carry a durable Publisher NodeID — a permanent file→publisher link on the append-only chain (the #14/F1 privacy corner) — and the M0 default refuses it. Two real silt -validator daemons form a committing chain; the test asserts on real chain-status commit counts and the validator's rejection line, never an echoed string. P0 (positive control) shows a chain run with -allow-publisher=true commits a Publisher entry — so the refusal below is real policy, not a broken publish. P1 shows the default chain commits a normal unlinkable publish and fetches it back bit-perfect (privacy isn't a broken product). P2 (the crux) shows the same default chain refuses an -allow-publisher publish — the real error surfaces over the wire (chain: entry carries a durable Publisher (records permanent linkage; publish unlinkably…)) and no new block commits. P3 shows a -token-quorum publish commits with a blind validator credential and no Publisher — authorized yet unlinkable (the F1 fix). A privacy regression (the default chain committing a Publisher entry, or the private path failing) is a hard FAIL; the metadata-correlation layer is a stated M0 tradeoff, not covered. Validated locally (PASS, all four). Wired into run-all.sh (gate tier).

Added

New field test: durability under permanent holder loss (integration/durability) — Tests the core promise cynically — does content outlive the nodes that held it? A seed + 16 holders + caretaker swarm publishes a file at replication=1 (every column single-copy, the honest stress), then permanently kills holders one at a time and never replaces them so the pool shrinks onto ever-fewer survivors. Crucially it kills without re-scaling: an earlier rm -f+--scale design let Docker recycle a dead holder's IP to a fresh empty identity, and the caretaker dialing old-NodeID@that-IP hit a TLS-pin impostor — a Docker artifact (real infra doesn't recycle IPs in seconds) that masqueraded as "content lost." Shrinking the swarm removes the artifact and isolates the real mechanic: reconstruct-from-parity (k=10 of n=16) + re-scatter onto survivors. It reads two separate oracles — the caretaker's repair below k log (authoritative, unrecoverable content loss) for durability, and a warm-peer swarm get (every survivor handed as a direct peer) for retrieval, so a discovery flake is never miscounted as loss. Finding: durability held — across 10 permanent departures (16→6 holders) no stripe ever fell below k and the caretaker performed 13 reconstructions, so the bytes provably survived — but a fresh client's end-to-end retrieval stayed bit-perfect only down to ~11 survivors, then degraded (the #43 retrieval surface under permanent loss: durable is not the same as retrievable). Exits 0 as a FINDING (EXPECT=pass to hard-fail; MIN_SURVIVORS=11 for the retrieval-healthy clean PASS). True membership rotation (fresh VMs = fresh IPs) is the cloud test's job. Wired into run-all.sh (slow tier).

Security

seam-5: A-axis truth-in-labelling + a count/entropy signal for the equal-bond split — Two red-team hardening findings on the operator-clustering heuristic. (F3, truth-in-labelling) two core/chain comments claimed the declared failure-domain is "transport-cross-checked at H5-B / refuses to route to a validator whose declared domain does not match its observed /24" — but handle learns peerDomains from gossip verbatim, with no /24 cross-check. The comments are corrected to say the domain is self-asserted, not transport-verified; the composition never relied on the cross-check (the shed gates on min(NakamotoOperators, NakamotoDomains), so free domains can only lower the min, never trip the wheels off early), so this is a labelling fix, not a mechanism change. (F1, new signal) an equal-bond split — one operator posting N identical min-bonds across N keys — drives HHI→1/n, Gini→0, TopShare→1/n, so it reads maximally decentralized on every weight-concentration signal and the ⅓ whale alarm never fires. Added C2.WeightUniformity (effective participants 1/HHI over actual, →1 for perfectly uniform) — the count/entropy companion that exposes the "many atoms, implausibly uniform" fingerprint the weight signals miss, surfaced in the daemon C2 status with an atomization note when a many-bond set reads implausibly uniform with no whale. Necessary-not-sufficient (a size-varying splitter evades it, and healthy decentralization is also uniform), so it does not close the honest-whale / M_est residue (#182) — it makes the naive split legible for out-of-band verification. Regression: TestC2Metric_WeightUniformityCatchesEqualBondSplit.

Security

seam-2: an untrusted objective validator refuses to start without cold-start scaffolding — The same blind red-team pass found that a stock untrusted objective validator (the default M0 path) shipped with -anchors/-mature-validators unset, so Mature returns true at genesis (MatureValidators<=0), the node latches everMature at the first block, and the anchor co-sign the young regime relies on never engages — a young or Sybil quorum could self-certify mature and capture. Only a liveness WARNING guarded it. This is the "fixed but off by default" meta-pattern Invariant B exists to catch (the enumeration had no cold-start row). Fixed: the daemon now refuses to start (like the existing -min-bond<=0 hard failure) unless the operator supplies either the anchor launch set (-anchors … + -mature-validators N, to bootstrap a fresh network) or a weak-subjectivity checkpoint (-ws-checkpoint HEIGHT:HASH, to safely join an already-mature one). Refuse-to-start is forced, not merely prudent: there is no sound synthesizable anchor set (weak-subjectivity irreducibility — you cannot bootstrap trust in the validator set from the validator set). Locked by a new Invariant-B S6 row (coldStartScaffoldOK). Off the untrusted objective path (trusted -min-rep 0 / legacy -objective=false) nothing changes.

Security

BREAK 2: the shipped desktop client now defaults its eclipse defenses ON — A second blind red-team pass found that silt client built its node from raw node.DefaultConfig, where the H5-B eclipse-resistance defenses ship OFF (DHTDomainCap = 0, RequireSignedProviders = false) — even though the daemon and the swarm add/get fetcher both default them on. So a routing-layer censor owning the NodeIDs closest to a root's keys but sitting in one failure domain (a ~$4 /24 key-surround) could make that root undiscoverable for a client user who consented to no takedown — a discovery-layer route to the "make a specific root unfetchable" outcome immutable #5 forbids at the denial layer. Fixed: the client now builds from clientNodeConfig (domain-diversity cap on, signed provider records required, freshness TTL) and signs its own records. The safe config is now the DEFAULT for the untrusted client posture — locked by a new Invariant-B S5 row (invariant_b_test.go) so it can't silently regress. The eclipse mechanism itself was already proven in redteam_h5b_test.go; this closes the shipped-default gap. (The multi-domain surround residual — a censor spread across enough failure domains — remains the owned survivor-Nakamoto/#180 residual, tracked separately.)

Security

seam-7: equivocation is slashed on DETECTION, not only on adoption — The red team found a validator could double-sign onto a losing fork — attesting the canonical head AND signing a conflicting block on a doomed/lighter fork (to confuse late joiners, split gossip, or bait a partition) — at zero standing cost, because slashEquivocators ran only when a node RECONCILED ONTO a heavier competing fork. A fork nobody adopts was never scanned. Fixed in SyncChain: every fetched peer chain is now scanned against the local one for cross-fork double-signs before the heavier test and regardless of whether we adopt it — a provably-guilty signer is slashed even if its fork loses. The evidence is self-verifying (chain.VerifyEquivocation), so an honest sequential signer is never caught; the change subsumes the old adopted-branch scan. Regression: TestSeam7_LosingForkEquivocatorIsSlashedOnDetection (A holds a heavier chain, B serves a lighter fork carrying the culprit's conflicting signature; A does not adopt but slashes). (The companion F2 — applying the eviction to the local objective set on a gossiped proof before a slash block commits — touches objective fork-weight uniformity between replicas and is deferred as a separate, carefully-scoped change.)

Security

R-2: the SurvivorNakamoto non-globality scalar ships (raw), doc corrected — The red team found docs/safety-denylist.md read as if the non-globality metric were shipped ("a checkable quantity"), but no such computation existed in core/ — only the raw CT log. Per a research consult (R-2), the raw scalar is a build target (the data — signed provider records + gossiped failure-domain labels — already exists); only the ZK/PIR privacy wrapper is post-M0. Built Node.SurvivorNakamoto(key): the survivor Nakamoto-coefficient over a key's live, accepted provider set = the number of DISTINCT failure domains those providers sit in — how many independent domains a censor must eclipse to make the content undiscoverable. A set spread across N domains reads N; the same providers collapsed into ONE read 1 (one key-surround from dark) — the censor fingerprint the raw provider count hides. The provider-resolution path now logs a collapse (several providers all in one declared domain) so silent routing censorship (the BREAK 2 residual) becomes a measurable event. Corrected safety-denylist.md to state exactly what ships (raw scalar in M0) vs. what is post-M0 (the certified, domain-hiding ZK-threshold + PIR-probe wrapper, H9/#180). Necessary-not-sufficient observability, never enforcement. Regression: TestSurvivorNakamoto_CountsDistinctFailureDomains.

Security

seam-6: the on-chain bond-renewal nonce is documented as predictable (bounded elsewhere) — The red team noted (a note, not a break) that BondRegNonce = H(prev_block_hash) is predictable: once prev commits a validator knows its next on-chain renewal challenge, so the on-chain path alone doesn't bound release-and-recompute-just-in-time. It cannot be made unpredictable without a randomness beacon (M0 has none) and must stay a pure function of committed history so every replica re-derives it identically for objective verification — so this is truth-in-labelling, not a mechanism change. The BondRegNonce comment now names the weakness and where it is bounded: the parallel live peer-audit issues an unpredictable nonce at random, and that audit now carries the BondMaxAnswerLatency reply-deadline (BREAK 1 / owned-residuals A5), so a released prover that must recompute past the ~0.25 knee fails it. (The demand→standing firewall tripwire the same pass asked to preserve is already regression-locked — sim/demand_costtowash_test.go + sim/demand_bonded_test.go assert standing is byte-identical under wash/self-dealt demand, and core/credit/invariant_a_test.go's reflection guard fails the build on any unclassified standing press.)

Security

BREAK 1: C1 restated to (1−ε) with an enforcing bond-answer-latency gate — A blind red-team pass found a partial-storage recompute discount on C1: on silt's single-layer DRSample bond graph a prover can delete a fraction ε of its plot (keeping the 32-byte leaves) and recompute any challenged block on demand, passing the exact bond.VerifySpaceTime the live wire runs while holding only (1−ε) of the disk. Recomputed bytes are content-identical to stored ones, so no content check can catch it (verified in code) — enforcement is necessarily the time leg. Measured on the shipped graph: recompute is ~free at ε≤0.10 and its work explodes past the ~0.25 knee. Per a research consult (web-verified against the proofs-of-space literature), the tight small-ε close is H-track (stacked tight-PoS + a Groth16 SNARK over a ~100 MB witness → a trusted setup), so M0 ships the honest Option B:

  • C1 restated from (1 − o(1))·q·C_honest to (1 − ε)·q·C_honest, ε=0.20 disclosed (m0.md, owned-residuals.md A5, m0-sybil-rebind §8.1).
  • Enforcement: a reply-latency gate on the live bond challenge — node.Config.BondMaxAnswerLatency / daemon -bond-answer-latency (default 1.5 s) — earns no standing for a reply slower than the (generously-margined) deadline; past the ~0.25 knee the recompute blows it. Soft (wall-clock ⇒ fastest-evaluator-sensitive), off in the sim (tick clock), on in the daemon.
  • Honest residual (A5): it deters the rational serial disk-saver; a parallel adversary can hold less disk but re-pays the recompute every audit, per identity (compute-for-storage re-pricing, not a free discount), with the parallelism required growing super-exponentially in ε (Brent: ~10² cores at ε≈0.25, ~10⁵ at 0.30, ~10¹³ near 0.5) — so realistic parallel exposure ≈ ε0.30, and audit frequency (-bond-audit) is a free tightening lever. Composes with BondTTL so the gate bounds even on-chain objective weight over time. The tight close is Option A (H-track).
  • Regressions: TestBreak1_LateBondAnswerEarnsNoStanding (a late answer earns no standing; the gate is off at deadline 0). No deterministic content check exists — do not add one.
Security

R-3: publish-token signers are chosen by a network-canonical ledger ordering — The red team found swarm add -token-quorum signed a publish token from an arbitrary subset of the caller's -peers, and since the committed PublishToken.Sigs records each signer's NodeID, a distinctive subset could collapse a publisher's anonymity set toward a singleton (full deanonymization, no broken crypto). Per research consult R-3, the fix is a canonical, ledger-derived signer set — the SAME for every publisher. Added MsgGetCanonicalIssuers: a chain-holding validator serves its deterministic canonical issuer ordering (validators ranked by committed bond, chain.CanonicalIssuers, which existed but was unwired). swarm add now fetches it and ranks its reachable validators by it (rankByCanonical), so the signer subset is no longer a per-publisher choice; falls back to -peers with an honest warning if no peer serves a chain. Locked by Invariant-B S7 (TestInvariantB_S7_PublishSignerSetIsCanonical — the selection is order-independent and follows the canonical ranking). Owned caveats (owned-residuals B4): holds subset-anonymity only (the fetcher IP/timing channel is the separate D-PRIV residual); a canonical quorum is a mild publish-liveness surface (mitigated by rotation-by-bond); and a chainless publisher ranks its reachable peers, so the hold is fully global only when publishers connect to the canonical set. The full crypto close (the issuer signs without learning which root) is the B2 blind publish token, H8/#179.

Changed

integration/fieldtest/integration/cloudtest/ — honest naming for the cloud substrate — The GCP harness is the cloud variant of the field test, so the directory now says so: renamed to integration/cloudtest/, its orchestrator fieldtest.shcloudtest.sh, and the per-run GCP resource label fieldtest=<run_id>cloudtest=<run_id> (nuke-by-label filters updated in lockstep, so teardown safety is unchanged). "Field test" remains the umbrella concept — local Docker is the free fast net, cloudtest/ is where GCP is the judge (field-test immutable #5). Docs, .gitignore, and cross-references updated; the stale "lands with PR #209 on a separate branch" note is dropped now that the harness is on main. No behaviour change — a rename + reference sweep. The silt_{local,cloud}_fieldtest_<date>.md operator-report names (both substrates are field tests) are intentionally unchanged.

Fixed

Repair no longer starves on stale records to dead holders — Field-test finding F2 (integration/churn/): the repair fetch loop (fetchStripeByColumnfetchFrom) dials providers serially, each dead holder costing a full RequestTimeout, and a single timeout re-sweeps the whole provider list up to FetchAttempts times — with nothing skipping a holder we just failed to reach (n.reachable was write-only). Routing-table eviction doesn't remove the provider record other nodes still hold, so the next lookup resurfaces the same corpse and re-dials it at full timeout every sweep; under churn one stripe could exceed RepairInterval on timeouts alone. Added a failed-holder negative cache: a request timeout stamps deadUntil[peer] = now + HolderCooldown (30s), and the fetch/repair dial path skips a holder still in cooldown — but only when a live alternative exists (anyLive), so the cache can never be the reason a fetch fails (a sole provider that timed out transiently and has since re-announced is still dialed, preserving cross-NAT reprovide, #69). Stamped centrally (so NetGet benefits too) but consulted only in the fetch path, leaving consensus/DHT re-probes untouched; the map is bounded (maxDeadHolders). Regressions in fetch_deadcache_test.go.

Fixed

The dead-holder negative cache now also covers the repair PROBE path and the DHT walk — Follow-on to the fetch-path fix above, which stamped deadUntil centrally but consulted it only in the fetch dial path, leaving the two other places a repair sweep dials stale records exposed: (1) the dispersion audit's probeShard MsgHasChunk loop and (2) the provider-discovery DHT walk (walk.step, MsgGetProviders), which a caretaker runs for every key it cares about. Under churn a caretaker with many stale routing/provider records to docker killed holders spent a full RequestTimeout per dead record, so a single sweep never completed — the caretaker never registered the loss and never repaired (surfaced by integration/churn/, which stalls on small swarms; the true degradation happens at GCP scale where k=10 actually strands stripes). Both paths now skip a holder still in cooldown — the walk fails it in the Kademlia lookup without a dial, the probe skips it — each only when a live alternative exists (anyLive), so a sole transiently-timed-out holder that has since recovered is still dialed (#69 preserved). Regressions in repair_deadcache_test.go (walk-skips, walk-without-cooldown control, probe-skips). A dial-storm reduction — necessary but, as the next entry found, not the whole churn story.

Fixed

Repair-under-churn now actually completes: parallel shard probing + a visible sweep (#235) — The integration/churn field test stalled: a caretaker killed holders never repaired. Root cause, found by instrumenting the sweep: repairRootWithLayout probed every shard strictly serially (probeNext(i+1) inside each probe's callback), so once holders die each dead-holder dial costs a full RequestTimeout in series — a large file's sweep can't finish within a RepairInterval, the caretaker never reaches repairStripes, and nothing is ever repaired. The healthy first sweep completed only because every dial returned in ~ms. Two fixes: (1) the probe phase now fans out with bounded concurrency (repairProbeConcurrency), so dead-holder timeouts overlap and a sweep completes in seconds under churn (safe on the single-threaded event loop — probe callbacks mutate sweep state on the loop, the same model as the DHT walk's in-flight fan-out); (2) the sweep is no longer silent — it logs repair sweep complete with the reachable-shard count, and its previously-invisible no-op early-returns (registry lookup failed, manifest not yet reassembled, layout not loadable) now log why, so a caretaker that can't sweep is diagnosable instead of looking identical to a healthy one (#235). With this, the caretaker reconstructs stranded stripes from parity and re-scatters, and integration/churn passes honestly (reachable drops after a kill → stripe repaired → bit-perfect re-fetch).

Fixed

silt swarm add -replication N — Expose the placement replication factor (previously a compiled-in 3) as a publisher flag; parity across holders backstops copies, so even 1 is viable. Lets a small-swarm test strand a column with a single kill, which makes caretaker repair deterministically reproducible on a laptop: integration/churn now runs at REPLICATION=1 as a fast, deterministic gate and documents REPLICATION=3 HOLDERS>=50 on the GCP harness as the faithful, shipped-default variant.

Added

New field test: retrieval / discoverability at scale (integration/retrieval) — Measures the most basic user promise — can I get my content back? — on a real multi-holder swarm as short-lived publisher/fetcher identities churn the DHT (#43/#60). It publishes files, optionally pollutes routing with throwaway ephemeral publishes, then measures the bit-perfect cold-fetch success rate from fresh ephemeral clients and gates it on a floor. Validated locally, it cleanly isolates the cause: raw scale (24 holders, no churn) fetches 100%, but adding 40 churning ephemeral identities drops cold discovery to 85% — a real, honest reproduction of the #43 ephemeral-identity routing degradation (POLLUTERS=0 vs POLLUTERS=40). A sub-floor rate is a FINDING (exits 0 like upgrade reproducing #237; EXPECT=pass flips it to a hard fail), never a faked green. Wired into run-all.sh (slow tier).

Added

Blind-session ease: an "LLM instructions" README section + interactive GCP setup — The field-test scripts stay simple and emit clean per-test output; the dated silt_local_fieldtest_<date>.md / silt_cloud_fieldtest_<date>.md roll-ups + per-test detail reports are written by the operating agent, guided by a new "For an LLM/agent operator" section in integration/README.md (plus a Quick-start). ./integration/cloudtest/cloudtest.sh setup is now an interactive step — it asks for the GCP project, walks the user through gcloud auth (login + the application-default creds Terraform needs), enables the required APIs, and writes config.env — so spinning up the cloud test needs no hand-editing.

Added

integration/run-all.sh + shared integration/lib.sh — the clone-and-run field-test experience — One driver runs the local Docker suites in sequence (each owns its topology, so they run one at a time), captures each suite's real RESULT: line + duration, and writes a shareable consolidated report.md plus a terminal summary; exit 0 iff every suite that ran is PASS or a deliberately-reproduced FINDING. Runs the fast gate set by default (FULL=1 adds the slow soak/upgrade suites; SUITES="…" picks a subset). lib.sh factors the shared shell — RESULT classification, timing, prereq checks — that each suite otherwise re-implements; the suites still stand alone. Suite → M0-claim mapping lives in the driver's catalog.

Added

Registry economics — -freeload role separation for the daemon (#47) — A daemon can now be started with -freeload to serve the registry / relay / routing role while refusing to store or serve content — so a public-infrastructure operator can run a rendezvous registry without being conscripted into hosting arbitrary content (the conflation that caps how many public registries the network can attract, which bootstrap/NAT-traversal depend on). The mechanism (node.SetFreeload, honored by the serve paths) already existed and was sim-only; this exposes it on the real daemon and announces the role. The node still carries DHT routing. (The leaner -registry-only mode — no storage node constructed at all — is the follow-up.) Covered by a real-TCP e2e; whole suite green with -race.

Added

H9 takedown transparency — the CT-style append-only log (#180) — New core/translog: an RFC 6962 (Certificate Transparency) append-only Merkle log — adopted, not invented (B8) — for honored revocations. It offers the two proofs that make a takedown auditable and non-silent: inclusion ("revocation R is entry i of the log at size N") so a specific takedown is provably recorded, and consistency ("the log at size M is a prefix of size N") so an operator can't quietly rewrite history — a dropped or back-dated revocation breaks the consistency proof. Exhaustively tested: prover-generated inclusion paths (every leaf × every size) and consistency proofs (every prefix pair) cross-check against the recomputed roots, and a tampered history fails. This is the M0-honest core of pluralistic takedown; the ZK non-globality predicate + PIR-routed probes on top are post-M0. Wired into the chain: every honored revocation/un-revocation is appended to the log in Chain.apply (a deterministic function of the committed blocks, rebuilt identically on replay), and the chain exposes RevocationLogRoot + inclusion/consistency proofs + the exported RevocationLeaf so an auditor can reconstruct a leaf from public block data — so silt can now prove a takedown was recorded and that its takedown history was never silently rewritten. Whole suite green with -race.

Added

D-DEMAND — the delivery receipt goes live on the wire (#181) — The core/demand primitive is now a real node capability. A fetcher AcquireDemandToken blind-withdraws a retrieval token from an issuer over the existing token-request wire (no new issuance path — the blind signature is domain-agnostic), then SubmitDeliveryReceipt sends the server a MsgDeliveryReceipt (the token + a PoR-bound, signed ack over the received bytes). The server verifies it against the issuer key it trusts and banks it into a neutral witnessed-demand observable (Node.WitnessedDemand) — never wired to consensus standing, so a forged or self-dealt receipt buys zero standing (the γ→1/N firewall). Only receipts naming this server are banked; replays (double-spent serial) and forged/mis-issued tokens are rejected over the wire. Integration sim covers the honest flow + both rejections. Whole suite green with -race. (Fee-burn cost-to-wash is P3; fetcher-unlinkability needs D3.)

Security

F-3: the whole-registry GET /all dump is off the public mux — Completes the red-team F-3 fix. /all serialized the entire registry O(N) with no pagination — an unbounded per-request cost. An interim change priced it by work, but that only bounds cost per source; a distributed dump (one request per source IP) no per-IP counter can touch. Since /all is used only by an operator's own CLI/UI — which reads the registry in-process (the daemon's local chainhost/fileregistry), never over this wire — it is now simply not served on the public mux: a remote client gets 404 / ErrAllNotServed and degrades to per-root /lookup; an operator listing its own registry is unaffected. This deletes both the amplification and the distributed variant. Also hardened the rate-limiter's per-IP bucket map with a hard size cap + sampled-LRU eviction so a source-IP-cycling flood can't grow it without bound (it would otherwise be its own cost vector). Regressions: TestBucketMapIsBounded + the round-trip test now asserts /all is not served. (The interim work-pricing charge is removed as superseded.)

Security

F-1: the maturity shed is now a genuine one-way ratchet (anchors never re-arm) — A blind red-team pass (re-)found that the launch-anchor "training wheels" were gated on the live Mature, which recomputes decentralization from the current bonded set — so an honest whale growing real bond past ⌊total/3⌋ could flip a matured chain back to immature and re-arm the zero-bond anchors, either halting the chain (if the anchors were gone) or handing them permanent, standing-free power (contradicting immutable #3, no permanent center). Fixed as a bundle:

  • One-way everMature latch — the anchor requirement (and anchors' bond-free eligibility) is now gated on whether the network has ever matured, a replay-derived consensus fact (latched in apply, re-derived on reload, carried across a reorg). Once matured, the anchors never re-arm.
  • Real-bond super-quorum de-maturation fallback — if a matured network later drops below the decentralization bar, a commit needs a real-bond super-majority (≥⅔ of live bonded weight, no anchor sign-off) instead of the retired anchors — center-less liveness that preserves accountable safety (ErrDeMatureQuorum).
  • Weak-subjectivity checkpoint — silt is now explicitly weakly subjective; a fresh/long-offline node pins a recent trusted block with -ws-checkpoint HEIGHT:HASH and refuses any reorg at or before it (ErrPreCheckpointReorg), the long-range-attack defense that makes the latch safe. The daemon prints checkpoint: HEIGHT:HASH for its committed head so operators can publish/cross-check it.
  • The two residuals are owned, not hidden (docs/design/m0.md §10): a bounded, socially-recoverable re-centralization risk (the honest whale — the same trade Ethereum/Cosmos/Bitcoin made) and the weak-subjectivity dependency itself. Regressions invert the red-team PoC (both halt and permanent-center horns killed; super-quorum enforced; long-range reorg refused).
Security

C2 concentration: the address-diversity (A) axis + an out-of-band honest-whale alarm — Two follow-ups to F-1, hardening the residual it deliberately leaves open (the honest whale — real bond concentrated by a real operator, unclosable on-chain by theorem, Kwon):

  • A axis wired into the shed. A validator's failure-domain (-domain) is now committed in its bond (BondReg.Domain, backward-compatibly signed) so the concentration metric counts address-diverse participants: bonds sharing a declared domain aggregate into one group (NakamotoDomains), and the maturity shed gates on min(NakamotoOperators, NakamotoDomains) — so a stake split across many keys in ONE domain cannot fake decentralization; retiring the launch anchors needs distinct domains, not just distinct keys. Turns the flat operator-margin M into a per-network-position cost. Honestly weak (a domain is declared, transport-cross-checked, not proven; /24s are rentable) — it prices concentration, it does not close it. With no -domain set, behavior is identical to before.
  • Concentration alarm. C2Metric now also reports HHI, the Gini coefficient, and the top bond's share; the daemon narrates them and raises a ⚠ CONCENTRATION ALARM when one bond holds ≥ ⅓ of bonded weight — a social/operational veto, explicitly not on-chain enforcement.
Changed

Truth-in-labelling sweep + split-defense safe-default — remediating the M0 blind red-team + acceptance passes — The reviews found the composition sound (no C1 discount, no C2 capture, demand→standing firewall holds both directions) but flagged a cluster of docs-ahead-of-code overclaims and two documentation gaps. Corrected:

  • The time (T) axis is relabelled as retention-only. Reputation() has no acquisition-time term (firstSeenTick is recorded but read by no standing calc), so full standing is granted on the first passing bond challenge and acquisition is priced by D alone. The docs (m0.md §3 & §4, TENETS.md, core/credit/credit.go) previously asserted T was a live acquisition factor ("cannot buy last month's uptime"); they now state T ships for retention only (decay/TTL) and that a time-acquisition ramp is deferred (a bare age gate is pre-farmable — the coin-age anti-pattern; the only sound form is a continuous bond-anchored VDF, M1+).
  • GET /all registry read-cost is now priced by work, not per request (F-3). A per-IP token bucket that charges one token regardless of endpoint metered the wrong quantity — /all serializes the whole registry O(N) for the same token as a 183-byte /lookup (~20,000× amplification at N=20k). /all now additionally charges ~one token per 64 entries served, draining the source's bucket into bounded debt, so a single caller can't repeatedly amplify one token into a full-registry dump. Regression: TestChargePricesAllByWork. (A distributed /all flood and full cursor pagination remain post-launch — the #48 entry now says so.)
  • The C2 operator margin M is safe-by-default. -operator-margin now auto-arms to a conservative M>1 for an untrusted (objective) validator — exactly as -min-bond-floor and -byzantine-quorum already do — instead of shipping M=1 (zero protection against one operator splitting real stake across NodeIDs to fake the decentralization that sheds the launch anchors). An explicit -operator-margin 1 still opts out for a trusted/single-operator swarm. M stays an honest heuristic (unverifiable on-chain, #182). Regression: TestOperatorMarginDefaultsAboveOneForUntrustedValidator.
  • The seam-4 demand-receipt one-liner (m0.md) no longer reads as closed. Two residual leaks (a receipt is forgeable with zero object bytes; a bonded-mode receipt links fetch→standing key) are neutralized today by the firewall (demand has no consensus consumer) but must be closed before any demand→standing fusion — now stated as such.
Fixed

Acceptance-pass documentation gaps — From the fresh-operator acceptance pass (all nine flows worked; these were doc/test issues, not broken capabilities):

  • docs/user-seam.md Role 4 "become a validator" walkthrough errored as written — the default objective fork-choice path needs -anchors to bootstrap a young network's on-chain bonded weight, which the walkthrough omitted (bonded 0, needs …). It now passes -objective=false to match the cited test TestBondEarnedStandingCommitsOverTCP, with a note on the objective/anchor launch path.
  • README.md said the default add mode was "convergent"; the default is -mode private (H6). Fixed.
  • examples/flow8-takedown.sh published with the (now private) default, giving two different roots so the takedown test denied one root and confirmed an unrelated one still served. It now publishes -mode convergent so both operators hold the same root, actually demonstrating per-operator takedown of a shared root.
Added

GCP field test — an automated multi-machine RC gate (#52) — integration/cloudtest/ spins up a real ~13-node silt network across three GCP regions (validators, storage, a -registry-only node, a relay, a fetcher, a NAT gateway + natted peers, and an adversary), runs the acceptance flows (publish → commit → fetch bit-perfect, earned-standing validator onboarding, multi-validator convergence, f=1 fault tolerance, restart survival, per-hash takedown, cross-NAT via the relay) plus the #184 adversarial consensus drills (equivocation→slash, partition→heal, forged/low-bond→reject) over the real wire, emits a shareable report (report.md / report.html), and tears the whole network down. Deterministic and self-configuring — every peer/anchor/attester reference is computed from silt id -id-seed + static internal IPs before any VM boots, so there is no discovery wait. Cost-bounded four ways (SPOT instances + per-VM TTL self-destruct + destroy-on-exit + optional budget alarm, with a nuke-by-label fallback), reusable by outside developers against their own GCP project, and SMOKE=1 trims it to a 4-node run for a pennies-scale plumbing check. This automates roadmap #52 and is the standing field-test gate for every release candidate.

Added

-registry-only — the leanest public-registry role (#47) — A daemon started with -registry-only serves a file-backed registry over HTTPS and constructs no storage node at all — no DHT, chunk store, chain, or caretaker. It sits below -freeload (which is still a full routing node that merely refuses to host content): a public-infrastructure operator can now run a rendezvous registry at minimal cost. The daemon returns to a tiny registry-serving path before any of the node machinery is built. Proven over real TLS (e2e/registry_only_test.go: a pinned client publishes + looks up an entry, and the daemon never announces a routing peer). With -freeload (PR #201) this completes #47.

Added

Registry read-cost bounding — keep public registries cheap to run (#48) — A registry is a costless public good only if a single caller can't drive unbounded cost, so the registry HTTP server now enforces a per-client-IP token-bucket rate limit (generous defaults — 20 req/s, burst 40 — so normal clients never notice; sustained floods from one source get 429) plus server timeouts (read-header / read / write / idle) against slowloris. Idle rate buckets are pruned on a timer so a caller cycling source IPs can't grow the bucket map without bound (the map would be its own cost vector). GET /all is additionally priced by work (see the Fixed entry below) — a flat token bucket alone meters request count, not the O(N) serialization /all does. Covers the read-cost-bounding lever of #48 for a single source; a distributed /all flood, full cursor pagination, liveness-pruning of dead entries, and federation/sharding remain as post-launch work.

Fixed

Prepaid publish credits were silently dropped over real TCP (#179) — tcpnet's hand-rolled wire codec (toWire/fromWire) never mapped the Credit field of a MsgTokenRequest, so the F4/D3 fee decoupling (paying for a token with a prepaid blind credit instead of charging the durable identity) only ever worked in the in-process sim — over real sockets the credit vanished and the issuer fell back to charging the requester. Added Credit to the wire struct + toWire/fromWire, with a TestCreditSurvivesWire round-trip guard (the exact #65-class silent-drop bug wire_por_test.go warns about). This repairs the publish-token credit path (F4) as well as enabling D3 below.

Added

D3 issuance-mixing, slices 1 & 2 — ephemeral-identity + relay-routed token withdrawal (#179) — Closes the remaining fetcher-unlinkability links in the demand receipt. The token issuer authenticates whoever dials it via the end-to-end TLS handshake, so a withdrawal made over a fetcher's durable identity tied that withdrawal to the fetcher (the blind signature hid only the token serial, not the network identity). Now client. WithdrawDemandTokenPrivately performs the withdrawal over a fresh ephemeral identity — a one-off keypair + transport, torn down on return — paying with a prepaid blind credit (Node. AcquireDemandTokenWithCredit) rather than a durable account, so the issuer authenticates only an unlinkable ephemeral key and charges nothing it can tie to the fetcher (slice 1 — the identity link). Given a relay-form issuer address (relay:R@host:port) the ephemeral transport dials the issuer THROUGH a content-blind relay, so the issuer's inbound connection is from the relay, not the fetcher — hiding the fetcher's IP as well (slice 2 — the IP link); the end-to-end TLS still authenticates the ephemeral key across the relay pipe. Proven over real TCP (client/privissue_test.go: a mock issuer records the authenticated identity — it is the ephemeral one, never the fetcher's, direct and relay-routed; a withdrawal with no valid credit is refused). Fetcher-unlinkability is now cryptographic + identity + IP; timing-correlation (epoch-batching) is deferred to the post-M0 H8 mixnet. Whole suite green with -race; full e2e suite green over real TCP.

Added

#184 verify — forged-block→reject and low-bond→reject over the REAL WIRE (#184) — The two ValidateProposal defences, proven against real daemons over TCP: an honest validator refuses to attest a proposal whose proposer signature is forged (corrupted after signing) or whose proposer lacks a qualifying bond. A single red-team primitive (Node.ProposeBadBlock, behind the daemon's -forge-block / -lowbond-propose flags) sends one crafted proposal to a peer and reports whether it was refused; the block is otherwise valid and built at the target's head, so the only reason for refusal is the fault under test (a bad signature → ErrBadSignature, or an under-bonded proposer → ErrLowReputation). TestForgedBlockRejectedOverTCP and TestLowBondProposerRejectedOverTCP. These complete #184's four consensus-safety cases over the real wire (with equivocation→slash and partition→heal). Whole suite green with -race; full e2e suite green over real TCP.

Added

D-DEMAND P1 — blind-withdrawn retrieval token (#181) — The demand token is now blind-withdrawn: core/blindtoken gains a domain-separated demand variant (BlindDemand/VerifyDemand — a demand token can't be presented as a publish token or credit under the same key), and core/demand upgrades the token from a placeholder issuer-signed serial to Withdraw → SignWithdrawal → Unblind. The issuer blind-signs the token without learning its serial, so the token that later redeems is cryptographically unlinkable to its withdrawal. Fetcher-unlinkability stays nominal until D3 issuance-mixing (H8) closes the IP/timing channel — the blind signature hides the serial, not the withdrawer's network identity. The P0 unforgeability red-team carries over (now over blind tokens), plus an unlinkability regression. Whole suite green with -race.

Added

D-DEMAND P0 — the blind demand receipt primitive (witnessed delivery, unforgeable-at-the-token-level) (#181) — First phase of the B axis (served-demand) of the systemic claim. New pure core/demand: an issuer-signed retrieval token, a PoR-bound delivery-ack (the fetcher signs a Shacham–Waters proof over the delivered bytes, with the challenge bound to serial‖object‖server), and a bank/redeem that credits a per-object witnessed-demand counter once per token. It proves exactly one thing — #receipts(C) ≤ #issued-tokens-spent-on-a-signed-C-delivery — and, per the decision's doc-truth rule, deliberately does not prove demand authenticity (a self-fetch is a real paid delivery; a Douceur limit, re-priced by cost-to-wash in P3, never proven).

  • NEUTRAL by construction. A redeemed receipt is an observable (Bank.Demand) that is never wired to consensus standing — so even a forged or self-dealt receipt buys zero standing, keeping the γ→1/N shared-content firewall intact (fusing demand into standing stays gated on #182). Standing is bond-only today.
  • Unforgeability red-team (each a permanent regression): a token not issuer-signed, a tampered/lifted receipt (server/object/fetcher/sig), a receipt claiming object C' while holding C's bytes (the PoR binding, not just the signature), a data-less "delivery", and a double-spent serial — all rejected; only an honest signed delivery credits demand. The public-per-object-key tag-forgery residual is documented (H7 precedent; inert because demand is neutral).
  • P1 blind withdrawal, fetcher-unlinkability (needs D3/H8), P2 fair-exchange dispute, and P3 cost-to-wash economics remain. Whole suite green with -race.
Added

C2 metric wiring — cost-to-corrupt from the committed bond ledger, split-resistant shed (#185) — The "no quiet capture" axis (C2 / D-C2) gets a first-class, published concentration measurement. chain.C2Metric computes {NakamotoBonds, NakamotoOperators, CostToCorruptBytes, TotalBondedBytes, Margin} over the committed on-chain BondReg ledger — never gossip, which kills the "lie about your size" skew half of the skew+split attack outright. It was previously a private helper that only gated the training-wheels shed; now it is a single measurement consumed by the shed and surfaced for operators.

  • Split-half defense via an operator margin. A BondReg carries no operator label, so real key→operator clustering is impossible on-chain; instead a config OperatorMargin M discounts the bond-distinct coefficient to NakamotoOperators = ⌊k̂/M⌋, and Mature() sheds the anchor training-wheels only when k̂ ≥ MatureValidators × M — so a stake split across many keys must clear k·M distinct bonds. M=1 (default) is the legacy/single-operator behavior, unchanged; the daemon exposes -operator-margin and narrates the metric on every commit (nakamoto N bonds → M operators | cost-to-corrupt … | wheels shed/engaged).
  • Honest residuals (D-C2, unchanged): operator clustering is heuristic by theorem (Kwon) — M only bounds it; M_est under adversarial NodeID placement is unquantified; the honest-whale / real cartel is outside C2. Byzantine-robust sampling and the private-lookup committee-certification consumer (H8/#179) are future. Unblocks the external C2 red-team (#183). Full unit coverage of the metric arithmetic + the split-resistant shed; whole suite green with -race.
Added

H7 proof-of-correct-repair — the false-repair red-team (acceptance gate) (#95) — The self-dealing adversary is driven against the wired verification handler over a live network (core/node/redteam_repair_claim_test.go), proving the crypto's verdict actually reaches the ledger, each case a permanent regression:

  • (a) garbage claim → slash. A claim naming a real position but a bogus shard id: the judge recomputes the position from the manifest-anchored survivors, sees the mismatch (a self-attributing fraud proof), and slashes the claimant — no bounty.
  • (c) compute-but-don't-store → denied, never slashed. A correct shard id on a data-less liar holder (keeps the proof + PoR tags, drops the bytes) fails its identity-bound retrievability challenge → denied but not punished (a shortfall may be transient). This also pins the (b) anti-double-count property: retrievability binds to the named holder, so "the correct bytes exist on the survivors" does not pay.
  • Positive control — an honest claim on a real holder clears both legs, the holder is paid, and no standing moves — so the deny/slash cases are discriminating, not blanket rejection.
  • (d) quorum discovery — every caretaker announcing under the careKey rendezvous is found, so none is silently excluded from the vote. (Domain-diverse quorum SELECTION — refusing a single-domain quorum — is explicit deferred hardening, tracked with caretaker-selection work.) Whole suite green with -race.
Added

H7 finite-but-renewable durability — instrument g + the funded horizon (slice 3) (#95) — silt does not promise perpetual cold-data solvency (that promise is the Arweave endowment identity in credits, and it holds only while the credit-denominated cost of storage keeps falling — which 2020s hardware evidence questions). So durability ships as an explicit finite-but-renewable contract, and this slice makes where an object sits on it measurable (decision D-S7):

  • The escrow now tracks a repair count (PayBounty increments it), and a per-object ports.DurabilitySnapshot (reserve, lifetime funded/paid, repairs) crosses the CreditLedger interface — read-only, classified neutral under the Invariant-A guard.
  • New pure instruments in core/credit read a snapshot: CostPerRepair (realised credits per shard-repair), Horizon (how long the reserve lasts at the observed burn — returning a finite flag so "no burn yet" reads as unproven, never perpetual achieved), and G — instrument g, the annualized trend of cost-per-repair, signed so g > 0 means cost is declining (the condition under which "perpetual" becomes earnable). g stays measured, never assumed.
  • A bounty payment now narrates the drawn-down reserve and cost-per-repair (Node.DurabilitySnapshot exposes the accounting for the observatory). Full unit coverage of the instruments + a repair-loop sim asserting the snapshot's repair count matches bounties released and the funded horizon is a positive finite runway; whole suite green with -race.
Added

H7 self-funding durability — the serve auto-skim goes live (#95) — The escrow that pays repair bounties is now topped up by the object's own traffic. The MsgFetchChunk serve path resolves each coded shard's object root from its storage proof and routes the serve through RecordServeToObject, which diverts a protocol-fixed slice (SkimNum/SkimDen, 1/8) of the serve revenue into that object's durability reserve — so popular data pays for its own repair while the server keeps the net. Shards with no proof-anchored root (manifest chunks, uncoded files) keep the plain serve.

  • Publisher/operator funding APINode.FundDurability(root, amount) prepays an object's reserve from the node's own balance (a pure balance move, never standing), so cold data can be endowed to outlive churn before it is popular enough to self-fund; Node.DurabilityReserve(root) reads the remaining horizon. ports.CreditLedger gains RecordServeToObject. (A publisher-side CLI subcommand waits on the client credit-balance model; the node API is the entry point today.)
  • The invariant holds. Serve income funds the balance economy and the durability reserve — never standing. The integration sim retrieves a whole file, watches the reserve fill from the serves (a slice of the bytes, not the whole thing), and asserts no node's Reputation moves. Full unit + sim coverage; whole suite green with -race.

Added

#184 verify — partition→heal proven over the REAL WIRE (partition control + reorg observability) (#184) — The M0 consensus denial "honest replicas cannot permanently diverge under a partition" now runs against real daemons over real TCP, not only the in-process sim (sim/reorg_test.go). A new -block-peers daemon flag (Node.SetBlockedPeers) simulates a severed link — the node drops all traffic to/from the listed peers — so a validator can be partitioned, each side can make its own progress, and then the link can HEAL (restart without the flag; the persisted chain reloads and reconciles). The e2e (TestPartitionHealsToHeavierForkOverTCP) splits two committing groups into divergent forks (a heavier two-block fork and a lighter one-block fork), then heals the lighter side, which reorgs onto the heavier fork — consensus reconverges on one history. Also adds a Node.OnReorg callback so the daemon surfaces a reorg on stdout (chain: reorged onto a heavier fork (dropped N block(s), …)) — a significant, operator-visible consensus event, and the precise signal the e2e asserts. -block-peers models a transport fault, not Byzantine behaviour (the node stays honest); a real deployment never sets it. Second of #184's four consensus-safety cases (after equivocation→slash); low-bond→reject and forged-block→reject follow. Whole suite green with -race; the e2e passes over real sockets.

Added

#184 verify — equivocation→slash proven over the REAL WIRE (adversarial-daemon harness) (#184) — The accountability property "a proven double-sign costs standing" (D2) is now exercised against real daemons over real TCP, not only in the in-process sim. A new -equivocate red-team daemon flag (quarantined in core/node/adversary.go, loudly announced, reached by no honest path) makes a validator DELIBERATELY double-sign: it places block X at height 1 on one honest peer and a heavier conflicting fork (Y, Z) on another. The honest detector syncs the heavier fork, reconciles across the two histories, and chain.FindEquivocations catches the adversary signing two different blocks at the same height — slashing it. Because fork-choice is summed qualified-attester weight, the heavier fork is deterministic, so the e2e (TestEquivocatorSlashedOverTCP) is not a timing race. Also adds a Node.OnSlash callback so the daemon surfaces slashing on stdout (chain: slashed equivocator …) — a real operator-visible accountability event, not only a debug-log line. Keeping the adversary in the shipped binary (behind the flag) means it runs in CI and lets an external red-team drive the same attack against a deployment to confirm the defence holds. First of #184's four consensus-safety cases; partition→heal, low-bond→reject, and forged-block→reject follow. Whole suite green with -race; the e2e passes over real sockets.

Added

D-DEMAND P2 — the optimistic fair-exchange abort-safety floor (#181) — The demand exchange is content C (server → fetcher) ⟷ a delivery receipt (fetcher → server). Fair exchange provably needs a TTP (Pagnia– Gärtner); silt's is the validator quorum as a threshold-distributed TTP (Asokan–Shoup–Waidner), invoked only on dispute. This ships the optimistic phase + both abort-SAFETY properties, which hold structurally today: (1) fetcher-side — an aborted exchange never consumes the token (a serial is spent only by a completed Redeem), so a server that takes the commitment and delivers nothing leaves the paid token reusable at another server (the fetcher can't be robbed of its token); (2) server-side — a fetcher's pre-release ExchangeCommitment (a signed promise made before content release, domain-separated from the receipt and carrying no PoR proof) can never redeem as demand, so a server can't turn "the fetcher engaged" into a fake completed delivery — the unforgeability bound #receipts(C) ≤ #completed correct deliveries survives the abort path. Regression-locked (token-reusable-after-abort, commitment-is-not-a-receipt, domain separation, optimistic path still credits). Gated, deliberately not built: the dispute-RESOLUTION half — turning a server-held commitment into a TTP-affidavit on fetcher default requires the quorum to verify delivery completed without the fetcher — i.e. verifiable escrow of the content key (Camenisch–Shoup) + threshold decryption t-of-n across validators. The threshold-decryption/DKG half is available in Go (dedis/kyber, drand-grade); the wall is the verifiable-escrow primitive (no adoptable pure-Go impl) plus the large new crypto trust surface — disproportionate to a neutral observable. Same strategy as H7 (floor now, heavy crypto as a fast-follow), different primitive. Demand-neutrality keeps this low-stakes: an unresolved abort only undercounts a neutral observable, never standing. ExchangeCommitment is the seam the future resolver consumes. Whole suite green with -race.

Added

D-DEMAND P3b — the bonded-fetcher credential (second cost-to-wash lever) (#181) — Witnessed demand can now be gated on a bond-distinct fetcher credential: with demand.Bank.RequireBondedFetcher (wired on the daemon as Node.RequireBondedFetchers), a delivery receipt counts toward an object's demand only if the fetcher's signing key is a bond-distinct identity in the committed on-chain bond ledger (chain.IsBonded — the same Sybil-priced, deduped supply the C2 metric measures), and demand then counts distinct bonded fetchers per object. So a self-dealer running one bonded identity can still mint N perfectly valid receipts (a self-fetch is a real paid delivery — Douceur is unbeaten), but witnessed demand rises by 1, not N — re-pricing wash to one real storage bond per faked unit of demand, the best achievable under no-center. This is the second lever alongside the already-shipped P3a fee-burn (each wash burns a real retrieval fee). Demand stays a neutral observable throughout — the gate changes what counts as demand, never whether demand touches consensus standing (it never does; the γ→1/N firewall is intact). Off by default (raw count, unchanged). Self-dealing red-team locked at both the pure layer (core/demand: unbonded → 0, one bonded identity washing 6 → demand 1, 4 distinct bonded → demand 4) and the real node wire (sim: one bonded identity washes 5 → demand 1, unbonded delivery → 0, a distinct bonded identity → +1). Residual: the credential shows the bonded key in the clear, so fetcher-unlinkability stays nominal until D3/H8 — the demand.BondCheck doc marks the exact seam for a blind bond-distinctness proof. Whole suite green with -race.

Added

H7 proof-of-correct-repair — the node/network quorum wiring (#95) — The core/repairproof gate is now wired into the live repair loop, so a durability bounty actually flows on a verified repair. When a caretaker rebuilds a lost shard and places it on a fresh holder (repairStripe), it emits a MsgRepairClaim naming that holder; the object's other caretakers — reached through a new careKey rendezvous (hash(root ‖ "silt/care/v1"), announced on Care), since only a care-link holder has the layout key needed to judge — each independently run both legs:

  • Verify (handleRepairClaim, core/node/repairclaim.go) — reload the layout, fetch k survivors by column (verifying each against its committed id, dropping what it didn't already host — a paramedic, not a hoarder), VerifyByRecompute the claimed position, then challenge the holder's retrievability under the identity-bound RepairChallengeSeed, and Decide.
  • Settle on the LOCAL ledger — release pays the new holder from the object's escrow (PayBounty, capped by the rarest-shard BountyFor multiplier); a self-attributing correctness lie slashes the claimant (SlashFalseRepair). Credit is per-node-local accounting, so each caretaker-judge settles independently and the τ-of-q quorum is the emergent property that τ honest judges reach release — no on-chain bounty transaction.
  • The invariant holds through the wire. A bounty is a pure balance motion — the integration sim churns a stripe, watches a peer caretaker verify and release the reserve, and asserts no node's consensus standing moves at all, so the γ→1/N shared-content hole stays shut. ports.CreditLedger gains PayBounty/SlashFalseRepair/FundEscrow/EscrowBalance; new RepairBountyBase/RepairQuorumTau config (bounty economy off by default). Full unit coverage (settlement truth table) + happy-path sim; whole suite green with -race. (The full self-dealing red-team — garbage claim → slash, relay double-count → denied, compute-but-don't-store → denied, quorum domain-packing — and the caretaker-discovery hardening land next.)
Added

H7 proof-of-correct-repair — the verification layer, slice 2 (logic + wire) (#95) — A repair bounty must pay only for a real, correct repair, never a bare claim. New core/repairproof composes the gate, unit-tested end to end short of the network wiring:

  • Correctness leg (VerifyByRecompute) — reconstruct the lost shard from k survivors and check it is byte-identical to the manifest-committed shard ID. Sound, pure-Go, publicly checkable, content-blind. (A soundness pressure-test proved the plaintext-blind homomorphic-commitment path impossible in pure Go over silt's GF(2⁸) storage — there is no ring homomorphism GF(2⁸)→F_r — so M0 ships this recompute floor; the blind upgrade is a documented fast-follow. See docs/design/h7-proof-of-repair.md §3, §13.)
  • Retrievability leg (VerifyRetrievability + RepairChallengeSeed) — a Shacham–Waters PoR challenge bound to the holder's own node identity, closing the relay/double-count attack (reuses core/por).
  • Release/slash gate (Decide) — release iff correctness holds and a τ-of-q retrievability quorum confirms; a failing correctness recompute is self-attributing fraud → slash. Backed by a new credit.SlashFalseRepair press (classified reduces under the Invariant-A guard: it can only ever lower standing).
  • Repair-role model decided from the real code: silt's repair is a paramedic split (the caretaker reconstructs but keeps nothing), so the bounty pays the new holder of the rebuilt shard (§8b). Wire types (MsgRepairClaim/MsgRepairVote, RepairClaim) landed; the node quorum handler + hot-path hook + sim/e2e are the next slice.

Added

H7 durability-escrow primitives — the S7 funding layer, slice 1 (#95) — The repair loop that keeps content alive under churn must be paid in equilibrium, not charity (the wound that killed Freenet/GNUnet). New in core/credit/escrow.go: a per-object durability reserve (FundEscrow), keyed by an object's root, that pays repair bounties; an auto-skim (RecordServeToObject) that routes a protocol-fixed fraction — SkimNum/SkimDen, 1/8 — of each object's serving revenue back into that object's reserve, so popular data self-funds its durability while cold data draws down what it prepaid; a rarest-shard bounty multiplier (BountyFor) that scales the payout by how under-replicated a stripe is, so repairing the last spare before data loss pays the most; and a PayBounty draw-down that pays what the reserve can cover (a short reserve = the object's funded horizon running out, finite-but-renewable, not an overdraft).

  • The one load-bearing invariant is enforced structurally. The durability budget lives in the balance economy and confers zero consensus standing — a durability credit that bought standing would re-open the shared-content γ→1/N hole (one physical copy of an erasure-coded shard answering for N pledges). The Invariant-A reflection guard (invariant_a_test.go) now classifies every escrow press neutral and fails the build if a new one ships unclassified; the behavioral half fires funding, skimming, and bounty-payout against a bondless identity and asserts Reputation never rises above zero. Standing is still minted by the bond press alone.
  • Prototype-first: these are the ledger primitives. Wiring the auto-skim into the live serve path and gating PayBounty on a verified proof-of-repair transcript are later H7 slices (2 and 3). Full unit coverage; whole suite green with -race.
Changed

External-audit honesty propagation: held-in-tension residuals carried from the spec down to the tenets, risk surface, and public site — Two independent audits of the docs pass (a research comprehension audit + a red-team intention audit) found comprehension faithful but a propagation gap: the honesty that was correct in m0.md §10 / issue #182 hadn't reached the tenet layer, the risk-tracking surface, or the public pages, so three things read as achieved that are deliberately open. No code changed. Fixes:

  • The S7 "one ledger" fusion (served-content ⇄ standing) reworded across TENETS.md S7 + m0.md §5 from an achieved fact to the design goal — today standing comes only from the dedicated identity-keyed bond plot, separate from served content, gated on the γ→1/N problem (#182). A builder implementing the old wording would have re-opened the Sybil break the separation prevents.
  • C_honest = D×A×T×B marked target composition vs. shipped subset (m0.md §3, TENETS Part 0): today standing is gated by the bond (D) axis alone — B (served demand) is unbuilt (#181), A (address diversity) is at the DHT layer, not in the standing number — so C1 is a conditional claim. Added the missing served-demand row to the m0.md §6 as-built map (NOT SHIPPED → #181).
  • γ→1/N added as an explicit open-risk row in risk-register.md + threat-catalog.md; the "proof-of-repair now EXISTS" durability headline softened to construction designed, not yet built (H7/#95) across threat-catalog.md, TENETS.md, decisions.md.
  • D-PRIV propagation: TENETS.md Part VIII table row "Privacy of access is absolute" corrected to the refusal-to-surveil form; decisions.md "publish-unlinkability is delivered" → chain-layer only; transport IP+timing OPEN until D3 (H8/#179).
  • Public site regenerated (index.html/node.html/docs.html): the Sybil-standing copy ("reputation = audits + bytes served / bandwidth counts toward reputation") corrected to bond-backed standing; the unlinkability hero requalified (opt-in blind tokens + IP+timing caveat); "alive forever" → finite-but-renewable; "no token" → "no speculative external token"; "private by architecture" → content-blind.
  • C2 "no quiet capture" promoted to a first-class decision entry (k*≥k̂/M, Kwon floor, honest-whale + adversarial-placement residues); added risk rows for g≤0 and CPR under adversarial NodeID placement; reconciled the threat-model.md BFT self-contradiction.
Changed

Full non-code file audit + remediation; stray binary removed — Audited all 106 tracked non-code files (purpose · last-updated · needed? · safe-to-remove/archive · staleness). Findings actioned; no Go behavior changed except one web-UI default (below).

  • Stray removed: shardnet — a 5.1 MB Mach-O binary committed under the project's old name — deleted and gitignored. No other committed strays or dead files found.
  • The one factual contradiction fixed: docs/risk-register.md still said center-less proof-of-repair was "routed to research"; it's delivered (D-S7) — corrected, plus finite-but-renewable durability and D-DEMAND (cost-to-wash pricing).
  • docs/threat-model.md reconciled (public disclosure doc): the Sybil/eclipse, PoR, free-rider/wash, colluding-quorum, and trust-assumption sections rewritten from the old "reputation quorum / storage bond / DHT eclipse unhardened / Gate 4" framing to the current C1 + C2 composition (objective bonded fork-choice, H5 eclipse hardening, D-DEMAND wash re-pricing, private-by-default).
  • website/docs.html consensus section + meta refreshed from "reputation-quorum" to the objective bonded-quorum / C1·C2 framing; link-format copy corrected for private-by-default.
  • H6 behavioral gap closed: cmd/silt/ui/publish.html defaulted the web publish mode to convergent; now defaults to private (matching the CLI), with the confirmation- attack caveat.
  • Staleness sweep: docs/math/02 + docs/math/07 (convergent-as-default → private), docs/math/05 (retired "Gate 4 #90" citation), docs/math/08 (H4 Byzantine quorum note), docs/design/cross-network.md (relay incentives → D-DEMAND), docs/threat-catalog.md + docs/safety-denylist.md (backfilled the 08-06 commission facts).
  • Archive hygiene: docs/fresh-eyes-council.md archived (a new council brief added at docs/reviews/fresh-eyes-council-brief.md); docs/design/bond-audit.md archived with a live wire-protocol stub left in place; 6 broken intra-archive relative links and 2 stale "LIVING/current" banners fixed; archive/README.md index updated. .gitignore deduped.
Changed

ROADMAP + BACKLOG reconciled to the current strategy; the retired Gate 0→6 spine removed — Both planning docs still narrated the old builder-phase spine ("V1 = Gate 0→6, Gate 4 is the M0 mechanism to build"), which predates the mechanism being built, the composition reset, and the research commission. ROADMAP.md rewritten to the honest current status (storage plane field-proven; M0 mechanism BUILT + H1–H6 hardened; mission reframed as C1 + C2 held in tension; commission answered) and the forward tracks that replace the gate spine: build (H7 durability/proof-of-repair — next; H8 metadata privacy/D3; H9 takedown CT-log + non-globality metric; D-DEMAND blind receipt; the C2-metric-from-ledger wiring; registry economics), verify (multi-machine field test + external red-team vs C1/C2 — the gate to "M0 held"), and the research frontier (shared-content sealing boundary; MSR proof-of-repair; CPR under adversarial placement). BACKLOG.md slimmed to genuinely-open captured ideas + repointed at docs/design/m0.md / docs/decisions.md as the source of truth (shipped placement / networking / observability / fresh-eyes work moved out — it lives in git + buildlog). No code changed. GitHub issues reconciled in the same pass (Gate-4 mechanism issues closed as built; new build/verify/research-frontier tracks filed).

Changed

Research commission answers folded into the decision ledger; the two routed-to-research constructions now EXIST — The follow-up research commission (silt-reviews/research/research-outcome/commission/, eight footnoted memos) answered the questions archive/reviews/research-brief.md had routed out. Recorded across docs/decisions.md, docs/design/m0.md, and docs/TENETS.md; no code behavior changed.

  • D-S7 — construction DELIVERED + durability restated finite-but-renewable. Center-less proof-of-correct-repair now exists as a composition of proven parts (a transparent binary-field polynomial commitment [FRI-Binius, no trusted setup] for correctness + Shacham–Waters PoR for retrievability + a DAS quorum for center-less checking) — ~100 B proof, no plaintext seen, no new primitive for the plain-RS case → build track H7. Durability ships as an explicit finite-but-renewable contract, not "perpetual": perpetual cold-data solvency is the Arweave endowment identity in credits and holds only while a positive credit-denominated cost decline g > 0 (which 2020s hardware no longer guarantees), so silt funds a renewable horizon and instruments g as the number that decides perpetual-vs-finite. (MSR/regenerating-code proof-of-repair stays genuinely open, off the critical path.)
  • D-TAKEDOWN — non-globality metric CONSTRUCTED. A survivor Nakamoto coefficient over failure domains, published as a certified lower bound ≥ t via a ZK threshold predicate that reveals only the scalar t (defeating the discovery-oracle) — as real as the (non-cryptographic) independence oracle. Stays low-urgency → H9.
  • D-DEMAND (new decision). Standing is priced on cost-to-wash, never receipt count. The blind demand receipt (Chaumian token + PoR-bound delivery-ack + quorum-as-TTP fair exchange) delivers unforgeable-delivery + fetcher-unlinkability, but demand authenticity is a Douceur limit — self-dealing is uncloseable by any receipt; wash is re-priced (burned fee + bonded-fetcher credential), not proven away.
  • The core open problem, named precisely. B5 proves C1 (no discount) is a theorem under H1–H3; the single surviving economy of scale is the shared-content sealing boundary (plain PoR over shared erasure-coded shards leaks γ→1/N, closed only by identity-keyed PoRep sealing). silt is not exposed today — standing uses a dedicated identity-keyed bond plot, not the shared shards — but fusing served content into standing without leaking γ→1/N is the open, academic-collaborator task (docs/design/m0.md §10). Cross-cutting engineering find (B1): compute the C2 concentration metric's weight from the committed on-chain bond ledger, not gossip — one measurement feeds three seams.

Changed

M0 reframed as a systemic composition (not a Sybil-proof primitive); tenets amended and docs reset — Adopting the research capstone (09-m0-as-composition.md), M0's Sybil corner is now stated as a systemic claim — C1 (no discount) + C2 (no quiet capture), held in tension — rather than a per-primitive "Sybil-proof" claim that is false by theorem (Douceur: no single primitive prevents Sybils under free identity + no permanent center). This changes what "done" means: a primitive failing a standalone is-it-Sybil-proof test is expected, not an M0 failure; the verdict target is the composition and its seams.

  • docs/TENETS.md amended (see the amendment log). Decisions derived from the accepted research package and recorded: D-PRIV — immutable #4 requalified from an absolute ("access never observable") to refuse-to-surveil (absolute) + access-unobservability held in tension at the metadata layer (the anonymity trilemma is a hard wall). D-S7 — S7 now states the durability funding model (internal escrowable credit reserve; no speculative external token); center-less proof-of-repair is the open construction, routed to research. D-TAKEDOWN — immutable #5 commits every honored revocation to a CT-style transparency log toward a formal non-globality guarantee. D-DISCLOSURE — new Don't #8 (no decryption backdoor at core). B8/S7/immutable-#3 threaded with the composition thesis (C1/C2; the one-ledger S7↔Sybil-budget fusion; the young→mature maturation bet).
  • New docs/design/m0.md — the single M0 spec (thesis + interlock + surface map S1–S8 + the 7 composition seams = the red-team/build target + open decisions + open problems).
  • New docs/decisions.md — the decision ledger, each entry splitting derived direction from deferred construction.
  • New archive/reviews/research-brief.md — open questions for the research team (the two constructions the memos self-flagged non-existent — center-less proof-of-repair and the non-globality metric — plus the seam stress-tests).
  • /archive/ — the finding-by-finding history moved out of the live tree (5 M0 design notes, 5 red-team/acceptance/audit reports, the genesis handoff) behind an index README; nothing deleted. The live tree now carries one current (composition) viewpoint.
  • Every remaining non-code doc reconciled to the composition framing (README, ROADMAP, threat-catalog, the 3 review briefs, and 10 others). No code behavior changed.
Fixed

H6 (privacy, Memo 02): default publish is private — no existence oracle for guessable content — convergent encryption derives the key from the plaintext, so the content address is a deterministic function of the plaintext: anyone who GUESSES it can compute the root and look it up to confirm you stored it (the confirmation attack), and it shipped as the DEFAULT. H6 flips the default publish mode to private (a random per-file key) across every publish path — silt add, swarm add, and the web UI — so identical content encrypts differently each time and can't be probed for; convergent is now explicit opt-in and prints a confirmation-attack warning. Regression: core/pipeline/redteam_h6_test.go — the attacker computes the convergent root of a guessed plaintext; under convergent a registry probe HITS (the oracle, documented), under the private default it MISSES, and two private uploads of identical content don't even collide. The Memo 02 "Proof-of-Ownership" idea was deliberately not added: a PoW-to-serve gate contradicts silt's capability model (the link/manifest IS the read capability) and possession is already gated by store-time hash verification + PoR audits, so private-by- default is the substantive fix (reasoning recorded in the strategy doc §7 H6).

Fixed

H5-B (DHT eclipse, Memo 08): failure-domain diversity — a single-domain key-surround can't suppress discovery — H5-A stopped provider records being forged; this stops them being suppressed. An adversary that grinds the NodeIDs closest to a content key (a ~$4 /24 key-surround) could hold every slot a lookup converges on and simply return nothing. Fix, reusing the gossiped failure-domain (Domain) signal as the diversity dimension: (1) the routing table caps same-domain peers per bucket (dht.Table.SetDiversity), so a one-domain Sybil cluster can't fill the buckets near a key and evict honest peers; (2) provider records are announced to a domain-spread near set, not just the NodeID-closest (announceTargets/diverseNear), so honest nodes in other domains hold the record; (3) after the distance walk converges onto the surrounding NodeIDs, resolution sweeps that domain-spread set (sweepProviders), so the honest holders are actually queried. DHTDomainCap gates it (0 = off); default -dht-domain-cap 2 for the daemon and the ephemeral fetcher. Regression: core/node/redteam_h5b_test.go — an adversary grinds the 10 closest NodeIDs to a key (one domain, suppressing); with diversity OFF the key is undiscoverable, with it ON discovery succeeds through honest other-domain nodes; plus unit tests for the domain-spread near set and the per-bucket routing cap. Residual: Domain is self-reported — binding it to the transport-observed /24 (or per-AS) is the full-strength hardening. Real-TCP e2e green; this completes surface S5 (with H5-A).

Fixed

H5-A (DHT eclipse, Memo 08): self-certifying provider records — records can't be silently forged — DHT provider records were unsigned NodeIDs: a node holding the k-closest slots to a content key could fabricate provider records for identities that never announced, or inject fake providers into the records it re-serves on lookup (the forgery half of the ~$4 key-surround). Fix: ports.ProviderRecord is a signed "I hold content under key K" claim bound to the provider's identity (sha256(pubkey) == ID) and the key, with an optional expiry. A node signs its own announcements with its identity key (SetSigner), the store path (acceptAnnounce) rejects any record that isn't a valid self-announce for the queried key, MsgGetProvidersReply re-serves the signed records, and a fetcher (acceptedProviderIDs) drops any record not signed-for-this-key-and-fresh — so a forged, mis-signed, expired, or cross-key-replayed record is silently discarded, while a fetcher still hash-verifies chunk bytes on receipt. RequireSignedProviders is on by default for the daemon (-signed-providers); unsigned legacy records still flow when it's off (sim/trusted). Wire: new ProviderRecord type + Provider/ProviderRecs message fields, mirrored in the tcpnet CBOR frame. Regressions: core/node/redteam_h5_test.go (a signed record binds to identity+key; a third-party or mis-signed announce is rejected at the store; a fetcher drops injected forged / cross-key records; unsigned records flow only in non-strict mode), real-TCP e2e green under strict signing. Follow-up (H5-B): the suppression half of key-surround (prefix-diversity routing + disjoint-path/wide- region announce, so a key stays discoverable when one /24 owns the k-closest NodeIDs).

Fixed

H4 (consensus safety, Memo 05): Byzantine quorum sizing + Nakamoto-coefficient shed metric — consensus safety rested on a FIXED quorum (default 3) and a training-wheels shed triggered by a HEAD-COUNT of distinct validators. Both are Sybil- fragile: a fixed 3 among 30 validators no longer guarantees two quorums share an honest node (quorum-intersection safety is lost as the set grows), and one operator spinning up many keys could trip the head-count maturity, then capture consensus once the anchors shed. Fix, per Memo 05 (safety is quorum arithmetic at the Byzantine threshold, not reputation weight): (1) Config.ByzantineQuorum sizes a commit's support set (proposer + attesters) at a supermajority n−f of the qualified bonded set (f = ⌊(n−1)/3⌋), so any two quorums intersect in ≥ f+1 ≥ 1 honest validator; the proposer gathers max(floor, RequiredQuorum) and ValidateCommit enforces it. (2) Mature now measures the Nakamoto coefficient over the participating non-anchor bonded set (validatorsSeen ∩ current bond) — the min number of bond-distinct operators needed to reach ⅓ of the weight — which is participation-gated (no fake-genesis decentralization), weight-aware (a set dominated by one bond has coefficient 1 → stays immature no matter how many satellite keys), and revertible (a lapsed bond drops out → the wheels re-engage, the post-shed escape hatch). Both default-on for the untrusted objective posture (effectiveByzantineQuorum, -byzantine-quorum). Regressions: core/chain/h4_consensus_test.go (TestBFTQuorumIntersectionAboveFaultBound proves two quorums always intersect above the fault bound for every set size; TestByzantineQuorumScalesWithValidatorSet + TestFixedQuorumUnsafeWithoutByzantineSizing; TestMaturityNakamotoResistsOneOperator shows one operator's many keys can't trip the wheels), cmd/silt/invariant_b_test.go (S4 default-on). Residual (documented): an operator that splits stake into many EQUAL bonds still inflates the coefficient — stake concentration is invisible on-chain — but it pays the full cost-to-corrupt and the Byzantine quorum bounds it to ≤ ⅓ of weight.

Fixed

H2 / RT-2 (Sybil, High): bond standing decays across time by default — release-and- coast denied — the blind red team broke the Sybil corner (over the G2 fix) through the time axis: a validator registered a genuine bond once, released the plot, and kept voting forever off that single one-time proof, because the bond TTL (BondTTLBlocks) shipped off by default — the third "fixed but off by default" instance. It could not simply be flipped on: renewal happened only when a validator proposed, so an attest-only validator would never renew and would lapse, costing the quorum its weight (a liveness trap). Fix: a non-proposer renewal pathnode.SubmitBondRenewal broadcasts a fresh self-signed BondReg (new MsgSubmitBondReg); a receiver re-verifies it for the current head (chain.ValidateBondReg) and queues it (pendingBondRegs); the next proposer folds the queued peer regs (deterministically ordered, head-filtered so one stale reg can't poison the block) into its block, mirroring pendingSlashes. The chain-sync sweep drives renewal, so an attest-only validator renews without ever proposing. Only then is the TTL made safe-by-default on the untrusted objective posture (effectiveBondTTL, mirroring the anti-release floor; explicit -bond-ttl 0 is the trusted opt-out). Regressions: sim TestObjectiveBondRenewalSustainsAttestOnlyValidator (attest-only validator sustains standing across many TTL windows via the wire renewal path while a released validator is pruned — no liveness regression), core/node/redteam_rt2_test.go (TTL off ⇒ coast survives, the vuln; TTL on ⇒ released plot decays out), cmd/silt/invariant_b_test.go (the untrusted default turns the TTL on).

Fixed

H3 (Sybil, systemic): Invariant-A/B guardrails so a standing press or an off-by-default mechanism cannot ship unaudited — the strategy doc's two meta-patterns ("we fix instances, not classes" and "fixed but off by default") each bit us three-plus times (F1→G2→RT-1; F6→F4→G4→RT-2). Turned both classes into compile-and-test obligations: core/credit/invariant_a_test.go enumerates every standing-granting press (a reflection guard forces each *Ledger method to be classified mints/reduces/neutral; a behavioral guard proves no non-mints press lifts a bondless identity; the sole mints press — the bond — is asserted identity-bound + deduped + bond-gated), and cmd/silt/invariant_b_test.go builds the default untrusted-validator config and asserts it denies the attack per mechanism (S1 anti-release floor on, S3 bond-TTL on). A new press that skips classification or a mechanism that ships off-by-default now fails loudly.

Fixed

H1 / RT-1 (Sybil, Critical): PoR audits no longer mint consensus standing — a disk-less relay farm earns nothing — a fresh blind red-team broke the Sybil corner (over the G2 fix) via the proof-of-retrievability audit press: credit.Reputation added auditsPassed·25 with no bond gate, and the PoR proof was a pure function of (chunkID, challenge, data) — not bound to the prover, and challenged with a shared, publicly-derivable seed. So a data-less identity could relay an honest holder's aggregated (μ, σ), pass, and reach propose/attest eligibility (100 rep) with zero storage — the code's own "a liar without the bytes cannot answer" comment was false (relay doesn't need the bytes, only a holder that has them). Fix (architectural, per docs/design/m0-hardening-strategy.md Invariant A + research memo 03: plain PoR over shared content is not Sybil- resistant): PoR audits grant no Sybil-resistant standing — removed the mint, so standing rests on the identity-bound storage bond alone; audits now fund only the balance economy and remain a negative integrity signal (a failed audit still subtracts, and can never be Sybil-amplified). Defense-in-depth: the challenge is now identity-bound (porProverSeed = H(base‖proverID)), so a relayed proof for one identity fails another's verify. Regressions: core/credit/redteam_rt1_test.go (audit passes grant 0 standing without a bond; an Invariant-A property test that no press mints standing without a bond; failed audits still penalize), core/node/redteam_rt1_test.go (relayed proof denied), sim/por_standing_test.go (holder passing audits over the wire earns 0 standing without a bond). Standing-granting sims/tests updated to earn standing via the bond press. Residual (tracked): a colluding bonded holder can still recompute a proof per Sybil to farm balance (not standing) — closing that needs sealed real-content replicas (backlog H7). Honest status: built + covered, awaiting external re-verification (B8).

Fixed

G2 (Sybil, Critical): the storage bond is now a VERIFIED proof-of-space — prefix plots can no longer back N standings from one disk — the fix-verification red-team broke the Sybil corner a second time, over the F1 fix code, with prefix plots: plotBlock/parentIndices keyed only on (secret, i) and never on the total block count n, so blocks 0..m-1 of an n-block plot were byte-identical to a standalone m-block plot with its OWN distinct Merkle root — and VerifySpaceTime only checked Merkle inclusion, never recomputed a label. Per-root dedup (F1) keys on equal roots, so it was structurally unable to catch a family of distinct prefix roots: the scheme was proof-of-STORAGE, not proof-of-SPACE, and one physical plot backed ~N standings (marginal cost of one more Sybil ≈ one 4 KiB block). The fix (a graph-labeling proof-of-space over silt's existing DRSample graph — DFKP CRYPTO'15 / ABH CCS'17, adopted not invented) seals the plot from a public, identity- and size-bound seed H("silt/bond/plot/v3" ‖ pk ‖ n) folded into both the labels and the parent draws, and adds a labeling-consistency challenge: the answer opens k challenged nodes with their predecessor and DRSample parents (Merkle- proven), and the verifier recomputes each label from the opened parent bytes under H(pk, n) and requires a match. Because the seed is public the verifier can do this without holding the plot, so identity and size become checked properties of the plot, not claimed ones: a prefix, a foreign-identity plot, or arbitrary committed bytes all fail the recompute. N standings now require N plots. k is a per-network knob (-bond-label-k, Config.BondLabelSamples, default 64; soundness error ≤ (1-ε)^k against an ε-short prover); leaving it unset resolves to 64 inside core/bond, so the check is never silently disabled. The seed and the labeling check ship together (a public seed without the check would regress griefing), and G3's "proof beats declaration" rule is load-bearing for the public seed's griefing-safety. Plot format v2 → v3 — a one-time fleet re-plot; the disk version guard forces it so a restart never reloads an insecure v2 plot. Regressions: core/bond/redteam_g2_test.go (a prefix passes possession but fails the labeling check; a plot for one key fails under another; arbitrary bytes fail; a prefix family forges zero standings; k unset still denies), sim/bond_sybil_g2_test.go (a Sybil pointing at another node's plot earns no standing over the live-audit wire), adapters/diskplot (a v2 file loads as absent → re-plot), and the objective/audit e2e paths carry the ~1.5 MB label proof over TCP and on-chain. Design: docs/design/m0-sybil-rebind.md. Honest status: built + covered across all three tiers, awaiting external red-team re-verification — not self-certified held (immutable B8: the tight ε→k constant and the on-chain proof-size / asymmetric-k mitigation are the carried open risks in the design note §8).

Fixed

Retest G4-residual: the anti-release floor is now ON BY DEFAULT for an untrusted validator#163 shipped the floor + re-challenge mechanism but defaulted both knobs to 0, and the daemon did not auto-enable them on the earned-standing M0 path (unlike -objective, which is auto-on when -min-rep > 0). So a stock, doc-following open validator still admitted a sub-floor, releasable bond to full objective standing — fixed but off by default is not fixed. The anti-release floor now gets the same treatment -objective has: it defaults to a derived 1 GiB for an untrusted validator (-validator + -min-rep > 0 + objective), the value the flag's own arithmetic implies (~270 MB/s plot throughput × the ~2 s challenge window ≈ 540 MiB, with ~2× margin). The daemon fails closed if -bond is under the floor — an actionable refusal beats running a validator that silently earns nothing — and an operator can still opt out explicitly with -min-bond-floor 0 for a trusted/demo swarm. A non-validator is unaffected. Regression: cmd/silt/bondfloor_default_test.go (the derived floor exists, exceeds what re-plots inside a challenge window, denies the default 64M bond, and an explicit choice — including 0 — always wins). Docs + the local walkthroughs opt out explicitly and now document the floor. Known gap, deliberately NOT defaulted on: -bond-ttl (the objective re-challenge cadence) stays off, because bond renewal currently happens only when a validator proposes (chainrole.go), and proposing is event-driven — an attest-only validator would never renew and would lapse, costing the quorum its standing. Defaulting the TTL on requires a renewal path for non-proposers first; tracked as follow-up.

Added

Design note: rebinding the storage bond to identity and size (M0 Sybil / G2)docs/design/m0-sybil-rebind.md. The Sybil corner is open: a red-team pass over the F1 fix code broke it again via prefix plots (blocks 0..m-1 of an n-block plot are byte-identical to a standalone m-block plot, and each prefix has its own distinct Merkle root, so per-root dedup never fires). The root cause is that VerifySpaceTime checks only Merkle inclusion and never recomputes a label — proof-of-storage, not proof-of-space — while identity is asserted by a signature over an attacker-chosen root rather than verified. The note specifies the fix (a public, identity- and size-bound plot seed plus a labeling-consistency challenge the verifier recomputes without holding the plot), its soundness parameters (k ≥ λ·ln2/ε), the wire format, the build sequence, and the ordering constraints — including that the public seed must never land before the labeling check, and that the G3 "proof beats declaration" fix is load-bearing for its griefing safety. Derived by an independent researcher pass with no build context. Not yet built; M0 is not held.

Fixed

Retest G4 (Sybil/time, High): the objective validator set now enforces an anti-release floor and re-challenges bonds on a cadence — the fresh pass found the "time" half of proof-of-space-TIME was not enforced on the OBJECTIVE fork-choice path: c.bonded was set once at registration on a one-time proof and never decayed or re-challenged, and chain.Config had no anti-release floor at all (only MinBond). So (a) a sub-floor bond — small enough to release and re-plot inside a challenge window — earned full objective standing, and (c) a validator could prove once, RELEASE its plot, and keep voting forever with zero resident storage (the node-side floor + live re-challenge lived only in the credit ledger the objective set never reads). Two additive, deterministic knobs close it: Config.MinBondBytes (an objective anti-release floor — a bond below it earns no standing, rejected on the normal path and uncredited at genesis) and Config.BondTTLBlocks (objective standing LAPSES this many blocks after a validator's latest registration unless it renews with a FRESH space-time proof — height-driven, so every replica decays in lockstep). A validator that releases its plot cannot answer the fresh challenge to renew, so its vote decays to nothing. The daemon wires both: the existing -min-bond-floor now also feeds the chain floor, and a new -bond-ttl sets the cadence. Both default to 0 (off), so legacy/sim configs are unchanged. Regressions: core/chain/redteam_verify_objective-antirelease_g4_test.go (sub-floor bonds earn zero standing / are rejected; standing decays without renewal and persists with it) and core/node/redteam_verify_objective-antirelease_g4_test.go (through the real bond.VerifySpaceTime: a validator that stops renewing lapses; a continuously-renewing one keeps standing).

Fixed

Retest G3 (Accountability, High regression): a genesis bond-squat can no longer lock out an honest validator — the fresh pass found the F1 per-root dedup (#158) became a griefing lever when combined with the pre-existing unvalidated genesis BondRegs: a malicious genesis pre-squats an honest validator's real plot root under an attacker key (no space-time proof — genesis regs are declared), so when the true holder later registers that root on the normal path with a REAL, verifier-accepted proof, apply's first-owner dedup sees the root already claimed and drops the honest credit — the holder earns 0, the squatter keeps unbacked standing. Fix: proof beats declaration. apply now tracks whether a root's owner claimed it with a verified proof (a height>0 registration, gated by validateBondRegs) or a mere declared genesis reg (bondRootProven); a verified registration DISPLACES an unproven declared claim (stripping the squatter's standing), while every other collision still earns nothing — so once proven, first-proven-owner wins and F1 is preserved. Regressions: core/chain/redteam_verify_genesis-bondsquat_g3_test.go (inverted PoC: V's proof displaces the squat; a second identity still can't share the proven root) and core/node/redteam_verify_genesis-bondsquat_g3_test.go (a real live bond registration displaces a genesis squat through the objective space-time verifier).

Fixed

Retest G1 (Accountability, Critical regression): a genesis block can no longer carry an equivocation Slash — the fix-verification red-team's fresh pass over the F1/F2 code found that #158's on-chain Block.Slashes reopened, for a stronger lever, exactly the door #159 (F3) closed for Revocations. AppendGenesis skips validateSlashes, and apply unconditionally evicts every Slashes culprit (slashed[id]=true, dropped from bonded, barred from re-earning, carried through adopt), so a genesis carrying an unverified Slash was a proof-free, pre-emptive, identity-level kill switch — a fortiori what immutable #5 forbids. A slash is only meaningful against equivocation within a chain's own history, of which a genesis has none, so AppendGenesis now rejects any genesis carrying Slashes (ErrGenesisTakedown), symmetric with the F3 guard; a slash must go through the normal path where validateSlashesVerifyEquivocation gates it on a real double-sign proof. Regressions: core/chain/redteam_verify_genesis-slash_g1_test.go (genesis slash denied, victim keeps standing, normal-path slash still fires on a real proof) and core/node/redteam_verify_genesis-slash_g1_test.go (a node in objective mode never establishes a genesis that evicts an honest bonded validator).

Fixed

Blind red-team F4 (integrity, S1): the auditor no longer trusts a prover's self-reported PoR block count on the file's last shard — the audit graded every leaf but the last against a block count it recomputed itself, while the LAST leaf took a lenient "tail" branch that accepted any 1..wantFull. Since porChallenge clamps the sample space to the prover's reported count, a liar holding only block 0 of an N-block shard could report PorBlocks=1, be challenged on block 0 alone, and pass — earning rent while holding ~1/N of the shard, with no slash and no repair. The premise behind the leniency was wrong: chunk.Split zero-pads the last frame up to ChunkSize (the true length rides in the frame header) and erasure pads short stripes, so every stored shard is full-size on the wire — there is no short tail to accommodate. The auditor now demands the same recomputed full block count for every leaf, so a prover can never shrink its own challenge. Regressions: core/node/redteam_verify_liar-por_0_test.go (inverted PoC — the shrink liar's grading predicate now fails, an honest holder still passes) and sim/audit_tailshrink_test.go (integration: a shrink liar on a single-chunk file's sole — previously lenient — leaf is slashed into debt while honest holders pass).

Fixed

Blind red-team F3 (Accountability): genesis can no longer pre-emptively revoke a never-published rootAppendGenesis calls apply directly and skips validateTakedowns, so a genesis block could carry Revocations naming a root never published — a pre-emptive takedown, exactly what immutable #5 forbids ("a takedown is never pre-emptive"), honored forever by any node running -honor-chain-revocations. AppendGenesis now rejects any genesis carrying Revocations or Unrevocations (ErrGenesisTakedown): a genesis seeds entries and declared launch bonds only; a takedown must go through the governed normal path where ErrRevokeUnknownRoot enforces that the root already exists. Regression core/chain/redteam_verify_censor_0_test.go (inverted PoC: genesis takedown rejected; the normal-path existence guard still fires).

Fixed

Blind red-team F1 (Sybil, Critical) + F2 (equivocation slash inert): the objective validator set now honors the two defenses it was bypassing — a second, blind red-team pass (ae005e9) found that promoting objective on-chain-bond fork-choice to the M0 default (#154) made it authoritative for standing, but it skipped two defenses that lived only in the non-authoritative core/credit reputation ledger. Both are now carried into core/chain. F1 — per-root bond dedup: the objective set never checked that a bond Root was unclaimed, and the space-time proof is not identity-bound, so N cheap identities could register the same plot's root+answer and each earn full MinBond fork-choice weight — one 4 MiB disk buying a whole write quorum. apply/validateBondRegs now enforce a bondRootOwner map (a root credits AT MOST ONE identity, the first to claim it; the owner may renew), so N Sybils cost N independent bonds again. F2 — on-chain equivocation slash: SlashEquivocation only mutated the reputation ledger, which objective mode never reads, so a proven double-signer kept full eligibility and weight. Slashing is now an on-chain record (Block.Slashes, a self-verifying equivocation proof) that on commit evicts the culprit from c.bonded and bars it from re-earning standing — applied in lockstep on every replica; a forged slash is rejected (ErrBadSlash), so forged-slash griefing stays denied. The node records detected equivocations on-chain in the next block it proposes. Regressions: core/chain/redteam_verify_* (shared-root denied, slash evicts, forged slash rejected) and core/node/objective_slash_test.go (over the loop: a node detects, records, and every replica evicts).

Security

M0 composition: every red-team finding (F1–F7) fixed and covered by tests — awaiting external re-verification — following the red-team break below, all seven findings now have a shipped fix with unit + in-process simulation coverage, and real-TCP e2e where a daemon surface exists. Sybil (byte-binding over a depth-robust graph + read-bound VDF + anti-release floor), Privacy (ephemeral publish identity + prepaid Chaumian credits + canonical issuer set), Accountability (existence-checked, per-operator, reversible takedowns), Consensus (objective on-chain-bond fork-choice with an anchor cold-start; F7 resolved by F6 + sound same-height slashing). The per-finding fix + how-to-verify guide for the next reviewer is docs/reviews/M0-REDTEAM-VERIFICATION.md. This is the builder's response, NOT a self-certification: M0 is held only when a fresh external red-team denies all three failure modes. Deliberately deferred residuals (honestly recorded): the public-IP issuance IP+timing refinement (F4; the stronger NodeID/fee/subset links are severed and NATed clients already relay), and flipping the objective-mode default (a launch-config decision).

Security

M0 external red-team: primitives real, composition unproven, M0 not yet held — the independent M0 red-team ran against shipped code (c1397e0) and broke all three corners in the novel composition. The adopted primitives held (the Wesolowski VDF and the Shacham–Waters PoR were attacked and denied). Full report: docs/reviews/M0-REDTEAM-REPORT.md; live status carried in docs/design/gate4-m0-mechanism.md. This supersedes earlier changelog language that presented the corners as resolved.

  • Accountability — 🟢 FIXED (below, #136).
  • Sybil — 🔴 BROKEN (F1/F2/F3): the PoST plot binds only the 32-byte block leaves, not the block bytes, so a prover holds ~1/128 of the storage it is charged for (→0 for small bonds, re-plotted inside the VDF window); and the VDF "time" half gates nothing because its challenge input is public. Earlier entries claiming "N distinct blobs of real storage" and "cannot release the space and re-plot" are false against this attack and are corrected in-code (core/bond/bond.go). Fix = bind to block bytes (memory-hard/DRG) + a pre-VDF plot read; mechanism design turn.
  • Privacy — 🔴 BROKEN (F4): the D3 issuance-mixing layer was never shipped, so AcquireToken de-anonymizes the publisher at token acquisition by IP+timing (and the fee debit). The residual was previously described as a "narrowed anonymity set"; in shipped code it is a singleton (direct de-anonymization). Fix = route issuance over the content-blind relay, epoch batch, decouple the fee; privacy design turn.
  • Consensus (D2) — 🔴 BROKEN (F6/F7): fork-choice weight is the subjective local reputation view, not objective on-chain bond, so two honest replicas diverge permanently; and cross-height double-backing evades the equivocation slash. Fix = objective bond-weighted fork-choice (depends on the Sybil fix) + slashing that distinguishes malicious double-backing from honest reorg-following; consensus design turn.
Docs

M0 mechanism design turn: per-corner fix write-ups — the three broken corners each get a skeptic-readable design doc that names the exact break (file:line), the adopt-don't-invent fix, the composition, the schema touch, and a falsifiable denial with the red-team's own PoC inverted as regression. Sybil (F1/F2/F3)docs/design/m0-sybil-bond.md: a proven depth-robust graph over full-byte labels (closes the 1/128 gap) + a pre-VDF plot-read seed (releasing the space forfeits the answer). Privacy (F4)docs/design/m0-privacy-issuance.md: D3 issuance-mixing — relay + ephemeral transport, epoch batching, canonical validator set, and a prepaid blinded-credit fee decoupling. Consensus (F6/F7)docs/design/m0-consensus.md: objective on-chain PoST-bond fork-choice weight + Casper-FFG-style surround-vote slashing that spares honest reorg-followers. The Sybil bond is the keystone (consensus depends on it); privacy is independent. Linked from docs/design/gate4-m0-mechanism.md. Design only — no code changed.

Fixed

Consensus (red-team F7): cross-height double-backing resolved — by F6 plus sound same-height slashing, without slashing honest reorg-followers — the report's F7 (sign fork A@1, sit out B@1, sign B@2 — never the same height on both, evading the same-height equivocation slash) is now resolved, and the resolution is the honest one rather than a wrong slashing rule. Worked through precisely and locked in core/chain/redteam_f7_test.go: (1) same-height double-signing is still slashed (FindEquivocations, the distinguishable misbehavior); (2) cross-height double-backing is provably indistinguishable from an honest reorg-follow from the blocks alone (a validator that attested A@1 then followed a heavier fork to attest B@2 produced identical evidence), so any rule slashing "signed two incompatible forks" would slash honest validators — a regression — and detection correctly does not flag it (the guard test); (3) objective fork-choice (F6) neutralizes it anyway — the double-backer cannot make both histories stand, the heavier-bond fork wins on every replica. The pre-F6 design had planned Casper-FFG surround-vote slashing; the analysis shows it is unnecessary here (F6 neutralizes) and, for this exact pattern, ineffective (the spans do not surround), so a finality gadget is not added for M0. docs/design/m0-consensus.md §2b carries the reasoning.

Changed

The default -token-quorum publish now uses the prepaid-credit path (closes red-team re-verification #4) — the re-verifier confirmed the fee-decoupling credit mechanism works but flagged that cmd/silt/swarm.go still acquired tokens via the legacy AcquireToken, so a default token-quorum publish still hit ChargePublish(from) per publish. acquirePublishToken now mints one prepaid credit per validator (the fee is charged at mint) and spends them for the k blind signatures, so the publish itself records no per-publish fee debit — the credit path the mechanism was built for is now the default publish path, exercised end-to-end over real TCP (e2e/TestUnlinkablePublishOverTCP). The whole flow runs from the swarm client's already-ephemeral identity. Residual (deliberately deferred, option B): the IP+timing transport link (relay-forced issuance + epoch batching) — NATed clients already relay; a public-IP client's issuance IP/timing is the last D3 piece.

Changed

Objective fork-choice is now the DEFAULT for an untrusted validator (closes red-team re-verification #6/#7) — the fix re-verifier confirmed objective mode heals divergent replicas but flagged that it was off by default, so a stock validator swarm still ran the legacy subjective path that diverges under partition. silt daemon -objective now defaults to true and is active for any untrusted validator (-min-rep > 0); a trusted swarm (-min-rep 0, self-commit) auto-disables it, and the legacy subjective path is now an explicit, labeled opt-out (-objective=false, which prints that it does NOT hold the M0 denial under an adversarial partition). A multi-validator quorum still bootstraps from the declared launch -anchors (the honest trustless- cold-start boundary); without them the daemon warns and a multi-validator swarm will not commit, rather than silently running the divergent path. Verified e2e: e2e/TestObjectiveConsensusCommitsOverTCP now runs with no -objective flag, proving the default path is objective; the legacy-path e2e/example flows opt in with -objective=false. This makes "two histories both stand" unreachable with stock validator flags — the residual the re-verifier asked to close.

Added

silt daemon -honor-chain-revocations and -revoke: operate on-chain takedowns, with an e2e proof (F5) — the accountability fix's per-operator honoring and quorum-gated, existence-checked revocation are now operable from the binary. -honor-chain-revocations subscribes this operator to on-chain takedowns (default OFF — following the chain never imposes someone else's takedowns; the operator-local -denylist is always honored). -revoke <root> makes a validator propose an on-chain takedown of a root once it has earned standing and the root is committed (retried on the loop-safe clock; the chain enforces existence + quorum). This completes the F5 test pyramid with the e2e tier (e2e/TestChainRevocationCommitsOverTCP): a validator drives a quorum revocation of a published root over real TCP and it commits. The per-operator honoring is covered at integration (sim/revocation_test.go).

Added

Anti-release bond floor (-min-bond-floor / Node.MinBondBytes): a bond too small to be safe against release + re-plot earns no standing (M0 Sybil F1/F2) — the byte-binding + read-bound-VDF plot makes a released prover recompute (memory-hard) before it can answer, but that only bites if re-plotting the pledged size takes LONGER than the challenge window. At the measured plot throughput (~270 MB/s, bond.BenchmarkSeal) a 500 ms window re-plots ~135 MiB and this daemon's ~2 s window ~540 MiB — so a bond at or below that could be released and recomputed just-in-time. A bond below Node.MinBondBytes now earns no standing, self or peer, at the live audit: bondAuditOnce gates both the self-credit and the peer-credit on the floor, so a valid answer for a sub-floor plot proves nothing about sustained possession. Exposed as silt daemon -min-bond-floor (default 0 = off, since every fast test/demo/NAT config uses tiny bonds; an open deployment sets it above window × throughput, e.g. 1G, and the daemon warns if -bond is below it). Coverage: unit (core/node/bondfloor_test.go — a sub-floor bond earns 0, an at-floor bond earns standing) and integration (sim/bond_floor_test.go — a sub-floor validator is denied standing over the live audit wire while an at-floor one earns it). BondVDFDelay remains the complementary time-floor knob. See docs/design/m0-sybil-bond.md.

Added

silt daemon -objective: run consensus on objective on-chain-bond fork-choice (F6), with an e2e proof — a validator can now enable objective mode from the binary: -objective (with -min-bond, and requiring -anchors + -mature-validators > 0 for the cold-start) wires the on-chain-bond verifier and makes the validator register its real bond live as it proposes, so eligibility, quorum, and fork-choice weight come from verifiable on-chain bonds instead of the local reputation view. This completes the F6 test pyramid with the e2e tier (e2e/TestObjectiveConsensusCommitsOverTCP): two -objective daemons bootstrap via anchors and drive a real objective quorum commit over real TCP, and the file round-trips bit-perfect — the bond-registration-and-verification protocol works end to end, not just in the sim. Objective mode remains opt-in at the daemon (the default stays the legacy reputation path); flipping the shipped default is the remaining step, tracked in docs/design/m0-consensus.md.

Fixed

Consensus (F6): the objective-fork-choice cold-start — an anchor-bootstrapped validator set that builds itself from real bonds — objective mode had a chicken-and-egg: a validator must be bonded ON CHAIN to propose/attest, but the first block that records bonds must itself be proposed and attested. It is now solved with the existing training-wheels anchors: in objective mode a declared anchor is eligible to propose/attest while the network is immature (Chain.launchAnchor), so the declared launch set commits the early blocks; validators register their real bonds live as they propose (Node.RegisterBondReg, attached by proposeBlock; Chain.Objective / NewBondReg / BondRegNonce are the seam); and the anchor eligibility sheds mechanically at maturity (Mature). It grants eligibility, never fork-choice weight — weight is always summed real bond, so a declared anchor can never outweigh a proven one, and a network that never decentralizes simply keeps its training wheels. Coverage: unit (core/chain/objective_coldstart_test.go — an anchor bootstraps an empty objective set then sheds at maturity) and integration (sim/objective_coldstart_test.go — an anchor-only network with a separate empty ledger per node bootstraps consensus, and proposers become really bonded on-chain by self-registration, agreed across replicas). Residual: the daemon -objective flag wiring + an e2e run over real daemons. See docs/design/m0-consensus.md.

Fixed

Test coverage backfill (build-immutable): the Accountability fix (F5) now has the integration tier — the F5 fix (on-chain revocation is existence-checked, per-operator opt-in, reversible) had unit + node-white-box coverage; this adds the integration tier over the full node loop (sim/revocation_test.go): a bonded quorum publishes a root, then commits an on-chain revocation of it over the wire, and — the load-bearing property — the takedown is honored per operator: a subscribing node (SetHonorChainRevocations) denies the root while a node on the identical chain that did not subscribe does not (never a global switch); and a quorum cannot revoke a root the chain never committed (ErrRevokeUnknownRoot). Adds Node.WouldDeny — operator-facing observability for the effective, per-operator takedown decision. e2e is explicitly deferred: the daemon does not yet expose chain-revocation proposing (no revoke command / auto-propose) or the honor-subscription flag, so the full quorum-revocation-honoring flow is not drivable end-to-end; it lands when those daemon features do. A stated tier choice, not a silent gap.

Fixed

Test coverage backfill (build-immutable): the Privacy fixes (F4) now have the integration tier — the fee-decoupling and canonical-issuer-set fixes shipped unit-only; this adds the outcome-driven integration tier. Fee decoupling (sim/credit_fee_test.go): a publisher mints prepaid credits over the real node loop (charged in bulk at mint), then publishes by SPENDING a credit over the wire — and its durable standing key balance is unchanged by the publish (the ledger-level link severed end-to-end), the token verifies, and re-spending a credit over the wire is refused (double-spend). Canonical issuer set (core/node/objectivechain_test.go): two nodes on the same objective genesis surface the IDENTICAL deterministic on-chain-bonded issuer set, and a node with no chain surfaces none. e2e for the transport-layer parts (relay + ephemeral + epoch) lands with those parts, which are not yet built.

Fixed

Test coverage backfill (build-immutable): the Sybil fix (F1/F2) now has the integration tier it was missing — the shipped Sybil fix carried only its unit tier (the red-team PoC inverted in core/bond); the build-immutable rule (V5) requires unit + integration + e2e. Added the integration tier: sim/bond_release_test.go drives the property through the live audit wire (gossip → MsgBondChallenge → answer → VerifySpaceTime → ledger) — a validator that pledges a bond, advertises it, then RELEASES the resident bytes (holding at most the 32-byte leaves, the attacker that frees the space to save disk) FAILS the live audit and earns ZERO standing, while an honest full-plot validator earns it. A bond.Commitment.ReleaseBlocks / Node.ReleaseBond adversary seam (cf. SetLiar for PoR) models the release. e2e is already covered by e2e/TestBondEarnedStandingCommitsOverTCP (two real daemons proving bonds to each other over real TCP, exercising the fixed read-bound-seed protocol); the released/leaves-only adversary is proven at unit+integration rather than e2e because forcing it end-to-end would mean shipping attack behavior in the production binary — an explicit, stated tier choice, not a silent gap.

Fixed

Consensus (red-team F6): objective fork-choice is now wired into the node, with integration + unit coverage — the F6 objective-weight mechanism (on-chain BondRegs) previously existed only in core/chain behind a verifier a caller had to supply. A node now wires it in one call: Node.EnableObjectiveChain injects the real space-time bond verifier (bond.VerifySpaceTime, the same check the audit loop runs), and Node.RegisterBondReg mints a signed registration from the node's held bond for live entry into the objective set (chain.NewBondReg / exported chain.BondRegNonce; EnableBond now records the identity signer so a bonded node can register before it joins consensus). Coverage now spans all three tiers per the build-immutable rule: unit — a live registration round-trips through the real verifier and a tampered space-time proof is rejected (core/node/objectivechain_test.go); integration — the red-team's non-healing-partition scenario inverted, with a separate empty ledger per node (so the local reputation view is useless, unlike sim/reorg_test.go's shared ledger): the partition still commits and heals to the heavier-bond fork on every replica (sim/objective_consensus_test.go). Residual: turning objective mode on by default in the daemon (a genesis/anchor-seeded validator cold-start plus a live-registration submission path), and an e2e multi-process run, remain; see docs/design/m0-consensus.md.

Fixed

Privacy (red-team F4 §2c): a canonical, on-chain issuer set so the validator subset a publisher asks leaks nothing — a publisher previously acquired publish tokens from whatever validator subset its -peers gave it, so a colluding issuer minority could narrow the anonymity set by which validators a given publish asked. Chain.CanonicalIssuers (and Node.CanonicalIssuers) now derives a deterministic issuer set from the on-chain bond (the same objective bonded map that heals fork-choice, F6): bonded validators ordered by size then NodeID, identical on every replica. Every publisher asks the same validators, so the subset choice carries no signal. Regression (core/chain/redteam_consensus_test.go) proves two maximally-divergent replicas produce the identical ordered set. This is one of the three network-layer parts of D3; the transport parts (routing issuance over the content-blind relay from an ephemeral identity, epoch batching) are still pending, so IP+timing correlation remains until they land. See docs/design/m0-privacy-issuance.md.

Fixed

Privacy corner (red-team F4): the per-publish fee no longer links a publish to its standing key — token issuance de-anonymized the publisher two independent ways: over a non-anonymous transport (IP+timing) and via ChargePublish(from), a per-request debit of the durable standing account. This lands the fee decoupling — prepaid publish credits (online Chaumian e-cash, Chaum 1982). A credit is a blind signature under the issuer's key but in a separate FDH domain (blindtoken.BlindCredit/VerifyCredit), so a credit can never be presented as a publish token or vice versa even under one key. The fee is charged in bulk at mint (a normal, charged token request blinded in the credit domain); at publish the requester spends a credit (Message.Credit, verified and marked spent in an online double-spend set) and the issuer does not charge the durable identity — severing the ledger-level link. The change is purely additive: a request with a credit spends it (no debit), a request with none takes the legacy charged path, so existing token flows are unchanged (whole suite + vet + -race green). New helpers Node.AcquireCredits (bulk mint) and Node.AcquireTokenWithCredits (spend). Regressions in core/node/redteam_privacy_test.go show a mint charging once, a publish charging nothing more, a spent credit refused (double-spend), a forged credit refused, and the credit/token domains proven non-interchangeable. Residual (honest): the network-layer link — routing issuance over the content-blind relay from an ephemeral identity, epoch batching, and a canonical validator set — is not yet built, so a colluding issuer minority can still correlate by IP+timing; the privacy corner does not fully hold until that lands. See docs/design/m0-privacy-issuance.md.

Fixed

Consensus corner (red-team F6): fork-choice is now objective — honest replicas stop diverging — fork-choice weight, the quorum count, and proposer/attester eligibility used the local reputation view (c.rep(id)), so two honest validators that had audited different peers computed different weights and forked permanently (the partition never healed). Fork-choice is now driven by on-chain PoST-bond registrations (Block.BondRegs): a validator records its bonded size with a fresh space-time proof any replica re-verifies (SetBondVerifier), bound to the block's parent so it can't be replayed to another height/fork and signed so it can't be claimed by a non-holder. Weight becomes the summed on-chain bond of a block's distinct attesters — a quantity every replica recomputes identically from the chain — so divergent local views can no longer disagree on which fork is heavier, and a lighter fork reorgs onto the heavier one on every honest node. The mechanism is additive and opt-in (Config.MinBond > 0): the field is omitempty so a block with no registrations hashes exactly as before (no BlockVersion bump), and the default path is unchanged — the legacy reputation-gated behavior and every existing test/sim are untouched. Regressions in core/chain/redteam_consensus_test.go show two maximally-divergent replicas computing the same weight, a partition healing to the heavier-bond fork, and a forged registration (bad proof or bad signature) denied. Residual (honest): the objective-mode wiring in the node/daemon (validators emitting registrations, a genesis-seeded validator cold-start, enabling MinBond in production) is a follow-up, and F7 — cross-height double-backing evading the same-height-only equivocation slash — is not yet fixed (it needs Casper-FFG-style surround-vote slashing that spares honest reorg-followers). See docs/design/m0-consensus.md.

Fixed

Sybil corner (red-team F1/F2/F3): the PoST bond now binds the bytes it charges for, and the VDF is bound to a plot read — the external M0 red-team broke the Sybil corner three ways; the first two are now fixed at the mechanism level (core/bond), per docs/design/m0-sybil-bond.md. (F1) plotBlock derived each 4 KiB block from only the 32-byte leaves of its predecessor and parents, so a prover could store just the leaves (1/128 of the bond) and recompute any probed block on demand. Each block now depends on the full bytes of its predecessor and its parents, selected over a proven depth-robust graph (DRSample, Alwen–Blocki–Harsha CCS'17) instead of the old flat-uniform parents — so reconstructing a block requires the parents' bytes recursively and the pebbling cost is Ω(n); the rational strategy is to store the S bytes, and the charged size equals the resident footprint. Verify never recomputes a block, so it stays O(log n). (F2) AnswerSpaceTime seeded the VDF from the public challengeSeed(root, nonce), so a zero-resident prover ran the VDF and then re-derived the sampled blocks — releasing the space forfeited nothing. The VDF is now seeded from a plot block read before the VDF (seedIndexchallengeSeedST): the answer carries that block plus its inclusion proof, the verifier recomputes the seed index and checks the proof, so a prover that released the space cannot produce the seed without the Ω(n) recompute. (F3) root-owner dedup is documented as only a same-root tiebreak; Sybil cost now lives in the byte-bound proof, and distinct identities still produce distinct plots. The plot on-disk format (adapters/diskplot) bumps to version 2 so a restart re-plots rather than reloading the old, insecure labeling (one-time re-plot on upgrade). The red-team PoCs are adopted inverted as regressions (core/bond/redteam_sybil_test.go), and BenchmarkSeal records the plot/re-plot constant (~270 MB/s) behind the "re-plot ≫ epoch" tuning. Residual (honest): the structural anti-release binding is in; the quantitative floor — a minimum bond size and BondVDFDelay such that even the smallest allowed bond cannot re-plot within one challenge window — is a deployment-tuning follow-up, and consensus fork-choice weight (F6) still depends on this bond being real. See the design doc's open-risks section.

Fixed

Accountability corner (red-team F5): on-chain revocation is no longer a global switch — the external M0 red-team broke the accountability tenet three ways through the chain's takedown path: a quorum could revoke a root it never published (no ownership or existence check); the takedown was honored by every chain-follower with no opt-out — a global switch the tenets say cannot exist; and it was irreversible. All three are fixed. (1) ValidateProposal and the commit path now reject a block whose Revocations name a root never committed on this chain (ErrRevokeUnknownRoot) — a quorum cannot censor content that isn't on the ledger, nor a competitor's unpublished hash. (2) Honoring on-chain revocations is now a per-operator subscriptionReplicaRegistry.HonorRevocations and node.SetHonorChainRevocations, both default off — so following the chain never silently imposes someone else's takedowns; the effect is "proportional to who trusts you" (TENETS §9), the same voluntary stance as the operator-local denylist, never a universal switch. (3) Added an un-revoke record (Block.Unrevocations, quorum-gated and committed in the block hash) so a takedown is reversible by the same governance that imposed it, not a permanent asymmetry. The red-teamer's own PoC now fails at its ownership check; adopted inverted as core/chain/redteam_f5_accountability_test.go and core/node/redteam_f5_subscription_test.go (unit + node-integration; the operator-local takedown sim remains the e2e). Traces to immutable #5, Don't #2, S4. The other red-team breaks (Sybil bond F1/F2, privacy issuance F4, subjective fork-choice F6, cross-height equivocation F7) remain open — see the M0 status note below — this fix closes the accountability corner only.

Fixed

Doc-truth reconciliation + a token round-trip playbook (acceptance round 3) — the third acceptance re-run PASSED again (all 9 flows, all 8 tenets, zero code defects); its findings were stale docs and one discoverability gap, several making the product look worse than it is. (F1) risk-register.md row 14 claimed a default publish still writes a permanent Publisher→root map — but the chain default now REJECTS Publisher entries (-allow-publisher=false), so a default publish records no author; updated to CLOSED-by-default, with blind tokens as the additional opt-in for full unlinkability. (F2) threat-catalog.md F1 still said "the RSA issuer key is in-RAM (persistence is a follow-up)"; it persists now (#126, adapters/diskissuer) — corrected. (F3) the website said publishing is "cryptographically unlinkable" as an unqualified property; qualified to "names no author by default — with opt-in blind tokens, cryptographically unlinkable," matching the honest in-repo docs. (F4) the headline walkthrough (local-test-network.md) never reached the trust-plane flows 4–7; added a "Tier 4 — become a validator" section pointing at examples/ and user-seam.md §Role 4. (F5, doc-note only per decision) documented that a denied root reads to a fetcher as ordinary data-loss (compliant nodes answer "not found" rather than advertising a refusal — deliberate; the fetcher retrieves from another operator). (F6) the F7 sub-claim "the tokens it issued stay valid across a restart" had no operator-level repro; added examples/flow-tokens-issuer-restart.sh — validators require blind tokens, a tokened publish commits (no Publisher), the issuer is restarted (its issuer.key reloads byte-identical, no re-mint), and a token issued by the restarted issuer still commits (peers accept it), with a token-less-publish-refused negative control. Also made silt chain-status's hint line un-ambiguous to grep. No mechanism changed. Traces to S5.

Fixed

Docs & UX polish (acceptance re-run new-F3/F4/F5/F6) — four minor/cosmetic gaps the passing re-run surfaced, each a small correctness or clarity fix, no mechanism change. (F3) the Tier-1 "erasure by hand" walkthrough listed objects as flat under .silt/objects/ and told you to rm .silt/objects/<a-few> — but objects nest one level under a 2-hex prefix (.silt/objects/<xx>/<hash>), so that command targets a whole prefix directory, and it could delete the single-copy manifest chunk and brick get; rewritten to use silt info … -shards to pick real data/parity shard hashes, delete them by their true path, and warn the manifest is single-copy on one node (README.md, docs/local-test-network.md). (F4) silt daemon -h described -registry as http://host:port — the exact form the key-pinning contract refuses; the flag help now reads ID@https://host:port (key-pinned — copy the daemon's 'registry:' line verbatim). (F5) the website's feature list didn't mention NAT traversal (thoroughly documented in the repo but invisible to a site visitor); added a "Reaches across NATs" card. (F6) silt get <siltcare:…> refused with link: not a silt:v1: link, which reads like a typo rather than an intentional capability boundary; link.Parse now recognises a care link and says so, and silt get points to silt info / silt daemon -care and the full link (unit test pins the clearer error). Traces to S5. See the M0 acceptance re-run report.

Fixed

Gate 4 (#52, acceptance F1): a restarted validator rejoins the chain instead of being stranded at its pre-restart height (D2) — the M0 acceptance field test found the one blocker: kill a validator, let the network commit a block without it, restart it on the same -store, and it never caught up — it sat at its old height forever while the live set advanced, so over time the validator set could only shrink. Two compounding causes, both rooted in the same mistake — treating reputation (a live, local, NON-persisted view, re-earned by bond audits) as if it were a property of a persisted block. (1) Reloading our own chain re-ran every block — including the genesis — through the full commit gate (chainstore.Replay called chain.Append), so at boot, before any bond audit had run, the empty reputation view failed the very first block: reputation below threshold: proposer <genesis-id> has 0, needs 100. The genesis is designed to bypass that gate (AppendGenesis); replaying it through the gate cannot work. (2) Catching up on missed blocks fired SyncChain exactly once, at boot, gated on -attesters, and BEFORE StartBondAudit — so it ran against an empty reputation view (adopting nothing, since it can't yet tell which fork carries real standing) and then never retried. The in-process consensus sim hid both because it PRE-POPULATES reputation before the latecomer syncs. The fix draws the trust boundary at whose disk it is. Our OWN committed history is reloaded by Chain.Reload, which re-verifies each block's cryptographic integrity — hash ancestry, the proposer signature, and a quorum of distinct verifying non-proposer attester signatures (so bit-rot, truncation, or tampering is still caught, B7) — but NOT the time-varying reputation gate, which a validator already satisfied when it committed the block live; genesis reloads via AppendGenesis as it always should have. A PEER's fork is a different trust class and still goes through Reconcile with full reputation re-validation. Catch-up is now a periodic, retrying StartChainSync loop (ChainSyncInterval, default 30s), UNGATED on -attesters (it targets the explicit set plus every validator learned from a gossiped bond, so a node restarted with only -bootstrap still rejoins), and the daemon runs it AFTER StartBondAudit so peer standing is being re-earned — a later sweep, once audits land, adopts the missed blocks and persists them. Tested (V5): unit — replaying our own [genesis, block1] with an EMPTY ledger now rejoins at height, while a tampered block is still rejected (ErrBadSignature); node — a restarted validator adopts NOTHING while its standing view is empty and catches up the instant bond audits restore peer standing, and syncTargets includes a bond-learned validator with no -attesters given. Honestly labelled: fork-choice weight is still the locally-qualified reputation view (fully-objective, partition-independent on-chain PoST-bond weight remains the recorded D2 hardening), and a bespoke multi-daemon restart harness is deferred to the acceptance re-run — the field test roadmap #52 exists to prove. Traces to M0, B7, D2, #52. See docs/design/gate4-m0-mechanism.md §3e.

Fixed

Gate 4 (acceptance F2/F7): the trust plane narrates itself — an operator can SEE standing, bond reload, and caretaker sweeps (S5) — the M0 mechanisms worked but ran silent, so the acceptance operator had to read source to confirm the earned-standing and self-heal claims. Four honest-observability fixes, all at -log info: (standing) a validator now narrates its own consensus standing every bond-audit sweep and the verdict of every peer bond challenge (standing, bond challenge), so the earned-standing mechanism the whole of M0 rests on is visible rising and decaying rather than inferred from a diffed chain.cbor; (bond reload) a restart that RELOADS its plot now says reloaded the … bond (no re-plot) instead of the identical sealed … wording a first-time plot uses — the "no re-plot" guarantee held, but the log had actively suggested the expensive path ran (EnableBond now reports reloaded-vs-sealed); (caretaker) the repair sweep logs stripe degraded, within repair slack — watching when it sees a loss that parity/replication still covers, so an operator who kills a holder sees the caretaker NOTICE rather than apparent silence — repair fires (stripe repaired) only once losses exceed the slack, which with the default replication takes more than "a couple" of deaths, and repair below k already marks the can't-yet-reconstruct case; (default on) a validator with no -log flag now defaults to -log info — the M0 stakes mean the normal path should narrate itself in the field, not stay dark until someone knows to ask (non-validators are unchanged: logging stays off). The flagship self-heal walkthrough (docs/local-test-network.md) is rewritten to set honest expectations (why killing "a couple" of holders correctly heals nothing visible, how to actually strand a stripe, and silt sim run churn for the dense version). A read-only Reputation accessor was added to the CreditLedger port for the narration. No mechanism changed — this is pure observability. Traces to M0, S5 (honest observability), B5. See the M0 acceptance report.

Fixed

Docs (acceptance F4/F5/F6/F8): the getting-started guides match reality — the acceptance operator hit four first-five-minutes doc snags, none breaking the product but each eroding "every step works / every counter reproduces": (F4) three guides (README.md, docs/local-test-network.md, docs/v1-test.md) said add "prints the root hash" / a "64-char hex string" then told you to get <root> — but add prints a full silt: link and get/info/swarm get need that whole link, so a literal newcomer hit an error; every such placeholder is now <silt-link> with the output described as a link (the top-level silt usage block was already correct). (F5) the quoted sim run economy -seed 21 figures were stale — refreshed to the actual deterministic output (Gini 0.00 → 0.63, top earner ~1.25 MB, freeloader ~444 KB, 20/36 second-round publishes ok). (F6) the silt sim run usage error listed only scatter and the top-level usage omitted half the scenarios — both now list all eight (scatter, churn, economy, audit, capacity, consensus, bondstanding, takedown), including the previously undocumented bondstanding. (F8) the user-seam.md store-layout table listed chain/ (a directory); the committed history is a single chain.cbor file. Traces to S5 (honest observability extends to the docs). See the M0 acceptance report.

Added

Validator onboarding (acceptance re-run new-F1/new-F2): silt id, silt chain-status, and a runnable examples/ playbook — the M0 acceptance re-run PASSED (all 9 flows, all 8 tenets, zero broken), leaving two "major" gaps that both blocked a literal newcomer from the validator flow without changing any mechanism. (new-F1) Role-4 setup was chicken-and-egg: -attesters <ID_B> needs B's NodeID, but nothing told you how to learn it before launch (the acceptance script resorted to booting a throwaway daemon to read its peer: line). New silt id [-id-seed N | -store DIR] [-listen ADDR] prints the NodeID a daemon would use without launching one — resolving the identity exactly as the daemon does — so the topology is wireable up front. (new-F2) there was no operator playbook for the multi-validator flows 5–7 and no way to confirm convergence except hashing chain.cbor by hand. New read-only silt chain-status [-store DIR] prints a replica's head height, head hash, and block/entry counts — identical head height AND hash across replicas proves they agree; a rising head after a restart proves catch-up. And a new top-level examples/ directory ships four bash playbooks (flow2-publish-fetch, flow4-earned-standing, flows567-convergence-fault-restart, flow8-takedown) — the flows-5–7 script IS the field test roadmap #52 owes itself, now runnable in one command. The playbooks track only the PIDs they start (no blanket pkill) and use both new commands. docs/user-seam.md Role 4 gains a concrete silt id-based recipe and points at examples/. All four playbooks pass end to end locally (including the restarted-validator chain catch-up on real daemons — the daemon-level confirmation of the F1 restart fix). Traces to S5 (an operator can see and reproduce what's true), #52. Adopted from the M0 acceptance reproduction scripts.

Added

Gate 4d (#93): the publish-token issuer key persists across restarts — a validator that issues blind-signed publish tokens generated a FRESH RSA key on every daemon start, which orphaned every token it had already FRESH RSA key on every daemon start, which orphaned every token it had already signed (they no longer verify) and staled every issuer public key its peers had cached. A new adapters/diskissuer persists the key (PKCS#1 DER, written atomically with 0600), and the daemon loads-or-creates it: first run mints the issuer identity, every restart keeps it — so outstanding tokens stay verifiable and the distributed issuer set is stable. A corrupt or foreign key file is a hard error, never silently overwritten with a new identity. Tested (V5): the restart property is pinned (two LoadOrCreates over the same dir return the same key), plus save/load round-trip, clean-absent, and corrupt-file handling; the real daemon (e2e + Docker NAT) starts and persists the key. Honestly labelled: this is the issuer-key half of §3d's "issuer survives restart"; on-chain issuer registration (so the qualified issuer set is chain-verifiable rather than fetched ad-hoc) is the remaining §3d piece, and it pairs with the deferred D3 canonical-validator-set work. Traces to M0 (the unlinkable-publish path stays live across restarts), B7. See docs/design/gate4-m0-mechanism.md §3d.

Added

Gate 4f (#100): equivocation is provable and slashable — double-signing costs standing (D2) — the consensus analogue of a storage liar: a validator that signs two DIFFERENT blocks at the SAME height (trying to make two competing histories both look supported) is now caught and penalised. Two parts: (prevention) an honest validator records the block hash it signed at each height and REFUSES to sign a different block there — it never equivocates, even if two competing proposals reach it before either commits; (penalty) a chain.Equivocation is a compact, self-verifying proof (the two conflicting blocks; any node recomputes their hashes, confirms same height + different block, and that the culprit's signature — as proposer OR attester — verifies in both), and chain.FindEquivocations extracts every cross-fork double-signer from two competing histories. When a node reconciles across a fork it slashes each proven equivocator in its local ledger (credit.SlashEquivocation), a crushing, permanent reputation penalty that buries the culprit below any threshold — so its proposals are refused and its attestations stop counting toward any fork's weight. An honest validator signing sequential heights is never implicated (the heights differ) and a forged accusation fails (the signatures won't verify). Tested (V5): unit — a double-sign is provable, a sequential signer and an unsigned accusation are not, the same block is not a conflict, and every cross-fork culprit is found while one-fork signers are spared; node — a validator REFUSES a second block at a height it attested, and reconciling across a fork slashes the double-signer below zero. Honestly labelled: strict lock-on-attest can stall a height's liveness if a proposal fails and its attesters are needed again there — proper resolution is round-based unlocking (Tendermint POLC), a recorded 4f hardening; on-chain equivocation records so every replica slashes in lockstep (vs. each acting on what it observes) is the other recorded follow-up. Traces to M0 (a double-signing proposer cannot stand two histories AND keep its standing), D2. See docs/design/gate4-m0-mechanism.md §3e.

Added

Gate 4f (#100): the chain can reconcile forks — reorg to the heavier history (D2) — the registry chain was append-only with no reorganisation ("first valid block at a height wins"), and SyncChain silently breaked on divergence, so a partitioned or diverged validator stayed forked forever. It now heals: Chain.Reconcile re-validates a peer's full chain end to end in a throwaway replica and, iff that history is strictly heavier (ties broken by the lower head hash, so every honest node picks the same winner), adopts it — rolling state back to the shared genesis and forward onto the heavier fork. Because all derived state (byRoot, spent, revoked, validatorsSeen) is a pure function of the blocks, the reorg is a whole-state swap, not fragile per-record undo. Fork-choice weight is the cumulative count of DISTINCT qualified non-proposer attestations across the chain — the heaviest history is the one the most earned standing has committed to, not merely the longest (which a fast Sybil could extend); signatures are objective, the qualification bar is the local reputation view (which converges among honest replicas). The fork is genesis-anchored, so a peer cannot swap in a heavier FOREIGN chain, and every block is re-validated, so a lying peer wastes time but cannot feed an invalid history. SyncChain now reconciles against each peer's full chain — one uniform path for catch-up, fork-heal, and no-op (an equal-length fork is invisible to "give me blocks above my head", which is why it compares whole chains). Tested (V5): unit — a heavier fork is adopted, a lighter one rejected, ties break deterministically by hash, a foreign genesis is refused, an under-quorum fork is re-validated and rejected; integration — a 10-node network partitions, each side commits its own history, then heals and the lighter side reorgs onto the heavier fork over the wire while the heavier side does not budge. Honestly labelled: fully-objective, partition-independent on-chain PoST-bond weight is the recorded D2 hardening (a self-asserted or locally-qualified weight can diverge under an adversarial partition); equivocation evidence + slashing is the next 4f increment; genesis-to-head diffs (vs. whole-chain fetch) are the scaling follow-up. Traces to M0 (consensus can't be captured by an off-head or partitioning proposer), D2. See docs/design/gate4-m0-mechanism.md §3e.

Changed

Gate 4b (#91): bind the bond plot to its identity — close the plot-amortisation gap — the Sybil cost only holds if each identity holds its OWN distinct plot; previously nothing stopped a single operator from pointing N node identities at ONE shared plot (all advertising the same root, answering from one copy on disk), collapsing the per-identity cost from S to S/N. Two changes close it, together: (C) the plot is now sealed from a per-identity secret derived from the node's signing key (EnableBond takes the signer; bond.Seal takes the secret) rather than the public NodeID — so only an identity's owner can generate its plot, and an outsider cannot precompute a victim's root to grief it; and (A) the ledger binds each bond root to the first identity that proves it (RecordBondChallenge gains a root; a per-root owner map), so a root builds standing for at most one identity — N identities sharing one plot earn one bond's worth of standing, not N, forcing N distinct plots = N×disk. Honest identities never collide (distinct secret ⇒ distinct root), so the dedup only ever bites deliberate sharing. This upgrades design §6's open amortisation question from "hand it to the red-team" to a built defence — noting it is still not a proof of correct plotting (no PoRep/SNARK); the secret + dedup make sharing a root un-grief-able and uneconomical rather than impossible. Tested (V5): the M0 outcome is pinned — three identities proving one shared root leave only the first with standing while a distinct plot earns normally (failing-first: without dedup all three would clear the bar); distinct secrets yield distinct roots; and the over-the-wire bond audit + restart reload paths stay green under the new derivation. Traces to M0 (the Sybil corner), D1. See docs/design/gate4-m0-mechanism.md §3b/§6.

Added

Gate 4b (#93): the bond plot persists — a restart reloads it, never re-plots — plotting the identity bond is deliberately expensive (that expense is the Sybil cost), so paying it again on every daemon restart would be wasteful and, for a large pledge, a long stall before a validator can prove standing. A new adapters/diskplot store persists the plot (one atomic file per identity: a small header with the block geometry and committed root, then the raw blocks), and EnableBond now loads-or-plots: if a persisted plot exists it is reloaded and its Merkle root re-derived from the bytes and checked against the committed root (B7 — persisted state is re-verified, not trusted), so a restart skips plotting entirely; a corrupt, truncated, or stale plot is detected and cleanly re-plotted. core/bond gains Reconstruct (rebuild a commitment from persisted blocks) and Blocks; a new ports.PlotStore seam keeps the node pure (nil = memory-only, fine for sims). The daemon wires it alongside the proof store (inheriting the #69/#93 restart discipline). Tested (V5): the adapter round-trips and flags truncated/foreign files; a reloaded bond answers a space-time challenge; and the node-level restart outcome is pinned — a second start with the same identity reloads instead of re-plotting (asserted via plot/reload counters), while a corrupted plot re-plots to the correct identity-bound root. Traces to M0, D1, B7. See docs/design/gate4-m0-mechanism.md §3b/§3d.

Changed

Gate 4b (#91): the bond is now proof-of-space-TIME — the VDF is wired into the live bond audit — completes the mechanism: standing is backed not just by held space (the plot) but by space held across time. A bond challenge now answers with a core/vdf proof over the fresh (root ‖ nonce) challenge, and the probed plot-block indices are derived from the VDF output — so a prover cannot know which blocks to keep ready until it has done BondVDFDelay sequential squarings, and therefore cannot release the pledged space and re-plot just-in-time, nor parallelise its way out of the elapsed-time floor. Verification stays O(log n) (checking a VDF is fast even though producing it was slow) plus the existing Merkle checks, so consensus cost on the core loop is unchanged. core/bond gains AnswerSpaceTime / VerifySpaceTime (additive — the space-only Answer/Verify remain), the answer carries the VDF proof inside the existing CBOR Answer (so no wire format change), and core/vdf gains Default — the RSA-2048 challenge modulus, an unknown-order group needing no fresh trusted setup (a documented launch anchor; class groups are the setup-free upgrade). BondVDFDelay is a new node-config tuning knob (Evolving): a modest default keeps the deterministic sim fast, a real deployment raises it for a stronger time floor; 0 disables the time binding. The daemon inherits it from DefaultConfig (the #65 dropped-field discipline), and the bondstanding sim now exercises the whole space-time path over the wire. Tested (V5): held bonds answer, a space-only answer / wrong-delay / forged-VDF-output all fail, and the probed blocks provably derive from the work not the raw nonce. Honestly labelled: producing the VDF currently runs on the audit path; moving the heavy work fully off the core loop and persisting the plot across restarts (B2 / #93) is the next 4b step. Traces to M0 (Sybil corner: space held across time), D1, B2. See docs/design/gate4-m0-mechanism.md §3b.

Changed

Gate 4b (#91): the bond is now a real space-hard plot, not independent blocks — replaces the honestly-labelled placeholder in core/bond (each block was cheap iterated SHA-256 over id‖index, so an attacker could recompute any block on demand and store nothing) with a sequential labeling plot: block i depends on its identity, index, immediate predecessor, and a few pseudo-random earlier blocks (a chain plus long-range parents — a DAG). Because a block depends on earlier ones, recomputing a single probed block forces recomputing its whole dependency subgraph, and the long-range parents defeat cheap checkpointing — so the rational strategy becomes to store the S bytes, which is exactly the space being charged for. This makes N Sybil identities cost N distinct blobs of real disk, the property the reputation→quorum path always assumed but never charged. The challenge/answer/verify seam is untouched — bond.Verify(root, size, nonce, Answer) stays a stateless O(log n) Merkle check — so only what fills the blocks changed. Honestly labelled: space-hardness is heuristic (not yet a formally depth-robust graph or a memory-hard label function — the hardening path), and the time half (binding a fresh epoch challenge to the core/vdf delay so the space must be held across time and the challenge can't be precomputed) is the next 4b step. Tested (V5): determinism + identity-binding, the dependency lever (perturbing a predecessor or long-range parent changes the block — the space-hardness property the old independent blocks lacked), and parent indices are always earlier + deterministic. Traces to M0 (Sybil corner), D1. See docs/design/gate4-m0-mechanism.md §3b.

Added

Gate 4b (#91): verifiable delay function primitive (core/vdf) — the sequential-work core of the proof-of-space-time bond, and the first 4b construction piece. A VDF evaluates in a prescribed number of inherently sequential steps (you cannot parallelise your way to the answer) yet emits a short proof anyone verifies almost instantly — exactly what a bond needs to bind a fresh epoch challenge to real elapsed, non-parallelisable time, so a Sybil can neither retroactively fake having held its pledged space across the epoch nor buy its way out of the wall clock with more cores. The construction is Wesolowski's VDF (EUROCRYPT 2019), adopted not invented (B8): over a group of unknown order (Z_N^* for an RSA modulus N), y = x^(2^T) mod N by T sequential squarings, with π = x^(⌊2^T/ℓ⌋) for a Fiat–Shamir prime computed in T steps via long division (never materialising the T-bit exponent), and verify π^ℓ·x^r ≟ y for r = 2^T mod ℓ in O(log ℓ + log T) — cheap enough to stay on the core loop. Security rests on N's factorisation being unknown (a documented trust anchor; the class-group variant removes it and is the noted upgrade path). Pure package (big integers and bytes only). Adversarially tested: relabelling a shorter computation as a longer one, a trivial π=1, tampered y/π, wrong-challenge, wrong-T, and non-canonical elements all fail; the delay loop is pinned against a direct x^(2^T) reference. Wiring the plot + epoch proof off-loop behind the existing bond.Verify seam is the next 4b change. Traces to M0 (the Sybil corner: space-time held, not asserted), D1, and B2 (the heavy work runs off the core loop). See docs/design/gate4-m0-mechanism.md §3b.

Added

Gate 4a (#90): wire the real proof-of-retrieval into the live audit path — the core/por primitive now replaces the toy scheme in the running node. An auditor verifies that a peer still holds a shard without fetching the bytes: at distribute time the publisher computes each shard's per-block authenticators under a key derived from the file's layout key (node.DerivePorKey, mirroring the link key hierarchy) and ships them beside the Merkle proof (StorageProof.PorTags); the storage node keeps them with the chunk; on challenge the prover aggregates its bytes + tags into a compact (μ, σ) response; the auditor derives the same key from its care-link and checks the response touching no data. gradeAnswers loses its ground-truth fetch entirely — a liar node that kept its tags but dropped the bytes now fails an audit that never fetches, and is slashed via credit.RecordAudit. The auditor recomputes each full shard's expected block count from the layout ChunkSize and rejects any prover under-reporting it (soundness against partial deletion for every full shard; the single short tail shard is the one documented residue for the V3 red-team). The key never crosses the wire and a storage node — lacking the layout key — cannot forge. Two hand-rolled codecs were extended so the tags don't vanish in the field (a #65-class trap): the TCP wire codec (adapters/tcpnet) and the on-disk proof store (adapters/diskproofs, so a restarted host can still prove what it re-announces, #69). Repaired/re-seeded shards are re-tagged from the caretaker's care-link. Coverage (V5): unit (deterministic key derivation + cross-capability agreement, GCM-overhead guard, wire + persistence round-trips), sim (liars slashed with zero ground-truth fetches during the sweep — proven by a per-kind message counter), and the real-daemon TCP + cross -NAT (incl. full-swarm restart) harnesses stay green carrying the enlarged proofs. Traces to M0 (presence proven, not asserted), B8, and B7/V3. See docs/design/gate4-m0-mechanism.md §3a.

Added

Gate 4a (#90): real proof-of-retrieval primitive (core/por) — the first Gate-4 construction piece. A verifier holding a small secret key can now check that a prover still holds a chunk's bytes without fetching them — the property the toy scheme (core/node/por.go, which grades against ground truth it fetches itself) deliberately lacked. The construction is the private-verification Compact Proof of Retrievability of Shacham & Waters (ASIACRYPT 2008) — a homomorphic linear authenticator over the Curve25519 field prime: per-block tags σᵢ = f_k(i) + Σⱼ αⱼ·mᵢⱼ, a seed-expanded challenge, and an O(s) aggregated response (μ, σ) whose size is independent of the chunk. A prover that deleted or altered any sampled block cannot make the verification equation hold without the secret αⱼ, which the tags do not reveal. The verify key is designed to ride the care-link, so caretakers audit over ciphertext while storage-node provers cannot forge. Pure package (bytes and keys only); wiring it into the manifest, node audit loop, and credit ledger is the next 4a change. Adversarially tested: tampered/deleted-block, key-less forgery, wrong-key, and wrong-unit proofs all fail. Traces to M0 (the Sybil corner: presence proven, not asserted), B8 (adopt the proven primitive), and B7/V3 (a non-holder fails the challenge). See docs/design/gate4-m0-mechanism.md §3a.

Fixed

Gates 1–3 completeness audit: closed missing regressions in the floors — a pre-Gate-4 audit verified the landed floors (Gate 1), register-after-distribute (Gate 2, #65), and NAT traversal (Gate 3, #27/#111) are whole at all three test tiers, and fixed the coverage gaps it found. The register-after-distribute failure outcome had no regression: the one sim test touching an unplaceable scatter used the old Add (publish-up-front) path, so it couldn't catch a dangling entry. The gate is now a single tested helper, pipeline.RegisterAfterDistribute (publish iff the scatter confirmed), that both the swarm add and daemon-UI publish paths call instead of hand-rolling "publish iff derr == nil" — covered by a pipeline unit test (both branches) and a sim test that drives the real node.Distribute failure and asserts the registry is left empty (S5). The relay's per-target session cap (PerPeerSessions, the #65 knob) gained an isolation test proving one target's fan-out can't be throttled by — or monopolise beyond its slot — another's; previously only the global MaxSessions branch was exercised. The default -dns-seed is documented as a deliberate empty (neutral infrastructure, community-run seeds — #27 Part A), not an unfinished hole.

Fixed

Transport frame cap was smaller than the minimum production chunk — a whole chunk rides in one length-prefixed frame, but the inbound read loop's cap was 32 MiB while the minimum production chunk is 64 MiB, so every production-sized chunk was dropped on receipt; the swarm could only move sim-sized (64 KiB) chunks. The cap is now derived from the manifest chunk-size ceiling plus envelope overhead (maxFrame = manifest.MaxChunkSize + frameOverhead), so the wire can always carry a chunk the manifest layer accepts and the two limits can't drift. Send now also rejects an over-cap frame with an explicit error instead of emitting one the peer silently drops (S1/S3). Traces to S1/S3 and anti-persona #14. Closes #104.

Security

Gate 1 (A5): panic-recover + fuzz the decode surface — a daemon that crashes on a malformed frame can't be field-tested and can't carry the "credible from day one" claim, so every untrusted-input decoder is now proven not to panic and is caught if it ever does. New Go fuzz targets cover the whole decode surface — the manifest CBOR decoder, the chunk-frame length header (plus a Split/Join round-trip), silt:/siltcare: link parsing, chain block/blocks decoders, the tcpnet wire envelope, and the relay control frame; their seed corpora run as a smoke test on every push/PR and a new nightly workflow mutates each for a real time budget (millions of execs, zero panics found). Underneath that proof sits a defence-in-depth recovery net (internal/safe): the tcpnet read loop and the relay client/server frame loops drop the connection on any panic, and the node's event loop contains a panicking task so one bad frame fails the request, not the process — an event-loop panic is logged at error level (a top-severity bug until fixed), never silent. Traces to tenets S1/S3 and anti-persona #14. Closes #87.

Security

Gate 1 (A6): bound the declared manifest chunk count + size — a manifest arrives as reassembled chunk data and declares its own chunk count and sizes; a declared number is a claim, not a fact (tenet B7), so a tiny manifest that declares a huge chunk array was a cheap memory-exhaustion vector (anti-persona #14). The manifest CBOR decoder is now bounded (MaxArrayElements = MaxChunks) so an over-declared array is refused as its header is read — before the slice is allocated — across both the plain and the sealed (layout/secrets) decode paths. Validate and OpenLayout add semantic checks that reject an oversize declared chunk size or count cleanly, per request, with the node still up. Bounds are exported and documented (MaxChunks, MaxChunkSize), sized with headroom over the 64 MiB production chunk. Traces to tenets B7 and S1/S3. Closes #88.

Security

Gate 1 (I1): lock the local UI / JSON API — the daemon's local HTTP API sent CORS , so any web page the operator visited could enumerate or drive their node. It is now locked: every request must carry a localhost Host (a DNS-rebinding page arrives as evil.com and is refused), any cross-origin request from a non-localhost page is rejected outright (localhost origins are reflected, not blanket-allowed, so the observatory still aggregates sibling daemons), and every state-changing call requires a per-daemon bearer token minted on first run (<store>/ui-token, 0600) and handed to the operator's browser on the UI URL (/?token=…). Reads keep their no-token localhost ergonomics. CORS is gone. Traces to Don't #3 (access-unsurveilled), B4 (privacy by construction), and S4 (no seizable single point). Closes #89.

Security

Chain permanence: version the Block schema before any Gate-4 record changeBlock carried no version, so any future change to what the block hash commits to or to validation semantics (real-bond commitments, mandatory tokens) would be a hard fork with nothing to gate the eras: Decode/DecodeBlocks would happily decode an old block and mis-validate it under new rules. Blocks now carry a Version (era) that Hash commits to and Decode/DecodeBlocks require — a version mismatch is an explicit ErrBlockVersion, never silent mis-validation, and because the hash covers it the era can't be swapped under a valid signature. Landed while the chain is still throwaway, so it costs nothing now and prevents a flag-day later; it is the prerequisite for the Gate-4 record-format changes (#90/#91/#92). Entry versioning is deliberately deferred: entries are always validated within a block whose version gates their rules, and standalone-registry entry semantics are what the tokened-publish design turn (#97) will settle. Closes #98.

Security

Register-after-distribute: a failed scatter no longer leaves a dangling registry entry (Gate 2, #65) — pipeline.Add published the registry entry as its final step, before the caller distributed the chunks to peers, so a loud placement failure left an entry pointing at content that never landed (no link reaches the user, but the registry — and network-size estimates — count phantom content; tenet S5). Publishing is now split from staging: a new pipeline.Stage stores the chunks and sealed manifest and returns the entry without registering it; the networked publish paths (swarm add, web-UI publish) register only after distribution is confirmed. Add still stages-and-publishes in one shot for callers that don't distribute separately (local add, genesis, sim). Fetch-side retry and raised relay session limits (the rest of #65) already landed. Closes #65.

Security

Unlinkable publish is now the default; the Gated registry is fenced off (M0 privacy, #97/#99) — publishing recorded a permanent Publisher → root link on the append-only chain because the publish clients attached the node's durable identity by default. The chain never required it; it was being written gratuitously and can never be undone. Now: the swarm add and web-UI publish paths attach no Publisher by default (publish is unlinkable — carry a blind-signed token, or nothing), and the chain refuses a Publisher-bearing entry unless the deployment is explicitly trusted (chain.Config.AllowPublisher, daemon -allow-publisher; swarm add -allow-publisher to opt a single publish back in). Genesis is exempt (it seeds via AppendGenesis and its proposer is public by design). Tokens stay an orthogonal opt-in (-token-quorum/-require-tokens) for a paid unlinkable publish, so earned-standing commit without tokens still works. The credit-Gated registry — which hard-requires a Publisher and has no token path — is documented sim/test-only and fenced off: an internal/depcheck architecture test fails the build if any cmd/ entry point constructs it (it is used only by the sim today). Traces to M0 (privacy corner), F1 / risk #14, immutable #3 (no permanent linkage). Closes #97 and #99.

Security

Hole-punch now actually fires end-to-end: two NATed daemons upgrade the relay path to a direct connection (Gate 3, #27/#111) — the Phase-3 wiring existed but never worked, and CI never caught it because it only ran the standalone probe, never the integrated daemons. Two bugs, both found locally via the Docker NAT harness (build-immutable V5): (1) the punch was only requested on a fresh relay dial, but a relay conn is reused for every subsequent frame, so a steady-state relay path never tried to go direct — now a reused relay-backed conn also (cooldown-gated) requests the punch; (2) the punch was requested but never bound — the relay control conn was dialed without SO_REUSEPORT, so the punch dial couldn't re-bind that port to reuse the NAT mapping the relay observed, so every attempt failed. The reuseport dial hook now lives in a shared internal/reuseport package used by both the transport and the relay client. Proven locally: cone punches (both daemons log a direct connection), symmetric correctly stays on the relay. integration/nat/ holepunch.sh (cone + symmetric) is now wired into the nat-holepunch CI job so this can never silently regress again. Closes #111.

Docs

Build-immutable: a bug fixed once stays fixed, caught locally — added tenet V5 and a new build-immutable category to docs/TENETS.md. Product-immutables define what silt is; build-immutables define how we build and are held at the same amendment bar. V5: every discovered defect ships in the same change as a failing-first regression test at its tier(s) (unit / integration-sim / e2e), runnable on a contributor's own machine, so a re-break surfaces locally in seconds — CI is the backstop, never the first line of defense. The three-tier Definition of Done (V1/V2) is elevated alongside it. Prompted by catching the integrated hole-punch gap (#27 Phase 3) locally via the Docker NAT harness rather than at CI.

Docs

Intention review actioned: M0 sharpened, S7 added, the V1 gate spine put on the board — a docs/canon + tracker pass, no code or behavior change, acting on an intent-level fresh-eyes review. M0 is requalified from "resolve" the trilemma to "hold it — refuse to trade any corner away," and bound to a falsifiable test (held iff an external red-team suite denies all three failure modes); privacy and accountability hold from day one while Sybil-resistance is the corner that bootstraps. "No center" becomes "no permanent center" (immutable #3 and T1), reconciling the invariant with the time-boxed launch-window anchors. A new tenet S7 — "durability must pay for itself" names the repair-loop economics that killed Freenet/GNUnet. B8 and V3 now require the adversary that certifies a novel composition to be external, not self-graded. On the tracker, the V1 gate spine is materialized as GitHub labels + issues (gates 0→6, critical path 1→4→6, pinned epic #94): the previously prose-only Gate 1 floors (#87/#88/#89) and Gate 4 "the car" (#90–#93, the real M0 mechanism) and Gate 5 durability economics (#95) are now filed and traced to their tenet. The site's roadmap/changelog generators gain relative- link and blockquote rendering so the volatile pages stay generated, never hand-edited.

Docs

Canon reconciled: mission, mechanisms, and a single roadmap spine — a docs/canon pass, no code or behavior change. TENETS.md is restructured into three tiers: a new mission-immutable M0 (silt exists to hold the privacy × accountability × Sybil trilemma — unlinkable publishing, content-level accountability, and Sybil-resistance held together without trading any corner away), six mechanism-immutables, and the build tenets, which gain B8 (use best-in-class, proven components; be novel only in how they are composed). ROADMAP.md is slimmed to a single GitHub V1-milestone spine: the retired M/Wave/Tier markers are dropped in favor of a "learning phase" framing, the 0.1.x/0.2.x line is relabeled experimental/learning, and the cadence is stated as 0.9.0 then 1.0.0. The issue tracker is reconciled (#78 and #79 closed as shipped, the V1 milestone created), the website roadmap is regenerated from source, and a sensitive term was removed from the public docs. The math notes on proof-of-retrieval (05) and quorum chains (08) are reconciled to match: the current PoR is labeled a challenge-time toy with a real published-scheme PoR as the V1 target, and consensus standing is described as bond-gated challenged storage on a labeled placeholder seal being hardened for V1.

Security

Publisher privacy: quorum-issued blind publish tokens (#14 / F1): the chain recorded a Publisher NodeID per root, letting an observer map a durable reputation key to every root it published (silt protects who-READS far better than who-WRITES). A publish is now authorized by a publish token — a random serial blind-signed by a QUORUM of distinct validators (a k-of-n Chaumian blind multisignature: no single issuer, no trusted-dealer/DKG). The publisher pays the fee with its durable identity to acquire the token, but the issuers never see the serial, so the committed entry carries the token and NO Publisher identity, and each serial spends exactly once (chain-wide double-spend rejection). Daemon: -require-tokens N makes the chain accept only token-carrying entries and validators issue; swarm add -token-quorum N acquires one over the wire. Proven at three tiers: unit (blind sig, quorum bundle, chain enforcement), sim (acquire-then-publish through the node loop), e2e (three validators, a 2-of-3 token over real TCP). Honest residuals (labeled): each signature is unlinkable (Chaum), but a colluding validator set narrows the anonymity set to same-epoch requesters of the same subset (use a canonical validator set); the RSA issuer key is in-RAM (cross-restart persistence is a follow-up).

Security

Launch-window training wheels (#79, risk 15): a young network is the easiest to capture — a Sybil quorum is cheap before the network has decentralized. A validator set may now declare anchors (-anchors, -anchor-quorum): while the network is immature, a commit ALSO requires anchor sign-off, so a Sybil quorum cannot write to a young registry. The requirement sheds mechanically once -mature-validators distinct non-anchor validators have attested a committed block — measured decentralization, never a flag day. Because attesting requires earned bond standing (#78), the maturity metric can't be cheaply inflated by Sybils. Anchors are plural (a threshold; no single anchor is load-bearing, cf. R4) and their power is transparent, on-chain, and time-limited — they can never gate a mature network. Off by default (empty anchors). Proven for the OUTCOME at unit (TestTrainingWheelsGateYoungNetworkThenShed) and sim (TestTrainingWheelsShedThroughTheNodeLoop — the shed through the real propose/attest/commit loop); e2e deliberately skipped and recorded (the shed is deterministic chain logic covered at unit+sim, and the -anchors wiring is confirmed by a daemon smoke check — a bespoke multi-daemon shed e2e is high-cost/low-value).

Security

Identity costs storage: bond-gated consensus standing (#78): reputation — the number the chain gates writes on — is no longer dominated by self-reported serving (which two colluding nodes could wash-mint for free, threat-catalog D1/D3). Standing now costs real, challenged, held storage: a validator seals an identity-bound storage bond (core/bond, -bond), and validators challenge each other's bonds over the wire (MsgBondChallenge/ MsgBondReply), verifying against only the committed Merkle root — no ground-truth fetch. Standing must be sustained (it decays if a bond stops being re-proven), so N Sybil identities cost N distinct bonds on N disks. Proven for the OUTCOME at three tiers: unit (core/bond), sim (TestBondAuditEarnsStandingOverTheNetwork — a no-bond node is refused, decay retires unsustained standing), and e2e (TestBondEarnedStandingCommitsOverTCP — two bonded validators earn standing over real TCP and commit on -min-rep 100). Honest limit: the bond is held in RAM and the seal is not yet memory-hard (proof-of-space-lite, labeled); disk-persistence + a memory-hard seal are tracked follow-ups. Design: docs/design/bond-audit.md.

Security

Safe consensus defaults (#79): silt daemon -validator now defaults to -quorum 3 -min-rep 100 (was -quorum 1 -min-rep 0), so a lone or fresh node can no longer rubber-stamp the registry — writing requires earned standing and a real quorum. A trusted one-box swarm opts into self-commit explicitly (-quorum 0 -min-rep 0), which now prints a loud trusted-deployment warning rather than being the silent default. Outcome proven end-to-end: e2e TestDefaultsRefuseRubberStampCommit asserts the default refuses a lone commit, with TestPublishCommitFetchOverTCP (explicit -quorum 0) as the positive control.

Added

Deterministic NAT/relay/hole-punch in the sim (#27): the in-process network (simnet) now models a home router — a NATed node dials out freely (each outbound opening the conntrack reverse mapping so replies get back in) but is un-dialable cold from off its LAN. Two NATed nodes on different LANs therefore meet through a designated relay (counted in Stats.Relayed), or HolePunch opens a direct path for cone NATs and correctly falls back to the relay for symmetric ones. A relayed delivery pointedly does not open a direct mapping, so a later direct dial still needs a punch. This is the tier-1, seed-reproducible mirror of the integration/nat Docker harness; it is zero-overhead and byte-identical for every existing scenario (no NAT configured → the fast path short-circuits and draws no extra randomness).

Added

Hole-punching: relay paths upgrade to direct connections (#27): when two NATed daemons talk through a relay, the relay now coordinates a hole-punch — it tells each the other's observed endpoint, and both dial it from their relay-registration port at once (SO_REUSEPORT, TCP simultaneous-open). Through a cone NAT the crossing SYNs establish a direct link, which the transport adopts so the bulk traffic leaves the relay; on symmetric NAT it simply fails and the relay path stays. The relay forwards no bytes for the direct path — it only swaps addresses. The punch primitive is proven end-to-end against real kernel NAT by the integration/nat harness, CI-gated (cone → direct connection, symmetric → relay); the relay coordination is unit-tested. This demotes the relay from every-byte carrier to rendezvous, the big cost win for cheap public infrastructure (S6). (The live two-daemon upgrade has a harness scenario in progress — the caretaker traffic-trigger needs the minimal-network provider resolution sorted.)

Added

NATed nodes learn their public endpoint, STUN-style (#27, the groundwork for hole-punching): when a node registers with a relay, the relay reports the host:port it observed the registration coming from — the node's NAT mapping. A node behind NAT cannot otherwise know its own public address, and hole-punching needs it (it's the endpoint a peer aims a simultaneous-open at). Surfaced as relay.Client.Observed / node.ObservedAddr and logged by the daemon. This is phase 1 of #27; the relay-coordinated punch, port-reuse dial, and relay→direct upgrade follow. The integration/nat harness asserts a NATed node learns its mapped public IP (the gateway's), not its LAN address.

Added

Automated cross-NAT integration harness (integration/nat/, and a nat-integration CI job): stands up two genuinely-NATed daemons plus a public relay in real container networks (real kernel NAT via iptables MASQUERADE, real TLS over real sockets), publishes from behind one NAT and fetches from behind another, and asserts the bytes come back bit-perfect having crossed the relay (verified by counting relay splices). This is the automatable replacement for the manual two-machine (Mac A ↔ Mac B) rig — the NAT/relay path that the in-process sim and flat-localhost e2e can't reach — and the seed harness for hole-punching (#27) and restart/re-provide (#69) scenarios. Runs on one host (CI, a dev box, or Docker Desktop); no second machine.

Fixed

The daemon no longer silently drops config fields (#71): cmd/silt built node.Config field-by-field, so any field added to DefaultConfig defaulted to its zero value in the real binary — how the #65 fetch-retry shipped inert and demand-responsive dispersion was off in the daemon while the roadmap listed it as done. The daemon and the ephemeral swarm add/get client now start from node.DefaultConfig and override only what genuinely differs (the daemon's 2s RequestTimeout), so new fields are inherited by default.

Fixed

A restarted daemon's content stays discoverable (#69, found in the #65 field test): provider records live only in peers' memory and die with the process, so a daemon re-announces everything on its disk at startup (AnnounceHeld) — but a coded shard must be announced under its column key hash(root‖column), where readers look, and that key is derived from the shard's storage proof. Proofs were kept only in memory, so after a restart the re-announce fell back to the bare chunk id and a disk full of intact content was invisible until it happened to be re-hosted. Storage proofs are now persisted alongside the chunks (adapters/diskproofs) and reloaded on startup, so the re-announce lands on the right key again — and the node can still answer storage-audit challenges after a restart. The integration/nat harness gained a RESTART=1 scenario that restarts the whole swarm and re-fetches to prove it.

Fixed

Fetches survive a saturated relay (#65): once the public rendezvous node hits its capacity cap, every byte to a NATed provider funnels through the relay, whose per-peer splice slots saturate under concurrent fan-out and return "relay at capacity" — and the fetch path had no retry, so a transiently-refused chunk was reported unreachable (the tail-of-sweep fetch failures seen from a second network). A chunk fetch now re-sweeps its providers with a backoff when every provider failed transiently (a timeout or relay refusal, not a clean "don't have it") — the freed slots make the retry succeed — the fetch-side analogue of the #63 placement retry (FetchAttempts/FetchBackoff, default 3× / 200 ms). A clean miss (nobody has the chunk) still returns after a single pass. The relay's concurrency defaults are also raised from 64/8 to 128/16 (global/per-peer): splices are short-lived, so this is realistic headroom for a rendezvous node while staying a bounded, operator-tunable cost (each splice is still byte-capped). Remaining, tracked in #65: register-after- distribute (a loud placement failure still leaves a dangling registry entry), and hole-punching (the structural fix that moves bulk bytes off the relay entirely).

Fixed

Publish no longer returns a link for a file the swarm can't rebuild (#64, the data-shard twin of #60): placement verified that manifest chunks landed durably, but data and parity shards were placed optimistically — a column that no node accepted was ignored, so under load a stripe could silently erode below its erasure threshold k and the publish still returned a valid-looking link (in the field, f123 came back stripe 0: only 9 of 16 shards, need k=10, unrecoverable). Distribute now tracks per-shard placement and, before returning a link, verifies every stripe kept enough placed shards to reconstruct (accounting for the known-zero padding of a short final stripe); a column that lands nowhere is retried with a fresh lookup (as manifest chunks already were), and if a stripe still can't be made recoverable the publisher fails loudly instead of handing back an unrebuildable link. The same check closes the identical silent-loss on uncoded files (which carry no parity, so every chunk is required). Extends tenet B7 — trust but verify; no optimistic operations from the manifest path to all of publish.

Fixed

Publish no longer returns a link for content it never stored (#60, found in the 300-file scaling re-test): under load, once the network passed its capacity cap, a manifest chunk could be placed on no node (all candidates full or unreachable) yet publish still registered the root and returned a valid-looking link — ~14% of files were stranded behind dangling links (fetch failed with "manifest chunks unreachable"). A manifest chunk that lands nowhere is now retried with a fresh lookup (these misses are usually transient — a relay hiccup once the nearest nodes cap out and load shifts onto NATed hosts), so publishes that used to strand now succeed; if it still can't be placed after several tries the publisher fails loudly instead of handing back an unretrievable link. This makes publish honor the new tenet B7 — trust but verify; no optimistic operations.

Fixed

Ghost routing entries no longer break discovery at scale (found in the 300-file scaling test, #43): every swarm add/swarm get ran as a short-lived client with a fresh identity, and nodes both routed to those clients and persisted them to peers.json — so a busy node's routing table filled with dead entries (in the test: 327 entries, 2 live, ~75% query timeouts), which broke provider discovery and made most fetches fail. Fixed at both ends: nodes persist only peers they have actually reached, and a short-lived client stamps its messages so peers never route to it.

Fixed

Re-publishing identical content is idempotent (#46): a failed publish could leave a root registered but return no link, and a retry then hit "root already published with different entry" — because idempotency compared the whole entry, including the per-invocation publisher identity. It now dedups on content, so a retry (or a second person adding the same file) succeeds instead of colliding.

Added

Opt-in in-RAM read cache for hot chunks (-cache SIZE, default off; #42): a cache hit serves trusted bytes from memory, skipping both the disk read and the per-read hash re-verification. Read-through LRU, cache-on-read only, and Delete evicts so purged content is never served.

Added

The daemon caretakes content published through its own UI by default (-care-published, #44): without a caretaker a published file's redundancy only decays as nodes churn — now the publishing daemon repairs its own roots, and both the UI and CLI say whether a caretaker is running.

Added

Paginated, shard-sorted roots list in the daemon UI (#45): the "identifiers this daemon holds shards of" table now paginates and sorts by shards held, instead of rendering every row (unusable at hundreds).

Added

A public build log — a chronological "how it was built and why" narrative under docs/buildlog/ (dated Markdown entries), rendered to website/buildlog.html by scripts/gen_buildlog.py on the same source-of-truth pipeline as the changelog and roadmap (CI fails if the page drifts). It's the reasoning behind the design — the forks, the dead ends, the decisions — distinct from the changelog (what shipped) and the roadmap (what's next), and strictly about building the infrastructure. Seeded with three entries: the one-process/ports-and- adapters prime directive, the placement spectrum, and cross-network reachability. Linked from the site's docs and footer.

Added

-log LEVEL — narrate the normal path, not just failures — both silt daemon and silt client take -log error|warn|info|debug, opening the debug.log sink at that threshold; -debug is now shorthand for -log debug. At info the happy path narrates — file distributed (chunks placed), block committed (quorum reached, by proposal or broadcast), file retrieved, alongside the existing stripe repaired, dispersion re-spread, and reachability verdict — so a real-world run can be checked against how the system is supposed to behave, not only when something breaks, and without the debug firehose. Free when off and off the hot path (per-chunk store events stay at debug); core still logs through the ports.Logger port and imports nothing new.

Added

Multi-process end-to-end tests over real TCP (CI hardening, BACKLOG Phase 2) — a new e2e/ suite builds the silt binary and runs three daemons as separate OS processes, publishes a 1 MiB file through the chain-backed registry over pinned HTTPS (driving a real consensus round to a committed block), then fetches it back across the swarm and asserts it returns bit-perfect. This exercises the whole wire path the in-process sim deliberately bypasses — exactly where #36's "a reply can never reach a NATed peer" bug hid until real sockets carried it. It runs as its own CI job; the unit and race jobs pass -short to skip the process spawning.

Added

Relay discovery by gossip (#27 polish) — a daemon offering -relay now stamps the service's dialable host:port on every outgoing envelope (borrowing the -advertise host when the relay listener is bound to a wildcard). Peers record these first-hand — a node only ever announces its own relay, and dialing pins the relay's identity, so gossip can direct but never impersonate. A daemon whose reachability verdict is NATed and that has no -relay-via adopts the first discovered relay automatically (and keeps watching until one appears): the two-Macs runbook now works with nothing but -bootstrap.

Added

Two-slot address book: direct preferred, relay fallback (#27 polish) — the transport now remembers up to two addresses per peer, one direct host:port and one relay:R@host:port, instead of one slot the two forms fought over (an mDNS-learned LAN address used to be clobbered by the peer's relay stamp, sending house-mates through a relay on another continent). Dials try direct first — no third hop — and fall back to the relay within the same delivery; a direct address is dropped only when the relay fallback reaches the peer, which proves the address stale rather than the peer down. Contact gossip passes on the relay form when one is known (a relay-advertising peer is NATed, so its direct address is LAN-scoped hearsay); peers.json persists both slots. The reachability dial-back ignores relay addresses outright: reachable-through-a-relay is exactly what "public" must not mean.

Fixed

NATed peers can actually converse (found in the first real cross-network test, #27): the transport dialed a fresh connection per message, so a reply required dialing into the requester — impossible behind NAT, and bootstrap came back with zero table entries. Replies (and all traffic) now ride the live connection the peer opened, and dialed connections are kept and reused. Two corollaries: a wildcard bind (0.0.0.0/[::]) is never stamped on outgoing messages (it used to poison peers' address books with an undialable address — a new -advertise HOST:PORT flag lets a public box say what to gossip), and a daemon that registers with a relay now re-bootstraps through it, since its first join attempt may have been unanswerable. The reachability dial-back deliberately never reuses a connection — its meaning is "a fresh inbound dial landed" — so AutoNAT stays honest.

Fixed

Relay-form addresses survive -bootstrap, DNS seeds, and peers.json — peer strings split on the first @, not the last, so ID@relay:RID@host:port parses instead of being silently dropped.

Added

Relay (#27, step 3 — the universal NAT fallback) — a NATed daemon can now be reached across networks through any reachable node running -relay ADDR. The shape is libp2p Circuit-Relay-v2's, without the dependency: the NATed node keeps one registered outbound connection to the relay (-relay-via RELAYID@HOST:PORT, taken up automatically when the reachability verdict says NATed) and advertises relay:R@host:port as its address; a sender dials the relay, the target dials back, and the relay splices the two streams. Crucially, the sender then runs its normal pinned end-to-end TLS handshake with the target through the splice — the relay moves opaque bytes it cannot read, alter, or forge, so "a frame's sender is whoever the handshake authenticated" holds unchanged across a relay. Relaying is a capability, not infrastructure: opt-in, capped (concurrent sessions, per-peer sessions, per-session bytes), no relay baked into the binary, and the relay-operator metadata exposure is documented in the threat model. CI proves the full path on localhost — including both-peers-NATed, every byte relayed — because "NATed" is modeled honestly as "accepts no inbound connections".

Added

-debug flag → debug.log on both silt daemon and silt client — a leveled logger behind a new ports.Logger interface (core stays pure; the file sink is adapters/logfile). One grep-able line per event: transport failures (dials, handshakes, forged frames), node events (request timeouts, repairs, dispersion re-spreads, the reachability verdict), and daemon milestones (discovery, bootstrap). Quiet by default and free when disabled; with -debug, a failure in the field leaves an artifact that can be attached to a bug report. Groundwork for testing cross-network reachability (#27) on real networks, where failures are one-shot and remote instead of deterministic and replayable.

Added

Zero-config LAN discovery (#27, first rung of cross-network reachability) — silt daemon now announces itself on the local network and folds any peer it hears into the routing table, so two nodes in the same house find each other with no -bootstrap, no DNS seed, and no infrastructure. It's link-local multicast (the same idea as mDNS, scoped to the LAN by TTL), and self-authenticating: an announcement carries a peer's ID@host:port, and the TLS handshake still must present a key hashing to that ID, so a rogue beacon can misdirect a dial but never impersonate a node. On by default; -mdns=false opts out, and a loopback-only -listen disables it with a note (nothing on the LAN could reach a loopback address anyway). See docs/design/cross-network.md.

Added

Reachability check (#27, our AutoNAT) — after bootstrap, a daemon asks a couple of known peers to dial it back at its advertised address. A landed dial-back both proves and delivers the verdict "public"; silence within a timeout is read, conservatively, as "behind NAT" (which only ever costs a relay we might not have needed, never a false claim of being reachable). The daemon logs the result and the dashboard shows it; the relay step will key its advertise-direct-vs-via-relay decision off it. No new message plumbing beyond two wire kinds; the pure core stays NodeID-only — reachability is simply whether the transport can deliver.

Undated

Removed

The core/demand v2 flat primitive (C1, the B-9 tail). B-9 (#764) retired the flat receipt at the node; the primitive behind it stayed callable because ~25 unit tests pinned properties on it. Those properties are now re-homed onto the anchored session lane and the primitive is DELETED: Bank.Redeem, Bank.Demand, DeliveryReceipt, receiptMsg, Ack, SubmittedReceipt / UnmarshalSubmittedReceipt / ErrOversizedReceipt, the bank's own spent set (spentKey, sweepExpiredSpent, reserveSpent, sweepIfEpochAdvanced, maxSpentTokens) and Node.WitnessedDemand. Zero production callers before the deletion (shown by grep in the commit). What stays: Token, Withdraw/SignWithdrawal/Unblind/ VerifyToken, Keyset, BondCheck, RequireBondedFetcher, the whole session lane. The bank now holds ONE per-object counter (WitnessedIncrements), pinned structurally by TestTheBankHoldsExactlyOnePerObjectCount; the double-spend guard is the credit ledger's shared paid-serial guard and nothing else. Every re-homed property keeps a driven ablation or names the existing test that pins it — see the commit message for the full old-test → new-test → ablation ledger.

Removed

The credit ledger's FLAT delivery leg (C1). Ledger.RedeemDeliveryCredit, Ledger.RedeemDeliveryCreditReason and the ports.CreditLedger.RedeemDeliveryCredit port method are DELETED. They had no production caller after B-9 (#764); the paying surface is SpendDeliveryAnchorsSettleDeliveryCloseDeliverySession, where the anchor is spent at session OPEN into the shared paid-serial guard. The guard, the epoch watermark, the provisional-lane supersede and the eviction reversal all stay — only the flat ENTRY POINT goes. ~22 test files re-homed onto the lane (settleOnLane / paidOnLane, which open a one-anchor session and settle its budget), and every red-team scar re-derived RED under a controlled revert on the lane, at its historical broken value where it had one (the cross-server pump still reports (K−1)·fee when the guard's already-paid screen is removed). Two flat-leg-only properties do NOT re-home and are replaced by strictly stronger ones, each stated in the test that replaces it: the UNGUARDED serial-less redeem (the lane has no unguarded path — a mis-sized serial is refused with ReasonAnchorMalformed, recording nothing), and the flat leg's internal ORDERING rule that a refused receipt gives up its self-mint (on the lane a refused OPEN admits no session, so the server is on the certified unwitnessed fallback; the economic question "can not-being-paid beat being paid" is asserted directly by TestG4_NotBeingPaidIsNeverBetterThanBeingPaid at every size, and from the other direction by TestDeliveryAcceptStrictlyDominatesSuppressionAtEverySize, both pinning the same +75 margin at 64 MiB).

Fixed

Consensus liveness — the h43 round-ladder desync (R-H43-ROUND-LADDER-DESYNC, D-CONSENSUS-ARMING; Lane A1). Run c450985-deep committed height 43 996 s (22.6 × T_b) after one validator stopped, because the round clock armed on LOCAL mempool content (core/node/rounds.go) — 10 of 13 seats never ran the pacemaker. Now: (A) the clock arms on a REPLICATED condition (pending work OR any verified consensus message for the working height) and the sweep counter is held, never zeroed, when disarmed; (B) a declared round is a suffix claim in the catch-up predicate and the assembled round certificate is a transferable wire object (MsgRoundCert, appended kind) sent to the round's designee plus the peers absent from it, verified once per round, budgeted per sender and capped at the governing set; (C) the designee proposes at the certificate's round instead of a re-derived local one; (D) — the mechanism the model-check exposed once (A)(B)(C) existed, R-H43-WORKLESS-DESIGNEE, certified by the delta certification — a work-holder forwards its pending ENTRIES (cap 4) to the round's designee on round entry, the #338 takeover is keyed to the round's designee (the designee has priority, never exclusivity), and a designee attempts once per (h, r) with the empty check before the era roots. Deterministic homes: core/node/modelcheck_h43_*_test.go (G-H43-1 was RED on main — the first round-liveness oracle with a non-uniform arming distribution, now arming below the catch-up threshold so only (A) can move the quiescent seats — G-H43-2…6, 9, 10, 10a, 11–15, and the round-0 refusal G-H43-14 for R-H43-CERT-ROUND-ZERO-UNVERIFIED, a MsgRoundCert at round 0 that verified nothing). Published bound restated (owner call 21 owed): f′+1 rounds after GST, f′ counting seats down or workless at their round; 190 s at f = 1 with the forward landing, ≤ 430 s via the takeover backstop at N = 12.

Fixed

R2.9a — the preserved firstSeenTick writer is a WALL CLOCK, not a request counter; four texts said the opposite and are corrected, and the residual they denied is now filed as R-BB-BOND-STAMP-TUPLE. A blind principal-engineer review measured this on two real bonded validators. core/node/bondaudit.go stamps uint64(n.clock.Now) + 1; the daemon builds its node clock as clk := walltime.New(loop); adapters/walltime returns time.Now.UnixNano. The claim that it was "the bond auditor's own request counter rather than a wall clock" was inherited from a stale doc comment on RecordBondChallenge in core/credit/credit.go, which is corrected at the source so it cannot be inherited a third time. The other three sites — core/credit/bbootstrap_off.go, the TestR29aBondChallengeStillStampsFirstSeenTick doc block, and the ratified D-BB-BUILD-TAG entry in docs/decisions.md — are corrected too; the ratified entry keeps its original bullet and carries an appended dated correction, so the record shows the error rather than hiding it. The consequence, disclosed rather than rounded off: the writer fires on a -validator node for that node's own id and for every bonded peer that answers a challenge, so an identity that is both a bonded peer and a fetcher still carries (identity, cumulative fetched bytes, first-seen wall-clock nanosecond) in a default build. The tag's claim therefore holds on the serve path, for the general requester population, and not for bonded validator peers. The residual is narrow, predates R2.9a, and is not closed hereRecordBondChallenge, DecayStale and standing retention are unchanged, and that surface is research-gated. New gate TestR29aBondAuditStampsAWallClockNanosecondNotACounter (core/node, untagged) drives two real audit sweeps an hour apart and requires the ticks to differ by the elapsed hour rather than by 1, so a request counter — including a high-seeded one — fails it; TestR29aBondChallengeStillStampsFirstSeenTick now uses a Unix-nanosecond-magnitude tick instead of 77, which had made the value look like a counter to every future reader.

Fixed

R2.9a — the guard against putting -bbootstrap back into a DEFAULT build now runs in the DEFAULT build. The assertion that cmd/silt/daemon.go declares no -bbootstrap lived in a file carrying //go:build bbootstrap, i.e. the one build that cannot have the defect: the same review added fs.Bool("bbootstrap", …) to the untagged daemon.go, and the resulting binary declared and accepted the flag while the ordinary test job stayed green. That assertion moves into TestR29aDefaultBuildHasNoBBootstrapFlag (untagged), and is strengthened on the way. It no longer greps one literal in one file: it asks go/build for the default build's own file set — the same constraint evaluation go build performs — and parses each file for any stdlib-flag declaration whose name contains bbootstrap, under any flag type and in any file. Both reintroductions redden it (measured: the reviewer's daemon.go fs.Bool, and a renamed fs.StringVar("bbootstrap-rows", …) planted in a different untagged file).

Changed

R2.9a — /api/status now serves a snapshot recomputed at most once every 5 s, the B_bootstrap age axis measures first FETCH rather than first ledger touch, and the per-object durability detail requires the API token. Three changes to what the status surface publishes; nothing counted, stored or computed changes. (1) The cached fixed-interval snapshot (G-BB-26, certified REQUIRED on two independent grounds). The handler recomputed the whole document per request. The B_bootstrap block's delta-trajectory residual was disclosed as "bounded by the poll rate" — but the poll rate is the READER's own choice and there is no rate limiter anywhere on the UI server, so that was not a bound and the disclosure is corrected in the same change. Independently, the recompute is an O(R) walk over an append-only, never-evicted account set plus the whole chunk store, run inside the node's event loop, per unauthenticated GET — build-immutable #8, "an unbounded system on a small box is not inefficient, it is unsafe." Caching makes the per-request cost O(1) and the per-interval cost O(R), so a GET flood is amplified at most once per interval instead of at the attacker's request rate, and an observer gets at most ⌊uptime/T⌋ distinct documents however fast it asks — from /api/status alone as first built; the sibling /api/economy/self was uncached, corrected in the Fixed entry above. T is a SECURITY PARAMETER and the value is PROVISIONAL pending owner ratification, named once in code the way SlashesBytesCap is. It is derived, not picked: bounded from above by the fit (the narrowest positive-width age bucket is 60 s, so anything well inside it is over-sampled by orders of magnitude and the cost to the estimate is zero) and by the operator (the shipped dashboard polls every 3,000 ms), and from below by privacy and loop cost. 5 s sits above the poll period and 12× inside the narrowest bucket. A stale document is identifiable as stale (Don't #4): every response carries snapshotTakenAtUnix (fixed for the life of one snapshot), snapshotAgeSec (computed at serve time, so it moves) and snapshotIntervalSec (T itself, on the wire beside the axis constants so an analyst can price the residual). A token-gated mutation invalidates the snapshot: without that, POST /api/fund debited the balance and the very next /api/status showed the old number for up to an interval, which reads as "the action failed" — a worse silent-loss shape than polling staleness, because the client knows it just wrote. The hook sits AFTER the token gate, so an unauthenticated reader still cannot drive the recompute rate the cache exists to cap. It was found by TestEconomyEndToEndOnLiveDaemon, not by reasoning. (2) The age axis is stamped on the FETCH path only (G-BB-24, residual R-BB-STAMP-BY-ANY-PATH). The stamp lived in Register, which every ledger path reaches through acct, so the axis recorded first ledger touch by any path — bond audit, PoR grading, bounty payment, false-repair slash — and every identity that is also a DHT participant published an age over-stated by however long it had been a peer before it first fetched, unbounded above by the ledger's uptime, on the input to a security parameter. It now lives at the one place fetchedBytes is written, which both fetch call sites are funnelled through. It is a second field, not a re-pointed guard, and that is not tidiness: RecordBondChallenge keeps writing firstSeenTick at a DIFFERENT EVENT — the first bond challenge the identity answered — so one shared field guarded on "unset" could not be fixed by moving the write. A peer the auditor reached first would keep the CHALLENGE instant and publish it as its fetch age, which is the defect. (The first version of this entry argued the split on UNITS, saying the auditor's tick was a request counter. It is a wall-clock nanosecond, as the entry above records; same unit, same clock, different event. The correction does not weaken the split.) The stamp is the instrument, so it compiles only under the bbootstrap tag: stampFirstFetch has an empty untagged twin, and a default build walks the fetch path writing no when at all. The bond auditor's writer is untouched. (3) The per-object durability detail is token-gated. durability.objects[] published, with no flag and no token, a per-content-root funded counter; the skim is one eighth and one served byte is one credit, so eight times the delta in funded is the EXACT byte count served of a NAMED root — the object half of who-fetches-what. It predates the B_bootstrap work entirely. /api/economy/self republished the same per-root numbers, so both surfaces are gated together; gating one alone would close nothing. Reducing precision was refuted before it was tried: rounding a CUMULATIVE counter does not stop delta extraction, because an observer polling across the rounding boundary still recovers the increments, and the increments are the leak. The cache in (1) bounds the extraction RATE and closes sub-interval attribution; at this deployment's traffic an interval still routinely holds one fetch, so it degrades the join rather than removing it. The aggregates stay openbountyOn, the node's own balance, stats.bytesServed, the pooled skimIn/bountyOut — because they name no root, and the observatory reads them cross-origin where a sibling's token by design never travels. Withheld is not empty: objects is ABSENT and detailWithheld: true rides the block, so a reader can tell a withholding from a node that caretakes nothing. The operator's own solvency view is unchanged — the durability horizon and the cliff early-warning are a shipped feature, the embedded UI already attaches the bearer token to every same-origin /api/ call, and cloudtest already reads funded with an Authorization header. Gates, each with a controlled-revert ablation recorded RED: BB-21 (two reads inside one interval with a fetch interleaved return byte-identical bBootstrap blocks, and the block moves again past the interval), the staleness-is-visible gate, BB-22 (seven non-fetch ledger paths leave no stamp; and a bond-challenged peer that fetches a day later is aged from the FETCH), and the F2 gates at unit and e2e tiers on both endpoints.

Testing

R2.9a DELTA — four gates on the floor, the polling leak and the two refuted separations (core/credit/r29a_minr_floor_test.go, core/node/r29a_minr_floor_test.go, cmd/silt/r29a_minr_floor_test.go). BB-15, the floor, at three tiers: the core/credit method, the core/node seam and the JSON on the wire, each asserting that at R_min − 1 no census key survives and at exactly R_min all of them do (the rule is ≥ R_min, not > R_min). A companion recomputes ⌈1/(1−q)⌉ by a different method than the constant does, so the number and its stated derivation cannot drift apart. BB-16, the polling oracle: one requester, six fetches, a snapshot between each, and an extractor that reports every single-identity bin transition in the delta sequence. It carries its own POSITIVE CONTROL, which the re-certification entry below moves onto the wire and makes stronger: the same extractor over a PUBLISHED series whose census has been padded over the floor, where it must find the trajectory (the same class of read the review measured as 80 → 84 → 86 → 88 → 89 → 90) — because a green result from an oracle that can never fire proves nothing. BB-18, the census is a SUPERSET: three real nodes on one simnet, where a peer fetching over the REPAIR path (fetchStripeByColumn, the function repair.go and repairclaim.go both enter on) and a viewer fetching a plain chunk by id land in the serving node's census in ONE CELL WITH COUNT 2. It asserts the contamination EXISTS and is not a defect to fix: whether repairing peers belong in the estimand's population is an open owner decision. BB-19, the dead discriminator: on a serving ledger servedBytes > 0 holds for exactly one account — the node's own — which is never in the census, so the proposed repair-vs-viewer split partitions the census into everyone and nobody; pinned at the ledger primitive and again on the real MsgFetchChunk serve path, where the fact that could regress is the argument order at the two call sites. Six controlled reverts, each RED then restored: dropping the floor call at the seam; rendering the counts despite suppression; withholding the grid while still publishing requesters (the "closes nothing" defect); hand-picking R_min; lowering the certified q edge (a compile error, by design); making the serve path skip coded-shard fetches; and crediting the requester's servedBytes.

Testing

R2.9a RE-CERT — BB-20, the equivalence gate at the wire, plus a package-scope export gate (cmd/silt/r29a_bb20_equivalence_test.go, core/credit/r29a_export_route_test.go). BB-20 runs G-BB-11′ as a property instead of checking a list: for any two ledger states below the floor sharing the same clock state, the published bBootstrap JSON must be BYTE-IDENTICAL. It asserts on the bytes /api/status emits, not at an internal seam, because the seam is where the two previous enumerations were checked and where both misses got through — so a census-derived field reaching the wire by ANY route reddens it without anyone having named the field. Three groups, each replaying one clock script across several below-floor censuses so clock state is equal BY CONSTRUCTION rather than by accident: varying ages and byte totals, an 8-day forward step (which discriminates ageExceedsUptime), and a backward step past a first-touch stamp (which discriminates clockStepBack's census arm). It was RED on the reviewed build and it found BOTH defects on its own — the forward-step and backward-step groups failed, one per defect — and it is green after the fix. It ships with a positive control that drives the same scripts ABOVE the floor and requires the blocks to DIFFER, so byte-identity below the floor cannot be an artifact of a fixture that varies nothing. The export-route gate parses every non-test .go file of core/credit by reading the directory, and requires that the only exported functions returning a BBootstrapHistogram are BBootstrapPublish and WithMinRequesterFloor (which can only floor). It closes the one hole the compiler cannot: a second exported reader added inside that package, which no name-based gate in another package could see. Its own teeth are pinned by running the predicate over a synthetic bypass that must flag a value return and a pointer return while ignoring an unexported method. Ablations, each RED then restored: copying ageExceedsUptime through the floor again; copying ageClampedToZero through; un-splitting the two clockStepBack arms; adding the reviewer's exact bypass export to a THIRD file inside core/credit (build clean, gate RED — the case a tree walk could never see); and the decisive one for the FORM — adding a census field nobody had foreseen and publishing it unconditionally on the wire without touching the floor, which stays GREEN under the construction and goes RED the moment the floor is reverted to clearing a list. BB-16's positive control moved onto the wire in the process: the raw census no longer leaves core/credit, so the control is now the SAME published path with the census padded over the floor by nine identities — which is the review's own measured refutation, encoded as a permanent gate.

Changed

O3-T gate hardening (test/docs-only; PE RULING-O3-direction-T-build-fa895f5 conditions 1-3). TestO3T_HeavierReadsOnlyHeightAndHeadHash is now ALIAS-AWARE: parameters resolved from the declaration, taint tracked through :=/=/var/range, any selector on an alias and any tainted argument to any call flagged — the PE's ablation D (ca := a; len(ca.bonded) at equal height, the #357 replica-local class that passed every gate) is RED; TestO3T_HeavierPinHasTeeth carries C, D, a range alias, a var-decl chain and a helper hand-off as an expected-site set. TestO3T_VerifierInventoryPin resolves crypto/ed25519 by IMPORT PATH (alias and dot imports hit, a decoy alias does not), matches VerifyWithOptions, and walks core/node too (rows verifyRoundChange round-change envelope, OpenRelaySession relay-open commitment — neither an attestation). The F6 fixture TestRedteamF6_ObjectiveForkChoiceAgreesAcrossDivergentReplicas names the axis it measures (admission under divergent rep views; it cannot discriminate a ranking term) and cites the interlock oracles for the ranking axis. ROADMAP: R-O4-CANON-HASH-COVERAGE is BUILT as widened I5 with the number awaiting the owner's ratification; the daemon.go:994 "heavier fork" line is recorded as a live S5 drill contract with the stale-prose list as owed docs true-up.

Security

Gate tail — three owed gates. (1) R-S5-STRING-REGISTRY: cmd/silt/observable_contract.go registers the 28 announced operator literals (S5 contracts) with their emitting file, dependant and runtime asserter; TestObservableContractStringsAreStillEmitted asserts each is still in the source, the file exists, no pair repeats, no marker is a strict substring of another, and every asserter is a real func Test; TestObservableContractHasTeeth ablates. Built because the scar reached count 3 (scar-observable-log-contract: at a34b61a the instance-2 gate pinned the symbol, not the literal, so a rename stayed green under -short); renaming the NOT banked literal now turns the unit tier RED. It proves presence, never reachability — the e2e assertions stay. (2) R-AST-PIN-GLOB: the fold live-state AST pin's glob widened to floorbox_*_v5.go (five floor-box files were outside it; the pin stays green on them). (3) R-FORKCHOICE-RAMP-GUARD: the Weight > 0 §1a assertion in forkchoice_ramp357_test.go is deleted, not repaired — it held only under an attestation era no node mints (O3 recommendation §3.5 item 3); Invariant D stands.

Changed

The e2e paid-delivery-lane test asserts the certified refusal (G-8 disposition (iii)). TestDeliveryReceiptBankedOverTCP is renamed TestPaidDeliveryLaneRefusesWithoutACommittedKeyBinding and re-scoped: on the -objective=false fixture the client must refuse naming the missing committed E→key binding, the server must bank nothing, and the daemon banner must announce that the binding needs an era-4/v5 chain. The positive arm cannot pass on that fixture at any readiness stamp, so it moves below e2e: new sim TestPaidDeliveryLaneThreeCallComposition drives FetchDemandIssuerKeysAcquireDemandTokenInWindowSubmitDeliveryReceipt in cmd/silt/swarm.go's own order, on a real v5 chain with a real committed binding, through the real wire handlers, TWICE, asserting fee − skim both times. Restoring the e2e positive arm is filed as R-E2E-ERA4-FIXTURE and is bound to the stamp-raising release, by upgrading the fixture to an objective bonded epoch-enabled topology — never by an activation override.

Fixed

A demand-key refusal that resolved nothing now names a reason (Tester finding). The swarm receipt guard was if keyErr != nil || pinned == 0 over a message formatting keyErr with %w, so the pinned-0-with-no-error branch — the branch every client hits on a chain that carries no committed E → key_E binding — printed the literal %!w(<nil>). Both branches now carry a cause (demandKeyResolutionError); the refusal itself is unchanged, since withdrawing against an unanchored key is what the committed binding exists to prevent.

Fixed

The per-epoch demand key store fsyncs its directory after the rename. The band's bytes were synced but the rename that publishes them was not, so a power cut could lose a whole freshly rotated band — every key for the epochs whose fingerprints the rotation just staged. The old file was always safe (temp+rename); this is what makes the NEW one durable. Same shape as adapters/markstore.

Fixed

The paid-serial guard sweeps at most once per epoch (R0.4b, red-team RT-E). At a full cap of still-live serials every refused redeem ran a full map scan — 1.32 ms per refused receipt at 65,536 entries, a free amplifier. Nothing can expire twice within one epoch, so the swept set is identical and the cost is now amortized O(1). Purely a cost fix.

Fixed

A cap-full delivery refusal is OBSERVABLE (Tester finding). Every non-paying redeem path returned a bare 0, indistinguishable from self-delivery, an already-paid serial, or a zero fee — and surfaced at the node inside a log line reading "delivery receipt banked", which is misleading because nothing was banked. RedeemDeliveryCreditReason now names the reason and GuardFullRefusals/SerialSweeps count it; a banked receipt that settled nothing gets its own WARN line. The announced "delivery receipt banked" marker is unchanged (S5).

Testing

The "delivery receipt paid NO credit" WARN line is now gated (Tester finding; observable-log contract, second instance). The line an operator reads when a banked receipt settles nothing was asserted by no test. TestBankedButUnpaidReceiptLogsTheWarnLine pins the exact event string, the WARN level, and both fields (reason, serial_guard_refusals) on the real node handler path, and re-asserts that the announced delivery receipt banked marker still ships beside it. Ablations run: reword the event, drop the counter field, or log it at INFO — all RED.

Testing

The demand key store's atomic write is gated (TestEpochStoreSurvivesACrashBeforeRename). Atomicity was asserted by inspection only. The gate drives both failure shapes: a stale .tmp-demandkeys-* artifact from a crash between CreateTemp and Rename (the committed band must load byte-identically past it, and must not be regenerated — its fingerprints are already committed, append-only), and a Save that cannot write (the committed band must survive). Ablation run: replace temp+rename with a direct os.WriteFile and the second case goes RED, because a direct write to an existing 0600 file succeeds even in a read-only directory and destroys the committed band.

Testing

Stale citation fixed: core/blindtoken/epoch_binding_test.go cited a core/credit guard-expiry gate under a name no test has ever carried; the real gate is TestGuardHealsUnderASharedKey. (The dead name is deliberately not repeated here — scripts/check_cited_tests.py reads a backticked Test… in the CHANGELOG as a citation, so writing it out would re-fail the lint that caught it.)

Testing

The cited gate that did not exist now exists (Tester finding). core/credit/delivery.go claimed paidSerialWindow was pinned to demand.DefaultWindow by TestPaidSerialWindowMatchesDemandWindow; a repo-wide grep found only the comment. Both the value pin and a behavioural seam gate (TestGuardLifetimeMatchesDemandKeysetLifetime, which walks the epoch clock asserting "the demand layer still verifies this token" and "the guard still remembers this serial" are the SAME predicate) are written. At the drifted value paidSerialWindow = 2 both go RED at epoch 3 — the Tester's measured control, where a second server re-collects an evicted serial.

Testing

Every red-team probe is now a permanent gate, each with a RUN ablation: TestDemandSignature DoesNotVerifyAtAnotherEpoch + TestDemandFDHInputBindsTheEpochByteExactly (b1, byte-exact); TestSharedKeyRotationDoesNotReopenThePump (G); TestComposedBoundary_SameFingerprintAtTwo EpochsDoesNotRedateTokens (G/I, the composed boundary R0.4b-8 under a re-registered key, plus the 2W+1 replay that refutes the distinctness rule); TestGuardHealsUnderASharedKey (D); TestStaleIssuerKeyRegDoesNotMuteTheProposer + …PreFlipBoot (A/A2); TestPinFollowsTheChainAcrossAReorg (H); TestCohortKeyIsADenialOnEveryShippedLane (C, both shipped shapes against a Byzantine issuer endpoint); TestDemandLaneOutlivesTheWindowAndA Restart (B, > W+1 epochs plus a restart, driving the daemon's own rotation step); TestSweepRunsAtMostOncePerEpoch (E, counts sweeps not time); TestReadinessStampImpliesIssuerKeyCoverage (F).

Testing

The provisional-lane + refused-redeem combination is gated (TestRefusedRedeemLeavesOnlyTheBilateralFallback): the Tester measured Σ up by exactly bytes when a serve records its provisional self-mint and the redeem is then refused. The bound is that this EQUALS the no-receipt baseline — the refusal adds nothing on top of the pre-existing unwitnessed bilateral fallback — and the gate asserts exactly that at three byte sizes.

Testing

The COLD-BOX tier + the fold-file live-state allowlist pin (Boulder 1, R-COLD-BOX-HARNESS): every recompute gate ran the recompute ON the chain that applied the history, so the test tier shared the producer's blind spot — third occurrence in this spine (R1.3 fold-caught premise, class-P suppression, live-state reads). core/chain/floorbox_recompute_coldbox_v5_test.go adds a permanent tier that drives the REAL entry on a New(cfg) box which never applied a block, holds no registry, and is fed only (prevStateRoot, committedStateRoot, b, w). The class-A / class-M / class-P agreement and adversarial-root gates are re-run in it, plus four driven D1 gates: the adversarial mid-epoch-joiner root must STALL, the honest block must AGREE, a forged MatureEpoch.OldValue must STALL in both polarities, and a live follower re-auditing a pre-handoff block must AGREE. Measured ablations: restoring the two live reads (c.matureEpoch + c.launchAnchor) drives THREE RED — the D1 safety gate by wrong-accept, the D1 liveness gate by false stall, and the Direction-2 live-follower gate by a post-root mismatch; the forged-OldValue anchor gate reddens instead under the complementary ablation (dropping the Direction-A tagMatureEpoch anchor), in both polarities. Every pre-existing gate stays green under both, which is precisely why the defect survived four sub-increments. The allowlist pin reddens on the live read by name and file:line. core/chain/floorbox_recompute_foldlivestate_pin_v5_test.go is the recurrence teeth: an AST walk over floorbox_recompute_*_v5.go that reddens on ANY c.<selector> outside a narrow allowlist (cfg, objective, epochsEnabled, operatorMargin, launchAnchorGiven, and self-dispatch). matureEpoch, everMature, launchAnchor, handedOff and every committed map are explicitly DENIED — allowlisting them would pin the defect in place. StateRootMaturityWitness joins the fold-input carrier coverage table (foldInputCoverageTable, the R-CARRIER-REFLECTION pin) with a row for its new MatureEpoch field, so the reflection walk stays exact.

Testing

Two cited-but-missing consensus guards, written (test-only; the cited-tests lint's OWED ledger, paid down by two): production comments on two consensus-facing surfaces claimed a test enforced a property, and no such test existed anywhere in the tree — the scar:cited-test-does-not-exist class. TestStateRootV5CoversExactlyTheV5Fields (core/chain/modelcheck_stateroot_determinism_test.go), cited by core/chain/statehash.go, is the current-era (v5) counterpart of the era-3 coverage guard: the tag set stateRootLeavesV5 actually emits on a fully-populated chain must EQUAL the union of stateRootTags, stateRootTagsV5 and stateRootDigestTagsV5 (28 tags), with both sides derived by construction — the emitted side from the live marshaller's leaf keys, the declared side from the lists — and the enumerated leaves tied to the root by asserting statehash.Root(stateRootLeavesV5) == StateRootForVersion(v5). The EXTRA direction was covered by nothing: every other guard iterates a LIST, so a leaf under an unlisted tag entered the consensus root invisible to all of them. Injected-defect evidence: dropping the tagEpochStart leaf names epochStart MISSING; adding an unlisted shadow leaf names it EXTRA. TestEveryDiskWritePathRunsTheEra4VersionCheck (core/chain/era4_writepath_version_test.go), cited by core/chain/era3validity.go, is the era-4 write-path guard — only the era-3 sibling existed. It has three legs: STRUCTURAL (every c.apply caller runs validateEra4Version, discovered by scanning rather than listed, so a future fast-sync/import path reddens), SCANNER COMPLETENESS (the only .apply outside chain.go is postApplyRoots' dry-run clone, so the chain.go-scoped scan is total), and BEHAVIORAL (Append, Reload and Reconcile are each driven with a signature-valid v4 block at H_era4 and must reject it with ErrEra4VersionRequired, applying nothing). Injected-defect evidence: removing the check from appendStructural names appendStructural structurally and shows Reload PERSISTING the forged boundary block; removing it from ValidateProposal reddens the transitive-guard check plus the Append and Reconcile legs. Both comments are corrected to cite what now actually holds them. Also closes the same worktree-walk unsoundness in scripts/check_claims.py that the cited-tests lint already excludes: a plain os.walk resolved claim-backing tests against .claude/worktrees/ copies of OTHER branches — measured, 121 test names resolvable only there — so a ledger claim could read as backed while main enforced nothing.

Testing

R-CARRIER-REFLECTION — the fold-input carrier reflection pin (Boulder 1 carried residual, owed before the R1.8 accept-flip; test-only, no production logic changed): closes the last hand-verified surface in the R1.4 witness-soundness cert. That cert held R-CARRIER-REFLECTION as BOUNDED-BUT-OPEN — TestAdversarialRootCoverageIsComplete reflects only the value/predicate carriers, so the fold-input carriers were "verified by hand" and a future added or renamed carrier field could slip the coverage table silently. TestFoldInputCarrierCoverageIsComplete (core/chain/floorbox_recompute_carrier_reflection_v5_test.go) now walks the TRANSITIVE struct closure of the state-root fold's witness bundle (StateRootWitness + SeenSetWitness) by reflection — 13 carrier types / 73 fields — and asserts EXACT EQUALITY against the declared coverage: the union of the existing r12CoverageTable and the new foldInputCoverageTable, which classifies all 47 previously-unpinned fold-input fields already-anchored with the specific anchor named (fold OldValue against prevStateRoot / Resolve / payload-derived set). A new carrier type, a new field, or a stale row all go RED. Injected-defect evidence: adding an unclassified field to StateRootDigestWitness, deleting the StateRootTTLWitness.Members row, and renaming StateRootTTLWitness.Members each drive the pin RED while the pre-existing coverage tests stay GREEN — the residual, demonstrated; a coupling-preserving refactor (extracted helper, renamed local) does not false-RED. TestFoldInputCarrierCoverageHasTeeth drives the SAME walk (not a re-implementation) over reflect.StructOf-synthesized violations in all five directions. Scope boundaries are asserted rather than assumed: the sibling predicate-recompute witnesses are out of scope because they are unreachable from the fold's bundle, and the pin reddens if one ever becomes reachable.

Docs

Measure + commit the C-5 G2 repair-RAM number (no logic change): the floor-box repair reconstruction footprint at the production minimum chunk is 1024 MiB resident (16 shards × 64 MiB via in-place ReconstructStripe, DefaultParams{K:10,N:16}) plus ~512 MiB GC-reclaimable churn = 1536 MiB allocation-inclusive peak (-benchmem B/op = 1,610,666,010 B, 237 allocs/op). Consequence on the 2 GB pony reference box (build-immutable #8): ONE repair fits, TWO concurrent production-chunk repairs OOM. Method: core/erasure/reconstruct_mem_test.go:79 (resident) + BenchmarkReconstructStripe_ProdChunk (peak), Apple M4, git HEAD d904d21. Resolves the long-standing "measurement owed" residual in docs/decisions.md C-5 G2 and docs/design/owned-residuals.md G2; gates R2.6 repair-payee and folds into the owed node-store coexistence test.

Fixed

RT-DELIV-3 — delivery-credit provKey now includes the server identity (Boulder 0 residual; economic-mechanism change, B3 conservation — Researcher re-cert pending): the provisional delivery-lane key was {requester, root}, omitting the server. In per-node prod each ledger has a single server (server = n.id), so the lane is uniquely identified and the shape is effectively unchanged. But the shared-ledger SIM routes every operator's serves into ONE Ledger; there, two distinct servers serving the SAME object to the SAME fetcher collided on one lane. The second serve's trackProvisional found the first server's lane and folded its net + skim into it, leaving the lane's stored server as the FIRST server. The terminal reversal (redeem or FIFO eviction) then reversed the COMBINED mint against the WRONG account — a conservation break that reverses or pays the wrong server and drifts Σbalances + Σescrow from the initial grant. Fix (core/credit/delivery.go): add server to provKey (mirroring how provisionalServe already stores the server from the R0.3 A4 fix), so each (server, requester, root) gets its own lane and every reversal debits the exact account credited. New gate core/credit/money_pump_test.go TestA4SharedLedgerServerCollisionConservation drives ≥2 distinct servers into one shared ledger (serve→ChargePublish→redeem) and pins the closed-system invariant Σbalances + Σescrow == initial grant + legitimate self-mints against a total computed independently from the operations (RED at delta=−1024 before, GREEN after). No prod behavior change (one server per ledger keeps the key constant); the fuzz/order tests were updated to the new key shape only.

Fixed

v5 trustless floor box — R1.2 witness-soundness: three gate-coverage gap closures (test-only) (core/chain/floorbox_recompute_adversarialroot_gaps_v5_test.go). Three gaps in the R1.2 gate set are independently closed so R1.4 certification rests on a complete foundation: (1) TestAdversarialRoot_ClassA_ForgedSlashed_StillBonded — the existing ForgedSlashed fixture slashes the culprit and removes it from bonded (chain.go:3288), so a forged Slashed=false with BondedPresent=true is caught by the BondedProof ABSENCE check before the Slashed absent-proof runs; ablating only the Slashed anchor in the original fixture still stalls (no wrong-accept). The new variant injects the culprit into BOTH slashed AND bonded (synthetic state impossible on a real chain but constructible in a test), so BondedProof proves PRESENT — the ONLY remaining defense is the Slashed absent-proof at atts_v5.go:169. Ablating it alone in this variant would produce a wrong-accept. Companion probe TestAdversarialRoot_ClassA_ForgedSlashed_BondedProofCatchesFirst documents the gap in the original fixture. (2) TestMatureEpochImpliesEverMature_InvariantPin — pins the emergent invariant matureEpoch ⇒ everMature (currently enforced only by rotateEpoch's early-return at chain.go:3395-3398). Sweeps real apply paths through genesis, the maturity-latch event, and the first epoch boundary; asserts the invariant at each step. Teeth demonstrated: injecting matureEpoch=true with everMature=false is detected by the invariant predicate — the pin would redden if a future rotateEpoch edit drops the everMature guard. (3) TestAdversarialRoot_ClassB_ForgedPreBondRegHeight — dedicated adversarial-root point gate for the class-B old-bucket source preBondRegHeight (read from ChangedLeaves[i].OldValue for bondRegHeight||id keys, bondreg_v5.go:405-411). Forges the OldValue from 0 (genesis reg-height) to 5, causing the box to emit a DELETE on the wrong old due-bucket (70 instead of 65); the fold produces the wrong root → mismatch → stall. The coverage meta-assertion (TestAdversarialRootCoverageIsComplete) field count is unchanged (10 FIX gates); no product code changed.

Fixed

Docs/comment true-up (no logic change): ROADMAP Rock 1 updated to the built recompute state (recompute reproduces the apply transition set — E/R spine + classes S/B/T/A/P + class-M latch — guarded by the 28/28 write-obligation leaf-diff; the accept-flip is the single remaining step; the R-boundary heavy posture and five committed digest roots are ratified). core/statehash/fold.go delete-sibling comments corrected: the soundness anchor is the caller's final computed-root vs committed-StateRoot equality (ImportSparseMerkleTrie.Root does not re-walk the seed), NOT a "seed-root equality check" — the code performs none. CHANGELOG apply TTL-sweep prose corrected >=> to match chain.go (b.Height-regH > ttl).

Known issues

#286 — quorum-2 objective chain never commits genesis, cross-region (STILL OPEN; RC-gate blocker). The first full 13-node multi-region GCP run and a re-run both FAIL: a fresh 4-validator, quorum-2, 3-region objective chain never commits a genesis block (0 blocks on all four; publishes time out), while a single-zone quorum-1 SMOKE commits in 5 s. The GCP re-run of the size-aware-deadline change above showed it does not fix this — it tuned the validator↔validator RPC path, not the binding one. Real two-layer cause, diagnosed live on GCP: Layer 1 — the HTTP publish path guillotines the gather with three stacked FLAT deadlines (10 s http.Client.Timeout in adapters/httpregistry, 30 s server WriteTimeout, 30 s chainhost.Host.Timeout); the 10 s client fires first → context deadline exceeded → chain never commits. Layer 2 — a deeper WAN-only genesis-gather defect: with ALL deadlines set to 300 s and full attester reachability, the ~1.5 MB first block STILL doesn't gather its 2 attestations even given minutes (attesters show zero receive activity at -log info). Layer 2 does not reproduce in the no-latency sim (TestWedge313_* commit these exact params), so it is genuinely WAN/scale-specific and needs -log debug from boot on a real multi-region run to root-cause. The structural close is a succinct bond proof (#299) — shrink the 1.5 MB genesis block so the whole class dissolves. Fix-in-progress: async publish (remove the Layer-1 guillotines) + gather-path debug logging (enable the Layer-2 root-cause). Credit: blind field test #2 supplemental2 re-validation.

0.1.12026-07-26

Changed

Swarm registry docs & error messages (#17) — the registry is key-pinned HTTPS, and now everything says so. The README swarm recipe and silt daemon -registry help use the <ID>@https://host:port form the daemon prints; passing a bare https:// or an http:// URL to a pinned registry returns a message that names the fix instead of a raw TLS error.

Changed

silt info summarizes by default (#18) — root, mode, size, chunk and stripe counts, erasure params; the full per-shard dump moved behind -shards. It was a wall of hashes on any real-sized file.

Changed

silt add leads with the share link (#19), labelled, and prints the care link after with a "repair only, cannot decrypt" caveat. The bare link stays on stdout so silt add file remains pipeable.

Changed

silt daemon pledges 5G by default (#21), matching silt client, so a fresh daemon contributes measurable, countable storage instead of an unlimited pledge that read as 0 B of network storage. -capacity "" still means unlimited.

Changed

Shorter, easier-to-copy links (#20) — a link now encodes its two 32-byte values in compact base64url (43 chars each) instead of 64-char hex, so a share link is ~30% shorter (137 → 95 chars). Old hex links still parse.

Changed

Observatory (#22) explains it shows only the daemons you list that run -ui (no swarm auto-discovery), that "daemons observed" is not the peer count, and now displays the swarm's self-estimate ("~N peers") right beside it so the two numbers reconcile.

Added

Build your own Silt test network — a public, end-to-end local walkthrough (sims → a real multi-node swarm that survives a node death), with all of the above fixes baked in.

0.1.02026-07-25

Added

Content-addressed storage — every fragment is named by the SHA-256 of its bytes; verification is intrinsic, so hosts are never trusted.

Added

Erasure coding — Reed-Solomon stripes (default any 10 of 16 rebuild the file); a repair loop restores redundancy as machines fail, and — like the initial placement — keeps each stripe's shards spread across distinct hosts as it rebuilds, so one machine's death never costs a stripe more than a single shard.

Added

Encryption at every level — chunks and manifests are both ciphertext; a file's share handle is a link (silt:v1:root:key) whose one-way key hierarchy also yields care links that grant repair and audit without the ability to decrypt.

Added

The swarm — Kademlia routing, provider records, and multi-node fetch over a deterministic simulator or real mutual-TLS sockets; identity is a keypair and a node's ID is the hash of its public key.

Added

Column placement — an erasure-coded file is placed by column (one shard position across every stripe), keyed by hash(root‖col), so a whole column lands together: one host holds one shard of each stripe, a reader finds a column in a single lookup, and losing a host costs a stripe exactly one shard (up to n−k columns can go and the file still rebuilds). Placement, retrieval, repair, and audits all speak columns.

Added

Failure-domain-aware placement — a node can declare a failure-domain label (AS / rack / geo / operator) and gossips it; placement and repair spread a file's columns across distinct domains, so an entire domain going dark costs a stripe as little as possible — not just distinct node IDs, but distinct domains.

Added

Dispersion audit — a caretaker doesn't just keep a stripe alive, it keeps it spread: each sweep it confirms which domains actually hold each column, and if any one domain holds enough of a stripe that losing it would drop below the recovery threshold, it seeds extra copies into other domains until no single domain failure could break the file.

Added

Demand-responsive dispersion — storage flexes with popularity. A node that finds itself serving a chunk hard pushes leased cache copies to more hosts (spread across domains) so readers divide across more sources; when the reads cool off, the copies expire and the file contracts back to its baseline. A flash-popular file fans out without permanently hoarding capacity.

Added

Capacity — nodes pledge a fixed budget (-capacity 2G); placement spills over as nodes fill, and every node estimates the whole network's size from local gossip alone.

Added

Proof-of-retrieval audits — hosts are challenged to prove possession with a fresh nonce; those that keep the proof but drop the data are slashed.

Added

The registry chain — an append-only chain kept by the operators; blocks commit only with a quorum of attestations from validators whose reputation (audits + serving) is earned, not bought.

Added

Genesis — every fresh network is born carrying a founding manifesto in block 0, declared identically on every node.

Added

Takedown by revocation — illegal or unwanted content is removed at the availability layer, not the ledger: an append-only revocation record, committed by the same reputation quorum, makes compliant nodes no-op on a denied opaque root (refusing to store, serve, prove, announce, or repair it) and purge what they hold — never decrypting anything. Operators may also load a local denylist they choose to honor (silt daemon -denylist). The project ships the mechanism and no list; it operates neither the network nor the policy.

Added

Web UI — an embedded dashboard, publish/fetch pages, and a network observatory, served by the daemon.

Added

Desktop client — one binary that consumes and serves at once, keeps a link-book library, and runs on macOS, Windows, and Linux.

Added

Public website (silthq.com) with brand, docs, operator guide, and build-from-source instructions.

Added

Continuous delivery — PR previews, a staging environment, and production deploys from main; a public changelog rendered from this file.

Added

Governance & strategy docs — the fresh-eyes council, risk register, launch plan, safety/takedown model, and GOVERNANCE.md.

← Back to silthq.com