feat: replay every canonical V3 pool event from the subscription (0.3.0-alpha.9) - #38
Merged
Merged
Conversation
KaiCode2
force-pushed
the
feat/subscription-sourced-logs
branch
from
August 22, 2026 20:08
53f264a to
dadc9cf
Compare
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
force-pushed
the
feat/subscription-sourced-logs
branch
from
August 22, 2026 21:23
dadc9cf to
b4ff304
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
logsBloomexcluding 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::Reconcilerestores the old behaviour outright.A header for a later block is deliberately not a proof:
newHeadsandlogsare 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,CollectProtocolandIncreaseObservationCardinalityNextpreviously returned a whole-storage purge, which zeroedslot0and left the pool revertingLOKon every quote until a full rebuild completed. They now replay exactly: both boundary ticks throughTick.update/Tick.clear, the bitmap words they flip, the oracle entry an in-range change writes, and active liquidity.A boundary tick's
Tick.Infowords 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.
Initializeremains 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 owndebug_traceTransaction prestateTracer(diffMode=true)diff — no repair, no purge, no provider read. Both quote directions then match the deployed QuoterV2 onamountOutandsqrtPriceX96Afteracross seven initialized-tick crossings.Two 70-minute three-chain dry-runs against the published alphas:
eth_getLogsBreaking
AmmSubscriberDriverConfiggainscanonical_log_source.AmmSubscriberDriverStatsis new.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.lockstill namesevm-fork-cache 0.4.0-alpha.4while 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.mddocuments the sequence: publish fork-cache alpha.5, runcargo update -p evm-fork-cache, then publish this crate. CI--lockedwill 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.