Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,20 @@ surface freezes at 1.0.

## [Unreleased]

_Nothing yet._

## [0.1.0] - 2026-06-25

This is the first release line. It captures the work done across the
pre-release development phases (see [`docs/ROADMAP.md`](docs/ROADMAP.md)).

### Changed

- **Breaking:** the in-memory chain ID is no longer hard-defaulted to Arbitrum
(`42161`). When no chain ID is set explicitly and no disk `CacheConfig` is
supplied, the cache now infers it from the provider (`eth_chainId`) and falls
back to `1` (Ethereum mainnet) only if that query fails. Set it explicitly with
the new `EvmCacheBuilder::chain_id` / `EvmCache::set_chain_id`.
- **Breaking:** extracted the old in-crate AMM adapter surface before public
release. Protocol-specific storage layouts, protocol metadata, injector
helpers, tick snapshots, and protocol log decoders belong in `evm-amm-state`;
Expand All @@ -33,12 +42,27 @@ pre-release development phases (see [`docs/ROADMAP.md`](docs/ROADMAP.md)).

### Added

- **Honest performance section & benchmarks.** The README "Performance" section
is framed against a *competent* baseline (a shared `foundry-fork-db`
`SharedBackend` + `checkpoint`/`revert` isolation), not a naive
fork-per-candidate strawman. It states plainly that within-block fetch count,
single-threaded CPU, and time-to-result are ~1× against that baseline, and
leads with the genuinely-unique wins: cross-block freshness (0 RPC fetches/block,
pinned in `tests/event_pipeline.rs` — `foundry-fork-db`'s cache is not
block-keyed), parallel `Send` fan-out (`benches/fanout.rs`, a modest measured
~1.2× on micro-sims), point-in-time consistency, and the act-then-validate
control plane (`benches/freshness.rs`). Adds `examples/fetch_minimization_counted.rs`
and `tests/fetch_minimization.rs` (the fetch-once mechanic, with the
shared-backend caveat stated). The internal copy-on-write snapshot cost model
moved to [`docs/INTERNALS.md`](docs/INTERNALS.md).
- **Forked EVM cache** (`cache::EvmCache`) backed by `foundry-fork-db` with lazy
RPC loading and on-disk persistence for accounts, storage, bytecode, and
immutable metadata.
- **`EvmCacheBuilder`** — a fluent constructor (`EvmCache::builder(provider)`)
subsuming the positional `with_cache` / `from_backend` constructors, with
block pin, EVM spec, cache-config, and shared-memory-capacity configuration.
block pin, EVM spec, cache-config, chain-ID, and shared-memory-capacity
configuration. `EvmCacheBuilder::chain_id` sets the `CHAINID` opcode value
explicitly (recommended); `EvmCache::set_chain_id` sets it post-construction.
- **Snapshots and overlays** — `create_snapshot()` produces an immutable,
`Send + Sync` `EvmSnapshot`; `EvmOverlay` is a cheap per-simulation clone for
isolated parallel evaluation.
Expand Down Expand Up @@ -235,7 +259,7 @@ pre-release development phases (see [`docs/ROADMAP.md`](docs/ROADMAP.md)).
layouts.
- Simulation entry points that distinguish failure modes return
`SimulationResult<T>` (`Result<T, SimError>`), separating decoded reverts,
EVM halts, and host errors. `SimulationErrorKind` remains as a deprecated alias.
EVM halts, and host errors.

### Fixed

Expand Down Expand Up @@ -327,4 +351,5 @@ pre-release development phases (see [`docs/ROADMAP.md`](docs/ROADMAP.md)).
- `EvmCache` requires a multi-thread tokio runtime for any RPC-touching path.
- See [`docs/KNOWN_ISSUES.md`](docs/KNOWN_ISSUES.md) for current limitations.

[Unreleased]: https://github.com/KaiCode2/evm-fork-cache/commits/main
[Unreleased]: https://github.com/KaiCode2/evm-fork-cache/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/KaiCode2/evm-fork-cache/releases/tag/v0.1.0
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ readme = "README.md"
repository = "https://github.com/KaiCode2/evm-fork-cache"
documentation = "https://docs.rs/evm-fork-cache"

# Keep the published crate lean: the per-phase development specs are internal
# planning artifacts (they reference an adapter surface extracted before release),
# and the CI workflow has no consumer value. The consumer-facing ROADMAP.md and
# KNOWN_ISSUES.md under docs/ are still shipped.
exclude = ["docs/phase-*-spec.md", ".github/"]

# Standalone workspace root: keeps this crate from being absorbed by any
# ancestor-directory workspace and gives it its own Cargo.lock.
[workspace]
Expand Down Expand Up @@ -63,6 +69,10 @@ harness = false
name = "simulation"
harness = false

[[bench]]
name = "fanout"
harness = false

[[bench]]
name = "freshness"
harness = false
Expand Down
137 changes: 117 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,20 @@ println!(
## Core concepts

The state stack flows bottom-to-top; reads flow up and the fork DB lazily fetches
misses from RPC:

```
EvmOverlay × N isolated, Send simulations (cheap Arc clones)
▲ clone × N
EvmSnapshot immutable, point-in-time, Send + Sync
▲ create_snapshot()
EvmCache lazy RPC fetch + local state cache + targeted writes/purge
▲ lazy fetch
RPC provider
misses from RPC. The event-log path writes hot state in with **no RPC** (the
reactive-sync control plane):

```mermaid
flowchart BT
RPC["RPC provider"] -->|"lazy fetch · once"| CACHE
LOGS["on-chain event logs"] -.->|"decode → write · 0 RPC"| CACHE
CACHE["<b>EvmCache</b> · !Send<br/>fetch · cache · targeted writes/purge"] -->|"create_snapshot()"| SNAP
SNAP["<b>EvmSnapshot</b> · Send + Sync<br/>immutable · Arc · point-in-time"] -->|"cheap Arc clone × N"| OV
OV["<b>EvmOverlay × N</b> · Send<br/>isolated parallel simulations"]
classDef hot fill:#102a17,stroke:#3fb950,color:#e6edf3;
classDef cool fill:#0d1f2d,stroke:#388bfd,color:#e6edf3;
class SNAP,OV hot;
class RPC,CACHE,LOGS cool;
```

- **`EvmCache`** owns the mutable fork: it fetches, caches, persists, and applies
Expand All @@ -149,7 +153,30 @@ The [`freshness`](src/freshness.rs) module layers a freshness controller on top:
classify each address/slot (`Pinned` / `Volatile` / `ValidThrough`), observe how
often slots change, pick what to verify each cycle with a `FreshnessPolicy`, and
run the optimistic loop that returns speculative results immediately and a
`Confirmed`/`Corrected`/`Unverified` verdict asynchronously.
`Confirmed`/`Corrected`/`Unverified` verdict asynchronously. Time-to-actionable-result
is gated on local simulation, not on the RPC validation that runs behind it:

```mermaid
sequenceDiagram
autonumber
participant S as Search loop
participant C as FreshnessController
participant V as Background validator
participant R as RPC
S->>C: run(candidate sims)
C->>C: snapshot + run optimistic sims
C-->>S: SpeculativeSim — optimistic results (~µs)
Note over S: act on speculative results now
C->>V: spawn (Send data only)
V->>R: verify volatile read-set (~L ms)
R-->>V: fresh values
alt nothing the sims read changed
V-->>S: validate().await → Confirmed
else a read slot changed
V->>V: re-run only the affected sims
V-->>S: Corrected { results, changed }
end
```

## Examples

Expand Down Expand Up @@ -220,26 +247,96 @@ println!("installed {} bytes at {}", etched.code_size, etched.target_address);
# }
```

## Performance &amp; honest trade-offs

It is easy to post huge multipliers against a *naive* loop (a fresh cold fork per
candidate that re-fetches everything and deep-clones to isolate). That is **not**
the loop a competent revm user writes. Measured against a **competent baseline** —
one shared [`foundry-fork-db`] `SharedBackend` (which caches and deduplicates
fetches) plus `checkpoint`/`revert` isolation on a single fork — this crate is
**roughly at parity on raw within-block speed**, and we say so plainly:

| Axis | vs a competent shared-backend / checkpoint-revert loop |
|---|---|
| RPC reads **within one block** | **~1×** — a shared `SharedBackend` also fetches each hot slot once |
| Single-threaded per-candidate CPU | **~1×** — `checkpoint`/`revert` isolation is as cheap as an overlay |
| Time-to-result vs *blocking* validation | not a fair comparison — a competent loop doesn't block on a fetch before acting |

The value of this crate is **not** a within-block speed multiplier. It is
correctness, cross-block freshness, and a structured control plane the bare
primitives don't give you:

**① Cross-block freshness — the one quantitative win (exact, CI-pinned integer).**
`foundry-fork-db`'s cache is **not block-keyed**: re-pinning to a new block does
not invalidate cached slots, so a refresh-by-refetch loop must re-read every slot
that changed *each block* to stay correct. Decoding the block's logs into targeted
writes keeps that hot state correct with **0 RPC fetches/block** — pinned in
[`tests/event_pipeline.rs`](tests/event_pipeline.rs). Sampled `reconcile()`
re-reads a fraction to catch drift (the honesty backstop). See
[`reactive_cache`](examples/reactive_cache.rs).

<p align="center">
<img alt="Cumulative RPC slot reads over 8 blocks: refresh-by-refetch climbs linearly to 64 while event-driven writes stay flat at 8 (warmed once, then 0 per block)" width="660"
src="https://raw.githubusercontent.com/KaiCode2/evm-fork-cache/main/assets/cross_block_freshness.svg">
</p>

> Honest caveat: an equally sophisticated peer running their *own* log
> subscription + delta applier also reaches 0 fetches/block. The crate's
> contribution is the packaged, cold-aware, reorg-safe, reconcilable vocabulary —
> not an unreachable number.

**② Parallel fan-out — available, modest, workload-dependent.**
`create_snapshot()` is an immutable `Send + Sync` view; cloning the `Arc` hands
each thread its own overlay, so candidates fan out across cores — which a single
mutable fork cannot do. The measured speedup is honest and modest: **~1.2×** over a
4,096-candidate batch on a 10-core M1 Pro, because these micro-sims are bound by
per-candidate allocation, not EVM compute. Heavier candidates (real txs doing
substantial execution) parallelize better; trivial ones barely. We don't headline a
core-count multiplier we can't reproduce. `cargo bench --bench fanout`;
[`parallel_overlays`](examples/parallel_overlays.rs).

**③ Point-in-time consistency.** Every overlay reads one frozen, consistent block
state. A lazily-filled shared backend can interleave reads taken at slightly
different moments unless carefully pinned; the snapshot removes that class of bug.

**④ Act-then-validate control plane (structure, not speed).** Run optimistically
against current state, return immediately, and validate the volatile read-set in
the background — re-running *only* the sims whose slots changed (`rerun_count`),
with `Confirmed`/`Corrected`/`Unverified` verdicts and block-pinned validation. A
searcher who simply acts on warm state is equally fast; the value is the **safe,
selective re-run**, not a latency multiplier. See
[`freshness_optimistic`](examples/freshness_optimistic.rs).

> [!NOTE]
> **Methodology &amp; candor.** Offline (mocked provider, state injected — no
> network). We deliberately do **not** lead with the headline multipliers a naive
> baseline would produce (~500× fewer reads, ~545× throughput, ~3,800× latency):
> all three collapse toward ~1× against a competent `SharedBackend` +
> `checkpoint`/`revert` loop — which is the very primitive this crate wraps. The
> "0 fetches/block" integer is real and CI-pinned (`cargo test --test
> event_pipeline`); the parallel-fan-out ratio is a Criterion median on an Apple
> M1 Pro, read as a ratio not an absolute. Live-RPC checks live behind the
> `RPC_URL` gate.

## Benchmarks

Criterion benchmarks live in [`benches/`](benches). The offline benches exercise
the current hot paths at a range of cache sizes, including the Phase 5
copy-on-write snapshot implementation and retained deep-clone baselines where
useful for A/B comparison:
Criterion benchmarks live in [`benches/`](benches) and run fully offline (mocked
provider) so they are reproducible:

| Bench | Measures |
| --- | --- |
| `simulation` | `create_snapshot` across cache sizes (100 → 10k accounts), overlay fan-out, `call_raw` throughput, sequential bundle execution, batched storage injection. |
| `freshness` | The optimistic loop end-to-end (CPU and latency-hiding), `verify_slots` at scale (1 → 1000 slots), and multi-sim fan-out. |
| `fanout` | **Parallel fan-out (②).** N candidates **sequential vs across cores** over one shared snapshot — the parallelism a live mutable fork can't do. |
| `freshness` | **Act-then-validate (④).** The optimistic loop CPU cost, selective re-run, and the latency-hiding shape (vs a baseline that elects to block). `verify_slots` at scale; multi-sim fan-out. |
| `event_pipeline` | **Cross-block freshness (①).** `ingest_logs` decode+apply throughput (1 → 1000 logs), `reorg_to` purge; the 0-fetch/block property is pinned in `tests/event_pipeline.rs`. |
| `state_update` | `apply_updates` throughput across batch sizes (1 → 1000 `Slot`s) and per-variant apply cost (`Slot` vs `Account` vs `Purge`). |
| `event_pipeline` | Per-decoder cost (ERC-20 `Transfer`, generic slot marker), `ingest_logs` decode+apply throughput (1 → 1000 logs), and `reorg_to` purge cost. |
| `simulation` | Hot-path micro-benches and snapshot-implementation regression guards (`create_snapshot` vs the deep-clone reference — an internal cost model, see [`docs/INTERNALS.md`](docs/INTERNALS.md)). |
| `access_list` | Touch-set merge and EIP-2930 list construction. |
| `revert_decoding` | Built-in and custom revert decoding, including decoder dispatch with many registered errors. |
| `revert_decoding` | Built-in (`Error`/`Panic`) and custom-error revert decoding, and decoder dispatch over a registered custom error. |
| `create3` | CREATE3 address derivation. |

```sh
cargo bench # all offline benches
cargo bench --bench simulation # one suite
cargo bench --bench fanout # one suite
```

The `rpc_mainnet` bench runs against **live mainnet state** to validate
Expand Down
48 changes: 48 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Security Policy

`evm-fork-cache` is a forked-EVM simulation engine used in search / MEV /
backtesting pipelines, where a correctness bug can have direct financial
consequences for downstream users. Security reports are taken seriously.

## Supported versions

This crate is **pre-1.0** and under active development. Only the latest published
`0.x` release line receives security fixes; there is no back-porting to older
`0.x` versions before 1.0. See [`CHANGELOG.md`](CHANGELOG.md) for what shipped in
each release.

| Version | Supported |
| ------- | --------- |
| latest `0.x` | ✅ |
| older `0.x` | ❌ |

## Reporting a vulnerability

**Please do not open a public issue for security-sensitive reports.**

Report privately via GitHub's
[private vulnerability reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability)
on this repository: open the **Security** tab → **Report a vulnerability**. If
that channel is unavailable, open a minimal public issue asking for a private
contact (without disclosing details) and a maintainer will follow up.

Please include:

- the crate version, Rust version, and feature flags;
- a description of the issue and its impact;
- a minimal reproduction if possible.

You can expect an initial acknowledgement within a few business days. Once a fix
is available it will be released and the advisory published, crediting the
reporter unless anonymity is requested.

## Scope and known limitations

This crate exposes deliberate, documented escape hatches that **bypass its safety
invariants** — notably `unchecked_blockchain_db()` / `unchecked_backend()` (which
sidestep the copy-on-write snapshot invalidation funnel) and the freshness model's
documented reconciliation scope (storage slots only, not account-level state).
These behaviors and their correct usage are described in
[`docs/KNOWN_ISSUES.md`](docs/KNOWN_ISSUES.md). Misuse of a documented escape hatch
is a usage error, not a vulnerability; a way to violate a documented invariant
*without* using an escape hatch is in scope. When in doubt, report it.
62 changes: 62 additions & 0 deletions assets/cross_block_freshness.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading