Skip to content

feat: replay every canonical V3 pool event from the subscription (0.3.0-alpha.9) - #38

Merged
KaiCode2 merged 1 commit into
mainfrom
feat/subscription-sourced-logs
Aug 22, 2026
Merged

feat: replay every canonical V3 pool event from the subscription (0.3.0-alpha.9)#38
KaiCode2 merged 1 commit into
mainfrom
feat/subscription-sourced-logs

Conversation

@KaiCode2

Copy link
Copy Markdown
Owner

Two changes that only make sense together: canonical logs now arrive over the WebSocket instead of a per-block fetch, and every canonical Uniswap V3 pool event now replays into exact state instead of purging the pool.

Requires evm-fork-cache 0.4.0-alpha.5.

Canonical logs from the subscription

The driver discarded every log the WebSocket delivered and re-fetched the same set over hash-pinned eth_getLogs, once per canonical block. The documented rationale conflated ordering — solvable locally, since logs carry block hash, number, transaction index and log index — with completeness, which genuinely was not, because a punctured subscription looked identical to a whole one.

A block seals from the subscription only on a proof that its log set is closed, and only two things count:

  1. a log delivered on the log subscription for a strictly later block; or
  2. the block's logsBloom excluding every registered interest — a bloom admits false positives and never false negatives, so exclusion positively proves there is no matching log to wait for.

Anything unproven reconciles exactly as before. CanonicalLogSource::Reconcile restores the old behaviour outright.

A header for a later block is deliberately not a proof: newHeads and logs are independent subscriptions. Nor is a timer. The straggler window is retained but now bounds how long the driver waits for a proof rather than authorising it to publish whatever has arrived.

Exact canonical V3 event replay

Mint, Burn, Collect, Flash, SetFeeProtocol, CollectProtocol and IncreaseObservationCardinalityNext previously returned a whole-storage purge, which zeroed slot0 and left the pool reverting LOK on every quote until a full rebuild completed. They now replay exactly: both boundary ticks through Tick.update/Tick.clear, the bitmap words they flip, the oracle entry an in-range change writes, and active liquidity.

A boundary tick's Tick.Info words are read only when its bitmap bit is set — a clear bit proves the struct is zero, so a mint opening a brand-new position needs no tick read at all, which matters because cold start warms bitmap words but not empty ticks. Where the parent holds a word anyway the inference is cross-checked both ways and a disagreement fails closed.

A boundary tick outside the warmed radius is unknown rather than contradicted, and costs a five-slot targeted resync instead of the pool. Initialize remains a fail-closed purge: a pool being initialised wants a cold start.

Verification against mainnet

The just-in-time liquidity block 25,673,790 on Ethereum BIFI/WETH (Swap, Mint, Swap, Burn, Collect, Swap) replays all six logs exactly, compared per transaction against the node's own debug_traceTransaction prestateTracer(diffMode=true) diff — no repair, no purge, no provider read. Both quote directions then match the deployed QuoterV2 on amountOut and sqrtPriceX96After across seven initialized-tick crossings.

Two 70-minute three-chain dry-runs against the published alphas:

published this branch
eth_getLogs 4,525 692
total JSON-RPC 7,893 4,544
quote-canary rejections 1 2

Breaking

  • AmmSubscriberDriverConfig gains canonical_log_source.
  • AmmSubscriberDriverStats is new.
  • Requires evm-fork-cache 0.4.0-alpha.5, whose checkpoint-format bumps mean existing checkpoints are rejected and must be rebuilt.

Release ordering — please read before merging

Cargo.lock still names evm-fork-cache 0.4.0-alpha.4 while the manifest pins =0.4.0-alpha.5. A tracked lockfile cannot record a checksum for an unpublished version, and the alternative was committing an unpublishable path entry. RELEASING.md documents the sequence: publish fork-cache alpha.5, run cargo update -p evm-fork-cache, then publish this crate. CI --locked will not resolve until that step.

Verification

fmt, clippy -D warnings, tests green across the feature matrix and all seven isolated per-adapter configs, on nightly, stable, and MSRV 1.90.0.

@KaiCode2
KaiCode2 force-pushed the feat/subscription-sourced-logs branch from 53f264a to dadc9cf Compare August 22, 2026 20:08
Two changes that only make sense together: canonical logs now arrive over the
WebSocket instead of a per-block fetch, and every canonical Uniswap V3 pool
event now replays into exact state instead of purging the pool.

CANONICAL LOGS FROM THE SUBSCRIPTION

The driver discarded every log the WebSocket delivered and re-fetched the same
set over hash-pinned `eth_getLogs`, once per canonical block. The documented
rationale conflated two problems: ordering, which is solvable locally because
logs carry block hash, block number, transaction index and log index; and
completeness, which genuinely was not, because a punctured subscription looked
identical to a whole one.

`evm-fork-cache 0.4.0-alpha.5` detects and heals notification loss and attests
the result, so ordering is handled where it always could be -- in the driver.

A block seals from the subscription only on a proof that its log set is closed,
and only two things count as one: a log delivered on the log subscription for a
strictly later block, or the block's `logsBloom` excluding every registered
interest. A bloom admits false positives and never false negatives, so
exclusion positively proves there is no matching log to wait for; it covers the
large majority of blocks and is what keeps a quiet pool off the provider.
Anything unproven reconciles exactly as before.

A header for a later block is deliberately not a proof: `newHeads` and `logs`
are independent subscriptions, so a header establishes only that the header
stream advanced. Nor is a timer. The straggler window is retained, but it now
bounds how long the driver waits for a proof rather than authorising it to
publish whatever has arrived.

EXACT CANONICAL V3 EVENT REPLAY

`Mint`, `Burn`, `Collect`, `Flash`, `SetFeeProtocol`, `CollectProtocol` and
`IncreaseObservationCardinalityNext` previously returned a whole-storage purge,
which zeroed `slot0` and left the pool reverting `LOK` on every quote until a
full rebuild completed. They now replay exactly: both boundary ticks through
`Tick.update`/`Tick.clear`, the bitmap words they flip, the oracle entry an
in-range change writes, and active liquidity.

A boundary tick's `Tick.Info` words are read only when its bitmap bit is set. A
clear bit proves the struct is zero, so a mint opening a brand-new position
needs no tick read -- which matters because cold start warms bitmap words but
not empty ticks. Where the parent holds a word anyway the inference is
cross-checked both ways and a disagreement fails closed.

A boundary tick outside the warmed radius is unknown rather than contradicted,
and costs a five-slot targeted resync instead of the pool. `Initialize` remains
a fail-closed purge: a pool being initialised wants a cold start.

Verified against mainnet: the just-in-time liquidity block 25,673,790 on
Ethereum BIFI/WETH (`Swap, Mint, Swap, Burn, Collect, Swap`) replays all six
logs exactly, compared per transaction against the node's own
`prestateTracer(diffMode=true)` diff, with no repair, no purge and no provider
read. Both quote directions then match the deployed QuoterV2 on `amountOut` and
`sqrtPriceX96After` across seven initialized-tick crossings.

MEASURED

Two 70-minute three-chain dry-runs against the published alphas: `eth_getLogs`
fell from 4,525 to 692 per 70 minutes and total JSON-RPC traffic from 7,893 to
4,544, while downstream quote-canary rejections stayed at the baseline rate
(2 against 1) rather than the 8 an earlier, unsound sealing rule produced.

Breaking: `AmmSubscriberDriverConfig` gains `canonical_log_source`
(`CanonicalLogSource::Reconcile` restores the previous behaviour outright), and
`AmmSubscriberDriverStats` is new. Requires `evm-fork-cache 0.4.0-alpha.5`.
@KaiCode2
KaiCode2 force-pushed the feat/subscription-sourced-logs branch from dadc9cf to b4ff304 Compare August 22, 2026 21:23
@KaiCode2
KaiCode2 merged commit 3e5b517 into main Aug 22, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant