Commit 3deb8b5
* chore(portal): wave-4 follow-up hygiene (FinalizedHead stubs, doc drift)
Promised in the PR #45 merge comment:
- portal-realtime.test.ts: two bare-number `finalizedHead: async () => 673`
stubs → object form `async () => ({ number: 673 })`. FinalizedHead is
object-only since #45 (the bare-number variant was dropped); these two
test-only stubs were the last holdouts.
- pr-quality-gate/SKILL.md: drop `realtime.ts` from the source↔test pair
list and `statediffs.json` from the fixture list — both deleted in #45.
- harness/euler-multichain/probe.mjs: rewrite the stale header comment; the
mirrored `portal/realtime.ts` module was deleted in #45. The comment now
describes what the probe does with no dead cross-reference.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(portal): factory_addresses write-side idempotence (#53) + INV-15 gate test (#21)
# INV-17 — write-side idempotence for factory child addresses (#53)
`ponder_sync.factory_addresses` is the only sync-store table with no
uniqueness story: upstream `insertChildAddresses` is a plain INSERT with no
`ON CONFLICT`, and the table has no `UNIQUE (factory_id, chain_id, address)`.
Dedup is read-side only (`getChildAddresses` min-merges to a set). A second
writer on the same store durably duplicates the whole child set (a
SIGKILL/resume chaos run ended with every child ×2). App behavior is
unaffected — the only consumer collapses duplicates — but store-identity/digest
tooling breaks and the table grows unbounded.
The fork owns only its own flush call site (the store method is upstream), so
the guard lives there: before each `insertChildAddresses`, dedupe the flush
against the store's persisted rows and insert a child only if it is not yet
persisted OR is re-discovered at a STRICTLY LOWER creation block — the
write-side analogue of the read-side min-merge (LEAST semantics). Re-flushing
the same set (a resumed/re-running writer with a fresh in-memory discovery
queue) inserts nothing; a lower re-discovery updates the effective min.
Scope (candor, documented on INV-17): this closes the resumed/sequential
single-writer case — the realistic production shape (single-writer app lock).
It does NOT close a true two-concurrent-writers TOCTOU race (both read absence,
both INSERT); only a DB UNIQUE + ON CONFLICT DO UPDATE LEAST would, and the
fork can't add that cleanly — factory_addresses is created by a hash-keyed
kysely migration, so a real constraint needs a NEW duplicate-collapsing
migration in upstream migrations.ts for both pinned versions. The #53
concurrent writer was a fixed harness-driver defect; the residual gap is
documented and left to a future DB-constraint change.
Tests (portal.test.ts), both mutation-verified:
- second writer re-flushing an already-persisted set → no re-insert
(revert to plain flush → "expected […] to have a length of 1 but got 2").
- strictly-lower re-discovery is re-inserted so read-side LEAST wins
(blanket DO-NOTHING predicate → "expected [] to have a length of 1 but got +0").
# INV-15 hardening gate test (#21 §1, TEST-ONLY)
Pins INV-15's silent dependence on `isLogFactoryMatched`'s `fromBlock` floor: a
factory creation log BELOW `factory.fromBlock` delivered to `scanWindow` must be
discarded by the matcher — else it would enter `pendingChildren` below the walk
floor and the min-merge guard would suppress re-queueing at its in-range block
(a restart loss). Mutation-verified: bypassing the matcher records the sub-floor
child → the test fails ("expected true to be false" at `.has(BELOW_FLOOR_CHILD)").
#21 §2's FIX-2 floor-refinement overscan is intentionally left OPEN.
Both gates green: scripts/sync-upstream.sh {0.16.6,0.15.17} --test → 208 tests
each (205 baseline + 3 added). biome error-clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(portal): PR #54 review round 2 — factory-alias hole, case-normalize, INV-17 candor
Fix round 2 on the INV-17 write-side idempotence guard (#53), per the gated review:
1. BLOCKER (factory-alias hole): the sync-store keys factory_addresses by STORE
IDENTITY (factory minus id/sourceId; sync-store/index.ts insertChildAddresses
~:459 and getChildAddresses ~:502 both strip { id, sourceId } and upsert/select
the factories row on the remaining value under UNIQUE (factory)). Two config
sources aliasing the same on-chain factory (identical minus id/sourceId) each
read empty then both insert -> duplicate child rows on the FIRST flush, breaking
the PR's own idempotence claim. The guard now canonicalizes the flush by store
identity (storeFactoryKey): ONE read + ONE dedupe + ONE insert per canonical
group. The mock store is RE-KEYED the same way the real store keys (strip
id/sourceId) — its prior factory.id keying is exactly why the hole was invisible.
New regression test 'alias-hole' (two aliased sources -> exactly one store row).
2. Case normalization: getChildAddresses returns stored text verbatim and
min-merges case-sensitively; children are lowercased at discovery. The persisted
lookup is now lowercase-normalized so a checksummed pre-existing row is still
deduped. New regression test 'case-normalization' (checksummed row -> no re-insert).
3. INV-17 doc (perf, candor): the guard is a live uncached getChildAddresses per
canonical pending factory per flush-bearing interval (worst ~O(N^2) over an
accreting backfill) — documented as a known cost with a per-process cache as the
follow-up candidate. Not implemented here (correctness first).
4. INV-17 wording: (a) the ×2-children observation is re-attributed to the chaos
DRIVER's two concurrent live instances (a fixed harness defect), NOT SIGKILL/
resume itself — consistent with the SCOPE section. (b) The single-writer claim is
upgraded from "sequential" to TRANSACTIONAL: read->dedupe->insert->insertIntervals
run in ONE tx-scoped syncStore transaction per interval (runtime/historical.ts
:1310-1352); residual risk is genuinely only the two-transactions/two-processes case.
Both gates green (0.16.6 and 0.15.17: 210/210, exit 0). Both mutations bite named
tests: revert canonicalization to factory.id -> alias-hole fails (expected 2 to be 1);
drop the lowercase-normalize -> case-normalization fails (length +0 but got 1). Biome
error-clean on the changed portal files.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent e2185c8 commit 3deb8b5
7 files changed
Lines changed: 485 additions & 23 deletions
File tree
- .agents/skills/pr-quality-gate
- harness/euler-multichain
- portal
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | | - | |
| 26 | + | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
62 | | - | |
63 | | - | |
| 62 | + | |
| 63 | + | |
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
3 | | - | |
4 | | - | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
| |||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
438 | 438 | | |
439 | 439 | | |
440 | 440 | | |
441 | | - | |
| 441 | + | |
442 | 442 | | |
443 | 443 | | |
444 | 444 | | |
| |||
884 | 884 | | |
885 | 885 | | |
886 | 886 | | |
887 | | - | |
| 887 | + | |
888 | 888 | | |
889 | 889 | | |
890 | 890 | | |
| |||
0 commit comments