example_custom_precompile_journal/
custom_evm.rs

1//! Custom EVM implementation with journal-accessing precompiles.
2
3use crate::precompile_provider::CustomPrecompileProvider;
4use revm::{
5    context::{ContextError, ContextSetters, ContextTr, Evm, FrameStack},
6    handler::{
7        evm::FrameTr, instructions::EthInstructions, EthFrame, EvmTr, FrameInitOrResult,
8        ItemOrResult,
9    },
10    inspector::{InspectorEvmTr, JournalExt},
11    interpreter::interpreter::EthInterpreter,
12    primitives::hardfork::SpecId,
13    Database, Inspector,
14};
15
16/// Custom EVM variant with journal-accessing precompiles.
17///
18/// This EVM extends the standard behavior by using a custom precompile provider
19/// that includes journal access functionality. It follows the same pattern as MyEvm
20/// but uses CustomPrecompileProvider instead of EthPrecompiles.
21#[derive(Debug)]
22pub struct CustomEvm<CTX, INSP>(
23    pub  Evm<
24        CTX,
25        INSP,
26        EthInstructions<EthInterpreter, CTX>,
27        CustomPrecompileProvider,
28        EthFrame<EthInterpreter>,
29    >,
30);
31
32impl<CTX, INSP> CustomEvm<CTX, INSP>
33where
34    CTX: ContextTr<Cfg: revm::context::Cfg<Spec = SpecId>>,
35{
36    /// Creates a new instance of CustomEvm with the provided context and inspector.
37    ///
38    /// # Arguments
39    ///
40    /// * `ctx` - The execution context that manages state, environment, and journaling
41    /// * `inspector` - The inspector for debugging and tracing execution
42    ///
43    /// # Returns
44    ///
45    /// A new CustomEvm instance configured with:
46    /// - The provided context and inspector
47    /// - Mainnet instruction set
48    /// - Custom precompiles with journal access
49    /// - A fresh frame stack for execution
50    pub fn new(ctx: CTX, inspector: INSP) -> Self {
51        Self(Evm {
52            ctx,
53            inspector,
54            instruction: EthInstructions::new_mainnet(),
55            precompiles: CustomPrecompileProvider::new_with_spec(SpecId::CANCUN),
56            frame_stack: FrameStack::new(),
57        })
58    }
59}
60
61impl<CTX, INSP> EvmTr for CustomEvm<CTX, INSP>
62where
63    CTX: ContextTr<Cfg: revm::context::Cfg<Spec = SpecId>>,
64{
65    type Context = CTX;
66    type Instructions = EthInstructions<EthInterpreter, CTX>;
67    type Precompiles = CustomPrecompileProvider;
68    type Frame = EthFrame<EthInterpreter>;
69
70    fn all(
71        &self,
72    ) -> (
73        &Self::Context,
74        &Self::Instructions,
75        &Self::Precompiles,
76        &FrameStack<Self::Frame>,
77    ) {
78        self.0.all()
79    }
80
81    fn all_mut(
82        &mut self,
83    ) -> (
84        &mut Self::Context,
85        &mut Self::Instructions,
86        &mut Self::Precompiles,
87        &mut FrameStack<Self::Frame>,
88    ) {
89        self.0.all_mut()
90    }
91
92    fn frame_init(
93        &mut self,
94        frame_input: <Self::Frame as FrameTr>::FrameInit,
95    ) -> Result<
96        ItemOrResult<&mut Self::Frame, <Self::Frame as FrameTr>::FrameResult>,
97        ContextError<<<Self::Context as ContextTr>::Db as Database>::Error>,
98    > {
99        self.0.frame_init(frame_input)
100    }
101
102    fn frame_run(
103        &mut self,
104    ) -> Result<
105        FrameInitOrResult<Self::Frame>,
106        ContextError<<<Self::Context as ContextTr>::Db as Database>::Error>,
107    > {
108        self.0.frame_run()
109    }
110
111    fn frame_return_result(
112        &mut self,
113        frame_result: <Self::Frame as FrameTr>::FrameResult,
114    ) -> Result<
115        Option<<Self::Frame as FrameTr>::FrameResult>,
116        ContextError<<<Self::Context as ContextTr>::Db as Database>::Error>,
117    > {
118        self.0.frame_return_result(frame_result)
119    }
120}
121
122impl<CTX, INSP> InspectorEvmTr for CustomEvm<CTX, INSP>
123where
124    CTX: ContextSetters<Cfg: revm::context::Cfg<Spec = SpecId>, Journal: JournalExt>,
125    INSP: Inspector<CTX, EthInterpreter>,
126{
127    type Inspector = INSP;
128
129    fn all_inspector(
130        &self,
131    ) -> (
132        &Self::Context,
133        &Self::Instructions,
134        &Self::Precompiles,
135        &FrameStack<Self::Frame>,
136        &Self::Inspector,
137    ) {
138        self.0.all_inspector()
139    }
140
141    fn all_mut_inspector(
142        &mut self,
143    ) -> (
144        &mut Self::Context,
145        &mut Self::Instructions,
146        &mut Self::Precompiles,
147        &mut FrameStack<Self::Frame>,
148        &mut Self::Inspector,
149    ) {
150        self.0.all_mut_inspector()
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use crate::{custom_evm::CustomEvm, precompile_provider::CUSTOM_PRECOMPILE_ADDRESS};
157    use revm::{
158        context::{Context, ContextSetters, TxEnv},
159        context_interface::{result::EVMError, ContextTr},
160        database::InMemoryDB,
161        handler::{Handler, MainnetHandler},
162        inspector::{Inspector, JournalExt},
163        interpreter::interpreter::EthInterpreter,
164        primitives::{address, Log, TxKind, U256},
165        state::AccountInfo,
166        MainContext,
167    };
168    use std::vec::Vec;
169
170    /// Custom inspector that captures logs
171    #[derive(Debug, Default)]
172    struct LogCapturingInspector {
173        captured_logs: Vec<Log>,
174    }
175
176    impl LogCapturingInspector {
177        fn new() -> Self {
178            Self {
179                captured_logs: Vec::new(),
180            }
181        }
182
183        fn logs(&self) -> &[Log] {
184            &self.captured_logs
185        }
186    }
187
188    impl<CTX> Inspector<CTX, EthInterpreter> for LogCapturingInspector
189    where
190        CTX: ContextTr + ContextSetters<Journal: JournalExt>,
191    {
192        fn log(&mut self, _context: &mut CTX, log: Log) {
193            // Capture logs as they're created
194            self.captured_logs.push(log);
195        }
196    }
197
198    #[test]
199    fn test_custom_precompile_creates_log() {
200        // Setup initial accounts
201        let user_address = address!("0000000000000000000000000000000000000001");
202        let mut db = InMemoryDB::default();
203
204        // Give the user some ETH for gas
205        let user_balance = U256::from(10).pow(U256::from(18)); // 1 ETH
206        db.insert_account_info(
207            user_address,
208            AccountInfo {
209                balance: user_balance,
210                nonce: 0,
211                code_hash: revm::primitives::KECCAK_EMPTY,
212                code: None,
213                account_id: None,
214            },
215        );
216
217        // Give the precompile some initial balance for transfers
218        db.insert_account_info(
219            CUSTOM_PRECOMPILE_ADDRESS,
220            AccountInfo {
221                balance: U256::from(1000), // 1000 wei
222                nonce: 0,
223                code_hash: revm::primitives::KECCAK_EMPTY,
224                code: None,
225                account_id: None,
226            },
227        );
228
229        // Create custom EVM with log capturing inspector
230        let context = Context::mainnet().with_db(db);
231        let inspector = LogCapturingInspector::new();
232        let mut evm = CustomEvm::new(context, inspector);
233
234        // Write value 42 to storage (this should create a log)
235        let storage_value = U256::from(42);
236        evm.0.ctx.set_tx(
237            TxEnv::builder()
238                .caller(user_address)
239                .kind(TxKind::Call(CUSTOM_PRECOMPILE_ADDRESS))
240                .data(storage_value.to_be_bytes_vec().into())
241                .gas_limit(100_000)
242                .build()
243                .unwrap(),
244        );
245
246        let result: Result<
247            _,
248            EVMError<core::convert::Infallible, revm::context::result::InvalidTransaction>,
249        > = MainnetHandler::default().run(&mut evm);
250
251        // Verify transaction succeeded
252        assert!(
253            result.is_ok(),
254            "Transaction should succeed, got: {result:?}"
255        );
256
257        match result.unwrap() {
258            revm::context::result::ExecutionResult::Success { logs, .. } => {
259                // Transaction succeeded, now check logs from execution result
260                // Note: Inspector might not be called for precompile logs,
261                // so we check the execution result logs instead
262
263                // Also check inspector logs (though they may be empty)
264                let inspector_logs = evm.0.inspector.logs();
265
266                // Combine both sources - use execution result logs if inspector logs are empty
267                let all_logs = if inspector_logs.is_empty() {
268                    &logs
269                } else {
270                    inspector_logs
271                };
272
273                // Verify that at least one log was captured
274                assert!(
275                    !all_logs.is_empty(),
276                    "Should have captured at least one log (either from inspector or execution result)"
277                );
278
279                // Find the log from our custom precompile
280                let precompile_log = all_logs
281                    .iter()
282                    .find(|log| log.address == CUSTOM_PRECOMPILE_ADDRESS);
283
284                assert!(
285                    precompile_log.is_some(),
286                    "Should have a log from the custom precompile. Found {} total logs",
287                    all_logs.len()
288                );
289
290                let log = precompile_log.unwrap();
291
292                // Verify log structure
293                assert_eq!(log.address, CUSTOM_PRECOMPILE_ADDRESS);
294                assert_eq!(log.data.topics().len(), 2, "Should have 2 topics");
295
296                // Topic 1 should be the caller address (left-padded to 32 bytes)
297                let topic1 = log.data.topics()[1];
298                let mut expected_caller_bytes = [0u8; 32];
299                expected_caller_bytes[12..32].copy_from_slice(user_address.as_slice());
300                let expected_caller_topic = revm::primitives::B256::from(expected_caller_bytes);
301                assert_eq!(
302                    topic1, expected_caller_topic,
303                    "Second topic should be caller address"
304                );
305
306                // Data should contain the value that was written (42)
307                let log_data_bytes = &log.data.data;
308                let logged_value = U256::from_be_slice(log_data_bytes);
309                assert_eq!(
310                    logged_value,
311                    U256::from(42),
312                    "Log data should contain the written value (42)"
313                );
314
315                println!("✅ Test passed! Log was successfully created and captured");
316                println!("   Log address: {}", log.address);
317                println!("   Number of topics: {}", log.data.topics().len());
318                println!("   Logged value: {logged_value}");
319                println!(
320                    "   Inspector logs: {}, Execution result logs: {}",
321                    inspector_logs.len(),
322                    logs.len()
323                );
324            }
325            revm::context::result::ExecutionResult::Revert { .. } => {
326                panic!("Transaction reverted unexpectedly");
327            }
328            revm::context::result::ExecutionResult::Halt { reason, .. } => {
329                panic!("Transaction halted unexpectedly: {reason:?}");
330            }
331        }
332    }
333
334    #[test]
335    fn test_read_operation_does_not_create_log() {
336        // Setup initial accounts
337        let user_address = address!("0000000000000000000000000000000000000001");
338        let mut db = InMemoryDB::default();
339
340        // Give the user some ETH for gas
341        let user_balance = U256::from(10).pow(U256::from(18)); // 1 ETH
342        db.insert_account_info(
343            user_address,
344            AccountInfo {
345                balance: user_balance,
346                nonce: 0,
347                code_hash: revm::primitives::KECCAK_EMPTY,
348                code: None,
349                account_id: None,
350            },
351        );
352
353        // Create custom EVM with log capturing inspector
354        let context = Context::mainnet().with_db(db);
355        let inspector = LogCapturingInspector::new();
356        let mut evm = CustomEvm::new(context, inspector);
357
358        // Read from storage (empty input - should not create a log)
359        evm.0.ctx.set_tx(
360            TxEnv::builder()
361                .caller(user_address)
362                .kind(TxKind::Call(CUSTOM_PRECOMPILE_ADDRESS))
363                .data(revm::primitives::Bytes::new()) // Empty data for read operation
364                .gas_limit(100_000)
365                .build()
366                .unwrap(),
367        );
368
369        let result: Result<
370            _,
371            EVMError<core::convert::Infallible, revm::context::result::InvalidTransaction>,
372        > = MainnetHandler::default().run(&mut evm);
373
374        // Verify transaction succeeded
375        assert!(
376            result.is_ok(),
377            "Transaction should succeed, got: {result:?}"
378        );
379
380        match result.unwrap() {
381            revm::context::result::ExecutionResult::Success { .. } => {
382                // Transaction succeeded, check that no logs were created
383                let logs = evm.0.inspector.logs();
384
385                // Verify that no logs from the precompile were captured
386                let precompile_log = logs
387                    .iter()
388                    .find(|log| log.address == CUSTOM_PRECOMPILE_ADDRESS);
389
390                assert!(
391                    precompile_log.is_none(),
392                    "Read operation should not create any logs"
393                );
394
395                println!("✅ Test passed! Read operation correctly did not create any logs");
396            }
397            revm::context::result::ExecutionResult::Revert { .. } => {
398                panic!("Transaction reverted unexpectedly");
399            }
400            revm::context::result::ExecutionResult::Halt { reason, .. } => {
401                panic!("Transaction halted unexpectedly: {reason:?}");
402            }
403        }
404    }
405}