Skip to content

Commit 4c7a845

Browse files
pgherveou0xRVEgithub-actions[bot]Robert van Eerdewijkathei
authored
[pallet-revive] revm refactor (#9818)
Refactor REVM implementation in `pallet-revive` This PR removes technical debt and eliminates tight coupling to specific REVM versions, facilitating integration with other projects (e.g., Foundry). After this refactoring, we primarily depend only on REVM's [`Bytecode`](https://docs.rs/revm/latest/revm/bytecode/struct.Bytecode.html) struct. ## Background Most of REVM's generic type system and trait abstractions are unused or had to be ignored to prevent bugs. Interactions with the host in pallet-revive are handled through the `Ext` trait, making REVM's other abstractions unnecessary or potentially harmful. Unused REVM abstractions included: - **Host trait**: Unused in the pallet, we relied on the `DummyHost` default mocked implementation - **Gas field**: Unused, the pallet uses its own gas accounting system - **Methods from `InputsTr`**: Unused most methods had panic implementations since they couldn't be relied upon - **Spec**: Unused: We only maintain the latest fork for each runtime ## Changes This refactor introduces: - **Interpreter**: Simplified struct containing only the fields actually needed - **Stack**: Simplified implementation using `sp_core::*` instead of `alloy_core::primitives::*` for better integration with the rest of the pallet - **Memory**: Simplified implementation providing only the methods actually needed - **Instructions**: - New instructions don't rely on macros and have a simplified signature: `Fn(&mut interpreter) -> ControlFlow<Halt>` - Removed function pointer table lookup for instructions in favor of match statements, - Unified gas charging: legacy u64 gas charging is now wrapped in `EVMGas` that implements `Token<T>` to provide the associated weight - Removed the `InterpreterAction`, this simplify the interpreter loop to: ```rust loop { let opcode = interpreter.bytecode.opcode(); interpreter.bytecode.relative_jump(1); exec_instruction(interpreter, opcode)?; } ``` - **Error handling**: Opcode that fail return Halt::Err(DispatchError), this remove the need from converting between InstructionResult and `ExecError` like we were previously doing and standardize errors generated on both backends --------- Co-authored-by: 0xRVE <[email protected]> Co-authored-by: cmd[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Robert van Eerdewijk <[email protected]> Co-authored-by: Alexander Theißen <[email protected]>
1 parent 3adf2e9 commit 4c7a845

39 files changed

+2510
-2169
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,7 @@ procfs = { version = "0.16.0" }
11811181
prometheus = { version = "0.13.0", default-features = false }
11821182
prometheus-endpoint = { path = "substrate/utils/prometheus", default-features = false, package = "substrate-prometheus-endpoint" }
11831183
prometheus-parse = { version = "0.2.2" }
1184+
proptest = { version = "1" }
11841185
prost = { version = "0.12.4" }
11851186
prost-build = { version = "0.13.2" }
11861187
pyroscope = { version = "0.5.8" }

prdoc/pr_9818.prdoc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
title: '[pallet-revive] revm refactor'
2+
doc:
3+
- audience: Runtime Dev
4+
description: |
5+
This PR refactors the REVM implementation in `pallet-revive`, reducing technical debt and decoupling it from specific REVM versions. This enables easier integration with other projects, such as Foundry, with dependencies limited to REVM's [`Bytecode`](https://docs.rs/revm/latest/revm/bytecode/struct.Bytecode.html).
6+
7+
**Background:**
8+
Many of REVM's generics and trait abstractions were unused or ignored to avoid bugs. The pallet interacts with the host via the `Ext` trait, rendering other REVM abstractions unnecessary.
9+
10+
Previously unused REVM components included:
11+
- `Host` trait: Unused, relied on the `DummyHost` mock.
12+
- Gas field: Not used, as the pallet uses its own accounting.
13+
- Methods from `InputsTr`: Most methods unused or had panic implementations.
14+
- `Spec`: Only maintaining the latest fork per runtime.
15+
16+
**Key Changes:**
17+
- Interpreter: Now a minimal struct with only necessary fields.
18+
- Stack: Uses `sp_core::U256` for better integration.
19+
- Memory: Simplified to needed methods only.
20+
- Instructions: Rewritten with simple signatures, using match statements instead of function pointer tables. Gas charging uses `EVMGas` wrapped in `Token<T>`. The interpreter loop has been significantly simplified.
21+
- Error Handling: Failed opcodes now return `Halt::Err(DispatchError)`, removing previous error conversions.
22+
23+
crates:
24+
- name: pallet-revive
25+
bump: patch
26+
- name: pallet-revive-fixtures
27+
bump: patch
28+

prdoc/pr_9887.prdoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
title: Rve/9565 pallet revive evm backend ensure memory dos safety2
2+
doc:
3+
- audience: Runtime Dev
4+
description: fixes https://github.com/paritytech/polkadot-sdk/issues/9565
5+
crates:
6+
- name: pallet-revive
7+
bump: patch

substrate/frame/revive/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ pallet-proxy = { workspace = true, default-features = true }
7474
pallet-revive-fixtures = { workspace = true, default-features = true }
7575
pallet-timestamp = { workspace = true, default-features = true }
7676
pallet-utility = { workspace = true, default-features = true }
77+
proptest = { workspace = true }
7778
sp-keystore = { workspace = true, default-features = true }
7879
sp-tracing = { workspace = true, default-features = true }
7980

substrate/frame/revive/fixtures/contracts/TransactionInfo.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ contract TransactionInfo {
66
return tx.origin;
77
}
88

9-
function gasprice() public view returns (uint256) {
9+
function gasprice() public view returns (uint64) {
1010
return uint64(tx.gasprice);
1111
}
1212

substrate/frame/revive/src/benchmarking.rs

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use crate::{
3434
storage::WriteOutcome,
3535
vm::{
3636
evm,
37-
evm::{instructions::instruction_table, EVMInterpreter},
37+
evm::{instructions::host, Interpreter},
3838
pvm,
3939
},
4040
Pallet as Contracts, *,
@@ -54,13 +54,7 @@ use frame_system::RawOrigin;
5454
use pallet_revive_uapi::{
5555
pack_hi_lo, precompiles::system::ISystem, CallFlags, ReturnErrorCode, StorageFlags,
5656
};
57-
use revm::{
58-
bytecode::{opcode::EXTCODECOPY, Bytecode},
59-
interpreter::{
60-
host::DummyHost, interpreter_types::MemoryTr, InstructionContext, Interpreter, SharedMemory,
61-
},
62-
primitives,
63-
};
57+
use revm::bytecode::Bytecode;
6458
use sp_consensus_aura::AURA_ENGINE_ID;
6559
use sp_consensus_babe::{
6660
digests::{PreDigest, PrimaryPreDigest},
@@ -2367,7 +2361,7 @@ mod benchmarks {
23672361
#[benchmark(pov_mode = Measured)]
23682362
fn evm_opcode(r: Linear<0, 10_000>) -> Result<(), BenchmarkError> {
23692363
let module = VmBinaryModule::evm_noop(r);
2370-
let inputs = evm::EVMInputs::new(vec![]);
2364+
let inputs = vec![];
23712365

23722366
let code = Bytecode::new_raw(revm::primitives::Bytes::from(module.code.clone()));
23732367
let mut setup = CallSetup::<T>::new(module);
@@ -2472,38 +2466,22 @@ mod benchmarks {
24722466
let mut setup = CallSetup::<T>::new(module);
24732467
let contract = setup.contract();
24742468

2475-
let mut address: [u8; 32] = [0; 32];
2476-
address[12..].copy_from_slice(&contract.address.0);
2477-
24782469
let (mut ext, _) = setup.ext();
2479-
let mut interpreter: Interpreter<EVMInterpreter<'_, _>> = Interpreter {
2480-
extend: &mut ext,
2481-
input: Default::default(),
2482-
bytecode: Default::default(),
2483-
gas: Default::default(),
2484-
stack: Default::default(),
2485-
return_data: Default::default(),
2486-
memory: SharedMemory::new(),
2487-
runtime_flag: Default::default(),
2488-
};
2489-
2490-
let table = instruction_table::<'_, _>();
2491-
let extcodecopy_fn = table[EXTCODECOPY as usize];
2470+
let mut interpreter = Interpreter::new(Default::default(), Default::default(), &mut ext);
24922471

24932472
// Setup stack for extcodecopy instruction: [address, dest_offset, offset, size]
2494-
let _ = interpreter.stack.push(primitives::U256::from(n));
2495-
let _ = interpreter.stack.push(primitives::U256::from(0u32));
2496-
let _ = interpreter.stack.push(primitives::U256::from(0u32));
2497-
let _ = interpreter.stack.push(primitives::U256::from_be_bytes(address));
2498-
2499-
let mut host = DummyHost {};
2500-
let context = InstructionContext { interpreter: &mut interpreter, host: &mut host };
2473+
let _ = interpreter.stack.push(U256::from(n));
2474+
let _ = interpreter.stack.push(U256::from(0u32));
2475+
let _ = interpreter.stack.push(U256::from(0u32));
2476+
let _ = interpreter.stack.push(contract.address);
25012477

2478+
let result;
25022479
#[block]
25032480
{
2504-
extcodecopy_fn(context);
2481+
result = host::extcodecopy(&mut interpreter);
25052482
}
25062483

2484+
assert!(result.is_continue());
25072485
assert_eq!(
25082486
*interpreter.memory.slice(0..n as usize),
25092487
PristineCode::<T>::get(contract.info()?.code_hash).unwrap()[0..n as usize],

substrate/frame/revive/src/exec.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use crate::{
3030
Pallet as Contracts, RuntimeCosts, LOG_TARGET,
3131
};
3232
use alloc::vec::Vec;
33-
use core::{fmt::Debug, marker::PhantomData, mem};
33+
use core::{fmt::Debug, marker::PhantomData, mem, ops::ControlFlow};
3434
use frame_support::{
3535
crypto::ecdsa::ECDSAExt,
3636
dispatch::DispatchResult,
@@ -290,6 +290,14 @@ pub trait PrecompileExt: sealing::Sealed {
290290
.adjust_gas(charged, RuntimeCosts::Precompile(actual_weight));
291291
}
292292

293+
/// Charges the gas meter with the given token or halts execution if not enough gas is left.
294+
fn charge_or_halt<Tok: crate::gas::Token<Self::T>>(
295+
&mut self,
296+
token: Tok,
297+
) -> ControlFlow<crate::vm::evm::Halt, crate::gas::ChargedAmount> {
298+
self.gas_meter_mut().charge_or_halt(token)
299+
}
300+
293301
/// Call (possibly transferring some amount of funds) into the specified account.
294302
fn call(
295303
&mut self,

substrate/frame/revive/src/gas.rs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@
1414
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1515
// See the License for the specific language governing permissions and
1616
// limitations under the License.
17-
18-
use crate::{exec::ExecError, weights::WeightInfo, Config, Error};
19-
use core::marker::PhantomData;
17+
use crate::{exec::ExecError, vm::evm::Halt, weights::WeightInfo, Config, Error};
18+
use core::{marker::PhantomData, ops::ControlFlow};
2019
use frame_support::{
2120
dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, PostDispatchInfo},
2221
weights::Weight,
@@ -219,16 +218,13 @@ impl<T: Config> GasMeter<T> {
219218
Ok(ChargedAmount(amount))
220219
}
221220

222-
/// Charge the specified amount of EVM gas.
223-
/// This is used for basic opcodes (e.g arithmetic, bitwise, ...) that don't have a dedicated
224-
/// benchmark
225-
pub fn charge_evm_gas(&mut self, gas: u64) -> Result<(), DispatchError> {
226-
let base_cost = T::WeightInfo::evm_opcode(1).saturating_sub(T::WeightInfo::evm_opcode(0));
227-
self.gas_left = self
228-
.gas_left
229-
.checked_sub(&base_cost.saturating_mul(gas))
230-
.ok_or_else(|| Error::<T>::OutOfGas)?;
231-
Ok(())
221+
/// Charge the specified token amount of gas or halt if not enough gas is left.
222+
pub fn charge_or_halt<Tok: Token<T>>(
223+
&mut self,
224+
token: Tok,
225+
) -> ControlFlow<Halt, ChargedAmount> {
226+
self.charge(token)
227+
.map_or_else(|_| ControlFlow::Break(Error::<T>::OutOfGas.into()), ControlFlow::Continue)
232228
}
233229

234230
/// Adjust a previously charged amount down to its actual amount.

substrate/frame/revive/src/limits.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ pub const PAGE_SIZE: u32 = 4 * 1024;
7373
/// Which should always be enough because Solidity allows for 16 local (stack) variables.
7474
pub const IMMUTABLE_BYTES: u32 = 4 * 1024;
7575

76+
/// upperbound of memory that can be used by the EVM interpreter.
77+
pub const EVM_MEMORY_BYTES: u32 = 1024 * 1024;
78+
79+
/// EVM interpreter stack limit.
80+
pub const EVM_STACK_LIMIT: u32 = 1024;
81+
7682
/// Limits that are only enforced on code upload.
7783
///
7884
/// # Note
@@ -254,7 +260,14 @@ const fn memory_required() -> u32 {
254260
let max_call_depth = CALL_STACK_DEPTH + 1;
255261

256262
let per_stack_memory = code::PURGABLE_MEMORY_LIMIT + TRANSIENT_STORAGE_BYTES * 2;
257-
let per_frame_memory = code::BASELINE_MEMORY_LIMIT + CALLDATA_BYTES * 2;
263+
264+
let evm_max_initcode_size = revm::primitives::eip3860::MAX_INITCODE_SIZE as u32;
265+
let evm_overhead = EVM_MEMORY_BYTES + evm_max_initcode_size + EVM_STACK_LIMIT * 32;
266+
let per_frame_memory = if code::BASELINE_MEMORY_LIMIT > evm_overhead {
267+
code::BASELINE_MEMORY_LIMIT
268+
} else {
269+
evm_overhead
270+
} + CALLDATA_BYTES * 2;
258271

259272
per_stack_memory + max_call_depth * per_frame_memory
260273
}

0 commit comments

Comments
 (0)