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};
27use database_interface::DatabaseCommit;
28use interpreter::{interpreter::EthInterpreter, InterpreterResult};
29use primitives::{address, eip8037, Address, Bytes, TxKind};
30use state::EvmState;
31
32/// The system address used for system calls.
33pub const SYSTEM_ADDRESS: Address = address!("0xfffffffffffffffffffffffffffffffffffffffe");
34
35/// Maximum number of SSTOREs a bal-devnet-7 system call reserves state gas for.
36pub const SYSTEM_MAX_SSTORES_PER_CALL: u64 = 16;
37
38/// Gas limit for system calls under bal-devnet-7.
39///
40/// Per `tests-bal@v7.0.0`, system calls get the base 30M regular-gas budget plus
41/// a state-gas reservoir sized for `SYSTEM_MAX_SSTORES_PER_CALL` storage writes.
42pub const SYSTEM_CALL_GAS_LIMIT: u64 = 30_000_000
43    + eip8037::SSTORE_SET_BYTES * eip8037::CPSB_GLAMSTERDAM * SYSTEM_MAX_SSTORES_PER_CALL;
44
45/// Creates the system transaction with default values and set data and tx call target to system contract address
46/// that is going to be called.
47///
48/// The caller is set to be [`SYSTEM_ADDRESS`].
49///
50/// It is used inside [`SystemCallEvm`] and [`SystemCallCommitEvm`] traits to prepare EVM for system call execution.
51pub trait SystemCallTx: Sized {
52    /// Creates new transaction for system call.
53    fn new_system_tx(system_contract_address: Address, data: Bytes) -> Self {
54        Self::new_system_tx_with_caller(SYSTEM_ADDRESS, system_contract_address, data)
55    }
56
57    /// Creates a new system transaction with a custom caller address.
58    fn new_system_tx_with_caller(
59        caller: Address,
60        system_contract_address: Address,
61        data: Bytes,
62    ) -> Self;
63}
64
65impl SystemCallTx for TxEnv {
66    fn new_system_tx_with_caller(
67        caller: Address,
68        system_contract_address: Address,
69        data: Bytes,
70    ) -> Self {
71        TxEnv::builder()
72            .caller(caller)
73            .data(data)
74            .kind(TxKind::Call(system_contract_address))
75            .gas_limit(SYSTEM_CALL_GAS_LIMIT)
76            .build()
77            .unwrap()
78    }
79}
80
81/// API for executing the system calls. System calls dont deduct the caller or reward the
82/// beneficiary. They are used before and after block execution to insert or obtain blockchain state.
83///
84/// It act similar to `transact` function and sets default Tx with data and system contract as a target.
85///
86/// # Note
87///
88/// Only one function needs implementation [`SystemCallEvm::system_call_one_with_caller`], other functions
89/// are derived from it.
90pub trait SystemCallEvm: ExecuteEvm {
91    /// System call is a special transaction call that is used to call a system contract.
92    ///
93    /// Transaction fields are reset and set in [`SystemCallTx`] and data and target are set to
94    /// given values.
95    ///
96    /// Block values are taken into account and will determent how system call will be executed.
97    fn system_call_one_with_caller(
98        &mut self,
99        caller: Address,
100        system_contract_address: Address,
101        data: Bytes,
102    ) -> Result<Self::ExecutionResult, Self::Error>;
103
104    /// System call is a special transaction call that is used to call a system contract.
105    ///
106    /// Transaction fields are reset and set in [`SystemCallTx`] and data and target are set to
107    /// given values.
108    ///
109    /// Block values are taken into account and will determent how system call will be executed.
110    fn system_call_one(
111        &mut self,
112        system_contract_address: Address,
113        data: Bytes,
114    ) -> Result<Self::ExecutionResult, Self::Error> {
115        self.system_call_one_with_caller(SYSTEM_ADDRESS, system_contract_address, data)
116    }
117
118    /// Internally calls [`SystemCallEvm::system_call_with_caller`].
119    fn system_call(
120        &mut self,
121        system_contract_address: Address,
122        data: Bytes,
123    ) -> Result<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error> {
124        self.system_call_with_caller(SYSTEM_ADDRESS, system_contract_address, data)
125    }
126
127    /// Internally calls [`SystemCallEvm::system_call_one`] and [`ExecuteEvm::finalize`] functions to obtain the changed state.
128    fn system_call_with_caller(
129        &mut self,
130        caller: Address,
131        system_contract_address: Address,
132        data: Bytes,
133    ) -> Result<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error> {
134        let result = self.system_call_one_with_caller(caller, system_contract_address, data)?;
135        let state = self.finalize();
136        Ok(ExecResultAndState::new(result, state))
137    }
138
139    /// System call is a special transaction call that is used to call a system contract.
140    ///
141    /// Transaction fields are reset and set in [`SystemCallTx`] and data and target are set to
142    /// given values.
143    ///
144    /// Block values are taken into account and will determent how system call will be executed.
145    #[deprecated(since = "0.1.0", note = "Use `system_call_one_with_caller` instead")]
146    fn transact_system_call_with_caller(
147        &mut self,
148        caller: Address,
149        system_contract_address: Address,
150        data: Bytes,
151    ) -> Result<Self::ExecutionResult, Self::Error> {
152        self.system_call_one_with_caller(caller, system_contract_address, data)
153    }
154
155    /// Calls [`SystemCallEvm::system_call_one`] with [`SYSTEM_ADDRESS`] as a caller.
156    #[deprecated(since = "0.1.0", note = "Use `system_call_one` instead")]
157    fn transact_system_call(
158        &mut self,
159        system_contract_address: Address,
160        data: Bytes,
161    ) -> Result<Self::ExecutionResult, Self::Error> {
162        self.system_call_one(system_contract_address, data)
163    }
164
165    /// Transact the system call and finalize.
166    ///
167    /// Internally calls combo of `transact_system_call` and `finalize` functions.
168    #[deprecated(since = "0.1.0", note = "Use `system_call` instead")]
169    fn transact_system_call_finalize(
170        &mut self,
171        system_contract_address: Address,
172        data: Bytes,
173    ) -> Result<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error> {
174        self.system_call(system_contract_address, data)
175    }
176
177    /// Calls [`SystemCallEvm::system_call_one`] and `finalize` functions.
178    #[deprecated(since = "0.1.0", note = "Use `system_call_with_caller` instead")]
179    fn transact_system_call_with_caller_finalize(
180        &mut self,
181        caller: Address,
182        system_contract_address: Address,
183        data: Bytes,
184    ) -> Result<ExecResultAndState<Self::ExecutionResult, Self::State>, Self::Error> {
185        self.system_call_with_caller(caller, system_contract_address, data)
186    }
187}
188
189/// Extension of the [`SystemCallEvm`] trait that adds a method that commits the state after execution.
190pub trait SystemCallCommitEvm: SystemCallEvm + ExecuteCommitEvm {
191    /// Transact the system call and commit to the state.
192    fn system_call_commit(
193        &mut self,
194        system_contract_address: Address,
195        data: Bytes,
196    ) -> Result<Self::ExecutionResult, Self::Error> {
197        self.system_call_with_caller_commit(SYSTEM_ADDRESS, system_contract_address, data)
198    }
199
200    /// Transact the system call and commit to the state.
201    #[deprecated(since = "0.1.0", note = "Use `system_call_commit` instead")]
202    fn transact_system_call_commit(
203        &mut self,
204        system_contract_address: Address,
205        data: Bytes,
206    ) -> Result<Self::ExecutionResult, Self::Error> {
207        self.system_call_commit(system_contract_address, data)
208    }
209
210    /// Calls [`SystemCallCommitEvm::system_call_commit`] with a custom caller.
211    fn system_call_with_caller_commit(
212        &mut self,
213        caller: Address,
214        system_contract_address: Address,
215        data: Bytes,
216    ) -> Result<Self::ExecutionResult, Self::Error>;
217
218    /// Calls [`SystemCallCommitEvm::system_call_commit`] with a custom caller.
219    #[deprecated(since = "0.1.0", note = "Use `system_call_with_caller_commit` instead")]
220    fn transact_system_call_with_caller_commit(
221        &mut self,
222        caller: Address,
223        system_contract_address: Address,
224        data: Bytes,
225    ) -> Result<Self::ExecutionResult, Self::Error> {
226        self.system_call_with_caller_commit(caller, system_contract_address, data)
227    }
228}
229
230impl<CTX, INSP, INST, PRECOMPILES> SystemCallEvm
231    for Evm<CTX, INSP, INST, PRECOMPILES, EthFrame<EthInterpreter>>
232where
233    CTX: ContextTr<Journal: JournalTr<State = EvmState>, Tx: SystemCallTx> + ContextSetters,
234    INST: InstructionProvider<Context = CTX, InterpreterTypes = EthInterpreter>,
235    PRECOMPILES: PrecompileProvider<CTX, Output = InterpreterResult>,
236{
237    fn system_call_one_with_caller(
238        &mut self,
239        caller: Address,
240        system_contract_address: Address,
241        data: Bytes,
242    ) -> Result<Self::ExecutionResult, Self::Error> {
243        // set tx fields.
244        self.set_tx(CTX::Tx::new_system_tx_with_caller(
245            caller,
246            system_contract_address,
247            data,
248        ));
249        // create handler
250        MainnetHandler::default().run_system_call(self)
251    }
252}
253
254impl<CTX, INSP, INST, PRECOMPILES> SystemCallCommitEvm
255    for Evm<CTX, INSP, INST, PRECOMPILES, EthFrame<EthInterpreter>>
256where
257    CTX: ContextTr<Journal: JournalTr<State = EvmState>, Db: DatabaseCommit, Tx: SystemCallTx>
258        + ContextSetters,
259    INST: InstructionProvider<Context = CTX, InterpreterTypes = EthInterpreter>,
260    PRECOMPILES: PrecompileProvider<CTX, Output = InterpreterResult>,
261{
262    fn system_call_with_caller_commit(
263        &mut self,
264        caller: Address,
265        system_contract_address: Address,
266        data: Bytes,
267    ) -> Result<Self::ExecutionResult, Self::Error> {
268        self.system_call_with_caller(caller, system_contract_address, data)
269            .map(|output| {
270                self.db_mut().commit(output.state);
271                output.result
272            })
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use crate::{MainBuilder, MainContext};
279
280    use super::*;
281    use context::{
282        result::{ExecutionResult, Output, ResultGas, SuccessReason},
283        Context, Transaction,
284    };
285    use database::InMemoryDB;
286    use primitives::{b256, bytes, StorageKey, U256};
287    use state::{AccountInfo, Bytecode};
288
289    const HISTORY_STORAGE_ADDRESS: Address = address!("0x0000F90827F1C53a10cb7A02335B175320002935");
290    static HISTORY_STORAGE_CODE: Bytes = bytes!("0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500");
291
292    #[test]
293    fn test_system_call() {
294        let mut db = InMemoryDB::default();
295        db.insert_account_info(
296            HISTORY_STORAGE_ADDRESS,
297            AccountInfo::default().with_code(Bytecode::new_legacy(HISTORY_STORAGE_CODE.clone())),
298        );
299
300        let block_hash =
301            b256!("0x1111111111111111111111111111111111111111111111111111111111111111");
302
303        let mut evm = Context::mainnet()
304            .with_db(db)
305            // block with number 1 will set storage at slot 0.
306            .modify_block_chained(|b| b.number = U256::ONE)
307            .build_mainnet();
308        let output = evm
309            .system_call(HISTORY_STORAGE_ADDRESS, block_hash.0.into())
310            .unwrap();
311
312        // bal-devnet-7 adds a state-gas reservoir on top of the 30M base limit.
313        assert_eq!(evm.ctx.tx().gas_limit(), SYSTEM_CALL_GAS_LIMIT);
314
315        assert_eq!(
316            output.result,
317            ExecutionResult::Success {
318                reason: SuccessReason::Stop,
319                gas: ResultGas::default().with_total_gas_spent(22143),
320                logs: vec![],
321                output: Output::Call(Bytes::default())
322            }
323        );
324        // only system contract is updated and present
325        assert_eq!(output.state.len(), 1);
326        assert_eq!(
327            output.state[&HISTORY_STORAGE_ADDRESS]
328                .storage
329                .get(&StorageKey::from(0))
330                .map(|slot| slot.present_value)
331                .unwrap_or_default(),
332            U256::from_be_bytes(block_hash.0),
333            "State is not updated {:?}",
334            output.state
335        );
336    }
337}