example_custom_precompile_journal/
precompile_provider.rs

1//! Custom precompile provider implementation.
2
3use revm::{
4    context::Cfg,
5    context_interface::{ContextTr, JournalTr, LocalContextTr, Transaction},
6    handler::{EthPrecompiles, PrecompileProvider},
7    interpreter::{CallInputs, Gas, InstructionResult, InterpreterResult},
8    precompile::{PrecompileError, PrecompileOutput, PrecompileResult},
9    primitives::{address, hardfork::SpecId, Address, Bytes, Log, B256, U256},
10};
11use std::{boxed::Box, string::String};
12
13// Define our custom precompile address
14pub const CUSTOM_PRECOMPILE_ADDRESS: Address = address!("0000000000000000000000000000000000000100");
15
16// Custom storage key for our example
17const STORAGE_KEY: U256 = U256::ZERO;
18
19/// Custom precompile provider that includes journal access functionality
20#[derive(Debug, Clone)]
21pub struct CustomPrecompileProvider {
22    inner: EthPrecompiles,
23    spec: SpecId,
24}
25
26impl CustomPrecompileProvider {
27    pub fn new_with_spec(spec: SpecId) -> Self {
28        Self {
29            inner: EthPrecompiles::default(),
30            spec,
31        }
32    }
33}
34
35impl<CTX> PrecompileProvider<CTX> for CustomPrecompileProvider
36where
37    CTX: ContextTr<Cfg: Cfg<Spec = SpecId>>,
38{
39    type Output = InterpreterResult;
40
41    fn set_spec(&mut self, spec: <CTX::Cfg as Cfg>::Spec) -> bool {
42        if spec == self.spec {
43            return false;
44        }
45        self.spec = spec;
46        // Create a new inner provider with the new spec
47        self.inner = EthPrecompiles::default();
48        true
49    }
50
51    fn run(
52        &mut self,
53        context: &mut CTX,
54        inputs: &CallInputs,
55    ) -> Result<Option<Self::Output>, String> {
56        // Check if this is our custom precompile
57        if inputs.bytecode_address == CUSTOM_PRECOMPILE_ADDRESS {
58            return Ok(Some(run_custom_precompile(context, inputs)?));
59        }
60
61        // Otherwise, delegate to standard Ethereum precompiles
62        self.inner.run(context, inputs)
63    }
64
65    fn warm_addresses(&self) -> Box<impl Iterator<Item = Address>> {
66        // Include our custom precompile address along with standard ones
67        let mut addresses = vec![CUSTOM_PRECOMPILE_ADDRESS];
68        addresses.extend(self.inner.warm_addresses());
69        Box::new(addresses.into_iter())
70    }
71
72    fn contains(&self, address: &Address) -> bool {
73        *address == CUSTOM_PRECOMPILE_ADDRESS || self.inner.contains(address)
74    }
75}
76
77/// Runs our custom precompile
78fn run_custom_precompile<CTX: ContextTr>(
79    context: &mut CTX,
80    inputs: &CallInputs,
81) -> Result<InterpreterResult, String> {
82    let input_bytes = match &inputs.input {
83        revm::interpreter::CallInput::SharedBuffer(range) => {
84            if let Some(slice) = context.local().shared_memory_buffer_slice(range.clone()) {
85                slice.to_vec()
86            } else {
87                vec![]
88            }
89        }
90        revm::interpreter::CallInput::Bytes(bytes) => bytes.0.to_vec(),
91    };
92
93    // For this example, we'll implement a simple precompile that:
94    // - If called with empty data: reads a storage value
95    // - If called with 32 bytes: writes that value to storage and transfers 1 wei to the caller
96
97    let result = if input_bytes.is_empty() {
98        // Read storage operation
99        handle_read_storage(context, inputs.gas_limit)
100    } else if input_bytes.len() == 32 {
101        if inputs.is_static {
102            return Err("Cannot modify state in static context".to_string());
103        }
104        // Write storage operation
105        handle_write_storage(context, &input_bytes, inputs.gas_limit)
106    } else {
107        Err(PrecompileError::Other("Invalid input length".into()))
108    };
109
110    match result {
111        Ok(output) => {
112            let mut interpreter_result = InterpreterResult {
113                result: if output.reverted {
114                    InstructionResult::Revert
115                } else {
116                    InstructionResult::Return
117                },
118                gas: Gas::new(inputs.gas_limit),
119                output: output.bytes,
120            };
121            let underflow = interpreter_result.gas.record_cost(output.gas_used);
122            if !underflow {
123                interpreter_result.result = InstructionResult::PrecompileOOG;
124            }
125            Ok(interpreter_result)
126        }
127        Err(e) => {
128            // If this is a top-level precompile call and error is non-OOG, record the message
129            if !e.is_oog() && context.journal().depth() == 1 {
130                context
131                    .local_mut()
132                    .set_precompile_error_context(e.to_string());
133            }
134            Ok(InterpreterResult {
135                result: if e.is_oog() {
136                    InstructionResult::PrecompileOOG
137                } else {
138                    InstructionResult::PrecompileError
139                },
140                gas: Gas::new(inputs.gas_limit),
141                output: Bytes::new(),
142            })
143        }
144    }
145}
146
147/// Handles reading from storage
148fn handle_read_storage<CTX: ContextTr>(context: &mut CTX, gas_limit: u64) -> PrecompileResult {
149    // Base gas cost for reading storage
150    const BASE_GAS: u64 = 2_100;
151
152    if gas_limit < BASE_GAS {
153        return Err(PrecompileError::OutOfGas);
154    }
155
156    // Read from storage using the journal
157    let value = context
158        .journal_mut()
159        .sload(CUSTOM_PRECOMPILE_ADDRESS, STORAGE_KEY)
160        .map_err(|e| PrecompileError::Other(format!("Storage read failed: {e:?}").into()))?
161        .data;
162
163    // Return the value as output
164    Ok(PrecompileOutput::new(
165        BASE_GAS,
166        value.to_be_bytes_vec().into(),
167    ))
168}
169
170/// Handles writing to storage and transferring balance
171fn handle_write_storage<CTX: ContextTr>(
172    context: &mut CTX,
173    input: &[u8],
174    gas_limit: u64,
175) -> PrecompileResult {
176    // Base gas cost for the operation
177    const BASE_GAS: u64 = 21_000;
178    const SSTORE_GAS: u64 = 20_000;
179
180    if gas_limit < BASE_GAS + SSTORE_GAS {
181        return Err(PrecompileError::OutOfGas);
182    }
183
184    // Parse the input as a U256 value
185    let value = U256::from_be_slice(input);
186
187    // Store the value in the precompile's storage
188    context
189        .journal_mut()
190        .sstore(CUSTOM_PRECOMPILE_ADDRESS, STORAGE_KEY, value)
191        .map_err(|e| PrecompileError::Other(format!("Storage write failed: {e:?}").into()))?;
192
193    // Get the caller address
194    let caller = context.tx().caller();
195
196    // Transfer 1 wei from the precompile to the caller as a reward
197    // First, ensure the precompile has balance
198    context
199        .journal_mut()
200        .balance_incr(CUSTOM_PRECOMPILE_ADDRESS, U256::from(1))
201        .map_err(|e| PrecompileError::Other(format!("Balance increment failed: {e:?}").into()))?;
202
203    // Then transfer to caller
204    let transfer_result = context
205        .journal_mut()
206        .transfer(CUSTOM_PRECOMPILE_ADDRESS, caller, U256::from(1))
207        .map_err(|e| PrecompileError::Other(format!("Transfer failed: {e:?}").into()))?;
208
209    if let Some(error) = transfer_result {
210        return Err(PrecompileError::Other(
211            format!("Transfer error: {error:?}").into(),
212        ));
213    }
214
215    // Create a log to record the storage write operation
216    // Topic 0: keccak256("StorageWritten(address,uint256)")
217    let topic0 = B256::from_slice(&[
218        0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde,
219        0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc,
220        0xde, 0xf0,
221    ]);
222    // Topic 1: caller address (indexed) - left-padded to 32 bytes
223    let mut topic1_bytes = [0u8; 32];
224    topic1_bytes[12..32].copy_from_slice(caller.as_slice());
225    let topic1 = B256::from(topic1_bytes);
226    // Data: the value that was written
227    let log_data = value.to_be_bytes_vec();
228
229    let log = Log::new(
230        CUSTOM_PRECOMPILE_ADDRESS,
231        vec![topic0, topic1],
232        log_data.into(),
233    )
234    .expect("Failed to create log");
235
236    context.journal_mut().log(log);
237
238    // Return success with empty output
239    Ok(PrecompileOutput::new(BASE_GAS + SSTORE_GAS, Bytes::new()))
240}