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