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
22pub mod io;
23use std::{
24    hash::{DefaultHasher, Hash, Hasher},
25    sync::Arc,
26};
27
28use alloy_eips::Encodable2718;
29use alloy_evm::IntoTxEnv;
30use alloy_primitives::{keccak256, Log};
31use alloy_rpc_types::{Filter, FilteredParams};
32use alloy_sol_types::{sol, SolCall, SolEvent};
33use alloy_trie::root::ordered_trie_root_with_encoder;
34use eyre::OptionExt;
35use io::EvmSketchInput;
36use reth_primitives::EthPrimitives;
37use revm::{context::TxEnv, database::CacheDB};
38use revm_primitives::{Address, Bytes, TxKind, B256, U256};
39use rsp_client_executor::io::{TrieDB, WitnessInput};
40
41mod anchor;
42pub use anchor::{
43    get_beacon_root_from_state, rebuild_merkle_root, Anchor, BeaconAnchor, BeaconAnchorId,
44    BeaconBlockField, BeaconStateAnchor, BeaconWithHeaderAnchor, ChainedBeaconAnchor, HeaderAnchor,
45    HISTORY_BUFFER_LENGTH,
46};
47
48mod errors;
49pub use errors::ClientError;
50
51pub use rsp_primitives::genesis::Genesis;
52
53use crate::io::Primitives;
54
55/// Input to a contract call.
56///
57/// Can be used to call an existing contract or create a new one. If used to create a new one,
58#[derive(Debug, Clone)]
59pub struct ContractInput {
60    /// The address of the contract to call.
61    pub contract_address: Address,
62    /// The address of the caller.
63    pub caller_address: Address,
64    /// The calldata to pass to the contract.
65    pub calldata: ContractCalldata,
66}
67
68/// The type of calldata to pass to a contract.
69///
70/// This enum is used to distinguish between contract calls and contract creations.
71#[derive(Debug, Clone)]
72pub enum ContractCalldata {
73    Call(Bytes),
74    Create(Bytes),
75}
76
77impl ContractCalldata {
78    /// Encode the calldata as a bytes.
79    pub fn to_bytes(&self) -> Bytes {
80        match self {
81            Self::Call(calldata) => calldata.clone(),
82            Self::Create(calldata) => calldata.clone(),
83        }
84    }
85}
86
87impl ContractInput {
88    /// Create a new contract call input.
89    pub fn new_call<C: SolCall>(
90        contract_address: Address,
91        caller_address: Address,
92        calldata: C,
93    ) -> Self {
94        Self {
95            contract_address,
96            caller_address,
97            calldata: ContractCalldata::Call(calldata.abi_encode().into()),
98        }
99    }
100
101    /// Creates a new contract creation input.
102    ///
103    /// To create a new contract, we send a transaction with TxKind Create to the
104    /// zero address. As such, the contract address will be set to the zero address.
105    pub fn new_create(caller_address: Address, calldata: Bytes) -> Self {
106        Self {
107            contract_address: Address::ZERO,
108            caller_address,
109            calldata: ContractCalldata::Create(calldata),
110        }
111    }
112}
113
114impl IntoTxEnv<TxEnv> for &ContractInput {
115    fn into_tx_env(self) -> TxEnv {
116        TxEnv {
117            caller: self.caller_address,
118            data: self.calldata.to_bytes(),
119            // Set the gas price to 0 to avoid lack of funds (0) error.
120            gas_price: 0,
121            kind: match self.calldata {
122                ContractCalldata::Create(_) => TxKind::Create,
123                ContractCalldata::Call(_) => TxKind::Call(self.contract_address),
124            },
125            chain_id: None,
126            ..Default::default()
127        }
128    }
129}
130
131#[cfg(feature = "optimism")]
132impl IntoTxEnv<op_revm::OpTransaction<TxEnv>> for &ContractInput {
133    fn into_tx_env(self) -> op_revm::OpTransaction<TxEnv> {
134        op_revm::OpTransaction { base: self.into_tx_env(), ..Default::default() }
135    }
136}
137
138sol! {
139    #[derive(Debug)]
140    enum AnchorType { BlockHash, Eip4788, Consensus }
141
142    /// Public values of a contract call.
143    ///
144    /// These outputs can easily be abi-encoded, for use on-chain.
145    #[derive(Debug)]
146    struct ContractPublicValues {
147        uint256 id;
148        bytes32 anchorHash;
149        AnchorType anchorType;
150        bytes32 genesisHash;
151        address callerAddress;
152        address contractAddress;
153        bytes contractCalldata;
154        bytes contractOutput;
155    }
156}
157
158impl ContractPublicValues {
159    /// Construct a new [`ContractPublicValues`]
160    ///
161    /// By default, commit the contract input, the output, and the block hash to public values of
162    /// the proof. More can be committed if necessary.
163    pub fn new(
164        call: ContractInput,
165        output: Bytes,
166        id: U256,
167        anchor: B256,
168        anchor_type: AnchorType,
169        genesis_hash: B256,
170    ) -> Self {
171        Self {
172            id,
173            anchorHash: anchor,
174            anchorType: anchor_type,
175            genesisHash: genesis_hash,
176            contractAddress: call.contract_address,
177            callerAddress: call.caller_address,
178            contractCalldata: call.calldata.to_bytes(),
179            contractOutput: output,
180        }
181    }
182}
183
184/// An executor that executes smart contract calls inside a zkVM.
185#[derive(Debug)]
186pub struct ClientExecutor<'a, P: Primitives> {
187    /// The block anchor.
188    pub anchor: &'a Anchor,
189    /// The chain specification.
190    pub chain_spec: Arc<P::ChainSpec>,
191    /// The database that the executor uses to access state.
192    pub witness_db: TrieDB<'a>,
193    /// All logs in the block.
194    pub logs: Vec<Log>,
195    /// The hashed genesis block specification.
196    pub genesis_hash: B256,
197}
198
199impl<'a> ClientExecutor<'a, EthPrimitives> {
200    /// Instantiates a new [`ClientExecutor`]
201    pub fn eth(state_sketch: &'a EvmSketchInput) -> Result<Self, ClientError> {
202        Self::new(state_sketch)
203    }
204}
205
206#[cfg(feature = "optimism")]
207impl<'a> ClientExecutor<'a, reth_optimism_primitives::OpPrimitives> {
208    /// Instantiates a new [`ClientExecutor`]
209    pub fn optimism(state_sketch: &'a EvmSketchInput) -> Result<Self, ClientError> {
210        Self::new(state_sketch)
211    }
212}
213
214impl<'a, P: Primitives> ClientExecutor<'a, P> {
215    /// Instantiates a new [`ClientExecutor`]
216    fn new(state_sketch: &'a EvmSketchInput) -> Result<Self, ClientError> {
217        let chain_spec = P::build_spec(&state_sketch.genesis)?;
218        let genesis_hash = hash_genesis(&state_sketch.genesis);
219        let header = state_sketch.anchor.header();
220        let sealed_headers = state_sketch.sealed_headers().collect::<Vec<_>>();
221
222        P::validate_header(&sealed_headers[0], chain_spec.clone())
223            .expect("the header is not valid");
224
225        // Verify the state root
226        assert_eq!(header.state_root, state_sketch.state.state_root(), "State root mismatch");
227
228        // Verify that ancestors form a valid chain
229        let mut previous_header = header;
230        for ancestor in sealed_headers.iter().skip(1) {
231            let ancestor_hash = ancestor.hash();
232
233            P::validate_header(ancestor, chain_spec.clone())
234                .unwrap_or_else(|_| panic!("the ancestor {} header in not valid", ancestor.number));
235            assert_eq!(
236                previous_header.parent_hash, ancestor_hash,
237                "block {} is not the parent of {}",
238                ancestor.number, previous_header.number
239            );
240            previous_header = ancestor;
241        }
242
243        if let Some(receipts) = &state_sketch.receipts {
244            // verify the receipts root hash
245            let root = ordered_trie_root_with_encoder(receipts, |r, out| r.encode_2718(out));
246            assert_eq!(state_sketch.anchor.header().receipts_root, root, "Receipts root mismatch");
247        }
248
249        let logs = state_sketch
250            .receipts
251            .as_ref()
252            .unwrap_or(&vec![])
253            .iter()
254            .flat_map(|r| r.logs().to_vec())
255            .collect();
256
257        Ok(Self {
258            anchor: &state_sketch.anchor,
259            chain_spec,
260            witness_db: state_sketch.witness_db(&sealed_headers)?,
261            logs,
262            genesis_hash,
263        })
264    }
265
266    /// Executes the smart contract call with the given [`ContractInput`] in SP1.
267    ///
268    /// Storage accesses are already validated against the `witness_db`'s state root.
269    pub fn execute(&self, call: ContractInput) -> eyre::Result<ContractPublicValues> {
270        let cache_db = CacheDB::new(&self.witness_db);
271        let tx_output =
272            P::transact(&call, cache_db, self.anchor.header(), U256::ZERO, self.chain_spec.clone())
273                .unwrap();
274        let tx_output_bytes = tx_output.result.output().ok_or_eyre("Error decoding result")?;
275        let resolved = self.anchor.resolve();
276
277        let public_values = ContractPublicValues::new(
278            call,
279            tx_output_bytes.clone(),
280            resolved.id,
281            resolved.hash,
282            self.anchor.ty(),
283            self.genesis_hash,
284        );
285
286        Ok(public_values)
287    }
288
289    /// Returns the decoded logs matching the provided `filter`.
290    ///
291    /// To be available in the client, the logs need to be prefetched in the host first.
292    pub fn get_logs<E: SolEvent>(&self, filter: Filter) -> Result<Vec<Log<E>>, ClientError> {
293        let params = FilteredParams::new(Some(filter));
294
295        self.logs
296            .iter()
297            .filter(|log| params.filter_address(&log.address) && params.filter_topics(log.topics()))
298            .map(|log| E::decode_log(log))
299            .collect::<Result<_, _>>()
300            .map_err(Into::into)
301    }
302}
303
304pub fn hash_genesis(genesis: &Genesis) -> B256 {
305    let mut s = DefaultHasher::new();
306    genesis.hash(&mut s);
307    let hash = s.finish();
308
309    keccak256(hash.to_be_bytes())
310}