Skip to main content

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::new(spec),
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::new(spec);
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 = inputs.input.bytes(context);
83
84    // For this example, we'll implement a simple precompile that:
85    // - If called with empty data: reads a storage value
86    // - If called with 32 bytes: writes that value to storage and transfers 1 wei to the caller
87
88    let result = if input_bytes.is_empty() {
89        // Read storage operation
90        handle_read_storage(context, inputs.gas_limit)
91    } else if input_bytes.len() == 32 {
92        if inputs.is_static {
93            return Err("Cannot modify state in static context".to_string());
94        }
95        // Write storage operation
96        handle_write_storage(context, &input_bytes, inputs.gas_limit)
97    } else {
98        Err(PrecompileError::Other("Invalid input length".into()))
99    };
100
101    match result {
102        Ok(output) => {
103            let mut interpreter_result = InterpreterResult {
104                result: if output.reverted {
105                    InstructionResult::Revert
106                } else {
107                    InstructionResult::Return
108                },
109                gas: Gas::new(inputs.gas_limit),
110                output: output.bytes,
111            };
112            let underflow = interpreter_result.gas.record_cost(output.gas_used);
113            if !underflow {
114                interpreter_result.result = InstructionResult::PrecompileOOG;
115            }
116            Ok(interpreter_result)
117        }
118        Err(e) => {
119            // If this is a top-level precompile call and error is non-OOG, record the message
120            if !e.is_oog() && context.journal().depth() == 1 {
121                context
122                    .local_mut()
123                    .set_precompile_error_context(e.to_string());
124            }
125            Ok(InterpreterResult {
126                result: if e.is_oog() {
127                    InstructionResult::PrecompileOOG
128                } else {
129                    InstructionResult::PrecompileError
130                },
131                gas: Gas::new(inputs.gas_limit),
132                output: Bytes::new(),
133            })
134        }
135    }
136}
137
138/// Handles reading from storage
139fn handle_read_storage<CTX: ContextTr>(context: &mut CTX, gas_limit: u64) -> PrecompileResult {
140    // Base gas cost for reading storage
141    const BASE_GAS: u64 = 2_100;
142
143    if gas_limit < BASE_GAS {
144        return Err(PrecompileError::OutOfGas);
145    }
146
147    // Read from storage using the journal
148    let value = context
149        .journal_mut()
150        .sload(CUSTOM_PRECOMPILE_ADDRESS, STORAGE_KEY)
151        .map_err(|e| PrecompileError::Other(format!("Storage read failed: {e:?}").into()))?
152        .data;
153
154    // Return the value as output
155    Ok(PrecompileOutput::new(
156        BASE_GAS,
157        value.to_be_bytes_vec().into(),
158    ))
159}
160
161/// Handles writing to storage and transferring balance
162fn handle_write_storage<CTX: ContextTr>(
163    context: &mut CTX,
164    input: &[u8],
165    gas_limit: u64,
166) -> PrecompileResult {
167    // Base gas cost for the operation
168    const BASE_GAS: u64 = 21_000;
169    const SSTORE_GAS: u64 = 20_000;
170
171    if gas_limit < BASE_GAS + SSTORE_GAS {
172        return Err(PrecompileError::OutOfGas);
173    }
174
175    // Parse the input as a U256 value
176    let value = U256::from_be_slice(input);
177
178    // Store the value in the precompile's storage
179    context
180        .journal_mut()
181        .sstore(CUSTOM_PRECOMPILE_ADDRESS, STORAGE_KEY, value)
182        .map_err(|e| PrecompileError::Other(format!("Storage write failed: {e:?}").into()))?;
183
184    // Get the caller address
185    let caller = context.tx().caller();
186
187    // Transfer 1 wei from the precompile to the caller as a reward
188    // First, ensure the precompile has balance
189    context
190        .journal_mut()
191        .balance_incr(CUSTOM_PRECOMPILE_ADDRESS, U256::from(1))
192        .map_err(|e| PrecompileError::Other(format!("Balance increment failed: {e:?}").into()))?;
193
194    // Then transfer to caller
195    let transfer_result = context
196        .journal_mut()
197        .transfer(CUSTOM_PRECOMPILE_ADDRESS, caller, U256::from(1))
198        .map_err(|e| PrecompileError::Other(format!("Transfer failed: {e:?}").into()))?;
199
200    if let Some(error) = transfer_result {
201        return Err(PrecompileError::Other(
202            format!("Transfer error: {error:?}").into(),
203        ));
204    }
205
206    // Create a log to record the storage write operation
207    // Topic 0: keccak256("StorageWritten(address,uint256)")
208    let topic0 = B256::from_slice(&[
209        0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde,
210        0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc,
211        0xde, 0xf0,
212    ]);
213    // Topic 1: caller address (indexed) - left-padded to 32 bytes
214    let mut topic1_bytes = [0u8; 32];
215    topic1_bytes[12..32].copy_from_slice(caller.as_slice());
216    let topic1 = B256::from(topic1_bytes);
217    // Data: the value that was written
218    let log_data = value.to_be_bytes_vec();
219
220    let log = Log::new(
221        CUSTOM_PRECOMPILE_ADDRESS,
222        vec![topic0, topic1],
223        log_data.into(),
224    )
225    .expect("Failed to create log");
226
227    context.journal_mut().log(log);
228
229    // Return success with empty output
230    Ok(PrecompileOutput::new(BASE_GAS + SSTORE_GAS, Bytes::new()))
231}