You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
harness(rg3): RG4-K7 — kill-during-rollback-apply covered against the mock (INV-10)
K7 (SIGKILL mid rollback-apply) was previously deferred because its
never-committed scenario forked BELOW the finalized anchor — an intrinsically
invalid reorg the product must (and does) fatal on as an unknown-parent gap.
That was a scenario-design bug, not a mock or product limit: the product has a
real accepted rollback-apply path (portal-realtime.ts:1105-1117, exercised today
by K5-409) and mock-portal.mjs already carries the handleRollbackApply handler.
This authors a correct scenario whose fork point sits strictly ABOVE the anchor,
so the product enters reconcile -> {kind:'reorg'}, rolls back the losing suffix,
and re-applies the winning branch; a SIGKILL mid-apply resumes byte-identical.
No product code changes — harness only (mock + scenario + orchestrate + prose).
- scenarios/k7-rollback.json: window 101..106 on 'main' (finalized anchor <=102),
a childDiscovery+awaitRedelivery handshake at 101 (deterministic non-vacuous
vault+deposit rows, mirroring k5-canonical — no factory-redelivery race), then
a rollbackApply forking at block 104 (parent 103:main, in-window above the
anchor) extended to block 112 so the app completes. killAt block 105 lands the
SIGKILL mid re-apply, after the rollback event and the first apply. The baseline
digest oracle uses the same file via baseline_scenario_for_class fall-through
(the killAt gate is released, not a path divergence — no separate canonical
file needed, unlike K5's 409 negotiation).
- mock-portal.mjs handleRollbackApply: cursor discipline (204 a mis-cursored
request, parity with handleStatus409) and a reorgApplied counter incremented
where the cross-branch fork block is actually served, exposed in handler stats
so a scenario that silently degraded to a plain append cannot self-report a
vacuous pass.
- orchestrate.sh: strengthen k7RollbackNonVacuousOk to mirror k6CutoverOrganicOk
— assert killsWithLiveStream===killCount, the empirical kill-block set is
exactly {105}, and reorgApplied>0 per run. Candor tightened to defer ONLY
live-protocol fork-choice fidelity to RG4/RG5; the crash-timing half is now
mock-covered.
- VALIDATION.md 5.11 / class table: K7 reclassified from deferred to
mock-covered (crash-timing), mapped to existing INV-10 (no new invariant);
live-protocol fork fidelity remains RG4/RG5.
- mock-portal.test.mjs: cursorMatchesStep routes a rollbackApply match only on
the natural post-window resume cursor.
Empirical proof (CHAOS_CLASSES=K7 CHAOS_KILLS_PER_CLASS=1, graft built fresh from
this tree): scenario picked up (not the deferral branch); baseline reorged at
block 103 and re-applied the rollback branch cleanly (no "Cannot reconcile
safely"); kill landed at block 105 mid-apply; resume byte-identical
(store+app digest identical, vault=1 deposit=1 non-vacuous, zero duplicate
finalized rows); k7RollbackNonVacuousOk=true with killBlocks=[105],
killsWithLiveStream=1, reorgApplied=1. mock-portal.test.mjs 10/10; biome clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: VALIDATION.md
+27-7Lines changed: 27 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1722,8 +1722,14 @@ cutover-handler entry (a 409 variant + a wrong-fork variant) · K6 at the histor
1722
1722
Per-class mock fidelity is recorded candidly in the harness and reproduced here — HIGH for K1/K2/K4
1723
1723
(direct 200/reorg/defer flows), MEDIUM for K3/K5 (redelivery / fork-handler reachability), and
1724
1724
**MEDIUM for K6** after Phase B (an organic mock cutover at the client's natural chunk-boundary
1725
-
request). K7 is **not included** in RG3 Phase B: the pre-flight hit the product unknown-parent fatal
1726
-
before a kill, so kill-during-rollback remains RG4/RG5.
1725
+
request). K7 (kill-during-rollback-apply) is added against the mock: the pre-flight fatal was a
1726
+
scenario-design bug (a fork point *below* the finalized anchor, which correctly fatals as an unknown-
1727
+
parent/deep-reorg gap), not a mock or product limit. With the fork point corrected to sit strictly
1728
+
*above* the anchor, the same product code path (`reconcile → {kind:'reorg'}` then rollback-apply,
1729
+
`portal-realtime.ts:1105`) that K5-409 already exercises drives an accepted rollback, and the SIGKILL
1730
+
lands mid-apply — so the **crash-timing** half of K7 is now mock-covered. What remains RG4/RG5 is only
1731
+
the **live-protocol fork-choice fidelity** (a real Portal 409/reorg with competing finalized sources),
1732
+
not kill-during-rollback as a class.
1727
1733
1728
1734
**Result (Phase A, from the operator's evidence archive — verbatim).**
1729
1735
@@ -1749,7 +1755,7 @@ All three RG3 numeric thresholds are met: **238 ≥ 200** kills across **7 ≥ 6
1749
1755
| Duplicate FINALIZED rows |**0**|
1750
1756
| K6 organic cutover |**10 / 10** (every kill on block 106, the chunk boundary) |
1751
1757
| K5 wrong-fork consumed in-window |**deferred** — default clean run consumed **0 / 1**; a stretched post-injection window makes the product correctly raise the wrong-fork finalize fatal |
1752
-
| K7 kill-during-rollback |**deferred** — pre-flight baseline fataled on unknown-parent/deep-reorg before any kill |
1758
+
| K7 kill-during-rollback-apply|**mock-covered (crash-timing)** — fork point corrected to sit ABOVE the finalized anchor (≤102), so `reconcile → {kind:'reorg'}` rolls back 104..106:main and re-applies the rollback branch; the SIGKILL lands mid-apply at block 105 and resumes byte-identical. `k7RollbackNonVacuousOk` self-certs kill-block=={105}, live /stream before every kill, and the mock served the cross-branch fork (`reorgApplied>0`). **Live-protocol fork fidelity remains RG4/RG5.**|
1753
1759
1754
1760
The full K1–K6 moderate rerun was attempted with
1755
1761
`CHAOS_KILLS_PER_CLASS=10 CHAOS_MIN_PER_CLASS=10 CHAOS_TOTAL_KILLS=70`, but it stopped before Phase-B
@@ -1793,10 +1799,24 @@ hidden:
1793
1799
`fromBlock=106` with the canonical parent hash). The cutover is **organic within the mock's
1794
1800
deterministic capability** — it is still not a live Portal cutover with competing finalized sources;
1795
1801
that remains RG4/RG5.
1796
-
-**K7 kill-during-rollback is deferred.** The pre-flight scenario attempted to stream block 104 on a
1797
-
rollback branch with parent block 103 while the finalized anchor was 105. The client treated that as
1798
-
the existing unknown-parent/deep-reorg fatal (`Cannot reconcile safely`) before any kill could be
1799
-
taken. The invariant was not weakened; K7 remains RG4/RG5.
1802
+
-**K7 kill-during-rollback-apply — crash-timing is mock-covered; live fork fidelity remains RG4/RG5.**
1803
+
The pre-flight scenario that fataled attempted to stream block 104 on a rollback branch with parent
1804
+
block 103 while the finalized anchor was 105 — a fork point *below* finality, which correctly fatals
1805
+
(`Cannot reconcile safely`; a fork below the finalized floor has no safe rollback). That was a
1806
+
**scenario-design bug**, not a mock or product limit: the mock already carries the
1807
+
`handleRollbackApply` handler and the product carries a real accepted rollback-apply path
1808
+
(`portal-realtime.ts:1105-1117`), exercised today by K5-409. K7 now moves the fork point strictly
1809
+
*above* the anchor (window 101..106 on `main`, anchor ≤102, reorg forking at block 104), so
1810
+
`reconcile` returns `{kind:'reorg'}` (not `gap`), the losing 104..106:main suffix is rolled back and
1811
+
the `rollback` branch re-applied. The SIGKILL fires at block 105 — after the rollback event and the
1812
+
first apply, mid re-apply — and the resume is byte-identical. `k7RollbackNonVacuousOk` self-certifies
1813
+
non-vacuity exactly as `k6CutoverOrganicOk` does: the empirical kill-block set is `{105}`, every kill
1814
+
followed ≥1 live `/stream` request, and the mock-side `reorgApplied` counter (>0 per run) proves the
1815
+
cross-branch fork was actually served rather than degrading to a plain append. This is crash-timing
1816
+
coverage of **INV-10**'s rollback arm — no new invariant. What genuinely **remains RG4/RG5** is only
1817
+
the *live-protocol fork-choice fidelity*: a real Portal 409/reorg with competing finalized sources
1818
+
and real parentHash chains. The mock proves the rollback-apply *code path* is crash-timing safe; it
1819
+
does not prove the Portal emits such branches — that is RG4/RG5.
1800
1820
-**The harness `invariant()` fast-path is a fatal-string tripwire, not the primary death detector.**
1801
1821
It grep-matches all eight real fatal throws in `portal/portal-realtime.ts` (the gap fatal, the
1802
1822
below-floor 409 fatal, the 409 no-progress cap, the deterministic-4xx fatal, the delivery-progress
Copy file name to clipboardExpand all lines: harness/chaos/realtime/orchestrate.sh
+23-3Lines changed: 23 additions & 3 deletions
Original file line number
Diff line number
Diff line change
@@ -511,7 +511,7 @@ const candor = {
511
511
K4: 'idle / 204 retry path',
512
512
K5: "synthetic fork via stale-parentBlockHash 409 (client consumes the canonical replacement chain and resumes byte-identically) PLUS a wrong-fork-finalize handler-REACHABILITY probe. handlerStats.wrongForkFinalizes counts mock-side handler entries (reachability) ONLY — the counter increments on handler entry, before the gate, so on a killed run the splice may not even execute; the injected wrong finalized head is NOT consumed by the client here (finalize poll cadence ~4s > the post-injection scenario window ~1.8s), so this is not clean-resume wrong-fork evidence. The client-side wrong-fork-finalize FATAL guard (hash-mismatch at finality) is proven deterministically by portal-realtime.test.ts ('a finalize whose canonical hash mismatches the local block is FATAL', finalizePollMs:0). Real L1-reorg wire fidelity NOT exercised.",
513
513
K6: "kill lands at the backfill→live chunk boundary (cutoverGate fires on the client's natural fromBlock=106 request, not a fixed block), so resume re-drives an organic cutover within the mock's deterministic capability. Probe-retry wire fidelity (the 3-attempt loop in clampFinalizedToPortalHead) NOT exercised — mock's /finalized-head succeeds first try; probe-retry fidelity rests on portal-realtime-wire.test.ts.",
514
-
K7: 'K7: kill lands mid rollback-apply (client sees parent mismatch at block 104 and must roll back to 103 before applying the rollback branch). Mock-driven: the rollback is scripted, not a live L1 reorg. Live reorg-during-rollback fidelity is RG4/RG5.',
514
+
K7: "kill lands mid rollback-apply: after 101..106 on main, the client resumes at fromBlock=107/parent=106:main, the mock serves a reorg branch forking at block 104 (104:rollback carries parent 103:main, in-window above the <=102 finalized anchor), so reconcile -> {kind:'reorg'} rolls back 104..106:main and re-applies the rollback branch 104..112 (extended to PONDER_END so the app completes); the SIGKILL fires at block 105, AFTER the rollback event and the first apply, mid re-apply. The fork point is strictly ABOVE the finalized anchor (<=102) so this is an ACCEPTED reorg, never the below-floor gap fatal. reorgApplied counts the mock actually serving the cross-branch fork block, so a scenario that degraded to a plain append cannot self-report a K7 pass. Mock-driven crash-timing coverage of INV-10's rollback arm (same product code path from portal-realtime.ts:1105 as K5-409's accepted reorg). What is NOT proven here: live-protocol fork-choice fidelity — a real Portal 409/reorg with competing finalized sources and real parentHash chains — remains RG4/RG5.",
0 commit comments