Skip to content

Commit 094a2c3

Browse files
authored
feat(anvil): added anvil_impersonateSignature (#11195)
* wip * wip * wip * wip * test * clippy * fixes * removed checks * extra check
1 parent febd245 commit 094a2c3

File tree

6 files changed

+144
-7
lines changed

6 files changed

+144
-7
lines changed

crates/anvil/core/src/eth/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,11 @@ pub enum EthRequest {
321321
with = "sequence"
322322
)]
323323
AutoImpersonateAccount(bool),
324+
325+
/// Registers a signature/address pair for faking `ecrecover` results
326+
#[serde(rename = "anvil_impersonateSignature", with = "sequence")]
327+
ImpersonateSignature(Bytes, Address),
328+
324329
/// Returns true if automatic mining is enabled, and false.
325330
#[serde(rename = "anvil_getAutomine", alias = "hardhat_getAutomine", with = "empty_params")]
326331
GetAutoMine(()),

crates/anvil/src/eth/api.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,9 @@ impl EthApi {
349349
EthRequest::AutoImpersonateAccount(enable) => {
350350
self.anvil_auto_impersonate_account(enable).await.to_rpc_result()
351351
}
352+
EthRequest::ImpersonateSignature(signature, address) => {
353+
self.anvil_impersonate_signature(signature, address).await.to_rpc_result()
354+
}
352355
EthRequest::GetAutoMine(()) => self.anvil_get_auto_mine().to_rpc_result(),
353356
EthRequest::Mine(blocks, interval) => {
354357
self.anvil_mine(blocks, interval).await.to_rpc_result()
@@ -1821,6 +1824,16 @@ impl EthApi {
18211824
Ok(())
18221825
}
18231826

1827+
/// Registers a new address and signature pair to impersonate.
1828+
pub async fn anvil_impersonate_signature(
1829+
&self,
1830+
signature: Bytes,
1831+
address: Address,
1832+
) -> Result<()> {
1833+
node_info!("anvil_impersonateSignature");
1834+
self.backend.impersonate_signature(signature, address).await
1835+
}
1836+
18241837
/// Returns true if auto mining is enabled, and false.
18251838
///
18261839
/// Handler for ETH RPC call: `anvil_getAutomine`

crates/anvil/src/eth/backend/cheats.rs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
//! Support for "cheat codes" / bypass functions
22
3-
use alloy_primitives::{Address, map::AddressHashSet};
3+
use alloy_evm::precompiles::{Precompile, PrecompileInput};
4+
use alloy_primitives::{
5+
Address, Bytes,
6+
map::{AddressHashSet, foldhash::HashMap},
7+
};
48
use parking_lot::RwLock;
9+
use revm::precompile::{
10+
PrecompileError, PrecompileOutput, PrecompileResult, secp256k1::ec_recover_run,
11+
utilities::right_pad,
12+
};
513
use std::sync::Arc;
614

715
/// Manages user modifications that may affect the node's behavior
@@ -61,6 +69,21 @@ impl CheatsManager {
6169
pub fn impersonated_accounts(&self) -> AddressHashSet {
6270
self.state.read().impersonated_accounts.clone()
6371
}
72+
73+
/// Registers an override so that `ecrecover(signature)` returns `addr`.
74+
pub fn add_recover_override(&self, sig: Bytes, addr: Address) {
75+
self.state.write().signature_overrides.insert(sig, addr);
76+
}
77+
78+
/// If an override exists for `sig`, returns the address; otherwise `None`.
79+
pub fn get_recover_override(&self, sig: &Bytes) -> Option<Address> {
80+
self.state.read().signature_overrides.get(sig).copied()
81+
}
82+
83+
/// Returns true if any ecrecover overrides have been registered.
84+
pub fn has_recover_overrides(&self) -> bool {
85+
!self.state.read().signature_overrides.is_empty()
86+
}
6487
}
6588

6689
/// Container type for all the state variables
@@ -70,4 +93,47 @@ pub struct CheatsState {
7093
pub impersonated_accounts: AddressHashSet,
7194
/// If set to true will make the `is_impersonated` function always return true
7295
pub auto_impersonate_accounts: bool,
96+
/// Overrides for ecrecover: Signature => Address
97+
pub signature_overrides: HashMap<Bytes, Address>,
98+
}
99+
100+
impl CheatEcrecover {
101+
pub fn new(cheats: Arc<CheatsManager>) -> Self {
102+
Self { cheats }
103+
}
104+
}
105+
106+
impl Precompile for CheatEcrecover {
107+
fn call(&self, input: PrecompileInput<'_>) -> PrecompileResult {
108+
if !self.cheats.has_recover_overrides() {
109+
return ec_recover_run(input.data, input.gas);
110+
}
111+
112+
const ECRECOVER_BASE: u64 = 3_000;
113+
if input.gas < ECRECOVER_BASE {
114+
return Err(PrecompileError::OutOfGas);
115+
}
116+
let padded = right_pad::<128>(input.data);
117+
let v = padded[63];
118+
let mut sig_bytes = [0u8; 65];
119+
sig_bytes[..64].copy_from_slice(&padded[64..128]);
120+
sig_bytes[64] = v;
121+
let sig_bytes_wrapped = Bytes::copy_from_slice(&sig_bytes);
122+
if let Some(addr) = self.cheats.get_recover_override(&sig_bytes_wrapped) {
123+
let mut out = [0u8; 32];
124+
out[12..].copy_from_slice(addr.as_slice());
125+
return Ok(PrecompileOutput::new(ECRECOVER_BASE, Bytes::copy_from_slice(&out)));
126+
}
127+
ec_recover_run(input.data, input.gas)
128+
}
129+
130+
fn is_pure(&self) -> bool {
131+
false
132+
}
133+
}
134+
135+
/// A custom ecrecover precompile that supports cheat-based signature overrides.
136+
#[derive(Clone, Debug)]
137+
pub struct CheatEcrecover {
138+
cheats: Arc<CheatsManager>,
73139
}

crates/anvil/src/eth/backend/mem/mod.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
config::PruneStateHistoryConfig,
88
eth::{
99
backend::{
10-
cheats::CheatsManager,
10+
cheats::{CheatEcrecover, CheatsManager},
1111
db::{Db, MaybeFullDatabase, SerializableState},
1212
env::Env,
1313
executor::{ExecutedTransactions, TransactionExecutor},
@@ -45,7 +45,7 @@ use alloy_evm::{
4545
Database, Evm,
4646
eth::EthEvmContext,
4747
overrides::{OverrideBlockHashes, apply_state_overrides},
48-
precompiles::PrecompilesMap,
48+
precompiles::{DynPrecompile, Precompile, PrecompilesMap},
4949
};
5050
use alloy_network::{
5151
AnyHeader, AnyRpcBlock, AnyRpcHeader, AnyRpcTransaction, AnyTxEnvelope, AnyTxType,
@@ -98,7 +98,7 @@ use foundry_evm::{
9898
traces::{CallTraceDecoder, TracingInspectorConfig},
9999
utils::{get_blob_base_fee_update_fraction, get_blob_base_fee_update_fraction_by_spec_id},
100100
};
101-
use foundry_evm_core::either_evm::EitherEvm;
101+
use foundry_evm_core::{either_evm::EitherEvm, precompiles::EC_RECOVER};
102102
use futures::channel::mpsc::{UnboundedSender, unbounded};
103103
use op_alloy_consensus::DEPOSIT_TX_TYPE_ID;
104104
use op_revm::{
@@ -1181,6 +1181,14 @@ impl Backend {
11811181
inject_precompiles(&mut evm, factory.precompiles());
11821182
}
11831183

1184+
let cheats = Arc::new(self.cheats.clone());
1185+
if cheats.has_recover_overrides() {
1186+
let cheat_ecrecover = CheatEcrecover::new(Arc::clone(&cheats));
1187+
evm.precompiles_mut().apply_precompile(&EC_RECOVER, move |_| {
1188+
Some(DynPrecompile::new_stateful(move |input| cheat_ecrecover.call(input)))
1189+
});
1190+
}
1191+
11841192
evm
11851193
}
11861194

@@ -3137,6 +3145,16 @@ impl Backend {
31373145
Ok(None)
31383146
}
31393147

3148+
/// Overrides the given signature to impersonate the specified address during ecrecover.
3149+
pub async fn impersonate_signature(
3150+
&self,
3151+
signature: Bytes,
3152+
address: Address,
3153+
) -> Result<(), BlockchainError> {
3154+
self.cheats.add_recover_override(signature, address);
3155+
Ok(())
3156+
}
3157+
31403158
/// Prove an account's existence or nonexistence in the state trie.
31413159
///
31423160
/// Returns a merkle proof of the account's trie node, `account_key` == keccak(address)

crates/anvil/src/evm.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
use std::fmt::Debug;
2-
31
use alloy_evm::{
42
Database, Evm,
53
eth::EthEvmContext,
64
precompiles::{DynPrecompile, PrecompileInput, PrecompilesMap},
75
};
6+
87
use foundry_evm_core::either_evm::EitherEvm;
98
use op_revm::OpContext;
109
use revm::{Inspector, precompile::PrecompileWithAddress};
10+
use std::fmt::Debug;
1111

1212
/// Object-safe trait that enables injecting extra precompiles when using
1313
/// `anvil` as a library.

crates/anvil/tests/it/anvil.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
use alloy_consensus::EMPTY_ROOT_HASH;
44
use alloy_eips::BlockNumberOrTag;
55
use alloy_hardforks::EthereumHardfork;
6-
use alloy_primitives::Address;
6+
use alloy_network::{ReceiptResponse, TransactionBuilder};
7+
use alloy_primitives::{Address, B256, hex};
78
use alloy_provider::Provider;
9+
use alloy_rpc_types::TransactionRequest;
810
use anvil::{NodeConfig, spawn};
911

1012
#[tokio::test(flavor = "multi_thread")]
@@ -135,3 +137,36 @@ async fn test_can_use_default_genesis_block_number() {
135137

136138
assert_eq!(0, provider.get_block(0.into()).await.unwrap().unwrap().header.number);
137139
}
140+
141+
#[tokio::test(flavor = "multi_thread")]
142+
async fn test_anvil_recover_signature() {
143+
let (api, handle) = spawn(NodeConfig::test()).await;
144+
let provider = handle.http_provider();
145+
alloy_sol_types::sol! {
146+
#[sol(rpc)]
147+
contract TestRecover {
148+
function testRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s, address expected) external pure {
149+
address recovered = ecrecover(hash, v, r, s);
150+
require(recovered == expected, "ecrecover failed: address mismatch");
151+
}
152+
}
153+
}
154+
let bytecode = hex::decode(
155+
"0x60808060405234601557610125908161001a8239f35b5f80fdfe60808060405260043610156011575f80fd5b5f3560e01c63bff0b743146023575f80fd5b3460eb5760a036600319011260eb5760243560ff811680910360eb576084356001600160a01b038116929083900360eb5760805f916020936004358252848201526044356040820152606435606082015282805260015afa1560e0575f516001600160a01b031603609057005b60405162461bcd60e51b815260206004820152602260248201527f65637265636f766572206661696c65643a2061646472657373206d69736d61746044820152610c6d60f31b6064820152608490fd5b6040513d5f823e3d90fd5b5f80fdfea264697066735822122006368b42bca31c97f2c409a1cc5186dc899d4255ecc28db7bbb0ad285dc82ae464736f6c634300081c0033",
156+
).unwrap();
157+
158+
let tx = TransactionRequest::default().with_deploy_code(bytecode);
159+
let receipt = provider.send_transaction(tx.into()).await.unwrap().get_receipt().await.unwrap();
160+
let contract_address = receipt.contract_address().unwrap();
161+
let contract = TestRecover::new(contract_address, &provider);
162+
163+
let sig = alloy_primitives::hex::decode("11".repeat(65)).unwrap();
164+
let r = B256::from_slice(&sig[0..32]);
165+
let s = B256::from_slice(&sig[32..64]);
166+
let v = sig[64];
167+
let fake_hash = B256::random();
168+
let expected = alloy_primitives::address!("0x1234567890123456789012345678901234567890");
169+
api.anvil_impersonate_signature(sig.clone().into(), expected).await.unwrap();
170+
let result = contract.testRecover(fake_hash, v, r, s, expected).call().await;
171+
assert!(result.is_ok(), "ecrecover failed: {:?}", result.err());
172+
}

0 commit comments

Comments
 (0)