Skip to content

Commit 310b21f

Browse files
dzhelezovclaude
andauthored
docs(validation): §4.5 adversarial network-fault campaign on the shipped 0.16.8-sqd.1 build (#150)
Records the fault-proxy campaign (merged #149, `5a2fe88`) re-run on the published subsquid-ponder@0.16.8-sqd.1 package as a new Layer-C §4.5: 8/8 body/transport faults (429/5xx/retry-after/reset/stall/gzip/ndjson/204) + head-freeze complete byte-identical to a clean baseline (digest 360af51…, intervals tile exactly), every fault non-vacuously fired; head-regression-100k / head-flap correctly recorded not-applicable (stream-mode property, not a pass). Honest AIMD labels (sparse vs recovery observed) per the #149 fix. Lands §2 shipped- build anchor #2 (planned → done) and adds a §4-intro forward reference. Numbers independently re-verified from on-disk verdicts; overclaim-reviewed. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5a2fe88 commit 310b21f

1 file changed

Lines changed: 97 additions & 4 deletions

File tree

VALIDATION.md

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,18 @@ identity**: the Portal wiring patch is byte-identical (sha256-equal) across the
9393
`0.16.8` grafts, and each upstream delta over that range lands off the Portal graft surface (Layer A
9494
above). A version bump therefore does **not** invalidate the matrix or the soak, and we deliberately do
9595
**not** re-run the paid matrix or restart the soak for it. Alongside that transfer, three **direct
96-
anchors on the shipped `0.16.8-sqd.1` package**one landed, two planned — tie the evidence to the
96+
anchors on the shipped `0.16.8-sqd.1` package**two landed, one planned — tie the evidence to the
9797
exact published build:
9898
1. the examples end-to-end freshness gate ran against the **published** package
9999
([#139](../../pull/139)) — the `euler-subgraph` example reproduced its exact baseline row counts
100100
(vaults / deposits / withdraws / borrows / repays), a no-regression check on the shipped tag — **done**;
101-
2. a fault-injection chaos campaign on a `0.16.8-sqd.1` build (Layer C, §4) — **planned**;
101+
2. an adversarial network/transport fault-injection campaign on the **published** `0.16.8-sqd.1`
102+
package (Layer C, §4.5) — the backfill completed **byte-identical** to a clean baseline under all 8
103+
body/transport faults plus a frozen finalized head — **done**;
102104
3. a ≥72 h A/B soak confirmation leg on the `0.16.8-sqd.1` build after the current soak completes its
103105
milestone — **planned**.
104-
The two pending anchors are tracked and not yet claimed as evidence; until they land, the shipped-build
105-
assurance rests on seam identity plus the published-package examples gate.
106+
The one pending anchor is tracked and not yet claimed as evidence; until it lands, the shipped-build
107+
assurance rests on seam identity, the published-package examples gate, and the §4.5 fault campaign.
106108

107109
### Layer B — Mutation-verified regression tests
108110

@@ -862,6 +864,11 @@ property:
862864
0 scopes out**: it records kills landing at genuine partial durable coverage and completions that
863865
resumed from partial persisted state.
864866

867+
Both tiers fault the **process** (`SIGKILL`). A separate, complementary campaign (§4.5, ACCEPTED
868+
2026-07-11) faults the **network** instead — injecting rate-limit/5xx/reset/stall/corruption faults on
869+
the wire to the Portal while the app keeps running — and proves the backfill still completes
870+
byte-identical. The two axes are complementary: neither subsumes the other.
871+
865872
### 4.1 Tier 0 — PGlite byte-identity (ACCEPTED 2026-07-04)
866873

867874
The chaos kill-loop was accepted against the campaign's acceptance criteria. Aggregate result:
@@ -1114,6 +1121,92 @@ To stress the dense-source / factory-child discovery path (many concurrent `data
11141121

11151122
- **This section does NOT assert, and deliberately does not claim, that a fresh dense process's first discovery scan is warmup-bounded.** On attempt 1 of a fresh process, the first `portal-dense-discovery-scan` covers the ENTIRE range in ~8 windows (a grid-aligned `[20528000, 20579207]`, ~51.2k blocks, starting just below the configured floor `20529207` — do not reconcile it against the 50000-block data range) because the concurrent `dataChunk` fan-out supplies a full-range `needTo` when Ponder feeds a large first interval. This is expected (a consequence of `planDiscovery`'s `to = max(needTo, min(endHint, through+quantum))` tracking a large interval, not the watermark) — it is NOT a #50 regression and NOT a fork bug, because a wide discovery scan fetches no data, does not delay first commit, and only front-loads factory-child discovery I/O.
11161123

1124+
### 4.5 Adversarial network/transport-fault campaign on the shipped `0.16.8-sqd.1` build (ACCEPTED 2026-07-11)
1125+
1126+
The §4.1–§4.4 campaigns kill the **app process** and prove crash/resume identity. This campaign leaves
1127+
the app running and instead injects **network and transport faults** on the wire between the app and
1128+
the **public** Portal, via an in-path fault proxy (`harness/chaos/proxy.mjs`, unit-tested in
1129+
`proxy.test.mjs`), driven by `harness/chaos/fault-proxy-campaign.sh`. It proves a complementary
1130+
property: under an adversarial network, the Portal backfill still **completes byte-identical to a clean
1131+
baseline** — a fault surfaces as a retry, never as silent data corruption or coverage loss. It ran on
1132+
the **shipped `0.16.8-sqd.1` package** (tarball `subsquid-ponder-0.16.8-sqd.1.tgz`, sha256
1133+
`b5a0daa6f8439a67afd36d715451f040a9addc1e7d55f7043400092ac3869b70`), the fault-proxy code merged in
1134+
[#149](../../pull/149) (`5a2fe88`), so it doubles as the §2 shipped-build anchor #2. Backend is
1135+
`postgres16` (`fsync=on`, recorded `postgres16-fsync-on`), app `euler-app` (factory
1136+
`0x29a56a1b8214D9Cf7c5561811750D5cBDb45CC8e`), chain 1 (ethereum) range `[20529207, 20579207]` (span
1137+
50000), Portal `portal.sqd.dev/datasets/ethereum-mainnet`. The clean baseline logical digest is
1138+
`360af5126a0efffc49b871594b8ac3ea` (baseline wall 300 s). Wall clock: `2026-07-11T00:50:02Z`
1139+
`2026-07-11T01:10:19Z` (~20 min). Artifacts are in `harness/chaos/.faultproxy/artifacts/`
1140+
(`aggregate.json``status: pass`).
1141+
1142+
Each scenario runs a fresh backfill through the fault-injecting proxy into its **own** throwaway
1143+
Postgres DB, then `harness/chaos/verify-resume-pg.sh` recomputes the logical digest (`pg-digest.mjs`)
1144+
**and** the intervals-tile-exactly check against the clean baseline; the pass gate is fail-closed
1145+
(§4.2). A scenario is a genuine PASS only if the injected fault **actually fired** (proxy counter ≥ 1),
1146+
the resulting store digest **equals** the baseline, and the intervals tile exactly.
1147+
1148+
**Body/transport-fault scenarios — 8/8 byte-identical, every fault non-vacuously fired:**
1149+
1150+
| scenario | fault fired (proxy counter) | digest == baseline | AIMD label |
1151+
|---|---|---|---|
1152+
| 429-burst | `r429`×2 | yes | sparse — insufficient samples to assert recovery |
1153+
| 5xx-storm | `r5xx`×6 | yes | recovery observed |
1154+
| retry-after | `r429`×5 | yes | sparse |
1155+
| tcp-reset-mid-ndjson | `reset`×5 | yes | sparse |
1156+
| 90s-stall | `stall`×3 | yes | recovery observed |
1157+
| truncated-gzip | `gzip`×5 | yes | sparse |
1158+
| malformed-ndjson | `ndjson`×2 | yes | recovery observed |
1159+
| spurious-204 | `r204`×4 | yes | sparse |
1160+
1161+
All eight report `missedReset = 0` and `missedNdjson = 0` (no fault the harness expected to inject went
1162+
undelivered), `verifyExit = 0`, and `digestEqual = true`.
1163+
1164+
**head-freeze — the one in-range head scenario (PASS, byte-identical recovery):** the finalized head is
1165+
frozen **below** `endBlock` 20579207. The frozen head stalls the Portal **without corrupting the
1166+
durable store**; a clean restart on the same DB with the fault removed completes to a store
1167+
byte-identical to the baseline (`byte-identical-recovery`, digest `360af51…`).
1168+
1169+
**head-regression-100k and head-flap — NOT-APPLICABLE (not counted as a pass):** both manipulate the
1170+
head so it stays **`endBlock`**, so it never intersects the fixed range `[20529207, 20579207]`. A
1171+
fixed-range historical backfill never rolls back an already-finalized+indexed block, so it cannot
1172+
exercise head-reorg/rollback — a **stream-mode** property, exercised by the stream-mode A/B soak (Layer
1173+
D / §5.2), not this historical harness. Recorded `pass:false, notApplicable:true, digest:"n/a"`.
1174+
1175+
**Independent re-verification by the orchestrator (load-bearing):** every one of the 9 in-scope
1176+
scenarios' `runs/<scenario>/digest` equals the baseline `360af5126a0efffc49b871594b8ac3ea`; every
1177+
in-scope `verdict.json` carries `verifyExit:0` and `digestEqual:true`; every `proxy-stats.json` shows
1178+
its expected fault counter ≥ 1 with `missedReset:0`/`missedNdjson:0`; the baseline `verify.log` shows
1179+
all seven interval fragments (one factory + six log) tiling `[20529207, 20579208]` exactly with a
1180+
"digest identical" line. The two not-applicable scenarios carry `pass:false, notApplicable:true,
1181+
digest:"n/a"` — so the "8 pass + 1 recovery + 2 n/a" tally is exactly what the on-disk verdicts say,
1182+
not a summarized claim.
1183+
1184+
**What this proves:**
1185+
1186+
1. **Adversarial-network byte-identity (PROVEN, 8/8 + head-freeze).** Under rate-limit bursts, 5xx
1187+
storms, `Retry-After`, mid-NDJSON TCP resets, multi-second stalls, truncated gzip, malformed NDJSON
1188+
lines, spurious 204s, and a frozen finalized head, the resumed/completed store is byte-identical to
1189+
an unperturbed baseline. The fault plane cannot inject a silent divergence.
1190+
2. **Honest AIMD labeling (the #149 fix).** Where enough gate transitions were sampled (5xx-storm,
1191+
90s-stall, malformed-ndjson) the trace shows multiplicative-decrease→additive-increase recovery
1192+
(`recovered:true`); where too few were sampled the label says exactly that (`sparse — insufficient
1193+
samples to assert recovery`) instead of overclaiming.
1194+
1195+
**Candor.**
1196+
1197+
- **The sparse-AIMD scenarios do NOT assert a full recovery cycle.** For 429-burst, retry-after,
1198+
tcp-reset-mid-ndjson, truncated-gzip, and spurious-204 the concurrency gate stayed within bounds
1199+
(`within:true`) but too few transitions were sampled in the short window to assert a
1200+
decrease→increase recovery, and the verdict labels each `sparse` and `recovered:false`. Byte-identity
1201+
is fully proven for these; only the *recovery* claim is withheld.
1202+
- **head-regression-100k / head-flap are NOT head-tolerance passes.** They are structurally
1203+
inapplicable to a fixed-range historical backfill (head stays above `endBlock`). Head-reorg/rollback
1204+
tolerance is a stream-mode property, covered by the stream-mode soak (Layer D / §5.2), and is not
1205+
claimed here.
1206+
- **This is a network/transport campaign, not a crash campaign.** It never kills the app process; it is
1207+
complementary to §4.1–§4.4 (which prove `SIGKILL` atomicity and resume-from-partial). Neither
1208+
subsumes the other.
1209+
11171210
---
11181211

11191212
## 5. Findings log

0 commit comments

Comments
 (0)