#![allow(missing_docs)]
use alloy_primitives::{address, Address, Bytes, U256};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use mega_evm::{
test_utils::{BytecodeBuilder, MemoryDatabase},
MegaSpecId,
};
use revm::{
bytecode::opcode::{
ADD, CALL, COINBASE, CREATE, CREATE2, DELEGATECALL, GAS, LOG0, LOG1, LOG2, LOG4, NUMBER,
POP, PUSH0, SELFDESTRUCT, SLOAD, SSTORE, STATICCALL, STOP, TIMESTAMP,
},
context::tx::TxEnvBuilder,
ExecuteEvm,
};
mod common;
use common::{
add_baseline_rows, add_baseline_rows_suffixed, build_mega_tx, make_mega_evm, CallParams,
LatestDbBuilder, SPEC_IDS,
};
const CALLER: Address = address!("0000000000000000000000000000000000100000");
const CONTRACT: Address = address!("0000000000000000000000000000000000100002");
const SECONDARY: Address = address!("0000000000000000000000000000000000100003");
fn execute(spec: MegaSpecId, db: MemoryDatabase, gas_limit: u64, data: Bytes) {
let mut evm = make_mega_evm(db, spec);
let tx = TxEnvBuilder::new()
.caller(CALLER)
.call(CONTRACT)
.gas_limit(gas_limit)
.data(data)
.build_fill();
let r = evm.transact(build_mega_tx(tx)).expect("transaction should succeed");
assert!(r.result.is_success(), "transaction should succeed: {:?}", r.result);
black_box(r);
}
fn execute_any_result(spec: MegaSpecId, db: MemoryDatabase, gas_limit: u64) {
let mut evm = make_mega_evm(db, spec);
let tx = TxEnvBuilder::new().caller(CALLER).call(CONTRACT).gas_limit(gas_limit).build_fill();
let r = evm.transact(build_mega_tx(tx)).expect("transaction should not error");
black_box(r);
}
fn make_db(bytecode: Bytes) -> MemoryDatabase {
MemoryDatabase::default()
.account_code(CONTRACT, bytecode)
.account_balance(CALLER, U256::from(10).pow(U256::from(18)))
}
fn make_latest_db(
bytecode: Bytes,
) -> revm_latest::database::CacheDB<revm_latest::database::EmptyDB> {
LatestDbBuilder::new()
.account_code(CONTRACT, bytecode)
.account_balance(CALLER, U256::from(10).pow(U256::from(18)))
.build()
}
fn standard_params() -> CallParams {
CallParams { caller: CALLER, target: CONTRACT, gas_limit: 10_000_000_000, ..Default::default() }
}
const VOLATILE_ITERATIONS: usize = 100;
fn generate_baseline_bytecode(iterations: usize) -> Bytes {
let mut builder = BytecodeBuilder::default();
for _ in 0..iterations {
builder = builder.push_number(1u64).push_number(2u64).append(ADD).append(POP);
}
builder.build()
}
fn generate_coinbase_bytecode(iterations: usize) -> Bytes {
let mut builder = BytecodeBuilder::default();
for _ in 0..iterations {
builder = builder.append(COINBASE).append(POP);
}
builder.build()
}
fn generate_timestamp_bytecode(iterations: usize) -> Bytes {
let mut builder = BytecodeBuilder::default();
for _ in 0..iterations {
builder = builder.append(TIMESTAMP).append(POP);
}
builder.build()
}
fn generate_number_bytecode(iterations: usize) -> Bytes {
let mut builder = BytecodeBuilder::default();
for _ in 0..iterations {
builder = builder.append(NUMBER).append(POP);
}
builder.build()
}
fn bench_volatile_data(c: &mut Criterion) {
let baseline = generate_baseline_bytecode(VOLATILE_ITERATIONS);
let coinbase = generate_coinbase_bytecode(VOLATILE_ITERATIONS);
let timestamp = generate_timestamp_bytecode(VOLATILE_ITERATIONS);
let number = generate_number_bytecode(VOLATILE_ITERATIONS);
let mut group = c.benchmark_group("volatile_data");
for &(spec_name, spec) in SPEC_IDS {
group.bench_function(format!("{spec_name}/baseline_add"), |b| {
b.iter(|| execute(spec, make_db(baseline.clone()), 10_000_000_000, Bytes::new()))
});
group.bench_function(format!("{spec_name}/coinbase"), |b| {
b.iter(|| execute(spec, make_db(coinbase.clone()), 10_000_000_000, Bytes::new()))
});
group.bench_function(format!("{spec_name}/timestamp"), |b| {
b.iter(|| execute(spec, make_db(timestamp.clone()), 10_000_000_000, Bytes::new()))
});
group.bench_function(format!("{spec_name}/number"), |b| {
b.iter(|| execute(spec, make_db(number.clone()), 10_000_000_000, Bytes::new()))
});
}
group.finish();
}
fn bench_gas_detention_computation(c: &mut Criterion) {
let computation_only = {
let mut builder = BytecodeBuilder::default();
for _ in 0..500 {
builder = builder.push_number(1u64).push_number(2u64).append(ADD).append(POP);
}
builder.build()
};
let volatile_then_compute = {
let mut builder = BytecodeBuilder::default();
builder = builder.append(COINBASE).append(POP);
for _ in 0..500 {
builder = builder.push_number(1u64).push_number(2u64).append(ADD).append(POP);
}
builder.build()
};
let mut group = c.benchmark_group("gas_detention_computation");
for &(spec_name, spec) in SPEC_IDS {
group.bench_function(format!("{spec_name}/compute_only_500"), |b| {
b.iter(|| {
execute(spec, make_db(computation_only.clone()), 10_000_000_000, Bytes::new())
})
});
group.bench_function(format!("{spec_name}/volatile_then_compute_500"), |b| {
b.iter(|| {
execute(spec, make_db(volatile_then_compute.clone()), 10_000_000_000, Bytes::new())
})
});
}
group.finish();
}
const LOG_ITERATIONS: usize = 50;
fn generate_log0_bytecode(iterations: usize, data_size: usize) -> Bytes {
let mut builder = BytecodeBuilder::default();
for _ in 0..iterations {
builder = builder.push_number(data_size as u64).push_number(0u64).append(LOG0);
}
builder.build()
}
fn generate_log2_bytecode(iterations: usize, data_size: usize) -> Bytes {
let mut builder = BytecodeBuilder::default();
for _ in 0..iterations {
builder = builder
.push_number(0xdead_beef_u64)
.push_number(0xcafe_babe_u64)
.push_number(data_size as u64)
.push_number(0u64)
.append(LOG2);
}
builder.build()
}
fn generate_log4_bytecode(iterations: usize, data_size: usize) -> Bytes {
let mut builder = BytecodeBuilder::default();
for _ in 0..iterations {
builder = builder
.push_number(0x1111_u64)
.push_number(0x2222_u64)
.push_number(0x3333_u64)
.push_number(0x4444_u64)
.push_number(data_size as u64)
.push_number(0u64)
.append(LOG4);
}
builder.build()
}
fn bench_log_opcodes(c: &mut Criterion) {
let variants: &[(&str, Bytes)] = &[
("log0_32b", generate_log0_bytecode(LOG_ITERATIONS, 32)),
("log0_256b", generate_log0_bytecode(LOG_ITERATIONS, 256)),
("log2_32b", generate_log2_bytecode(LOG_ITERATIONS, 32)),
("log4_32b", generate_log4_bytecode(LOG_ITERATIONS, 32)),
("log4_256b", generate_log4_bytecode(LOG_ITERATIONS, 256)),
];
let mut group = c.benchmark_group("log_opcodes");
let params = standard_params();
for (variant, bytecode) in variants {
let make_pinned = || make_db(bytecode.clone());
let make_latest = || make_latest_db(bytecode.clone());
add_baseline_rows_suffixed(&mut group, variant, ¶ms, &make_pinned, &make_latest);
for &(spec_name, spec) in SPEC_IDS {
group.bench_function(format!("{spec_name}/{variant}"), |b| {
b.iter(|| execute(spec, make_pinned(), 10_000_000_000, Bytes::new()))
});
}
}
group.finish();
}
const SSTORE_ITERATIONS: usize = 100;
fn generate_sstore_bytecode(iterations: usize) -> Bytes {
let mut builder = BytecodeBuilder::default();
for i in 0..iterations {
builder = builder.push_number(i as u64 + 1).push_number(i as u64).append(SSTORE);
}
builder.build()
}
fn generate_sload_bytecode(iterations: usize) -> Bytes {
let mut builder = BytecodeBuilder::default();
for i in 0..iterations {
builder = builder.push_number(i as u64).append(SLOAD).append(POP);
}
builder.build()
}
fn generate_sstore_sload_bytecode(iterations: usize) -> Bytes {
let mut builder = BytecodeBuilder::default();
for i in 0..iterations {
builder = builder
.push_number(i as u64 + 1)
.push_number(i as u64)
.append(SSTORE)
.push_number(i as u64)
.append(SLOAD)
.append(POP);
}
builder.build()
}
fn bench_sstore(c: &mut Criterion) {
let variants: &[(&str, Bytes)] = &[
("sstore_100", generate_sstore_bytecode(SSTORE_ITERATIONS)),
("sload_100", generate_sload_bytecode(SSTORE_ITERATIONS)),
("sstore_sload_100", generate_sstore_sload_bytecode(SSTORE_ITERATIONS)),
];
let mut group = c.benchmark_group("sstore_heavy");
let params = standard_params();
for (variant, bytecode) in variants {
let make_pinned = || make_db(bytecode.clone());
let make_latest = || make_latest_db(bytecode.clone());
add_baseline_rows_suffixed(&mut group, variant, ¶ms, &make_pinned, &make_latest);
for &(spec_name, spec) in SPEC_IDS {
group.bench_function(format!("{spec_name}/{variant}"), |b| {
b.iter(|| execute(spec, make_pinned(), 10_000_000_000, Bytes::new()))
});
}
}
group.finish();
}
fn make_create_bytecode(n_deploys: usize) -> Bytes {
let init_code: [u8; 5] = [0x60, 0x00, 0x60, 0x00, 0xf3];
let mut builder = BytecodeBuilder::default();
builder = builder.mstore(0, init_code);
for _ in 0..n_deploys {
builder = builder
.push_number(5u64) .push_number(0u64) .push_number(0u64) .append(CREATE)
.append(POP); }
builder.build()
}
fn make_create2_bytecode(n_deploys: usize) -> Bytes {
let init_code: [u8; 5] = [0x60, 0x00, 0x60, 0x00, 0xf3];
let mut builder = BytecodeBuilder::default();
builder = builder.mstore(0, init_code);
for i in 0..n_deploys {
builder = builder
.push_number(i as u64) .push_number(5u64) .push_number(0u64) .push_number(0u64) .append(CREATE2)
.append(POP);
}
builder.build()
}
fn bench_create_deploy(c: &mut Criterion) {
let variants: &[(&str, Bytes)] =
&[("create_10", make_create_bytecode(10)), ("create2_10", make_create2_bytecode(10))];
let mut group = c.benchmark_group("create_deploy");
group.sample_size(10);
let params = standard_params();
for (variant, bytecode) in variants {
let make_pinned = || make_db(bytecode.clone());
let make_latest = || make_latest_db(bytecode.clone());
add_baseline_rows_suffixed(&mut group, variant, ¶ms, &make_pinned, &make_latest);
for &(spec_name, spec) in SPEC_IDS {
group.bench_function(format!("{spec_name}/{variant}"), |b| {
b.iter(|| execute(spec, make_pinned(), 10_000_000_000, Bytes::new()))
});
}
}
group.finish();
}
fn bench_selfdestruct(c: &mut Criterion) {
let selfdestruct_code: Bytes = vec![PUSH0, SELFDESTRUCT].into();
let selfdestruct_specs: &[(&str, MegaSpecId)] = &[
("equivalence", MegaSpecId::EQUIVALENCE),
("rex2", MegaSpecId::REX2),
("rex4", MegaSpecId::REX4),
];
let mut group = c.benchmark_group("selfdestruct");
for &(spec_name, spec) in selfdestruct_specs {
group.bench_function(spec_name, |b| {
b.iter(|| {
execute_any_result(spec, make_db(selfdestruct_code.clone()), 10_000_000_000)
})
});
}
group.finish();
}
fn bench_call_value_empty_account(c: &mut Criterion) {
let existing_target: Address = address!("cccccccccccccccccccccccccccccccccccccccc");
let empty_target: Address = address!("dddddddddddddddddddddddddddddddddddddddd");
let make_call_with_value = |target: Address, n: usize| -> Bytes {
let mut builder = BytecodeBuilder::default();
for _ in 0..n {
builder = builder
.push_number(0u64) .push_number(0u64) .push_number(0u64) .push_number(0u64) .push_number(1u64) .push_address(target)
.append(GAS)
.append(CALL)
.append(POP);
}
builder.build()
};
let call_existing = make_call_with_value(existing_target, 50);
let call_empty = make_call_with_value(empty_target, 50);
let mut group = c.benchmark_group("call_value_empty_account");
let params = standard_params();
{
let make_pinned =
|| make_db(call_existing.clone()).account_balance(existing_target, U256::from(1));
let make_latest = || {
LatestDbBuilder::new()
.account_code(CONTRACT, call_existing.clone())
.account_balance(CALLER, U256::from(10).pow(U256::from(18)))
.account_balance(existing_target, U256::from(1))
.build()
};
add_baseline_rows_suffixed(
&mut group,
"existing_account_50",
¶ms,
&make_pinned,
&make_latest,
);
for &(spec_name, spec) in SPEC_IDS {
group.bench_function(format!("{spec_name}/existing_account_50"), |b| {
b.iter(|| execute(spec, make_pinned(), 10_000_000_000, Bytes::new()))
});
}
}
{
let make_pinned = || make_db(call_empty.clone());
let make_latest = || make_latest_db(call_empty.clone());
add_baseline_rows_suffixed(
&mut group,
"empty_account_50",
¶ms,
&make_pinned,
&make_latest,
);
for &(spec_name, spec) in SPEC_IDS {
group.bench_function(format!("{spec_name}/empty_account_50"), |b| {
b.iter(|| execute(spec, make_pinned(), 10_000_000_000, Bytes::new()))
});
}
}
group.finish();
}
const ORACLE_ADDRESS: Address = address!("6342000000000000000000000000000000000001");
fn make_staticcall_bytecode(target: Address, selector: [u8; 4]) -> Bytes {
let mut builder = BytecodeBuilder::default();
builder = builder.mstore(0, selector);
builder = builder
.push_number(32u64)
.push_number(32u64)
.push_number(4u64)
.push_number(0u64)
.push_address(target)
.append(GAS)
.append(STATICCALL)
.append(POP);
builder.build()
}
fn make_repeated_staticcall_bytecode(
target: Address,
selector: [u8; 4],
iterations: usize,
) -> Bytes {
let mut builder = BytecodeBuilder::default();
builder = builder.mstore(0, selector);
for _ in 0..iterations {
builder = builder
.push_number(32u64)
.push_number(32u64)
.push_number(4u64)
.push_number(0u64)
.push_address(target)
.append(GAS)
.append(STATICCALL)
.append(POP);
}
builder.build()
}
fn bench_system_contract(c: &mut Criterion) {
let access_control_addr: Address = address!("6342000000000000000000000000000000000004");
let is_disabled_selector: [u8; 4] = [0x9e, 0x8e, 0x7b, 0xc0];
let limit_control_addr: Address = address!("6342000000000000000000000000000000000005");
let remaining_gas_selector: [u8; 4] = [0xde, 0x85, 0xee, 0xf5];
let empty_contract_code = Bytes::from_static(&[STOP]);
const SYSTEM_CONTRACT_SPECS: &[(&str, MegaSpecId)] =
&[("rex4", MegaSpecId::REX4), ("rex5", MegaSpecId::REX5)];
{
let access_control_code =
make_staticcall_bytecode(access_control_addr, is_disabled_selector);
let limit_control_code =
make_staticcall_bytecode(limit_control_addr, remaining_gas_selector);
let regular_call_code = make_staticcall_bytecode(SECONDARY, [0x00, 0x00, 0x00, 0x00]);
let mut group = c.benchmark_group("system_contract_single");
for &(spec_name, spec) in SYSTEM_CONTRACT_SPECS {
group.bench_function(format!("{spec_name}/access_control"), |b| {
b.iter(|| {
execute(
spec,
make_db(access_control_code.clone()),
10_000_000_000,
Bytes::new(),
)
})
});
group.bench_function(format!("{spec_name}/limit_control"), |b| {
b.iter(|| {
execute(spec, make_db(limit_control_code.clone()), 10_000_000_000, Bytes::new())
})
});
group.bench_function(format!("{spec_name}/regular_contract"), |b| {
b.iter(|| {
let db = make_db(regular_call_code.clone())
.account_code(SECONDARY, empty_contract_code.clone());
execute(spec, db, 10_000_000_000, Bytes::new())
})
});
}
group.finish();
}
{
let n = 100;
let access_control_code =
make_repeated_staticcall_bytecode(access_control_addr, is_disabled_selector, n);
let limit_control_code =
make_repeated_staticcall_bytecode(limit_control_addr, remaining_gas_selector, n);
let regular_call_code =
make_repeated_staticcall_bytecode(SECONDARY, [0x00, 0x00, 0x00, 0x00], n);
let mut group = c.benchmark_group("system_contract_100x");
for &(spec_name, spec) in SYSTEM_CONTRACT_SPECS {
group.bench_function(format!("{spec_name}/access_control"), |b| {
b.iter(|| {
execute(
spec,
make_db(access_control_code.clone()),
10_000_000_000,
Bytes::new(),
)
})
});
group.bench_function(format!("{spec_name}/limit_control"), |b| {
b.iter(|| {
execute(spec, make_db(limit_control_code.clone()), 10_000_000_000, Bytes::new())
})
});
group.bench_function(format!("{spec_name}/regular_contract"), |b| {
b.iter(|| {
let db = make_db(regular_call_code.clone())
.account_code(SECONDARY, empty_contract_code.clone());
execute(spec, db, 10_000_000_000, Bytes::new())
})
});
}
group.finish();
}
}
fn bench_delegatecall_system_contract(c: &mut Criterion) {
let access_control_addr: Address = address!("6342000000000000000000000000000000000004");
let is_disabled_selector: [u8; 4] = [0x9e, 0x8e, 0x7b, 0xc0];
let staticcall_code = make_staticcall_bytecode(access_control_addr, is_disabled_selector);
let delegatecall_code = {
let mut builder = BytecodeBuilder::default();
builder = builder.mstore(0, is_disabled_selector);
builder = builder
.push_number(32u64) .push_number(32u64) .push_number(4u64) .push_number(0u64) .push_address(access_control_addr)
.append(GAS)
.append(DELEGATECALL)
.append(POP);
builder.build()
};
let mut group = c.benchmark_group("delegatecall_system_contract");
let spec = MegaSpecId::REX4;
group.bench_function("rex4/staticcall_intercepted", |b| {
b.iter(|| execute(spec, make_db(staticcall_code.clone()), 10_000_000_000, Bytes::new()))
});
group.bench_function("rex4/delegatecall_not_intercepted", |b| {
b.iter(|| execute_any_result(spec, make_db(delegatecall_code.clone()), 10_000_000_000))
});
group.finish();
}
fn bench_oracle_sload(c: &mut Criterion) {
let sload_bytecode = generate_sload_bytecode(50);
let mut group = c.benchmark_group("oracle_sload");
for &(spec_name, spec) in SPEC_IDS {
group.bench_function(format!("{spec_name}/regular_sload_50"), |b| {
b.iter(|| execute(spec, make_db(sload_bytecode.clone()), 10_000_000_000, Bytes::new()))
});
group.bench_function(format!("{spec_name}/oracle_sload_50"), |b| {
b.iter(|| {
let db = MemoryDatabase::default()
.account_code(ORACLE_ADDRESS, sload_bytecode.clone())
.account_balance(CALLER, U256::from(10).pow(U256::from(18)));
let mut evm = make_mega_evm(db, spec);
let tx = TxEnvBuilder::new()
.caller(CALLER)
.call(ORACLE_ADDRESS)
.gas_limit(10_000_000_000)
.build_fill();
let r = evm.transact(build_mega_tx(tx)).expect("should succeed");
assert!(r.result.is_success(), "oracle sload should succeed: {:?}", r.result);
black_box(r);
})
});
}
group.finish();
}
fn generate_mixed_workload_bytecode() -> Bytes {
let mut builder = BytecodeBuilder::default();
builder = builder.append(TIMESTAMP).append(POP).append(COINBASE).append(POP);
for i in 0..20u64 {
builder = builder.push_number(i + 1).push_number(i).append(SSTORE);
}
for _ in 0..10 {
builder =
builder.push_number(0xdead_beef_u64).push_number(32u64).push_number(0u64).append(LOG1);
}
for _ in 0..50 {
builder = builder.push_number(1u64).push_number(2u64).append(ADD).append(POP);
}
builder.build()
}
fn bench_mixed_workload(c: &mut Criterion) {
let bytecode = generate_mixed_workload_bytecode();
let mut group = c.benchmark_group("mixed_workload");
let params = standard_params();
let make_pinned = || make_db(bytecode.clone());
let make_latest = || make_latest_db(bytecode.clone());
add_baseline_rows(&mut group, ¶ms, &make_pinned, &make_latest);
for &(spec_name, spec) in SPEC_IDS {
group.bench_function(spec_name, |b| {
b.iter(|| execute(spec, make_pinned(), 10_000_000_000, Bytes::new()))
});
}
group.finish();
}
criterion_group!(
benches,
bench_volatile_data,
bench_gas_detention_computation,
bench_log_opcodes,
bench_sstore,
bench_create_deploy,
bench_selfdestruct,
bench_call_value_empty_account,
bench_system_contract,
bench_delegatecall_system_contract,
bench_oracle_sload,
bench_mixed_workload,
);
criterion_main!(benches);