Skip to content

Commit afc5c4a

Browse files
dzhelezovclaude
andauthored
docs(validation): §5.12 — realtime /stream liveness hardening evidence (RT-1/RT-2/RT-3, INV-22…25) (#164)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dcb718a commit afc5c4a

1 file changed

Lines changed: 107 additions & 2 deletions

File tree

VALIDATION.md

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ following are **pending** and must not be read as proven:
4040
- A **long-duration soak** sign-off. The A/B soak differ runs hourly and its findings are already
4141
public issues (below), but no multi-day green-soak claim is made in this document yet.
4242
- **Zero-RPC realtime** without an experimental label. The Portal `/stream` realtime path is under
43-
active hardening (see the stream-realtime issues and PR #26); this document does not certify it for
44-
unattended production use.
43+
active hardening (see the stream-realtime issues and PR #26); the `/stream` **liveness hardening**
44+
stack (RT-1/RT-2/RT-3, INV-22…25, §5.12) has landed as **code + unit-test** evidence, but that raises
45+
the reliability floor — it does **not** certify the path for unattended production use, which remains
46+
explicitly not claimed.
4547

4648
---
4749

@@ -1782,6 +1784,109 @@ campaign plan) — no more, and stated as no more.
17821784

17831785
---
17841786

1787+
### 5.12 Realtime /stream liveness hardening (RT-1 / RT-2 / RT-3 — INV-22…25)
1788+
1789+
This subsection records the `/stream` **liveness + defense hardening** stack that landed as RT-1
1790+
([#161](../../pull/161)), RT-2 ([#163](../../pull/163)), and RT-3 ([#162](../../pull/162)), adding
1791+
invariants **INV-22, INV-23, INV-24, INV-25** to [`portal/INVARIANTS.md`](portal/INVARIANTS.md). As
1792+
with everything in this document, it states plainly what it does and does **not** prove. The evidence
1793+
**class is the same as §5.8(B)**: *code + mutation-verified **unit** tests* (`portal-realtime.test.ts`,
1794+
`portal-realtime-wire.test.ts`, `portal-cutover-guard.test.ts`) — construction properties pinned by
1795+
tests that go RED on the pre-fix code, **not** paid-matrix byte-identity (§3) and **not** a
1796+
live-protocol reorg/finalize claim (that is RG4/RG5, §5.11). These raise the *reliability floor* of the
1797+
stream path; they do not change its **experimental** label (§1, §6).
1798+
1799+
**INV-22 — Realtime heartbeat finality cadence (RT-1 SC2).** The finalize poll and the B1 defer
1800+
watchdog run on a wall-clock cadence (≤2× `finalizePollMs`) **regardless of delivery**: `streamHotBlocks`
1801+
emits zero-payload `{kind:'tick'}` heartbeats before its no-data sleeps and `runFinalizeCadence()` runs
1802+
on every tick, so a no-delivery outage — 204s, bodyless 200s, reconnect loops, transient fetch errors,
1803+
or 429/5xx load — **cannot freeze finality** until another block arrives. Pinned by
1804+
`portal-realtime.test.ts`*"portalRealtimeEvents: heartbeat ticks keep finalize polling on a 204-only
1805+
wall-clock cadence (RT-1 SC2 T2)"* (asserts repeated `finalizedHead` calls under a zero-delivery
1806+
204-only stream), mutation-verified. The cadence itself is a construction property, so there is no
1807+
runtime assert (timing-flaky) — the test is the pin.
1808+
1809+
**INV-23 — Idle-bounded read + tick-transparent line-wait (RT-1 SC1).** Completes INV-22 for the one
1810+
no-delivery state loop-turn ticks cannot reach: a **silent-OPEN** connection (headers OK, body never
1811+
delivers, no FIN/RST). While the NDJSON read is suspended, `streamHotBlocks` yields a `{kind:'tick'}`
1812+
every `tickSleepMs` of silence **without cutting the connection or dropping/reordering a line** (a single
1813+
pending `it.next()` is held across the heartbeat race and never re-requested until it settles), and after
1814+
`idleMs` of *cumulative* silence recycles the connection (reconnect from `cursor`, **not** fatal). A
1815+
slow-but-alive stream is **never cut** — the idle guard re-arms on every received chunk. Pinned across
1816+
three `portal-realtime.test.ts` cases (silent-open tick clock, idle-bound reconnect-from-cursor,
1817+
never-cut) plus `portal-realtime-wire.test.ts` env-precedence for `resolveStreamIdleMs`
1818+
(`PORTAL_STREAM_IDLE_MS`, default 120_000). Mutation-verified with two **independent** tells — removing
1819+
the tick-transparent wait fails the silent-open test (`SILENT-OPEN-STARVED`); removing the idle bound
1820+
fails the reconnect test (`IDLE-RECONNECT-STARVED`) — each failing on its own and on the pre-SC1 code.
1821+
1822+
**INV-24 — Delivery-progress watchdog (RT-1 SC3).** When the probed finalized head has climbed
1823+
`deliveryProgressThreshold` (default 16) blocks **beyond the highest DELIVERED block** while **zero**
1824+
blocks were delivered for ≥ `deliveryProgressMaxMs` (default 600_000), `portalRealtimeEvents` fails
1825+
**LOUD** (throws with a diagnostic dump, never an RPC fallback) — turning an otherwise-silent
1826+
"finalize-poll succeeds, nothing reaches the store" stall into a restartable fatal. The baseline is the
1827+
delivered **HEIGHT**`max(first-observed finalized head, highest delivered block)`**not** the
1828+
finalized head at last delivery (the RT-G10 fix): because the `/stream` delivers unfinalized tip blocks
1829+
that run far ahead of finality, a **quiet chain** or a **tip-freeze-while-finality-catches-up**
1830+
(finalized ≤ delivered) **never** trips it; only genuinely new blocks the stream is failing to deliver
1831+
do. Pinned by six numbered `portal-realtime.test.ts` cases (T1 genuine stall fatal · T2/T3
1832+
progress-conditioning non-trip · T4 finality-catching-up must-NOT-trip · T5 climb-past-delivered
1833+
still-fatal · T6 the deliver-before-first-poll first-probe `max` fold) plus the E1 fetch-error
1834+
non-delivery seam (`onFetchError` yields a tick and stamps no delivery). Mutation-verified **load-bearing
1835+
in both directions**:
1836+
neutering the finalized-head term (`0 ≥ threshold` never true) starves the genuine-stall tests
1837+
(`PROGRESS-WATCHDOG-STARVED`) while the false-fatal repro stays green, and the symmetric sign-flip
1838+
re-introduces the false-fatal — plus a third mutation dropping the first-probe fold re-introduces the
1839+
deliver-before-first-poll false-fatal (T6).
1840+
1841+
**INV-25 — Isolated-cutover finality floor (RT-2).** The isolated single-chain historical→realtime
1842+
cutover clamp (`getHistoricalEventsIsolated`) now passes `floor = params.syncProgress.finalized.number`
1843+
to `clampFinalizedToPortalHead`, mirroring the multichain/omnichain cutover sites. **CANDID: this floor
1844+
is INERT defense-in-depth / documentation-parity — it does NOT fix a live bug.** The load-bearing
1845+
protection against adopting a below-boundary finalized block is the **upstream break-before-adopt guard**
1846+
right below the clamp (a stale-LOW probe yields a negative delta ≤ `finalityBlockCount`, so the loop
1847+
**breaks before** the boundary assignment); **RG0 proved the isolated path was already safe without the
1848+
floor** (plan §RG0). Because the floor equals the boundary the guard compares
1849+
against, on the stale-LOW path it **masks a guard-weakening mutation**, which is exactly why the change is
1850+
pinned by grafted-**source** pins rather than a (vacuous) behavioral test through the isolated path:
1851+
`portal-cutover-guard.test.ts`*"INV-25 / RT-2: isolated-cutover finality floor"* slices the isolated
1852+
body and pins **both protections INDEPENDENTLY****Pin A** the shipped `floor:` line, **Pin B** the
1853+
load-bearing break-before-adopt guard. Each is mutation-verified alone: dropping the `floor:` line fails
1854+
Pin A while Pin B stays green (removing the floor has **no** behavioral signal — its inertness made
1855+
concrete); weakening the guard predicate (`<=``>=`) fails Pin B while Pin A stays green. RT-2 lands it
1856+
for cross-site parity and as belt-and-braces should the adopt-guard ever be refactored — **not** as a fix.
1857+
1858+
**RT-3 — RING_CAP 2048→8192 + eviction-fatal proof (RT-G2).** Robustness hardening of the 409
1859+
delivered-hash-ring fork-negotiation that INV-10 / §5.8(B) already cover (issue [#33](../../issues/33)):
1860+
RT-3 raises the module-const `RING_CAP` from 2048 to **8192** and adds the eviction-path proof — it is
1861+
**not** a new invariant. Rationale (from the diff): with the production B1 bound (`finalizeDeferMaxMs`
1862+
600_000 ms), 600000 / 8192 ≈ 73 ms, so any chain with block time ≳75 ms cannot grow the unfinalized
1863+
window past 8192 blocks above the deferred anchor before B1 fires — pushing the F5 availability edge (a
1864+
deep 409 step-down landing on a *size-evicted* ring height) out of reach; the ~8192-entry hash map is
1865+
about a megabyte, negligible. It also corrects the F5 CANDOR note: at a >RING_CAP window the **eviction**
1866+
fatal (*"no delivered-hash ring entry for the resume parent"*) fires FIRST — `pruneRing` evicts the
1867+
LOWEST heights, which sit ABOVE the finalized floor, so a no-match descent crosses the evicted band
1868+
before it can reach the floor; the floor fatal fires only when the whole window fits under RING_CAP
1869+
(nothing evicted). Two mutation-verified `portal-realtime.test.ts` cases reuse the scripted `/stream` 409
1870+
step-down harness: a *~4096-block window FITS under 8192* (no eviction → the descent reaches the FLOOR
1871+
fatal, asserting NOT the eviction message; reverting RING_CAP→2048 evicts the low band and flips it to the
1872+
eviction shape → both asserts fail), and a *>8192 (8200) window* evicts the low heights so the descent
1873+
hits the LOUD eviction fatal (neutering the loop-top missing-ring throw trips a `NEVER-EVICTED` sentinel
1874+
— a loud, fast failure instead of a 15 s Vitest timeout). Either way it is a **loud restart, never wrong
1875+
data** — equivalent to today's restart behavior.
1876+
1877+
**What this does NOT prove.** These are **liveness + defense** invariants proven **by construction** and
1878+
pinned by **mutation-verified unit tests** — the same class as §5.8(B), on the same axis as §5.2's
1879+
stream-realtime correctness wave. They RAISE the reliability floor of the `/stream` path (a no-delivery
1880+
outage cannot silently freeze finality; a silent-open connection is kept alive and recycled; a genuine
1881+
delivery stall fails loud; the 409 ring's eviction edge is pushed out of the practical envelope and made
1882+
a loud restart). They do **NOT** lift the path's **experimental** label; they do **NOT** establish
1883+
live-protocol reorg survival or finalize-boundary reconciliation (RG4/RG5, §5.11); the paid-matrix
1884+
byte-identity claims (§3) are historical-backfill, not realtime. And **INV-25's floor is explicitly
1885+
inert** — the load-bearing safety on that path is the pre-existing break-before-adopt guard (RG0), not
1886+
this document's newest bullet. The stream path remains experimental (§1, §6).
1887+
1888+
---
1889+
17851890
## 6. Current status — what a reader can rely on today
17861891

17871892
**Proven today (with reproducible evidence in this repo):**

0 commit comments

Comments
 (0)