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 alloy_sol_types::sol! {
291 /// A Solidity struct representing a commitment used for validation within Steel proofs.
292 ///
293 /// This struct is used to commit to a specific claim, such as the hash of an execution block
294 /// or a beacon chain state root. It includes an identifier combining the claim type (version)
295 /// and a specific instance identifier (e.g., block number), the claim digest itself, and a
296 /// configuration ID to ensure the commitment is valid for the intended network configuration.
297 /// This structure is designed to be ABI-compatible with Solidity for on-chain verification.
298 #[derive(Default, PartialEq, Eq, Hash)]
299 struct Commitment {
300 /// Packed commitment identifier and version.
301 ///
302 /// This field encodes two distinct pieces of information into a single 256-bit unsigned integer:
303 /// 1. **Version (Top 16 bits):** Bits `[255..240]` store a `u16` representing the type or version
304 /// of the claim being made. See [CommitmentVersion] for defined values like
305 /// `Block` or `Beacon`.
306 /// 2. **Identifier (Bottom 64 bits):** Bits `[63..0]` store a `u64` value that uniquely identifies
307 /// the specific instance of the claim. For example, for a `Block` commitment, this
308 /// would be the block number. For a `Beacon` commitment, it would be the slot number.
309 ///
310 /// Use [Commitment::decode_id] to unpack this field into its constituent parts in Rust code.
311 /// The packing scheme ensures efficient storage and retrieval while maintaining compatibility
312 /// with Solidity's `uint256`.
313 ///
314 /// [CommitmentVersion]: crate::CommitmentVersion
315 uint256 id;
316
317 /// The cryptographic digest representing the core claim data.
318 ///
319 /// This is the actual data being attested to. The exact meaning depends on the `version` specified in the `id` field.
320 bytes32 digest;
321
322 /// A cryptographic digest identifying the network and prover configuration.
323 ///
324 /// This ID acts as a fingerprint of the context in which the commitment was generated,
325 /// including details like the Ethereum chain ID, active hard forks (part of the chain spec),
326 /// and potentially prover-specific settings. Verification must ensure this `configID`
327 /// matches the verifier's current environment configuration to prevent cross-chain or
328 /// misconfigured proof validation.
329 bytes32 configID;
330 }
331 }
332}
333
334// Publicly export only the Commitment struct definition generated by the sol! macro.
335pub use private::Commitment;
336
337/// Version identifier for a [Commitment], indicating the type of claim.
338///
339/// This enum defines the valid types of commitments that can be created and verified.
340/// The raw `u16` value of the enum variant is stored in the top 16 bits of the
341/// [Commitment::id] field.
342#[derive(Debug, Copy, Clone, PartialEq, Eq, enumn::N)]
343#[repr(u16)]
344#[non_exhaustive]
345pub enum CommitmentVersion {
346 /// Version 0: Commitment to an execution block hash indexed by its block number.
347 Block = 0,
348 /// Version 1: Commitment to a beacon block root indexed by its EIP-4788 child timestamp.
349 Beacon = 1,
350 /// Version 2: Commitment to a beacon block root indexed by its slot.
351 Consensus = 2,
352}
353
354impl Commitment {
355 /// The size in bytes of the ABI-encoded commitment (3 fields * 32 bytes/field = 96 bytes).
356 pub const ABI_ENCODED_SIZE: usize = 3 * 32;
357
358 /// Creates a new [Commitment] by packing the version and identifier into the `id` field.
359 #[inline]
360 pub const fn new(version: u16, id: u64, digest: B256, config_id: B256) -> Commitment {
361 Self {
362 id: Commitment::encode_id(id, version), // pack id and version
363 digest,
364 configID: config_id,
365 }
366 }
367
368 /// Decodes the packed `Commitment.id` field into the identifier part and the version.
369 ///
370 /// This function extracts the version from the top 16 bits and returns the remaining part of
371 /// the `id` field (which contains the instance identifier in its lower 64 bits) along with the
372 /// `u16` version.
373 #[inline]
374 pub fn decode_id(&self) -> (U256, u16) {
375 // define a mask to isolate the lower 240 bits (zeroing out the top 16 version bits)
376 let id_mask =
377 uint!(0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff_U256);
378 let id_part = self.id & id_mask;
379
380 // extract the version by right-shifting the most significant limb (limbs[3]) by 48 bits
381 let version = (self.id.as_limbs()[3] >> 48) as u16;
382
383 (id_part, version)
384 }
385
386 /// ABI-encodes the commitment into a byte vector according to Solidity ABI specifications.
387 #[inline]
388 pub fn abi_encode(&self) -> Vec<u8> {
389 SolValue::abi_encode(self)
390 }
391
392 /// Packs a `u64` identifier and a `u16` version into a single `U256` value.
393 const fn encode_id(id: u64, version: u16) -> U256 {
394 U256::from_limbs([id, 0, 0, (version as u64) << 48])
395 }
396}
397
398impl Debug for Commitment {
399 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400 let (id, version_code) = self.decode_id();
401 let version = match CommitmentVersion::n(version_code) {
402 Some(v) => format!("{v:?}"),
403 None => format!("Unknown({version_code:x})"),
404 };
405
406 f.debug_struct("Commitment")
407 .field("version", &version)
408 .field("id", &id)
409 .field("digest", &self.digest)
410 .field("configID", &self.configID)
411 .finish()
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418 use alloy_primitives::B256;
419
420 #[test]
421 fn size() {
422 let tests = vec![
423 Commitment::default(),
424 Commitment::new(
425 u16::MAX,
426 u64::MAX,
427 B256::repeat_byte(0xFF),
428 B256::repeat_byte(0xFF),
429 ),
430 ];
431 for test in tests {
432 assert_eq!(test.abi_encode().len(), Commitment::ABI_ENCODED_SIZE);
433 }
434 }
435
436 #[test]
437 fn versioned_id() {
438 let tests = vec![(u64::MAX, u16::MAX), (u64::MAX, 0), (0, u16::MAX), (0, 0)];
439 for test in tests {
440 let commit = Commitment::new(test.1, test.0, B256::default(), B256::default());
441 let (id, version) = commit.decode_id();
442 assert_eq!((id.to(), version), test);
443 }
444 }
445}