Skip to content

Commit a981cde

Browse files
mattsseclaudeklkvr
authored
feat: add RPC utilities for block and state overrides (#108)
* feat: add RPC utilities for block and state overrides Migrates RPC utility functions from reth's revm_utils module for simulating transactions with custom block parameters and account state. - apply_block_overrides for modifying block environment - apply_state_overrides for account state modifications - OverrideBlockHashes trait with implementations for CacheDB and State - StateOverrideError enum generic over database errors - Feature-gated with "rpc-util" flag Closes #107 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * test: add storage override tests Adds tests for overriding account storage using both state_diff and state methods to ensure storage values are correctly applied. * fmt * overrides --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Arsenii Kulikov <[email protected]>
1 parent 01298e4 commit a981cde

File tree

4 files changed

+290
-1
lines changed

4 files changed

+290
-1
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ alloy-consensus = { version = "1.0.0", default-features = false }
4646
alloy-primitives = { version = "1.0.0", default-features = false }
4747
alloy-sol-types = { version = "1.0.0", default-features = false }
4848
alloy-hardforks = { version = "0.2" }
49+
alloy-rpc-types-eth = { version = "1.0.0", default-features = false }
4950

5051
# op-alloy
5152
alloy-op-hardforks = { version = "0.2" }

crates/evm/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ alloy-primitives.workspace = true
1919
alloy-sol-types.workspace = true
2020
alloy-eips.workspace = true
2121
alloy-hardforks.workspace = true
22+
alloy-rpc-types-eth = { workspace = true, optional = true }
2223

2324
revm.workspace = true
2425
op-revm = { workspace = true, optional = true }
@@ -47,6 +48,8 @@ std = [
4748
"derive_more/std",
4849
"op-revm?/std",
4950
"thiserror/std",
50-
"op-alloy-consensus?/std"
51+
"op-alloy-consensus?/std",
52+
"alloy-rpc-types-eth?/std"
5153
]
5254
op = ["op-revm", "op-alloy-consensus"]
55+
overrides = ["dep:alloy-rpc-types-eth"]

crates/evm/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ pub use error::*;
2121
pub mod tx;
2222
pub use tx::*;
2323
pub mod precompiles;
24+
#[cfg(feature = "overrides")]
25+
pub mod overrides;
2426
pub mod tracing;
2527

2628
mod either;

crates/evm/src/overrides.rs

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
//! RPC utilities for working with EVM.
2+
//!
3+
//! This module provides helper functions for RPC implementations, including:
4+
//! - Block and state overrides
5+
6+
use alloc::collections::BTreeMap;
7+
use alloy_primitives::{keccak256, map::HashMap, Address, B256, U256};
8+
use alloy_rpc_types_eth::{
9+
state::{AccountOverride, StateOverride},
10+
BlockOverrides,
11+
};
12+
use revm::{
13+
bytecode::BytecodeDecodeError,
14+
context::BlockEnv,
15+
database::{CacheDB, State},
16+
state::{Account, AccountStatus, Bytecode, EvmStorageSlot},
17+
Database, DatabaseCommit,
18+
};
19+
20+
/// Errors that can occur when applying state overrides.
21+
#[derive(Debug, thiserror::Error)]
22+
pub enum StateOverrideError<E> {
23+
/// Invalid bytecode provided in override.
24+
#[error(transparent)]
25+
InvalidBytecode(#[from] BytecodeDecodeError),
26+
/// Both state and state_diff were provided for an account.
27+
#[error("Both 'state' and 'stateDiff' fields are set for account {0}")]
28+
BothStateAndStateDiff(Address),
29+
/// Database error occurred.
30+
#[error(transparent)]
31+
Database(E),
32+
}
33+
34+
/// Helper trait implemented for databases that support overriding block hashes.
35+
///
36+
/// Used for applying [`BlockOverrides::block_hash`]
37+
pub trait OverrideBlockHashes {
38+
/// Overrides the given block hashes.
39+
fn override_block_hashes(&mut self, block_hashes: BTreeMap<u64, B256>);
40+
41+
/// Applies the given block overrides to the env and updates overridden block hashes.
42+
fn apply_block_overrides(&mut self, overrides: BlockOverrides, env: &mut BlockEnv)
43+
where
44+
Self: Sized,
45+
{
46+
apply_block_overrides(overrides, self, env);
47+
}
48+
}
49+
50+
impl<DB> OverrideBlockHashes for CacheDB<DB> {
51+
fn override_block_hashes(&mut self, block_hashes: BTreeMap<u64, B256>) {
52+
self.cache
53+
.block_hashes
54+
.extend(block_hashes.into_iter().map(|(num, hash)| (U256::from(num), hash)))
55+
}
56+
}
57+
58+
impl<DB> OverrideBlockHashes for State<DB> {
59+
fn override_block_hashes(&mut self, block_hashes: BTreeMap<u64, B256>) {
60+
self.block_hashes.extend(block_hashes);
61+
}
62+
}
63+
64+
/// Applies the given block overrides to the env and updates overridden block hashes in the db.
65+
pub fn apply_block_overrides<DB>(overrides: BlockOverrides, db: &mut DB, env: &mut BlockEnv)
66+
where
67+
DB: OverrideBlockHashes,
68+
{
69+
let BlockOverrides {
70+
number,
71+
difficulty,
72+
time,
73+
gas_limit,
74+
coinbase,
75+
random,
76+
base_fee,
77+
block_hash,
78+
} = overrides;
79+
80+
if let Some(block_hashes) = block_hash {
81+
// override block hashes
82+
db.override_block_hashes(block_hashes);
83+
}
84+
85+
if let Some(number) = number {
86+
env.number = number.saturating_to();
87+
}
88+
if let Some(difficulty) = difficulty {
89+
env.difficulty = difficulty;
90+
}
91+
if let Some(time) = time {
92+
env.timestamp = time;
93+
}
94+
if let Some(gas_limit) = gas_limit {
95+
env.gas_limit = gas_limit;
96+
}
97+
if let Some(coinbase) = coinbase {
98+
env.beneficiary = coinbase;
99+
}
100+
if let Some(random) = random {
101+
env.prevrandao = Some(random);
102+
}
103+
if let Some(base_fee) = base_fee {
104+
env.basefee = base_fee.saturating_to();
105+
}
106+
}
107+
108+
/// Applies the given state overrides (a set of [`AccountOverride`]) to the database.
109+
pub fn apply_state_overrides<DB>(
110+
overrides: StateOverride,
111+
db: &mut DB,
112+
) -> Result<(), StateOverrideError<DB::Error>>
113+
where
114+
DB: Database + DatabaseCommit,
115+
{
116+
for (account, account_overrides) in overrides {
117+
apply_account_override(account, account_overrides, db)?;
118+
}
119+
Ok(())
120+
}
121+
122+
/// Applies a single [`AccountOverride`] to the database.
123+
fn apply_account_override<DB>(
124+
account: Address,
125+
account_override: AccountOverride,
126+
db: &mut DB,
127+
) -> Result<(), StateOverrideError<DB::Error>>
128+
where
129+
DB: Database + DatabaseCommit,
130+
{
131+
let mut info = db.basic(account).map_err(StateOverrideError::Database)?.unwrap_or_default();
132+
133+
if let Some(nonce) = account_override.nonce {
134+
info.nonce = nonce;
135+
}
136+
if let Some(code) = account_override.code {
137+
// we need to set both the bytecode and the codehash
138+
info.code_hash = keccak256(&code);
139+
info.code = Some(Bytecode::new_raw_checked(code)?);
140+
}
141+
if let Some(balance) = account_override.balance {
142+
info.balance = balance;
143+
}
144+
145+
// Create a new account marked as touched
146+
let mut acc =
147+
revm::state::Account { info, status: AccountStatus::Touched, storage: Default::default() };
148+
149+
let storage_diff = match (account_override.state, account_override.state_diff) {
150+
(Some(_), Some(_)) => return Err(StateOverrideError::BothStateAndStateDiff(account)),
151+
(None, None) => None,
152+
// If we need to override the entire state, we firstly mark account as destroyed to clear
153+
// its storage, and then we mark it is "NewlyCreated" to make sure that old storage won't be
154+
// used.
155+
(Some(state), None) => {
156+
// Destroy the account to ensure that its storage is cleared
157+
db.commit(HashMap::from_iter([(
158+
account,
159+
Account {
160+
status: AccountStatus::SelfDestructed | AccountStatus::Touched,
161+
..Default::default()
162+
},
163+
)]));
164+
// Mark the account as created to ensure that old storage is not read
165+
acc.mark_created();
166+
Some(state)
167+
}
168+
(None, Some(state)) => Some(state),
169+
};
170+
171+
if let Some(state) = storage_diff {
172+
for (slot, value) in state {
173+
acc.storage.insert(
174+
slot.into(),
175+
EvmStorageSlot {
176+
// we use inverted value here to ensure that storage is treated as changed
177+
original_value: (!value).into(),
178+
present_value: value.into(),
179+
is_cold: false,
180+
},
181+
);
182+
}
183+
}
184+
185+
db.commit(HashMap::from_iter([(account, acc)]));
186+
187+
Ok(())
188+
}
189+
190+
#[cfg(test)]
191+
mod tests {
192+
use super::*;
193+
use alloy_primitives::{address, bytes};
194+
use revm::database::EmptyDB;
195+
196+
#[test]
197+
fn test_state_override_state() {
198+
let code = bytes!(
199+
"0x63d0e30db05f525f5f6004601c3473c02aaa39b223fe8d0a0e5c4f27ead9083c756cc25af15f5260205ff3"
200+
);
201+
let to = address!("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599");
202+
203+
let mut db = State::builder().with_database(CacheDB::new(EmptyDB::new())).build();
204+
205+
let acc_override = AccountOverride::default().with_code(code.clone());
206+
apply_account_override(to, acc_override, &mut db).unwrap();
207+
208+
let account = db.basic(to).unwrap().unwrap();
209+
assert!(account.code.is_some());
210+
assert_eq!(account.code_hash, keccak256(&code));
211+
}
212+
213+
#[test]
214+
fn test_state_override_cache_db() {
215+
let code = bytes!(
216+
"0x63d0e30db05f525f5f6004601c3473c02aaa39b223fe8d0a0e5c4f27ead9083c756cc25af15f5260205ff3"
217+
);
218+
let to = address!("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599");
219+
220+
let mut db = CacheDB::new(EmptyDB::new());
221+
222+
let acc_override = AccountOverride::default().with_code(code.clone());
223+
apply_account_override(to, acc_override, &mut db).unwrap();
224+
225+
let account = db.basic(to).unwrap().unwrap();
226+
assert!(account.code.is_some());
227+
assert_eq!(account.code_hash, keccak256(&code));
228+
}
229+
230+
#[test]
231+
fn test_state_override_storage() {
232+
let account = address!("0x1234567890123456789012345678901234567890");
233+
let slot1 = B256::from(U256::from(1));
234+
let slot2 = B256::from(U256::from(2));
235+
let value1 = B256::from(U256::from(100));
236+
let value2 = B256::from(U256::from(200));
237+
238+
let mut db = CacheDB::new(EmptyDB::new());
239+
240+
// Create storage overrides
241+
let mut storage = HashMap::<B256, B256>::default();
242+
storage.insert(slot1, value1);
243+
storage.insert(slot2, value2);
244+
245+
let acc_override = AccountOverride::default().with_state_diff(storage);
246+
apply_account_override(account, acc_override, &mut db).unwrap();
247+
248+
// Get the storage value using the database interface
249+
let storage1 = db.storage(account, U256::from(1)).unwrap();
250+
let storage2 = db.storage(account, U256::from(2)).unwrap();
251+
252+
assert_eq!(storage1, U256::from(100));
253+
assert_eq!(storage2, U256::from(200));
254+
}
255+
256+
#[test]
257+
fn test_state_override_full_state() {
258+
let account = address!("0x1234567890123456789012345678901234567890");
259+
let slot1 = B256::from(U256::from(1));
260+
let slot2 = B256::from(U256::from(2));
261+
let value1 = B256::from(U256::from(100));
262+
let value2 = B256::from(U256::from(200));
263+
264+
let mut db = State::builder().with_database(CacheDB::new(EmptyDB::new())).build();
265+
266+
// Create storage overrides using state (not state_diff)
267+
let mut storage = HashMap::<B256, B256>::default();
268+
storage.insert(slot1, value1);
269+
storage.insert(slot2, value2);
270+
271+
let acc_override = AccountOverride::default().with_state(storage);
272+
let mut state_overrides = StateOverride::default();
273+
state_overrides.insert(account, acc_override);
274+
apply_state_overrides(state_overrides, &mut db).unwrap();
275+
276+
// Get the storage value using the database interface
277+
let storage1 = db.storage(account, U256::from(1)).unwrap();
278+
let storage2 = db.storage(account, U256::from(2)).unwrap();
279+
280+
assert_eq!(storage1, U256::from(100));
281+
assert_eq!(storage2, U256::from(200));
282+
}
283+
}

0 commit comments

Comments
 (0)