risc0_steel/lib.rs
1// Copyright 2025 RISC Zero, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![cfg_attr(not(doctest), doc = include_str!("../../../README.md"))]
16#![deny(rustdoc::broken_intra_doc_links)]
17#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
18
19/// Re-export of [alloy], provided to ensure that the correct version of the types used in the
20/// public API are available in case multiple versions of [alloy] are in use.
21#[cfg(feature = "host")]
22pub use alloy;
23pub use revm;
24
25use ::serde::{de::DeserializeOwned, Deserialize, Serialize};
26use alloy_evm::{Database, Evm, EvmError, IntoTxEnv};
27use alloy_primitives::{
28 uint, Address, BlockNumber, Bloom, Bytes, ChainId, Log, Sealable, Sealed, B256, U256,
29};
30use alloy_rpc_types::Filter;
31use alloy_sol_types::SolValue;
32use config::ChainSpec;
33use revm::{
34 context::{result::HaltReasonTr, BlockEnv},
35 Database as RevmDatabase,
36};
37use std::{error::Error, fmt, fmt::Debug};
38
39pub mod account;
40pub mod beacon;
41mod block;
42pub mod config;
43mod contract;
44pub mod ethereum;
45pub mod event;
46pub mod history;
47#[cfg(feature = "host")]
48pub mod host;
49mod merkle;
50mod mpt;
51pub mod serde;
52mod state;
53#[cfg(test)]
54mod test_utils;
55mod verifier;
56
57pub use account::Account;
58pub use beacon::BeaconInput;
59pub use block::BlockInput;
60pub use contract::{CallBuilder, Contract};
61pub use event::Event;
62pub use history::HistoryInput;
63pub use mpt::MerkleTrie;
64pub use state::{StateAccount, StateDb};
65pub use verifier::SteelVerifier;
66
67/// The serializable input to derive and validate an [EvmEnv] from.
68#[non_exhaustive]
69#[derive(Clone, Serialize, Deserialize)]
70pub enum EvmInput<F: EvmFactory> {
71 /// Input committing to the corresponding execution block hash.
72 Block(BlockInput<F>),
73 /// Input committing to the corresponding Beacon Chain block root.
74 Beacon(BeaconInput<F>),
75 /// Input recursively committing to multiple Beacon Chain block root.
76 History(HistoryInput<F>),
77}
78
79impl<F: EvmFactory> EvmInput<F> {
80 /// Converts the input into a [EvmEnv] for execution.
81 ///
82 /// This method verifies that the state matches the state root in the header and panics if not.
83 #[inline]
84 pub fn into_env(self, chain_spec: &ChainSpec<F::Spec>) -> GuestEvmEnv<F> {
85 match self {
86 EvmInput::Block(input) => input.into_env(chain_spec),
87 EvmInput::Beacon(input) => input.into_env(chain_spec),
88 EvmInput::History(input) => input.into_env(chain_spec),
89 }
90 }
91}
92
93/// A trait linking the block header to a commitment.
94pub trait BlockHeaderCommit<H> {
95 /// Creates a verifiable [Commitment] of the `header`.
96 fn commit(self, header: &Sealed<H>, config_id: B256) -> Commitment;
97}
98
99/// A generalized input type consisting of a block-based input and a commitment wrapper.
100///
101/// The `commit` field provides a mechanism to generate a commitment to the block header
102/// contained within the `input` field.
103#[derive(Clone, Serialize, Deserialize)]
104pub struct ComposeInput<F: EvmFactory, C> {
105 input: BlockInput<F>,
106 commit: C,
107}
108
109impl<F: EvmFactory, C: BlockHeaderCommit<F::Header>> ComposeInput<F, C> {
110 /// Creates a new composed input from a [BlockInput] and a [BlockHeaderCommit].
111 pub const fn new(input: BlockInput<F>, commit: C) -> Self {
112 Self { input, commit }
113 }
114
115 /// Disassembles this `ComposeInput`, returning the underlying input and commitment creator.
116 pub fn into_parts(self) -> (BlockInput<F>, C) {
117 (self.input, self.commit)
118 }
119
120 /// Converts the input into a [EvmEnv] for verifiable state access in the guest.
121 pub fn into_env(self, chain_spec: &ChainSpec<F::Spec>) -> GuestEvmEnv<F> {
122 let mut env = self.input.into_env(chain_spec);
123 env.commit = self.commit.commit(&env.header, env.commit.configID);
124
125 env
126 }
127}
128
129/// A database abstraction for the Steel EVM.
130pub trait EvmDatabase: RevmDatabase {
131 /// Retrieves all the logs matching the given [Filter].
132 ///
133 /// It returns an error, if the corresponding logs cannot be retrieved from DB.
134 /// The filter must match the block hash corresponding to the DB, it will panic otherwise.
135 fn logs(&mut self, filter: Filter) -> Result<Vec<Log>, <Self as RevmDatabase>::Error>;
136}
137
138/// Alias for readability, do not make public.
139pub(crate) type GuestEvmEnv<F> = EvmEnv<StateDb, F, Commitment>;
140
141/// Abstracts the creation and configuration of a specific EVM implementation.
142///
143/// This trait acts as a factory pattern, allowing generic code (like `Contract` and `CallBuilder`)
144/// to operate with different underlying EVM engines (e.g., `revm`) without being
145/// tightly coupled to a specific implementation. Implementers define the concrete types
146/// associated with their chosen EVM and provide the logic to instantiate it.
147pub trait EvmFactory {
148 /// The concrete EVM execution environment type created by this factory.
149 type Evm<DB: Database>: Evm<
150 DB = DB,
151 Tx = Self::Tx,
152 HaltReason = Self::HaltReason,
153 Error = Self::Error<DB::Error>,
154 Spec = Self::Spec,
155 >;
156 /// The transaction environment type compatible with `Self::Evm`.
157 type Tx: IntoTxEnv<Self::Tx> + Send + Sync + 'static;
158 /// The error type returned by `Self::Evm` during execution.
159 type Error<DBError: Error + Send + Sync + 'static>: EvmError;
160 /// The type representing reasons why `Self::Evm` might halt execution.
161 type HaltReason: HaltReasonTr + Send + Sync + 'static;
162 /// The EVM specification identifier (e.g., Shanghai, Cancun) used by `Self::Evm`.
163 type Spec: Ord + Serialize + Debug + Copy + Send + Sync + 'static;
164 /// The block header type providing execution context (e.g., timestamp, number, basefee).
165 type Header: EvmBlockHeader<Spec = Self::Spec>
166 + Clone
167 + Serialize
168 + DeserializeOwned
169 + Send
170 + Sync
171 + 'static;
172
173 /// Creates a new transaction environment instance for a basic call.
174 ///
175 /// Implementers should create an instance of `Self::Tx`,
176 /// populate it with the target `address` and input `data`, and apply appropriate
177 /// defaults for other transaction fields (like caller, value, gas limit, etc.)
178 /// required by the specific EVM implementation.
179 fn new_tx(address: Address, data: Bytes) -> Self::Tx;
180
181 /// Creates a new instance of the EVM defined by `Self::Evm`.
182 fn create_evm<DB: Database>(
183 db: DB,
184 chain_id: ChainId,
185 spec: Self::Spec,
186 header: &Self::Header,
187 ) -> Self::Evm<DB>;
188}
189
190/// Represents the complete execution environment for EVM contract calls.
191///
192/// This struct encapsulates all necessary components to configure and run an EVM instance
193/// compatible with the specified [EvmFactory]. It serves as the primary context object passed
194/// around during EVM execution setup and interaction, both on the host (for preflight) and in the
195/// guest.
196pub struct EvmEnv<D, F: EvmFactory, C> {
197 /// The database instance providing EVM state (accounts, storage).
198 ///
199 /// This is wrapped in an `Option` because ownership might need to be temporarily
200 /// transferred during certain operations, particularly when moving execution into
201 /// a blocking task or thread on the host during preflight simulation.
202 db: Option<D>,
203 /// The Chain ID of the EVM network (EIP-155).
204 chain_id: ChainId,
205 /// The EVM specification identifier, representing the active hardfork (e.g., Shanghai,
206 /// Cancun).
207 spec: F::Spec,
208 /// The sealed block header providing context for the current transaction execution.
209 header: Sealed<F::Header>,
210 /// Auxiliary context or commitment handler.
211 commit: C,
212}
213
214impl<D, F: EvmFactory, C> EvmEnv<D, F, C> {
215 /// Creates a new environment.
216 pub(crate) fn new(
217 db: D,
218 chain_spec: &ChainSpec<F::Spec>,
219 header: Sealed<F::Header>,
220 commit: C,
221 ) -> Self {
222 let spec = *chain_spec
223 .active_fork(header.number(), header.timestamp())
224 .unwrap();
225 Self {
226 db: Some(db),
227 chain_id: chain_spec.chain_id,
228 spec,
229 header,
230 commit,
231 }
232 }
233
234 /// Returns the sealed header of the environment.
235 #[inline]
236 pub fn header(&self) -> &Sealed<F::Header> {
237 &self.header
238 }
239
240 pub(crate) fn db(&self) -> &D {
241 // safe unwrap: self cannot be borrowed without a DB
242 self.db.as_ref().unwrap()
243 }
244
245 #[cfg(feature = "host")]
246 pub(crate) fn db_mut(&mut self) -> &mut D {
247 // safe unwrap: self cannot be borrowed without a DB
248 self.db.as_mut().unwrap()
249 }
250}
251
252impl<D, F: EvmFactory> EvmEnv<D, F, Commitment> {
253 /// Returns the [Commitment] used to validate the environment.
254 #[inline]
255 pub fn commitment(&self) -> &Commitment {
256 &self.commit
257 }
258
259 /// Consumes and returns the [Commitment] used to validate the environment.
260 #[inline]
261 pub fn into_commitment(self) -> Commitment {
262 self.commit
263 }
264}
265
266/// An EVM abstraction of a block header.
267pub trait EvmBlockHeader: Sealable {
268 /// Associated type for the EVM specification (e.g., London, Shanghai)
269 type Spec: Copy;
270
271 /// Returns the hash of the parent block's header.
272 fn parent_hash(&self) -> &B256;
273 /// Returns the block number.
274 fn number(&self) -> BlockNumber;
275 /// Returns the block timestamp.
276 fn timestamp(&self) -> u64;
277 /// Returns the state root hash.
278 fn state_root(&self) -> &B256;
279 /// Returns the receipts root hash of the block.
280 fn receipts_root(&self) -> &B256;
281 /// Returns the logs bloom filter of the block
282 fn logs_bloom(&self) -> &Bloom;
283
284 /// Returns the EVM block environment equivalent to this block header.
285 fn to_block_env(&self, spec: Self::Spec) -> BlockEnv;
286}
287
288// Keep everything in the Steel library private except the commitment.
289mod private {
290 use serde::{Deserialize, Serialize};
291
292 alloy_sol_types::sol! {
293 /// A Solidity struct representing a commitment used for validation within Steel proofs.
294 ///
295 /// This struct is used to commit to a specific claim, such as the hash of an execution block
296 /// or a beacon chain state root. It includes an identifier combining the claim type (version)
297 /// and a specific instance identifier (e.g., block number), the claim digest itself, and a
298 /// configuration ID to ensure the commitment is valid for the intended network configuration.
299 /// This structure is designed to be ABI-compatible with Solidity for on-chain verification.
300 #[derive(Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
301 struct Commitment {
302 /// Packed commitment identifier and version.
303 ///
304 /// This field encodes two distinct pieces of information into a single 256-bit unsigned integer:
305 /// 1. **Version (Top 16 bits):** Bits `[255..240]` store a `u16` representing the type or version
306 /// of the claim being made. See [CommitmentVersion] for defined values like
307 /// `Block` or `Beacon`.
308 /// 2. **Identifier (Bottom 64 bits):** Bits `[63..0]` store a `u64` value that uniquely identifies
309 /// the specific instance of the claim. For example, for a `Block` commitment, this
310 /// would be the block number. For a `Beacon` commitment, it would be the slot number.
311 ///
312 /// Use [Commitment::decode_id] to unpack this field into its constituent parts in Rust code.
313 /// The packing scheme ensures efficient storage and retrieval while maintaining compatibility
314 /// with Solidity's `uint256`.
315 ///
316 /// [CommitmentVersion]: crate::CommitmentVersion
317 uint256 id;
318
319 /// The cryptographic digest representing the core claim data.
320 ///
321 /// This is the actual data being attested to. The exact meaning depends on the `version` specified in the `id` field.
322 bytes32 digest;
323
324 /// A cryptographic digest identifying the network and prover configuration.
325 ///
326 /// This ID acts as a fingerprint of the context in which the commitment was generated,
327 /// including details like the Ethereum chain ID, active hard forks (part of the chain spec),
328 /// and potentially prover-specific settings. Verification must ensure this `configID`
329 /// matches the verifier's current environment configuration to prevent cross-chain or
330 /// misconfigured proof validation.
331 bytes32 configID;
332 }
333 }
334}
335
336// Publicly export only the Commitment struct definition generated by the sol! macro.
337pub use private::Commitment;
338
339/// Version identifier for a [Commitment], indicating the type of claim.
340///
341/// This enum defines the valid types of commitments that can be created and verified.
342/// The raw `u16` value of the enum variant is stored in the top 16 bits of the
343/// [Commitment::id] field.
344#[derive(Debug, Copy, Clone, PartialEq, Eq, enumn::N)]
345#[repr(u16)]
346#[non_exhaustive]
347pub enum CommitmentVersion {
348 /// Version 0: Commitment to an execution block hash indexed by its block number.
349 Block = 0,
350 /// Version 1: Commitment to a beacon block root indexed by its EIP-4788 child timestamp.
351 Beacon = 1,
352 /// Version 2: Commitment to a beacon block root indexed by its slot.
353 Consensus = 2,
354}
355
356impl Commitment {
357 /// The size in bytes of the ABI-encoded commitment (3 fields * 32 bytes/field = 96 bytes).
358 pub const ABI_ENCODED_SIZE: usize = 3 * 32;
359
360 /// Creates a new [Commitment] by packing the version and identifier into the `id` field.
361 #[inline]
362 pub const fn new(version: u16, id: u64, digest: B256, config_id: B256) -> Commitment {
363 Self {
364 id: Commitment::encode_id(id, version), // pack id and version
365 digest,
366 configID: config_id,
367 }
368 }
369
370 /// Decodes the packed `Commitment.id` field into the identifier part and the version.
371 ///
372 /// This function extracts the version from the top 16 bits and returns the remaining part of
373 /// the `id` field (which contains the instance identifier in its lower 64 bits) along with the
374 /// `u16` version.
375 #[inline]
376 pub fn decode_id(&self) -> (U256, u16) {
377 // define a mask to isolate the lower 240 bits (zeroing out the top 16 version bits)
378 let id_mask =
379 uint!(0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff_U256);
380 let id_part = self.id & id_mask;
381
382 // extract the version by right-shifting the most significant limb (limbs[3]) by 48 bits
383 let version = (self.id.as_limbs()[3] >> 48) as u16;
384
385 (id_part, version)
386 }
387
388 /// ABI-encodes the commitment into a byte vector according to Solidity ABI specifications.
389 #[inline]
390 pub fn abi_encode(&self) -> Vec<u8> {
391 SolValue::abi_encode(self)
392 }
393
394 /// Packs a `u64` identifier and a `u16` version into a single `U256` value.
395 const fn encode_id(id: u64, version: u16) -> U256 {
396 U256::from_limbs([id, 0, 0, (version as u64) << 48])
397 }
398}
399
400impl Debug for Commitment {
401 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402 let (id, version_code) = self.decode_id();
403 let version = match CommitmentVersion::n(version_code) {
404 Some(v) => format!("{v:?}"),
405 None => format!("Unknown({version_code:x})"),
406 };
407
408 f.debug_struct("Commitment")
409 .field("version", &version)
410 .field("id", &id)
411 .field("digest", &self.digest)
412 .field("configID", &self.configID)
413 .finish()
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420 use alloy_primitives::B256;
421
422 #[test]
423 fn size() {
424 let tests = vec![
425 Commitment::default(),
426 Commitment::new(
427 u16::MAX,
428 u64::MAX,
429 B256::repeat_byte(0xFF),
430 B256::repeat_byte(0xFF),
431 ),
432 ];
433 for test in tests {
434 assert_eq!(test.abi_encode().len(), Commitment::ABI_ENCODED_SIZE);
435 }
436 }
437
438 #[test]
439 fn versioned_id() {
440 let tests = vec![(u64::MAX, u16::MAX), (u64::MAX, 0), (0, u16::MAX), (0, 0)];
441 for test in tests {
442 let commit = Commitment::new(test.1, test.0, B256::default(), B256::default());
443 let (id, version) = commit.decode_id();
444 assert_eq!((id.to(), version), test);
445 }
446 }
447}