sp1_cc_client_executor/
lib.rs

1//! # RSP Client Executor Lib
2//!
3//! This library provides the core functionality for executing smart contract calls within a
4//! zero-knowledge virtual machine (zkVM) environment. It includes utilities for blockchain
5//! state validation, EVM execution, and proof generation.
6//!
7//! ## Main Components
8//!
9//! - [`ClientExecutor`]: The primary executor for smart contract calls in zkVM
10//! - [`ContractInput`]: Input specification for contract calls and creations
11//! - [`ContractPublicValues`]: Public outputs that can be verified on-chain
12//! - [`Anchor`]: Various blockchain anchoring mechanisms for state validation
13//!
14//! ## Features
15//!
16//! - Execute smart contracts with full EVM compatibility
17//! - Validate blockchain state against Merkle proofs
18//! - Support for multiple anchor types (block hash, EIP-4788, consensus)
19//! - Log filtering and event decoding
20//! - Zero-knowledge proof generation for contract execution
21
22use std::sync::Arc;
23
24use alloy_consensus::Header;
25use alloy_eips::Encodable2718;
26use alloy_evm::IntoTxEnv;
27use alloy_primitives::{keccak256, Log};
28use alloy_rpc_types::{Filter, FilteredParams};
29use alloy_sol_types::{sol, SolCall, SolEvent, SolValue};
30use alloy_trie::root::ordered_trie_root_with_encoder;
31use eyre::bail;
32use io::EvmSketchInput;
33use reth_chainspec::EthChainSpec;
34use reth_primitives::EthPrimitives;
35use revm::{
36    context::{result::ExecutionResult, TxEnv},
37    database::CacheDB,
38};
39use revm_primitives::{hardfork::SpecId, Address, Bytes, TxKind, B256, U256};
40use rsp_client_executor::io::{TrieDB, WitnessInput};
41
42mod anchor;
43pub use anchor::{
44    get_beacon_root_from_state, rebuild_merkle_root, Anchor, BeaconAnchor, BeaconAnchorId,
45    BeaconStateAnchor, BeaconWithHeaderAnchor, ChainedBeaconAnchor, HeaderAnchor,
46    BLOCK_HASH_LEAF_INDEX, HISTORY_BUFFER_LENGTH, STATE_ROOT_LEAF_INDEX,
47};
48
49pub mod io;
50
51mod errors;
52pub use errors::ClientError;
53
54pub use rsp_primitives::genesis::Genesis;
55
56use crate::{anchor::ResolvedAnchor, io::Primitives};
57
58/// Input to a contract call.
59///
60/// Can be used to call an existing contract or create a new one. If used to create a new one,
61#[derive(Debug, Clone)]
62pub struct ContractInput {
63    /// The address of the contract to call.
64    pub contract_address: Address,
65    /// The address of the caller.
66    pub caller_address: Address,
67    /// The calldata to pass to the contract.
68    pub calldata: ContractCalldata,
69}
70
71/// The type of calldata to pass to a contract.
72///
73/// This enum is used to distinguish between contract calls and contract creations.
74#[derive(Debug, Clone)]
75pub enum ContractCalldata {
76    Call(Bytes),
77    Create(Bytes),
78}
79
80impl ContractCalldata {
81    /// Encode the calldata as a bytes.
82    pub fn to_bytes(&self) -> Bytes {
83        match self {
84            Self::Call(calldata) => calldata.clone(),
85            Self::Create(calldata) => calldata.clone(),
86        }
87    }
88}
89
90impl ContractInput {
91    /// Create a new contract call input.
92    pub fn new_call<C: SolCall>(
93        contract_address: Address,
94        caller_address: Address,
95        calldata: C,
96    ) -> Self {
97        Self {
98            contract_address,
99            caller_address,
100            calldata: ContractCalldata::Call(calldata.abi_encode().into()),
101        }
102    }
103
104    /// Creates a new contract creation input.
105    ///
106    /// To create a new contract, we send a transaction with TxKind Create to the
107    /// zero address. As such, the contract address will be set to the zero address.
108    pub fn new_create(caller_address: Address, calldata: Bytes) -> Self {
109        Self {
110            contract_address: Address::ZERO,
111            caller_address,
112            calldata: ContractCalldata::Create(calldata),
113        }
114    }
115}
116
117impl IntoTxEnv<TxEnv> for &ContractInput {
118    fn into_tx_env(self) -> TxEnv {
119        TxEnv {
120            caller: self.caller_address,
121            data: self.calldata.to_bytes(),
122            // Set the gas price to 0 to avoid lack of funds (0) error.
123            gas_price: 0,
124            kind: match self.calldata {
125                ContractCalldata::Create(_) => TxKind::Create,
126                ContractCalldata::Call(_) => TxKind::Call(self.contract_address),
127            },
128            chain_id: None,
129            ..Default::default()
130        }
131    }
132}
133
134#[cfg(feature = "optimism")]
135impl IntoTxEnv<op_revm::OpTransaction<TxEnv>> for &ContractInput {
136    fn into_tx_env(self) -> op_revm::OpTransaction<TxEnv> {
137        op_revm::OpTransaction { base: self.into_tx_env(), ..Default::default() }
138    }
139}
140
141sol! {
142    #[derive(Debug)]
143    enum AnchorType { BlockHash, Timestamp, Slot }
144
145    /// Public values of a contract call.
146    ///
147    /// These outputs can easily be abi-encoded, for use on-chain.
148    #[derive(Debug)]
149    struct ContractPublicValues {
150        uint256 id;
151        bytes32 anchorHash;
152        AnchorType anchorType;
153        bytes32 chainConfigHash;
154        address callerAddress;
155        address contractAddress;
156        bytes contractCalldata;
157        bytes contractOutput;
158    }
159
160    #[derive(Debug)]
161    struct ChainConfig {
162        uint chainId;
163        string activeForkName;
164    }
165}
166
167impl ContractPublicValues {
168    /// Construct a new [`ContractPublicValues`]
169    ///
170    /// By default, commit the contract input, the output, and the block hash to public values of
171    /// the proof. More can be committed if necessary.
172    pub fn new(
173        call: ContractInput,
174        output: Bytes,
175        id: U256,
176        anchor: B256,
177        anchor_type: AnchorType,
178        chain_config_hash: B256,
179    ) -> Self {
180        Self {
181            id,
182            anchorHash: anchor,
183            anchorType: anchor_type,
184            chainConfigHash: chain_config_hash,
185            contractAddress: call.contract_address,
186            callerAddress: call.caller_address,
187            contractCalldata: call.calldata.to_bytes(),
188            contractOutput: output,
189        }
190    }
191}
192
193/// An executor that executes smart contract calls inside a zkVM.
194#[derive(Debug)]
195pub struct ClientExecutor<'a, P: Primitives> {
196    // The execution block header
197    pub header: &'a Header,
198    /// The block anchor.
199    pub anchor: ResolvedAnchor,
200    /// The chain specification.
201    pub chain_spec: Arc<P::ChainSpec>,
202    /// The database that the executor uses to access state.
203    pub witness_db: TrieDB<'a>,
204    /// All logs in the block.
205    pub logs: Option<Vec<Log>>,
206    /// The hashed chain config, computed from the chain id and active hardfork hash (following
207    /// EIP-2124).
208    pub chain_config_hash: B256,
209}
210
211impl<'a> ClientExecutor<'a, EthPrimitives> {
212    /// Instantiates a new [`ClientExecutor`]
213    pub fn eth(state_sketch: &'a EvmSketchInput) -> Result<Self, ClientError> {
214        Self::new(state_sketch)
215    }
216}
217
218#[cfg(feature = "optimism")]
219impl<'a> ClientExecutor<'a, reth_optimism_primitives::OpPrimitives> {
220    /// Instantiates a new [`ClientExecutor`]
221    pub fn optimism(state_sketch: &'a EvmSketchInput) -> Result<Self, ClientError> {
222        Self::new(state_sketch)
223    }
224}
225
226impl<'a, P: Primitives> ClientExecutor<'a, P> {
227    /// Instantiates a new [`ClientExecutor`]
228    fn new(sketch_input: &'a EvmSketchInput) -> Result<Self, ClientError> {
229        let chain_spec = P::build_spec(&sketch_input.genesis)?;
230        let header = sketch_input.anchor.header();
231        let chain_config_hash = Self::hash_chain_config(chain_spec.as_ref(), header);
232
233        let sealed_headers = sketch_input.sealed_headers().collect::<Vec<_>>();
234
235        P::validate_header(&sealed_headers[0], chain_spec.clone())
236            .expect("the header is not valid");
237
238        // Verify the state root
239        assert_eq!(header.state_root, sketch_input.state.state_root(), "State root mismatch");
240
241        // Verify that ancestors form a valid chain
242        let mut previous_header = header;
243        for ancestor in sealed_headers.iter().skip(1) {
244            let ancestor_hash = ancestor.hash();
245
246            P::validate_header(ancestor, chain_spec.clone())
247                .unwrap_or_else(|_| panic!("the ancestor {} header in not valid", ancestor.number));
248            assert_eq!(
249                previous_header.parent_hash, ancestor_hash,
250                "block {} is not the parent of {}",
251                ancestor.number, previous_header.number
252            );
253            previous_header = ancestor;
254        }
255
256        let header = sketch_input.anchor.header();
257        let anchor = sketch_input.anchor.resolve();
258
259        if let Some(receipts) = &sketch_input.receipts {
260            // verify the receipts root hash
261            let root = ordered_trie_root_with_encoder(receipts, |r, out| r.encode_2718(out));
262            assert_eq!(sketch_input.anchor.header().receipts_root, root, "Receipts root mismatch");
263        }
264
265        let logs = sketch_input
266            .receipts
267            .as_ref()
268            .map(|receipts| receipts.iter().flat_map(|r| r.logs().to_vec()).collect());
269
270        Ok(Self {
271            header,
272            anchor,
273            chain_spec,
274            witness_db: sketch_input.witness_db(&sealed_headers)?,
275            logs,
276            chain_config_hash,
277        })
278    }
279
280    /// Executes the smart contract call with the given [`ContractInput`] in SP1.
281    ///
282    /// Storage accesses are already validated against the `witness_db`'s state root.
283    ///
284    /// Note: It's the caller's responsability to commit the pubic values returned by
285    /// this function. [`execute_and_commit`] can be used instead of this function
286    /// to automatically commit if the execution is successful.
287    ///
288    /// [`execute_and_commit`]: ClientExecutor::execute_and_commit
289    pub fn execute(&self, call: ContractInput) -> eyre::Result<ContractPublicValues> {
290        let cache_db = CacheDB::new(&self.witness_db);
291        let tx_output =
292            P::transact(&call, cache_db, self.header, U256::ZERO, self.chain_spec.clone()).unwrap();
293
294        let tx_output_bytes = match tx_output.result {
295            ExecutionResult::Success { output, .. } => output.data().clone(),
296            ExecutionResult::Revert { output, .. } => bail!("Execution reverted: {output}"),
297            ExecutionResult::Halt { reason, .. } => bail!("Execution halted : {reason:?}"),
298        };
299
300        let public_values = ContractPublicValues::new(
301            call,
302            tx_output_bytes,
303            self.anchor.id,
304            self.anchor.hash,
305            self.anchor.ty,
306            self.chain_config_hash,
307        );
308
309        Ok(public_values)
310    }
311
312    /// Executes the smart contract call with the given [`ContractInput`] in SP1
313    /// and commit the result to the public values stream.
314    ///
315    /// Storage accesses are already validated against the `witness_db`'s state root.
316    pub fn execute_and_commit(&self, call: ContractInput) {
317        let public_values = self.execute(call).unwrap();
318        sp1_zkvm::io::commit_slice(&public_values.abi_encode());
319    }
320
321    /// Returns the decoded logs matching the provided `filter`.
322    ///
323    /// To be available in the client, the logs need to be prefetched in the host first.
324    pub fn get_logs<E: SolEvent>(&self, filter: Filter) -> Result<Vec<Log<E>>, ClientError> {
325        if let Some(logs) = &self.logs {
326            let params = FilteredParams::new(Some(filter));
327
328            logs.iter()
329                .filter(|log| {
330                    params.filter_address(&log.address) && params.filter_topics(log.topics())
331                })
332                .map(|log| E::decode_log(log))
333                .collect::<Result<_, _>>()
334                .map_err(Into::into)
335        } else {
336            Err(ClientError::LogsNotPrefetched)
337        }
338    }
339
340    fn hash_chain_config(chain_spec: &P::ChainSpec, execution_header: &Header) -> B256 {
341        let chain_config = ChainConfig {
342            chainId: U256::from(chain_spec.chain_id()),
343            activeForkName: P::active_fork_name(chain_spec, execution_header),
344        };
345
346        keccak256(chain_config.abi_encode_packed())
347    }
348}
349
350/// Verifies a chain config hash.
351///
352/// Note: For OP stack chains, use [`verifiy_chain_config_optimism`].
353pub fn verifiy_chain_config_eth(
354    chain_config_hash: B256,
355    chain_id: u64,
356    active_fork: SpecId,
357) -> Result<(), ClientError> {
358    let chain_config =
359        ChainConfig { chainId: U256::from(chain_id), activeForkName: active_fork.to_string() };
360
361    let hash = keccak256(chain_config.abi_encode_packed());
362
363    if chain_config_hash == hash {
364        Ok(())
365    } else {
366        Err(ClientError::InvalidChainConfig)
367    }
368}
369
370#[cfg(feature = "optimism")]
371/// Verifies a chain config hash on a OP stack chain.
372pub fn verifiy_chain_config_optimism(
373    chain_config_hash: B256,
374    chain_id: u64,
375    active_fork: op_revm::OpSpecId,
376) -> Result<(), ClientError> {
377    let active_fork: &'static str = active_fork.into();
378    let chain_config =
379        ChainConfig { chainId: U256::from(chain_id), activeForkName: active_fork.to_string() };
380
381    let hash = keccak256(chain_config.abi_encode_packed());
382
383    if chain_config_hash == hash {
384        Ok(())
385    } else {
386        Err(ClientError::InvalidChainConfig)
387    }
388}