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    Bytecode::new_raw(code.into())
51}
52
53/// Minimal contract that just STOPs.
54fn make_stop_bytecode() -> Bytecode {
55    Bytecode::new_raw([opcode::STOP].into())
56}
57
58/// Constructs bytecode that does a single CALL (no value) to `target`, then STOPs.
59fn make_subcall_bytecode(target: Address) -> Bytecode {
60    let mut code = vec![
61        opcode::PUSH1,
62        0x00, // retSize
63        opcode::PUSH1,
64        0x00, // retOffset
65        opcode::PUSH1,
66        0x00, // argsSize
67        opcode::PUSH1,
68        0x00, // argsOffset
69        opcode::PUSH1,
70        0x00,           // value (no transfer)
71        opcode::PUSH20, // target address
72    ];
73    code.extend_from_slice(target.as_slice());
74    code.extend_from_slice(&[opcode::GAS, opcode::CALL, opcode::POP, opcode::STOP]);
75    Bytecode::new_raw(code.into())
76}
77
78pub fn run(criterion: &mut Criterion) {
79    // Variant 1: 1000 subcalls each transferring 1 wei
80    {
81        let mut db = InMemoryDB::default();
82        db.insert_account_info(
83            BENCH_CALLER,
84            AccountInfo {
85                balance: U256::from(u128::MAX),
86                ..Default::default()
87            },
88        );
89        db.insert_account_info(
90            BENCH_TARGET,
91            AccountInfo {
92                balance: U256::from(u128::MAX),
93                code: Some(make_loop_call_bytecode(SUBCALL_TARGET_A, 1)),
94                ..Default::default()
95            },
96        );
97        db.insert_account_info(
98            SUBCALL_TARGET_A,
99            AccountInfo {
100                code: Some(make_stop_bytecode()),
101                ..Default::default()
102            },
103        );
104
105        let mut evm = Context::mainnet()
106            .with_db(db)
107            .modify_cfg_chained(|c| {
108                c.disable_nonce_check = true;
109                c.tx_gas_limit_cap = Some(u64::MAX);
110            })
111            .build_mainnet();
112
113        let tx = TxEnv::builder()
114            .caller(BENCH_CALLER)
115            .kind(TxKind::Call(BENCH_TARGET))
116            .gas_limit(u64::MAX)
117            .build()
118            .unwrap();
119
120        criterion.bench_function("subcall_1000_transfer_1wei", |b| {
121            b.iter_batched(
122                || tx.clone(),
123                |input| evm.transact_one(input).unwrap(),
124                criterion::BatchSize::SmallInput,
125            );
126        });
127    }
128
129    // Variant 2: 1000 subcalls with no value transfer (same account)
130    {
131        let mut db = InMemoryDB::default();
132        db.insert_account_info(
133            BENCH_CALLER,
134            AccountInfo {
135                balance: U256::from(u128::MAX),
136                ..Default::default()
137            },
138        );
139        db.insert_account_info(
140            BENCH_TARGET,
141            AccountInfo {
142                code: Some(make_loop_call_bytecode(SUBCALL_TARGET_A, 0)),
143                ..Default::default()
144            },
145        );
146        db.insert_account_info(
147            SUBCALL_TARGET_A,
148            AccountInfo {
149                code: Some(make_stop_bytecode()),
150                ..Default::default()
151            },
152        );
153
154        let mut evm = Context::mainnet()
155            .with_db(db)
156            .modify_cfg_chained(|c| {
157                c.disable_nonce_check = true;
158                c.tx_gas_limit_cap = Some(u64::MAX);
159            })
160            .build_mainnet();
161
162        let tx = TxEnv::builder()
163            .caller(BENCH_CALLER)
164            .kind(TxKind::Call(BENCH_TARGET))
165            .gas_limit(u64::MAX)
166            .build()
167            .unwrap();
168
169        criterion.bench_function("subcall_1000_same_account", |b| {
170            b.iter_batched(
171                || tx.clone(),
172                |input| evm.transact_one(input).unwrap(),
173                criterion::BatchSize::SmallInput,
174            );
175        });
176    }
177
178    // Variant 3: 1000 subcalls where each target does another subcall (nested)
179    {
180        let mut db = InMemoryDB::default();
181        db.insert_account_info(
182            BENCH_CALLER,
183            AccountInfo {
184                balance: U256::from(u128::MAX),
185                ..Default::default()
186            },
187        );
188        db.insert_account_info(
189            BENCH_TARGET,
190            AccountInfo {
191                code: Some(make_loop_call_bytecode(SUBCALL_TARGET_A, 0)),
192                ..Default::default()
193            },
194        );
195        db.insert_account_info(
196            SUBCALL_TARGET_A,
197            AccountInfo {
198                code: Some(make_subcall_bytecode(SUBCALL_TARGET_B)),
199                ..Default::default()
200            },
201        );
202        db.insert_account_info(
203            SUBCALL_TARGET_B,
204            AccountInfo {
205                code: Some(make_stop_bytecode()),
206                ..Default::default()
207            },
208        );
209
210        let mut evm = Context::mainnet()
211            .with_db(db)
212            .modify_cfg_chained(|c| {
213                c.disable_nonce_check = true;
214                c.tx_gas_limit_cap = Some(u64::MAX);
215            })
216            .build_mainnet();
217
218        let tx = TxEnv::builder()
219            .caller(BENCH_CALLER)
220            .kind(TxKind::Call(BENCH_TARGET))
221            .gas_limit(u64::MAX)
222            .build()
223            .unwrap();
224
225        criterion.bench_function("subcall_1000_nested", |b| {
226            b.iter_batched(
227                || tx.clone(),
228                |input| evm.transact_one(input).unwrap(),
229                criterion::BatchSize::SmallInput,
230            );
231        });
232    }
233}