Skip to main content

revm_handler/
system_call.rs

1//! System call logic for external state transitions required by certain EIPs (notably [EIP-2935](https://eips.ethereum.org/EIPS/eip-2935) and [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788)).
2//!
3//! These EIPs require the client to perform special system calls to update state (such as block hashes or beacon roots) at block boundaries, outside of normal EVM transaction execution. REVM provides the system call mechanism, but the actual state transitions must be performed by the client or test harness, not by the EVM itself.
4//!
5//! # Example: Using `system_call` for pre/post block hooks
6//!
7//! The client should use [`SystemCallEvm::system_call`] method to perform required state updates before or after block execution, as specified by the EIP:
8//!
9//! ```rust,ignore
10//! // Example: update beacon root (EIP-4788) at the start of a block
11//! let beacon_root: Bytes = ...; // obtained from consensus layer
12//! let beacon_contract: Address = "0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02".parse().unwrap();
13//! evm.system_call(beacon_contract, beacon_root)?;
14//!
15//! // Example: update block hash (EIP-2935) at the end of a block
16//! let block_hash: Bytes = ...; // new block hash
17//! let history_contract: Address = "0x0000F90827F1C53a10cb7A02335B175320002935".parse().unwrap();
18//! evm.system_call(history_contract, block_hash)?;
19//! ```
20//!
21//! See the book section on [External State Transitions](../../book/src/external_state_transitions.md) for more details.
22use crate::{
23    frame::EthFrame, instructions::InstructionProvider, ExecuteCommitEvm, ExecuteEvm, Handler,
24    MainnetHandler, PrecompileProvider,
25};
26use context::{result::ExecResultAndState, ContextSetters, ContextTr, Evm, JournalTr, TxEnv};
27#[cfg(feature = "asyncdb")]
28use database_interface::async_db::{on_fiber_result_with_stack, AsyncResult};
29use database_interface::DatabaseCommit;
30use interpreter::{interpreter::EthInterpreter, InterpreterResult};
31use primitives::{address, eip8037, Address, Bytes, TxKind};
32use state::EvmState;
33#[cfg(feature = "asyncdb")]
34use std::ptr::NonNull;
35
36/// The system address used for system calls.
37pub const SYSTEM_ADDRESS: Address = address!("0xfffffffffffffffffffffffffffffffffffffffe");
38
39/// Maximum number of SSTOREs a system call reserves state gas for under EIP-8037.
40pub const SYSTEM_MAX_SSTORES_PER_CALL: u64 = 16;
41
42/// Regular-gas budget of a system call: the `gas_left` a system contract runs on.
43///
44/// This is the pre-EIP-8037 system call gas limit. Under EIP-8037 it stays the
45/// regular budget while state gas is provided through the reservoir.
46pub const SYSTEM_CALL_REGULAR_GAS_LIMIT: u64 = 30_000_000;
47
48/// State-gas reservoir of a system call under EIP-8037, sized for
49/// `SYSTEM_MAX_SSTORES_PER_CALL` fresh storage writes.
50pub const SYSTEM_CALL_STATE_GAS_RESERVOIR: u64 =
51    eip8037::SSTORE_SET_BYTES * eip8037::CPSB_GLAMSTERDAM * SYSTEM_MAX_SSTORES_PER_CALL;
52
53/// Gas limit for system calls under EIP-8037.
54///
55/// System calls get the base 30M regular-gas budget plus
56/// a state-gas reservoir sized for `SYSTEM_MAX_SSTORES_PER_CALL` storage writes.
57/// The split between the two is applied by [`Handler::system_call_gas`].
58///
59/// [`Handler::system_call_gas`]: crate::Handler::system_call_gas
60pub const SYSTEM_CALL_GAS_LIMIT: u64 =
61    SYSTEM_CALL_REGULAR_GAS_LIMIT + SYSTEM_CALL_STATE_GAS_RESERVOIR;
62
63/// Creates the system transaction with default values and set data and tx call target to system contract address
64/// that is going to be called.
65///
66/// The caller is set to be [`SYSTEM_ADDRESS`].
67///
68/// It is used inside [`SystemCallEvm`] and [`SystemCallCommitEvm`] traits to prepare EVM for system call execution.
69pub trait SystemCallTx: Sized {
70    /// Creates new transaction for system call.
71    fn new_system_tx(system_contract_address: Address, data: Bytes) -> Self {
72        Self::new_system_tx_with_caller(SYSTEM_ADDRESS, system_contract_address, data)
73    }
74
75    /// Creates a new system transaction with a custom caller address.
76    fn new_system_tx_with_caller(
77        caller: Address,
78        system_contract_address: Address,
79        data: Bytes,
80    ) -> Self;
81}
82
83impl SystemCallTx for TxEnv {
84    fn new_system_tx_with_caller(
85        caller: Address,
86        system_contract_address: Address,
87        data: Bytes,
88    ) -> Self {
89        TxEnv::builder()
90            .caller(caller)
91            .data(data)
92            .kind(TxKind::Call(system_contract_address))
93            .gas_limit(SYSTEM_CALL_GAS_LIMIT)
94            .build()
95            .unwrap()
96    }
97}
98
99/// API for executing the system calls. System calls dont deduct the caller or reward the
100/// beneficiary. They are used before and after block execution to insert or obtain blockchain state.
101///
102/// It act similar to `transact` function and sets default Tx with data and system contract as a target.
103///
104/// # Note
105///
106/// Only one function needs implementation [`SystemCallEvm::system_call_one_with_caller`], other functions
107/// are derived from it.
108pub trait SystemCallEvm: ExecuteEvm {
109    /// System call is a special transaction call that is used to call a system contract.
110    ///
111    /// Transaction fields are reset and set in [`SystemCallTx`] and data and target are set to
112    /// given values.
113    ///
114    /// Block values are taken into account and will determent how system call will be executed.
115    fn system_call_one_with_caller(
116        &mut self,
117        caller: Address,
118        system_contract_address: Address,
119        data: Bytes,
120    ) -> Result<Self::ExecutionResult, Self::Error>;
121
122    /// System call is a special transaction call that is used to call a system contract.
123    ///
124    /// Transaction fields are reset and set in [`SystemCallTx`] and data and target are set to
125    /// given values.
126    ///
127    /// Block values are taken into account and will determent how system call will be executed.
128    fn system_call_one(
129        &mut self,
130        system_contract_address: Address,
131        data: Bytes,
132    ) -> Result<Self::ExecutionResult, Self::Error> {
133        self.system_call_one_with_caller(SYSTEM_ADDRESS, system_contract_address, data)
134    }
135
136    /// Internally calls [`SystemCallEvm::system_call_with_caller`].
137    fn system_call(
138        &mut self,
139        system_contract_address: Address,
140        data: Bytes,
141    ) -> Result<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error> {
142        self.system_call_with_caller(SYSTEM_ADDRESS, system_contract_address, data)
143    }
144
145    /// Internally calls [`SystemCallEvm::system_call_one`] and [`ExecuteEvm::finalize`] functions to obtain the changed state.
146    fn system_call_with_caller(
147        &mut self,
148        caller: Address,
149        system_contract_address: Address,
150        data: Bytes,
151    ) -> Result<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error> {
152        let result = self.system_call_one_with_caller(caller, system_contract_address, data)?;
153        let state = self.finalize();
154        Ok(ExecResultAndState::new(result, state))
155    }
156
157    /// System call is a special transaction call that is used to call a system contract.
158    ///
159    /// Transaction fields are reset and set in [`SystemCallTx`] and data and target are set to
160    /// given values.
161    ///
162    /// Block values are taken into account and will determent how system call will be executed.
163    #[deprecated(since = "0.1.0", note = "Use `system_call_one_with_caller` instead")]
164    fn transact_system_call_with_caller(
165        &mut self,
166        caller: Address,
167        system_contract_address: Address,
168        data: Bytes,
169    ) -> Result<Self::ExecutionResult, Self::Error> {
170        self.system_call_one_with_caller(caller, system_contract_address, data)
171    }
172
173    /// Calls [`SystemCallEvm::system_call_one`] with [`SYSTEM_ADDRESS`] as a caller.
174    #[deprecated(since = "0.1.0", note = "Use `system_call_one` instead")]
175    fn transact_system_call(
176        &mut self,
177        system_contract_address: Address,
178        data: Bytes,
179    ) -> Result<Self::ExecutionResult, Self::Error> {
180        self.system_call_one(system_contract_address, data)
181    }
182
183    /// Transact the system call and finalize.
184    ///
185    /// Internally calls combo of `transact_system_call` and `finalize` functions.
186    #[deprecated(since = "0.1.0", note = "Use `system_call` instead")]
187    fn transact_system_call_finalize(
188        &mut self,
189        system_contract_address: Address,
190        data: Bytes,
191    ) -> Result<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error> {
192        self.system_call(system_contract_address, data)
193    }
194
195    /// Calls [`SystemCallEvm::system_call_one`] and `finalize` functions.
196    #[deprecated(since = "0.1.0", note = "Use `system_call_with_caller` instead")]
197    fn transact_system_call_with_caller_finalize(
198        &mut self,
199        caller: Address,
200        system_contract_address: Address,
201        data: Bytes,
202    ) -> Result<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error> {
203        self.system_call_with_caller(caller, system_contract_address, data)
204    }
205}
206
207/// Extension of the [`SystemCallEvm`] trait that adds a method that commits the state after execution.
208pub trait SystemCallCommitEvm: SystemCallEvm + ExecuteCommitEvm {
209    /// Transact the system call and commit to the state.
210    fn system_call_commit(
211        &mut self,
212        system_contract_address: Address,
213        data: Bytes,
214    ) -> Result<Self::ExecutionResult, Self::Error> {
215        self.system_call_with_caller_commit(SYSTEM_ADDRESS, system_contract_address, data)
216    }
217
218    /// Transact the system call and commit to the state.
219    #[deprecated(since = "0.1.0", note = "Use `system_call_commit` instead")]
220    fn transact_system_call_commit(
221        &mut self,
222        system_contract_address: Address,
223        data: Bytes,
224    ) -> Result<Self::ExecutionResult, Self::Error> {
225        self.system_call_commit(system_contract_address, data)
226    }
227
228    /// Calls [`SystemCallCommitEvm::system_call_commit`] with a custom caller.
229    fn system_call_with_caller_commit(
230        &mut self,
231        caller: Address,
232        system_contract_address: Address,
233        data: Bytes,
234    ) -> Result<Self::ExecutionResult, Self::Error>;
235
236    /// Calls [`SystemCallCommitEvm::system_call_commit`] with a custom caller.
237    #[deprecated(since = "0.1.0", note = "Use `system_call_with_caller_commit` instead")]
238    fn transact_system_call_with_caller_commit(
239        &mut self,
240        caller: Address,
241        system_contract_address: Address,
242        data: Bytes,
243    ) -> Result<Self::ExecutionResult, Self::Error> {
244        self.system_call_with_caller_commit(caller, system_contract_address, data)
245    }
246}
247
248/// Async extension of the [`SystemCallEvm`] trait that runs execution on an async fiber.
249#[cfg(feature = "asyncdb")]
250pub trait SystemCallEvmAsync: SystemCallEvm {
251    /// System call executed on an async fiber.
252    fn system_call_one_with_caller_async(
253        &mut self,
254        caller: Address,
255        system_contract_address: Address,
256        data: Bytes,
257    ) -> impl core::future::Future<Output = AsyncResult<Self::ExecutionResult, Self::Error>> + Send + '_;
258
259    /// System call executed on an async fiber.
260    fn system_call_one_async(
261        &mut self,
262        system_contract_address: Address,
263        data: Bytes,
264    ) -> impl core::future::Future<Output = AsyncResult<Self::ExecutionResult, Self::Error>> + Send + '_
265    {
266        self.system_call_one_with_caller_async(SYSTEM_ADDRESS, system_contract_address, data)
267    }
268
269    /// System call executed and finalized on an async fiber.
270    fn system_call_with_caller_async(
271        &mut self,
272        caller: Address,
273        system_contract_address: Address,
274        data: Bytes,
275    ) -> impl core::future::Future<
276        Output = AsyncResult<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error>,
277    > + Send
278           + '_;
279
280    /// System call executed and finalized on an async fiber.
281    fn system_call_async(
282        &mut self,
283        system_contract_address: Address,
284        data: Bytes,
285    ) -> impl core::future::Future<
286        Output = AsyncResult<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error>,
287    > + Send
288           + '_ {
289        self.system_call_with_caller_async(SYSTEM_ADDRESS, system_contract_address, data)
290    }
291}
292
293impl<CTX, INSP, INST, PRECOMPILES> SystemCallEvm
294    for Evm<CTX, INSP, INST, PRECOMPILES, EthFrame<EthInterpreter>>
295where
296    CTX: ContextTr<Journal: JournalTr<State = EvmState>, Tx: SystemCallTx> + ContextSetters,
297    INST: InstructionProvider<Context = CTX, InterpreterTypes = EthInterpreter>,
298    PRECOMPILES: PrecompileProvider<CTX, Output = InterpreterResult>,
299{
300    fn system_call_one_with_caller(
301        &mut self,
302        caller: Address,
303        system_contract_address: Address,
304        data: Bytes,
305    ) -> Result<Self::ExecutionResult, Self::Error> {
306        // set tx fields.
307        self.set_tx(CTX::Tx::new_system_tx_with_caller(
308            caller,
309            system_contract_address,
310            data,
311        ));
312        // create handler
313        MainnetHandler::default().run_system_call(self)
314    }
315}
316
317#[cfg(feature = "asyncdb")]
318impl<CTX, INSP, INST, PRECOMPILES> SystemCallEvmAsync
319    for Evm<CTX, INSP, INST, PRECOMPILES, EthFrame<EthInterpreter>>
320where
321    CTX: ContextTr<Journal: JournalTr<State = EvmState>, Tx: SystemCallTx> + ContextSetters,
322    INST: InstructionProvider<Context = CTX, InterpreterTypes = EthInterpreter>,
323    PRECOMPILES: PrecompileProvider<CTX, Output = InterpreterResult>,
324    Self: ExecuteEvm,
325    <Self as ExecuteEvm>::ExecutionResult: Send,
326    <Self as ExecuteEvm>::State: Send,
327    <Self as ExecuteEvm>::Error: Send,
328{
329    #[inline]
330    fn system_call_one_with_caller_async(
331        &mut self,
332        caller: Address,
333        system_contract_address: Address,
334        data: Bytes,
335    ) -> impl core::future::Future<Output = AsyncResult<Self::ExecutionResult, Self::Error>> + Send + '_
336    {
337        let stack = NonNull::from(&mut self.async_stack);
338        // SAFETY: The returned future owns the exclusive `&mut self` borrow, so nothing else can
339        // access the EVM stack slot until that future is dropped.
340        unsafe {
341            on_fiber_result_with_stack(stack, move || {
342                self.system_call_one_with_caller(caller, system_contract_address, data)
343            })
344        }
345    }
346
347    #[inline]
348    fn system_call_with_caller_async(
349        &mut self,
350        caller: Address,
351        system_contract_address: Address,
352        data: Bytes,
353    ) -> impl core::future::Future<
354        Output = AsyncResult<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error>,
355    > + Send
356           + '_ {
357        let stack = NonNull::from(&mut self.async_stack);
358        // SAFETY: The returned future owns the exclusive `&mut self` borrow, so nothing else can
359        // access the EVM stack slot until that future is dropped.
360        unsafe {
361            on_fiber_result_with_stack(stack, move || {
362                self.system_call_with_caller(caller, system_contract_address, data)
363            })
364        }
365    }
366}
367
368impl<CTX, INSP, INST, PRECOMPILES> SystemCallCommitEvm
369    for Evm<CTX, INSP, INST, PRECOMPILES, EthFrame<EthInterpreter>>
370where
371    CTX: ContextTr<Journal: JournalTr<State = EvmState>, Db: DatabaseCommit, Tx: SystemCallTx>
372        + ContextSetters,
373    INST: InstructionProvider<Context = CTX, InterpreterTypes = EthInterpreter>,
374    PRECOMPILES: PrecompileProvider<CTX, Output = InterpreterResult>,
375{
376    fn system_call_with_caller_commit(
377        &mut self,
378        caller: Address,
379        system_contract_address: Address,
380        data: Bytes,
381    ) -> Result<Self::ExecutionResult, Self::Error> {
382        self.system_call_with_caller(caller, system_contract_address, data)
383            .map(|output| {
384                self.db_mut().commit(output.state);
385                output.result
386            })
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use crate::{MainBuilder, MainContext};
393
394    use super::*;
395    use context::{
396        result::{ExecutionResult, Output, ResultGas, SuccessReason},
397        Context, Transaction,
398    };
399    use database::InMemoryDB;
400    use primitives::{b256, bytes, StorageKey, U256};
401    use state::{AccountInfo, Bytecode};
402
403    const HISTORY_STORAGE_ADDRESS: Address = address!("0x0000F90827F1C53a10cb7A02335B175320002935");
404    static HISTORY_STORAGE_CODE: Bytes = bytes!("0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500");
405
406    #[test]
407    fn test_system_call() {
408        let mut db = InMemoryDB::default();
409        db.insert_account_info(
410            HISTORY_STORAGE_ADDRESS,
411            AccountInfo::default().with_code(Bytecode::new_legacy(HISTORY_STORAGE_CODE.clone())),
412        );
413
414        let block_hash =
415            b256!("0x1111111111111111111111111111111111111111111111111111111111111111");
416
417        let mut evm = Context::mainnet()
418            .with_db(db)
419            // block with number 1 will set storage at slot 0.
420            .modify_block_chained(|b| b.number = U256::ONE)
421            .build_mainnet();
422        let output = evm
423            .system_call(HISTORY_STORAGE_ADDRESS, block_hash.0.into())
424            .unwrap();
425
426        // EIP-8037 adds a state-gas reservoir on top of the 30M base limit.
427        assert_eq!(evm.ctx.tx().gas_limit(), SYSTEM_CALL_GAS_LIMIT);
428
429        assert_eq!(
430            output.result,
431            ExecutionResult::Success {
432                reason: SuccessReason::Stop,
433                gas: ResultGas::default().with_total_gas_spent(22143),
434                logs: vec![],
435                output: Output::Call(Bytes::default())
436            }
437        );
438        // only system contract is updated and present
439        assert_eq!(output.state.len(), 1);
440        assert_eq!(
441            output.state[&HISTORY_STORAGE_ADDRESS]
442                .storage
443                .get(&StorageKey::from(0))
444                .map(|slot| slot.present_value)
445                .unwrap_or_default(),
446            U256::from_be_bytes(block_hash.0),
447            "State is not updated {:?}",
448            output.state
449        );
450    }
451
452    /// EIP-8037: the 30M base budget of a system call is its regular gas
453    /// (`gas_left`), and the state-gas margin sized for
454    /// `SYSTEM_MAX_SSTORES_PER_CALL` writes lives in the reservoir.
455    /// `GAS` inside a system contract therefore reports the regular budget
456    /// only, and a fresh-slot `SSTORE` is paid from the reservoir.
457    #[test]
458    fn test_system_call_eip8037_state_gas_reservoir() {
459        use primitives::{eip8037, hardfork::SpecId};
460
461        const SYSTEM_CONTRACT: Address = address!("0x000000000000000000000000000000000000c0de");
462        // GAS PUSH0 SSTORE STOP: store gasleft() into fresh slot 0.
463        static CODE: Bytes = bytes!("0x5a5f5500");
464
465        let mut db = InMemoryDB::default();
466        db.insert_account_info(
467            SYSTEM_CONTRACT,
468            AccountInfo::default().with_code(Bytecode::new_legacy(CODE.clone())),
469        );
470
471        let mut evm = Context::mainnet()
472            .with_db(db)
473            .modify_cfg_chained(|cfg| cfg.set_spec_and_mainnet_gas_params(SpecId::AMSTERDAM))
474            .build_mainnet();
475        let output = evm.system_call(SYSTEM_CONTRACT, Bytes::default()).unwrap();
476        assert!(output.result.is_success(), "{:?}", output.result);
477
478        let gas_seen = output.state[&SYSTEM_CONTRACT]
479            .storage
480            .get(&StorageKey::from(0))
481            .map(|slot| slot.present_value)
482            .unwrap_or_default();
483        // `GAS` charges its own 2 gas before pushing the remaining regular gas.
484        assert_eq!(gas_seen, U256::from(SYSTEM_CALL_REGULAR_GAS_LIMIT - 2));
485
486        // The fresh-slot SSTORE is a state-gas charge drawn from the reservoir.
487        let sstore_state_gas = eip8037::SSTORE_SET_BYTES * eip8037::CPSB_GLAMSTERDAM;
488        assert_eq!(output.result.gas().block_state_gas_used(), sstore_state_gas);
489        assert!(sstore_state_gas <= SYSTEM_CALL_STATE_GAS_RESERVOIR);
490    }
491}