Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions prdoc/pr_10366.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
title: '[pallet-revive] update evm create benchmark'
doc:
- audience: Runtime Dev
description: |
Add a benchmark for the EVM CREATE instruction.

We are currently reusing the `seal_instantiate` benchmark from PVM instantiation, which is incorrect because instantiating an EVM contract takes different arguments and follows a different code path than creating a PVM contract.

This benchmark performs the following steps:

- Generates init bytecode of size i, optionally including a balance with dust.
- Executes the init code that triggers a single benchmark opcode returning a runtime code of the maximum allowed size (qrevm::primitives::eip170::MAX_CODE_SIZE`).
crates:
- name: pallet-revive
bump: patch
50 changes: 48 additions & 2 deletions substrate/frame/revive/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use crate::{
storage::WriteOutcome,
vm::{
evm,
evm::{instructions::host, Interpreter},
evm::{instructions, instructions::utility::IntoAddress, Interpreter},
pvm,
},
Pallet as Contracts, *,
Expand Down Expand Up @@ -2090,6 +2090,52 @@ mod benchmarks {
Ok(())
}

// t: with or without some value to transfer
// d: with or without dust value to transfer
// i: size of the init code (max 49152 bytes per EIP-3860)
#[benchmark(pov_mode = Measured)]
fn evm_instantiate(
t: Linear<0, 1>,
d: Linear<0, 1>,
i: Linear<{ 10 * 1024 }, { 48 * 1024 }>,
) -> Result<(), BenchmarkError> {
use crate::vm::evm::instructions::BENCH_INIT_CODE;
let mut setup = CallSetup::<T>::new(VmBinaryModule::dummy());
setup.set_origin(ExecOrigin::from_account_id(setup.contract().account_id.clone()));
setup.set_balance(caller_funding::<T>());

let (mut ext, _) = setup.ext();
let mut interpreter = Interpreter::new(Default::default(), Default::default(), &mut ext);

let value = {
let value: BalanceOf<T> = (1_000_000u32 * t).into();
let dust = 100u32 * d;
Pallet::<T>::convert_native_to_evm(BalanceWithDust::new_unchecked::<T>(value, dust))
};

let init_code = vec![BENCH_INIT_CODE; i as usize];
let _ = interpreter.memory.resize(0, init_code.len());
interpreter.memory.set_data(0, 0, init_code.len(), &init_code);

// Setup stack for create instruction [value, offset, size]
let _ = interpreter.stack.push(U256::from(init_code.len()));
let _ = interpreter.stack.push(U256::zero());
let _ = interpreter.stack.push(value);

let result;
#[block]
{
result = instructions::contract::create::<false, _>(&mut interpreter);
}

assert!(result.is_continue());
let addr = interpreter.stack.top().unwrap().into_address();
assert!(AccountInfo::<T>::load_contract(&addr).is_some());
assert_eq!(Pallet::<T>::code(&addr).len(), revm::primitives::eip170::MAX_CODE_SIZE);
assert_eq!(Pallet::<T>::evm_balance(&addr), value, "balance should hold {value:?}");
Ok(())
}

// `n`: Input to hash in bytes
#[benchmark(pov_mode = Measured)]
fn sha2_256(n: Linear<0, { limits::code::BLOB_BYTES }>) {
Expand Down Expand Up @@ -2578,7 +2624,7 @@ mod benchmarks {
let result;
#[block]
{
result = host::extcodecopy(&mut interpreter);
result = instructions::host::extcodecopy(&mut interpreter);
}

assert!(result.is_continue());
Expand Down
7 changes: 2 additions & 5 deletions substrate/frame/revive/src/vm/evm/instructions/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,8 @@ pub fn create<const IS_CREATE2: bool, E: Ext>(
let [value, code_offset, len] = interpreter.stack.popn()?;
let len = as_usize_or_halt::<E::T>(len)?;

// TODO: We do not charge for the new code in storage. When implementing the new gas:
// Introduce EthInstantiateWithCode, which shall charge gas based on the code length.
// See #9577 for more context.
interpreter.ext.charge_or_halt(RuntimeCosts::Instantiate {
input_data_len: len as u32, // We charge for initcode execution
interpreter.ext.charge_or_halt(RuntimeCosts::Create {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What aboute CREATE2? Not sure if this is attackable but for completeness I'd expect the extra argument pop and different code path for the salted address to be accounted for.

Copy link
Contributor Author

@pgherveou pgherveou Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah true we could add it in there, or find out which one is slowest and use that, we will do that as a follow up, I think the overall impact on the weight is negligible

init_code_len: len as u32,
balance_transfer: Pallet::<E::T>::has_balance(value),
dust_transfer: Pallet::<E::T>::has_dust(value),
})?;
Expand Down
9 changes: 9 additions & 0 deletions substrate/frame/revive/src/vm/evm/instructions/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,12 @@ pub fn invalid<E: Ext>(interpreter: &mut Interpreter<E>) -> ControlFlow<Halt> {
interpreter.ext.gas_meter_mut().consume_all();
ControlFlow::Break(Error::<E::T>::InvalidInstruction.into())
}

/// bench_init opcode.
/// Returns a runtime code that fills the maximum allowed code size.
#[cfg(feature = "runtime-benchmarks")]
pub fn bench_init_code() -> ControlFlow<Halt> {
let runtime_code =
alloc::vec![revm::bytecode::opcode::STOP; revm::primitives::eip170::MAX_CODE_SIZE];
ControlFlow::Break(Halt::Return(runtime_code))
}
13 changes: 8 additions & 5 deletions substrate/frame/revive/src/vm/evm/instructions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,11 @@ mod bitwise;
/// Block information instructions (COINBASE, TIMESTAMP, etc.).
mod block_info;
/// Contract operations (CALL, CREATE, DELEGATECALL, etc.).
mod contract;
pub mod contract;
/// Control flow instructions (JUMP, JUMPI, REVERT, etc.).
mod control;
/// Host environment interactions (SLOAD, SSTORE, LOG, etc.).
#[cfg(feature = "runtime-benchmarks")]
pub mod host;
#[cfg(not(feature = "runtime-benchmarks"))]
mod host;
/// Memory operations (MLOAD, MSTORE, MSIZE, etc.).
mod memory;
/// Stack operations (PUSH, POP, DUP, SWAP, etc.).
Expand All @@ -44,7 +41,10 @@ mod system;
/// Transaction information instructions (ORIGIN, GASPRICE, etc.).
mod tx_info;
/// Utility functions and helpers for instruction implementation.
mod utility;
pub mod utility;

#[cfg(feature = "runtime-benchmarks")]
pub const BENCH_INIT_CODE: u8 = 0xfe; // Arbitrary unused opcode for benchmarking

pub fn exec_instruction<E: Ext>(
interpreter: &mut Interpreter<E>,
Expand Down Expand Up @@ -212,6 +212,9 @@ pub fn exec_instruction<E: Ext>(
REVERT => control::revert(interpreter),
SELFDESTRUCT => host::selfdestruct(interpreter),

#[cfg(feature = "runtime-benchmarks")]
BENCH_INIT_CODE => control::bench_init_code(),

_ => control::invalid(interpreter),
}
}
2 changes: 1 addition & 1 deletion substrate/frame/revive/src/vm/evm/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ impl<T: Config> Stack<T> {
}

/// Get a reference to the top stack item without removing it
#[cfg(test)]
#[cfg(any(test, feature = "runtime-benchmarks"))]
pub fn top(&self) -> Option<&U256> {
self.stack.last()
}
Expand Down
8 changes: 8 additions & 0 deletions substrate/frame/revive/src/vm/runtime_costs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ pub enum RuntimeCosts {
CallInputCloned(u32),
/// Weight of calling `seal_instantiate`.
Instantiate { input_data_len: u32, balance_transfer: bool, dust_transfer: bool },
/// Weight of calling `Create` opcode.
Create { init_code_len: u32, balance_transfer: bool, dust_transfer: bool },
/// Weight of calling `Ripemd160` precompile for the given input size.
Ripemd160(u32),
/// Weight of calling `Sha256` precompile for the given input size.
Expand Down Expand Up @@ -308,9 +310,15 @@ impl<T: Config> Token<T> for RuntimeCosts {
CallInputCloned(len) => cost_args!(seal_call, 0, 0, len),
Instantiate { input_data_len, balance_transfer, dust_transfer } =>
T::WeightInfo::seal_instantiate(
balance_transfer.into(),
dust_transfer.into(),
input_data_len,
),
Create { init_code_len, balance_transfer, dust_transfer } =>
T::WeightInfo::evm_instantiate(
balance_transfer.into(),
dust_transfer.into(),
init_code_len,
),
HashSha256(len) => T::WeightInfo::sha2_256(len),
Ripemd160(len) => T::WeightInfo::ripemd_160(len),
Expand Down
Loading
Loading