Skip to main content

revme/cmd/bench/
subcall.rs

1use criterion::Criterion;
2use revm::{
3    bytecode::opcode,
4    context::TxEnv,
5    database::{InMemoryDB, BENCH_CALLER, BENCH_TARGET},
6    primitives::{address, Address, TxKind, U256},
7    state::{AccountInfo, Bytecode},
8    Context, ExecuteEvm, MainBuilder, MainContext,
9};
10
11const SUBCALL_TARGET_A: Address = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
12const SUBCALL_TARGET_B: Address = address!("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
13
14/// Constructs bytecode that loops 1000 times, each iteration doing a CALL to `target`
15/// with the given `value` (0 or 1 wei).
16fn make_loop_call_bytecode(target: Address, value: u8) -> Bytecode {
17    let mut code = vec![
18        opcode::PUSH2,
19        0x03,
20        0xE8,             // PUSH2 1000 — loop counter
21        opcode::JUMPDEST, // loop_start at offset 3
22        opcode::PUSH1,
23        0x00, // retSize
24        opcode::PUSH1,
25        0x00, // retOffset
26        opcode::PUSH1,
27        0x00, // argsSize
28        opcode::PUSH1,
29        0x00, // argsOffset
30        opcode::PUSH1,
31        value,          // value
32        opcode::PUSH20, // target address
33    ];
34    code.extend_from_slice(target.as_slice());
35    code.extend_from_slice(&[
36        opcode::GAS, // forward all remaining gas
37        opcode::CALL,
38        opcode::POP, // discard success/failure
39        opcode::PUSH1,
40        0x01, // decrement counter
41        opcode::SWAP1,
42        opcode::SUB,
43        opcode::DUP1, // duplicate counter for JUMPI check
44        opcode::PUSH1,
45        0x03,          // jump target (JUMPDEST offset)
46        opcode::JUMPI, // jump back if counter != 0
47        opcode::POP,   // clean up remaining counter (0)
48        opcode::STOP,
49    ]);
50    let bytecode = Bytecode::new_raw(code.into());
51    let _ = bytecode.legacy_jump_table();
52    bytecode
53}
54
55/// Minimal contract that just STOPs.
56fn make_stop_bytecode() -> Bytecode {
57    let bytecode = Bytecode::new_raw([opcode::STOP].into());
58    let _ = bytecode.legacy_jump_table();
59    bytecode
60}
61
62/// Constructs bytecode that does a single CALL (no value) to `target`, then STOPs.
63fn make_subcall_bytecode(target: Address) -> Bytecode {
64    let mut code = vec![
65        opcode::PUSH1,
66        0x00, // retSize
67        opcode::PUSH1,
68        0x00, // retOffset
69        opcode::PUSH1,
70        0x00, // argsSize
71        opcode::PUSH1,
72        0x00, // argsOffset
73        opcode::PUSH1,
74        0x00,           // value (no transfer)
75        opcode::PUSH20, // target address
76    ];
77    code.extend_from_slice(target.as_slice());
78    code.extend_from_slice(&[opcode::GAS, opcode::CALL, opcode::POP, opcode::STOP]);
79    let bytecode = Bytecode::new_raw(code.into());
80    let _ = bytecode.legacy_jump_table();
81    bytecode
82}
83
84pub fn run(criterion: &mut Criterion) {
85    // Variant 1: 1000 subcalls each transferring 1 wei
86    {
87        let mut db = InMemoryDB::default();
88        db.insert_account_info(
89            BENCH_CALLER,
90            AccountInfo {
91                balance: U256::from(u128::MAX),
92                ..Default::default()
93            },
94        );
95        db.insert_account_info(
96            BENCH_TARGET,
97            AccountInfo {
98                balance: U256::from(u128::MAX),
99                code: Some(make_loop_call_bytecode(SUBCALL_TARGET_A, 1)),
100                ..Default::default()
101            },
102        );
103        db.insert_account_info(
104            SUBCALL_TARGET_A,
105            AccountInfo {
106                code: Some(make_stop_bytecode()),
107                ..Default::default()
108            },
109        );
110
111        let mut evm = Context::mainnet()
112            .with_db(db)
113            .modify_cfg_chained(|c| {
114                c.disable_nonce_check = true;
115                c.tx_gas_limit_cap = Some(u64::MAX);
116            })
117            .build_mainnet();
118
119        let tx = TxEnv::builder()
120            .caller(BENCH_CALLER)
121            .kind(TxKind::Call(BENCH_TARGET))
122            .gas_limit(u64::MAX)
123            .build()
124            .unwrap();
125
126        criterion.bench_function("subcall_1000_transfer_1wei", |b| {
127            b.iter_batched(
128                || tx.clone(),
129                |input| evm.transact_one(input).unwrap(),
130                criterion::BatchSize::SmallInput,
131            );
132        });
133    }
134
135    // Variant 2: 1000 subcalls with no value transfer (same account)
136    {
137        let mut db = InMemoryDB::default();
138        db.insert_account_info(
139            BENCH_CALLER,
140            AccountInfo {
141                balance: U256::from(u128::MAX),
142                ..Default::default()
143            },
144        );
145        db.insert_account_info(
146            BENCH_TARGET,
147            AccountInfo {
148                code: Some(make_loop_call_bytecode(SUBCALL_TARGET_A, 0)),
149                ..Default::default()
150            },
151        );
152        db.insert_account_info(
153            SUBCALL_TARGET_A,
154            AccountInfo {
155                code: Some(make_stop_bytecode()),
156                ..Default::default()
157            },
158        );
159
160        let mut evm = Context::mainnet()
161            .with_db(db)
162            .modify_cfg_chained(|c| {
163                c.disable_nonce_check = true;
164                c.tx_gas_limit_cap = Some(u64::MAX);
165            })
166            .build_mainnet();
167
168        let tx = TxEnv::builder()
169            .caller(BENCH_CALLER)
170            .kind(TxKind::Call(BENCH_TARGET))
171            .gas_limit(u64::MAX)
172            .build()
173            .unwrap();
174
175        criterion.bench_function("subcall_1000_same_account", |b| {
176            b.iter_batched(
177                || tx.clone(),
178                |input| evm.transact_one(input).unwrap(),
179                criterion::BatchSize::SmallInput,
180            );
181        });
182    }
183
184    // Variant 3: 1000 subcalls where each target does another subcall (nested)
185    {
186        let mut db = InMemoryDB::default();
187        db.insert_account_info(
188            BENCH_CALLER,
189            AccountInfo {
190                balance: U256::from(u128::MAX),
191                ..Default::default()
192            },
193        );
194        db.insert_account_info(
195            BENCH_TARGET,
196            AccountInfo {
197                code: Some(make_loop_call_bytecode(SUBCALL_TARGET_A, 0)),
198                ..Default::default()
199            },
200        );
201        db.insert_account_info(
202            SUBCALL_TARGET_A,
203            AccountInfo {
204                code: Some(make_subcall_bytecode(SUBCALL_TARGET_B)),
205                ..Default::default()
206            },
207        );
208        db.insert_account_info(
209            SUBCALL_TARGET_B,
210            AccountInfo {
211                code: Some(make_stop_bytecode()),
212                ..Default::default()
213            },
214        );
215
216        let mut evm = Context::mainnet()
217            .with_db(db)
218            .modify_cfg_chained(|c| {
219                c.disable_nonce_check = true;
220                c.tx_gas_limit_cap = Some(u64::MAX);
221            })
222            .build_mainnet();
223
224        let tx = TxEnv::builder()
225            .caller(BENCH_CALLER)
226            .kind(TxKind::Call(BENCH_TARGET))
227            .gas_limit(u64::MAX)
228            .build()
229            .unwrap();
230
231        criterion.bench_function("subcall_1000_nested", |b| {
232            b.iter_batched(
233                || tx.clone(),
234                |input| evm.transact_one(input).unwrap(),
235                criterion::BatchSize::SmallInput,
236            );
237        });
238    }
239}