Commit b61c641
fix(portal): correctness wave 2A — head-clamp, discovery floor, extend-local checks, trace receipts, head-pin (#15)
* fix(portal): head-clamp ceilings, construction-time discovery floor, extend-local field checks, trace receipts, head-pin (bug-hunt wave 2A) [WIP: source only]
Five correctness fixes from a triple-blind bug hunt. SOURCE changes only in this
commit; regression tests + INVARIANTS.md rows + mutation-verification are follow-up
(see the WIP PR body). Two of the five were empirically reproduced (BUG-A discovery
floor, BUG-B head-clamp).
FIX 1 — head-clamp root cause (BUG-B). dataEnd() = min(backfillEnd??∞, portalHead??∞)
so desiredTo/coveredTo/endHint/raEnd are all head-clamped even when every source is
bounded; removed the `spec.backfillEnd !== undefined` escape from the INV-9 assert
(now strictly stronger — holds by construction).
FIX 2 — discovery floor at construction (BUG-A). Floor derived from the spec
(min over factories of fromBlock ?? 0), pinned at construction and re-pinned per call
before any fetch; requiredFactoryIntervals/interval-start only refine it downward.
Dropped the `discStartIdx === undefined` escapes from the INV-3 asserts.
FIX 3 — extend-local neededMissing. The needed-field crash check now compares the
matched-data map sizes before/after THIS runStreams call, so a data-bearing base +
event-less extend tail (dataset lacking a needed column) is tolerated, not crash-looped.
FIX 4 — trace/transfer receipts. buildTraces now emits toSyncReceipt for matched trace
txs when a trace/transfer filter has hasTransactionReceipt (deduped by hash across all
branches), so buildEvents no longer throws "Missing transaction receipt".
FIX 5 — PORTAL_FINALIZED_HEAD pin. ensureChunkSize only adopts the live probe as the
head when there is no pin (scaling still uses the live value), so the pin governs
finality/delegation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(portal): regression tests + INVARIANTS rows for correctness wave 2A (7 tests, mutation-verified)
The source fixes for all five wave-2A bugs landed in the parent commit; this
adds the regression coverage, invariant-catalog rows, and the fixed-gaps
provenance that make them a merge-gated package. Every test bites: reverting
the corresponding fix hunk (tests kept) fails the named test — verbatim
assertions recorded in INVARIANTS.md "Wave 2A".
FIX 1 (head-clamp, BUG-B) — two tests in portal.test.ts:
• a BOUNDED backfill whose toBlock is PAST the head is head-clamped so a later
interval over the newly-finalized tail is fetched, not blind-cache-hit. The
distinct shape vs the existing unbounded frontier-extend test: dataEnd() used
to return backfillEnd and ignore the head, recording phantom coverage to the
toBlock. Mutant (dataEnd = backfillEnd ?? head ?? ∞) → block 150's log lost.
• discovery variant: a factory child created in the newly-finalized tail is
discovered + its log fetched once the head advances (head-clamped discovery).
FIX 2 (construction-time discovery floor, BUG-A) — one test:
• an EARLY spanning-chunk fetch issued with requiredFactoryIntervals=[] still
runs discovery: the child log is fetched AND its address flushed. Mutant
(floor pinned only when requiredFactoryIntervals.length > 0) → the
strengthened INV-3 tripwire fires (through:-1) exactly where the old
discStartIdx===undefined escape used to hide the missed discovery.
FIX 3 (extend-local neededMissing, crash-loop) — a pair:
• an EVENT-LESS extend tail lacking a needed field over a data-bearing base is
TOLERATED (mutant → the whole-cd check throws over the tail: blocks [101,200]).
• an extend tail that DOES add matched rows while lacking a needed field STILL
fails LOUD (over-tolerance guard; passes on both versions).
FIX 4 (trace/transfer receipts) — one test:
• a trace filter with hasTransactionReceipt inserts a receipt for every matched
trace tx. Mutant (buildTraces called without onReceipt) → zero receipts.
FIX 5 (PORTAL_FINALIZED_HEAD pin) — one test in portal-shell.test.ts (INV-9 matrix):
• the pin survives ensureChunkSize probing the live head — an interval above the
pin but below the live head still DELEGATES to RPC. Revert-check
(unconditional portalHead = h) → the above-pin interval is served, not delegated.
INVARIANTS.md: INV-3/INV-9/INV-13/INV-15 rows updated (touched enforcement +
tests), INV-4's stale discStartIdx reference corrected, and five "Wave 2A"
fixed-gaps entries with the triple-blind provenance and mutation evidence.
Gates: sync-upstream.sh --test exit 0 on both 0.16.6 (160/160) and 0.15.17
(160/160); biome check portal/ 0 errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(portal): pin the transfer half of FIX 4 receipts; INVARIANTS candor on the FIX 3 trace-tail residual
Two findings from a cross-model adversarial review of PR #15:
FINDING 3 (test gap, fixed with code): `needTraceReceipts` in portal-assemble.ts
ORs a trace half and a transfer half, but the only FIX-4 regression test used a
`type: 'trace'` filter — so deleting `|| spec.transferFilters.some(...)` left every
test green and the transfer half unpinned. Add a regression test with a
`type: 'transfer'` filter + `hasTransactionReceipt: true` whose matched (non-zero-value)
transfer tx must get exactly one receipt inserted. Mutation-verified: removing the
transfer half fails ONLY the new test (`expected [] to have a length of 1 but got +0`
at the `inserted.receipts` assertion) while the trace test stays green; restoring →
161/161 on both 0.16.6 and 0.15.17.
FINDING 2 (INVARIANTS candor, wording only, no code change): FIX 3's "event-less extend
tail is tolerated" claim is too strong for trace/transfer configs. `matchedSize()` counts
`cd.traceBlocks`, but the trace query is server-side UNFILTERED (INV-5) with matching
applied only later at assembly — so a tail returning traces that are ALL client-side
unmatched still grows `traceBlocks` and re-arms the needed-field crash. Tighten the INV-13
row and the Wave-2A FIX-3 entry to state this residual precisely: the tolerance holds only
for genuinely row-less tails; a returned-but-unmatched trace/transfer tail is a residual
FALSE-FATAL (fail-loud — availability, not correctness — and strictly narrower than the
pre-FIX-3 whole-`cd` check). Referenced as a tracked follow-up issue.
Credit: cross-model adversarial review on PR #15.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent 5af08b9 commit b61c641
5 files changed
Lines changed: 1158 additions & 75 deletions
0 commit comments