Skip to content

Phase 4: event pipeline + adapters (Pillar B.2) - #3

Merged
KaiCode2 merged 6 commits into
phase-3-state-updatesfrom
phase-4-event-pipeline
Jun 16, 2026
Merged

Phase 4: event pipeline + adapters (Pillar B.2)#3
KaiCode2 merged 6 commits into
phase-3-state-updatesfrom
phase-4-event-pipeline

Conversation

@KaiCode2

Copy link
Copy Markdown
Owner

Builds Pillar B.2 — the reader half of the event → state pipeline, stacked on the Phase 3 PR (#2). Decode an on-chain Log into the Phase 3 StateUpdate vocabulary, apply it, and run the reactive maintenance (reconcile, reorg) that keeps event-derived state honest. Decoders are pure functions of (log, pre-state); the !Send-cache discipline is preserved by keeping the tested core synchronous.

What's here

  • events module (generic core): EventDecoder/StateView traits, DecoderRegistry, and EventPipeline with ingest_logs (decode + apply log-by-log), reorg_to (purge-and-resync), and reconcile (sampled re-read: correct and alarm, via the new EvmCache::reconcile_slots). A thin async drive/LogSource convenience layers the core over a stream.
  • Erc20TransferDecoder (generic core): Transfer → relative Sub/Add balance SlotDeltas (skipping the zero-address mint/burn leg).
  • UniswapV3Decoder/UniswapV3Layout (protocols): Swap → masked slot0 (new price/tick, preserving the unlocked/observation/fee bits — a clobbered unlockedLOK revert) + absolute liquidity; Mint/Burn → per-tick liquidityGross/liquidityNet, initialized flag, tickBitmap, in-range global liquidity, computed against the StateView and cold-aware.
  • StateUpdate::SlotMasked (state_update): a cold-aware RMW masked write ((old & !mask) | (value & mask)), so a pure decoder can update a packed word's selected bits without clobbering the rest; cold writes surface in StateDiff.skipped_masks.

Decisions (locked with the maintainer 2026-06-16)

  1. Packed-slot updates → StateUpdate::SlotMasked.
  2. V3 coverage → Swap and Mint/Burn (full ticks).
  3. Reorg → purge-and-resync touched addresses (ValidThrough is the lever).
  4. Reconcile → sampled re-read, correct and alarm.

Verification (both feature configs)

cargo fmt --check; clippy --all-targets -- -D warnings (default) + --lib --no-default-features; cargo test 321 passed (289 tests + 32 doctests) and --no-default-features 269 (241 + 28); RUSTDOCFLAGS=-D warnings cargo doc; cargo bench --no-run. The offline examples/reactive_cache.rs runs end-to-end.

Process

Spec (docs/phase-4-spec.md) → red acceptance tests authored before implementation → implementation handed to a sub-agent → independently reviewed + re-verified (the V3 math checked beyond the tests: negative-tick bitmap, slot0 sign round-trip, tick-word packing consistency with inject_v3_ticks).

Known limitation (documented)

V3 event-derived tick maintenance does not reconstruct feeGrowthOutside/oracle state (not derivable from events). Swap price/liquidity quoting is unaffected; fee accounting isn't maintained. Sampled reconcile + reorg purge are the backstop. See docs/KNOWN_ISSUES.md.

🤖 Generated with Claude Code

KaiCode2 and others added 6 commits June 16, 2026 12:48
Build contract for the reader half of Pillar B: an EventDecoder trait +
StateView, a DecoderRegistry, an ERC-20 Transfer decoder, a UniswapV3
Swap/Mint/Burn adapter, and the EventPipeline (ingest_logs / reorg_to /
reconcile) that drives reactive cache updates. Adds the cold-aware
StateUpdate::SlotMasked vocabulary variant so a pure decoder can express a
partial update to a packed storage word (V3 slot0) without clobbering bits it
does not own. Decisions locked with the user 2026-06-16 (SlotMasked; full
Swap+Mint/Burn V3 coverage; purge-and-resync reorgs; sampled correct+alarm
reconciliation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Authored before implementation, per the phased workflow — these define
correctness for Pillar B.2 and will validate the deliverable:

- tests/state_update.rs: SlotMasked cold-aware masked-write tests (sets only
  masked bits / no-op / cold skip+surface via skipped_masks / both-layer
  write-through / full-mask-vs-cold / serde round-trip).
- tests/event_pipeline.rs: DecoderRegistry dispatch (address-scoped + global);
  ERC-20 Transfer -> Sub/Add SlotDeltas (mint/burn zero-address legs, per-token
  slot override, ingest conserves balances via real SLOAD, cold skip); pipeline
  reorg_to purge-and-resync + ReorgConfig scope; reconcile correct+alarm /
  match / no-fetcher error; UniswapV3 adapter (Swap preserves slot0
  unlocked/observation bits + absolute liquidity + cold-skip; Mint gross/net
  signs + initialize + bitmap flip + in/out-of-range global liquidity; Burn
  uninitialize+clear via same-block sequencing; cold tick skip; unregistered
  pool no-op).

Verified the alloy event-ABI plumbing (sol! Swap/Mint/Burn construction +
encode_log_data + decode_log round-trip) in isolation before committing. Tests
are RED until the Phase 4 surface lands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add StateUpdate::SlotMasked (new(old & !mask) | (value & mask)), the
slot_masked constructor, the SkippedMask leaf record, and the
StateDiff.skipped_masks field (+ merge / has_skipped / skipped_len).
Apply arm in cache/mod.rs mirrors the SlotDelta arm: cold-aware RMW via
write_slot_through. Re-export SkippedMask. Makes the tests/state_update.rs
SlotMasked block green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- events/mod.rs (generic core): StateView, EventDecoder, DecoderRegistry,
  EventPipeline (ingest_logs log-by-log, reorg_to purge-and-resync,
  reconcile correct+alarm, derived_slots), BlockDigest, ReconcileReport,
  ReorgConfig, and the async LogSource/drive convenience.
- events/erc20.rs (generic core): Erc20TransferDecoder -> Sub/Add balance
  SlotDeltas, skipping the zero-address leg; reuses parse_transfer.
- events/uniswap_v3.rs (protocols): UniswapV3Decoder/UniswapV3Layout.
  Swap -> masked slot0 (preserves unlocked/observation bits) + absolute
  liquidity; Mint/Burn -> per-tick gross/net, initialized flag, tickBitmap
  bit, global liquidity (in-range), all cold-aware via StateView with a
  mask==MAX,value==0 could-not-compute marker.
- cache/mod.rs: impl StateView for EvmCache; refactor verify_slots into
  verify_slots_inner (+fetched_ok count) and add reconcile_slots, which
  errors on a total fetch failure (honest-freshness) so the pipeline's
  reconcile surfaces an unverifiable re-read rather than a false all-clear.
- lib.rs: pub mod events + re-exports.
- tests/event_pipeline.rs: #[allow(dead_code)] on the unused tick_word test
  helper (no assertion/behaviour change) so clippy --all-targets -D warnings
  stays clean.

All 24 event_pipeline tests + existing suites green on both feature configs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the Phase 4 deliverable on top of the sub-agent's src/ implementation:

- examples/reactive_cache.rs (offline): register an ERC-20 + UniswapV3 decoder,
  ingest a block of logs (a Transfer + a Swap), show the BlockDigest, the
  preserved slot0 `unlocked` bit, a reconcile drift alarm+correction, and a
  reorg purge. Feature-gated (UniswapV3 adapter) with a fallback main.
- benches/event_pipeline.rs (offline, registered in Cargo.toml): per-event
  decode cost (ERC-20 Transfer ~435ns, V3 Swap ~68ns, V3 Mint ~1.0us),
  ingest_logs decode+apply throughput (linear, ~112ns/log to 1000), reorg_to
  purge cost (~378us/1000 addrs).
- CHANGELOG: Phase 4 `### Added` (event pipeline + adapters + SlotMasked).
- ROADMAP: Phase 4 row -> Done + a "Landed on ..." section.
- KNOWN_ISSUES: refreshed the Pillar B status (reader+writer halves done, live
  WS transport not) and recorded the §6.4 V3 fee-growth/oracle maintenance gap.
- README: reactive_cache + event_pipeline rows.
- tests/event_pipeline.rs: removed an unused tick_word helper (mine; the
  sub-agent had `#[allow(dead_code)]`-silenced it) + fmt.

Independently verified green on both feature configs: fmt; clippy --all-targets
(default) + --lib --no-default-features; cargo test (321 passed = 289 tests + 32
doctests) + --no-default-features (269 = 241 + 28); RUSTDOCFLAGS=-D warnings doc;
cargo bench --no-run. The example runs offline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Validates the event → state pipeline against real EVM execution: run a swap in a
ground-truth revm instance and replay ONLY its emitted logs into a twin cache,
then assert the token balances and the packed pool slot0 (price/tick) match
bit-for-bit.

- fixtures/EventGroundTruthPool.sol + test_v3_pool_creation.hex: a faithful
  UniswapV3-pool stand-in whose slot0 is a Solidity struct with the identical
  field widths to UniswapV3Pool.Slot0 — so the *compiler* does the real
  bit-packing and our StateUpdate::SlotMasked is the thing under test. Its swap
  does real ERC-20 transfers (canonical Transfer logs) + a compiler-masked slot0
  update + the canonical Swap event.
- tests/event_ground_truth.rs (protocols-gated): deploy two MockERC20 tokens + the
  pool into a ground-truth cache (deterministic CREATE addresses), seed liquidity,
  execute a real swap (capture its 2 Transfer + 1 Swap logs), build the identical
  pre-swap state in a twin cache, feed only the logs through EventPipeline, and
  assert balances + slot0 + liquidity equal the ground truth. Explicitly checks
  the slot0 unlocked + observation-index bits survive the masked update.

Result: the event-derived state reproduces the ground-truth EVM execution exactly
(swapper/pool balances of both tokens, packed slot0, liquidity). Full suite green
both feature configs (322 default incl. the new test, 269 --no-default-features);
fmt, clippy --all-targets + --lib --no-default-features, doc, bench --no-run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KaiCode2
KaiCode2 merged commit 80eb91f into phase-3-state-updates Jun 16, 2026
2 checks passed
@KaiCode2
KaiCode2 deleted the phase-4-event-pipeline branch June 30, 2026 15:13
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