Skip to content

Commit b61c641

Browse files
dzhelezovclaude
andauthored
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

File tree

0 commit comments

Comments
 (0)