-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreactive_cache.rs
More file actions
170 lines (152 loc) · 6.14 KB
/
Copy pathreactive_cache.rs
File metadata and controls
170 lines (152 loc) · 6.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
//! Reactive cache updates from the event stream (Pillar B.2).
//!
//! Decodes on-chain logs into the [`StateUpdate`](evm_fork_cache::StateUpdate)
//! vocabulary and applies them to a fork cache, keeping hot state fresh without
//! an RPC round-trip per change. This example wires up:
//!
//! 1. A [`DecoderRegistry`] with the built-in [`Erc20TransferDecoder`].
//! 2. An [`EventPipeline`] whose `ingest_logs` decodes and applies a block's logs.
//! 3. Freshness pinning, sampled reconcile against a stub fetcher, and reorg purge.
//!
//! Runs fully offline against a mocked provider and in-memory logs.
//!
//! Run with:
//!
//! ```sh
//! cargo run --example reactive_cache
//! ```
#[path = "support/mock.rs"]
mod mock;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use alloy_eips::BlockId;
use alloy_primitives::{Address, Bytes, Log, U256, keccak256};
use alloy_sol_types::SolValue;
use anyhow::Result;
use evm_fork_cache::Erc20TransferDecoder;
use evm_fork_cache::cache::StorageBatchFetchFn;
use evm_fork_cache::events::{DecoderRegistry, EventPipeline};
use evm_fork_cache::freshness::{AlwaysVerify, FreshnessController, FreshnessRegistry, Validity};
/// Hashed `balanceOf[owner]` slot for the MockERC20 fixture (mapping at slot 3).
fn balance_slot(owner: Address) -> U256 {
let key = keccak256((owner, U256::from(mock::MOCK_ERC20_BALANCE_SLOT)).abi_encode());
U256::from_be_bytes(key.0)
}
/// Build an ERC-20 `Transfer(from, to, value)` log.
fn transfer_log(token: Address, from: Address, to: Address, value: U256) -> Log {
let sig = keccak256(b"Transfer(address,address,uint256)");
Log::new_unchecked(
token,
vec![sig, from.into_word(), to.into_word()],
Bytes::copy_from_slice(&value.to_be_bytes::<32>()),
)
}
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> {
let mut cache = mock::offline_cache().await?;
let token = Address::repeat_byte(0x11);
let alice = Address::repeat_byte(0x22);
let bob = Address::repeat_byte(0x33);
mock::install_default_account(&mut cache, Address::ZERO);
mock::install_default_account(&mut cache, alice);
mock::install_default_account(&mut cache, bob);
mock::install_mock_erc20(&mut cache, token);
let alice_slot = balance_slot(alice);
let bob_slot = balance_slot(bob);
cache
.db_mut()
.insert_account_storage(token, alice_slot, U256::from(1_000))?;
cache
.db_mut()
.insert_account_storage(token, bob_slot, U256::from(0))?;
let mut registry = DecoderRegistry::new();
registry.register(Arc::new(Erc20TransferDecoder::new(U256::from(
mock::MOCK_ERC20_BALANCE_SLOT,
))));
let mut pipeline = EventPipeline::new(registry);
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
// Install a counting fetcher up front so we can show that *ingest* performs no
// RPC reads (it decodes logs into writes), while *reconcile* below samples a
// few slots on purpose. It returns the (deliberately drifted) chain value for
// bob so the reconcile step has something to correct.
let fetches = Arc::new(AtomicUsize::new(0));
let counter = fetches.clone();
let fresh: HashMap<(Address, U256), U256> =
HashMap::from([((token, bob_slot), U256::from(260))]);
let fetcher: StorageBatchFetchFn =
Arc::new(move |requests: Vec<(Address, U256)>, _block: BlockId| {
counter.fetch_add(requests.len(), Ordering::Relaxed);
requests
.into_iter()
.map(|(a, s)| (a, s, Ok(fresh.get(&(a, s)).copied().unwrap_or(U256::ZERO))))
.collect()
});
cache.set_storage_batch_fetcher(fetcher);
let block = 100u64;
let digest = pipeline.ingest_logs(
&mut cache,
block,
&[transfer_log(token, alice, bob, U256::from(250))],
);
println!("=== ingested block {} ===", digest.block);
println!(
" RPC slot fetches during ingest: {} <- a poller would refetch {} touched slot(s)/block",
fetches.load(Ordering::Relaxed),
digest.touched_slots.len(),
);
println!(
" decoded {} log(s) -> {} slot change(s), {} skipped",
digest.decoded_logs,
digest.applied.slots.len(),
digest.applied.skipped_len(),
);
println!(
" alice balance: {} bob balance: {}",
mock::balance_of(&mut cache, token, alice)?,
mock::balance_of(&mut cache, token, bob)?,
);
for (addr, slot) in &digest.touched_slots {
controller
.registry_mut()
.set_slot(*addr, *slot, Validity::Pinned);
}
controller.on_new_block(block);
println!(
"\npinned {} event-derived slot(s) into the freshness registry",
digest.touched_slots.len()
);
fetches.store(0, Ordering::Relaxed); // measure reconcile's sampling cost
let report = pipeline.reconcile(&mut cache, &[(token, bob_slot)])?;
println!("\n=== reconcile (sampled {} slot) ===", report.checked);
println!(
" RPC slot fetches during reconcile: {} <- the sampled honesty backstop",
fetches.load(Ordering::Relaxed),
);
if report.mismatched.is_empty() {
println!(" no drift: event-derived state matches chain");
} else {
for c in &report.mismatched {
println!(
" DRIFT: {} slot {} : {} -> {} (corrected)",
c.address, c.slot, c.old, c.new
);
}
}
println!(
" bob balance after reconcile: {}",
mock::balance_of(&mut cache, token, bob)?
);
// `EventPipeline::new` uses the default `ReorgConfig`, whose
// `PurgeScope::AllStorage` clears the storage of every address touched after
// the new head — so bob's balance slot reads back as uncached (`None`) and the
// next access would re-fetch it from RPC.
let purge = pipeline.reorg_to(&mut cache, 99);
println!("\n=== reorg to block 99 (default scope: AllStorage) ===");
println!(
" purged {} address(es); bob balance slot now cached as {:?}",
purge.purged.len(),
cache.cached_storage_value(token, bob_slot),
);
Ok(())
}