risc0_steel/
block.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
15use crate::{
16    config::ChainSpec, serde::Eip2718Wrapper, state::StateDb, BlockHeaderCommit, Commitment,
17    CommitmentVersion, EvmBlockHeader, EvmEnv, EvmFactory, GuestEvmEnv, MerkleTrie,
18};
19use ::serde::{Deserialize, Serialize};
20use alloy_consensus::ReceiptEnvelope;
21use alloy_primitives::{map::HashMap, Bytes, Sealable, Sealed, B256};
22use std::marker::PhantomData;
23
24/// Input committing to the corresponding execution block hash.
25#[derive(Clone, Serialize, Deserialize)]
26pub struct BlockInput<F: EvmFactory> {
27    header: F::Header,
28    state_trie: MerkleTrie,
29    storage_tries: Vec<MerkleTrie>,
30    contracts: Vec<Bytes>,
31    ancestors: Vec<F::Header>,
32    receipts: Option<Vec<Eip2718Wrapper<ReceiptEnvelope>>>,
33    #[serde(skip)]
34    phantom: PhantomData<F>,
35}
36
37/// Implement [BlockHeaderCommit] for the unit type.
38/// This makes it possible to treat an `HostEvmEnv<D, H, ()>`, which is used for the [BlockInput]
39/// in the same way as any other `HostEvmEnv<D, H, BlockHeaderCommit>`.
40impl<H: EvmBlockHeader> BlockHeaderCommit<H> for () {
41    fn commit(self, header: &Sealed<H>, config_id: B256) -> Commitment {
42        Commitment::new(
43            CommitmentVersion::Block as u16,
44            header.number(),
45            header.seal(),
46            config_id,
47        )
48    }
49}
50
51impl<F: EvmFactory> BlockInput<F> {
52    /// Converts the input into a [EvmEnv] for verifiable state access in the guest.
53    pub fn into_env(self, chain_spec: &ChainSpec<F::Spec>) -> GuestEvmEnv<F> {
54        // verify that the state root matches the state trie
55        let state_root = self.state_trie.hash_slow();
56        assert_eq!(self.header.state_root(), &state_root, "State root mismatch");
57
58        // seal the header to compute its block hash
59        let header = self.header.seal_slow();
60
61        // validate that ancestor headers form a valid chain
62        let mut block_hashes =
63            HashMap::with_capacity_and_hasher(self.ancestors.len() + 1, Default::default());
64        block_hashes.insert(header.number(), header.seal());
65
66        let mut previous_header = header.inner();
67        for ancestor in &self.ancestors {
68            let ancestor_hash = ancestor.hash_slow();
69            assert_eq!(
70                previous_header.parent_hash(),
71                &ancestor_hash,
72                "Invalid ancestor chain: block {} is not the parent of block {}",
73                ancestor.number(),
74                previous_header.number()
75            );
76            block_hashes.insert(ancestor.number(), ancestor_hash);
77            previous_header = ancestor;
78        }
79
80        // verify the root hash of the included receipts and extract their logs
81        let logs = self.receipts.map(|receipts| {
82            let root = alloy_trie::root::ordered_trie_root_with_encoder(&receipts, |r, out| {
83                alloy_eips::eip2718::Encodable2718::encode_2718(r, out)
84            });
85            assert_eq!(header.receipts_root(), &root, "Receipts root mismatch");
86
87            receipts
88                .into_iter()
89                .flat_map(|wrapper| match wrapper.into_inner() {
90                    ReceiptEnvelope::Legacy(t) => t.receipt.logs,
91                    ReceiptEnvelope::Eip2930(t) => t.receipt.logs,
92                    ReceiptEnvelope::Eip1559(t) => t.receipt.logs,
93                    ReceiptEnvelope::Eip4844(t) => t.receipt.logs,
94                    ReceiptEnvelope::Eip7702(t) => t.receipt.logs,
95                })
96                .collect()
97        });
98
99        let db = StateDb::new(
100            self.state_trie,
101            self.storage_tries,
102            self.contracts,
103            block_hashes,
104            logs,
105        );
106        let commit = Commitment::new(
107            CommitmentVersion::Block as u16,
108            header.number(),
109            header.seal(),
110            chain_spec.digest(),
111        );
112
113        EvmEnv::new(db, chain_spec, header, commit)
114    }
115}
116
117#[cfg(feature = "host")]
118pub mod host {
119    use super::BlockInput;
120    use crate::{
121        host::db::{ProofDb, ProviderDb},
122        serde::Eip2718Wrapper,
123        EvmBlockHeader, EvmFactory,
124    };
125    use alloy::{network::Network, providers::Provider};
126    use alloy_primitives::Sealed;
127    use anyhow::{anyhow, ensure};
128    use log::debug;
129    use std::{fmt::Display, marker::PhantomData};
130
131    impl<F: EvmFactory> BlockInput<F> {
132        /// Creates the `BlockInput` containing the necessary EVM state that can be verified against
133        /// the block hash.
134        pub(crate) async fn from_proof_db<N, P>(
135            mut db: ProofDb<ProviderDb<N, P>>,
136            header: Sealed<F::Header>,
137        ) -> anyhow::Result<Self>
138        where
139            N: Network,
140            P: Provider<N>,
141            F::Header: TryFrom<<N as Network>::HeaderResponse>,
142            <F::Header as TryFrom<<N as Network>::HeaderResponse>>::Error: Display,
143        {
144            assert_eq!(db.inner().block(), header.seal(), "DB block mismatch");
145
146            let (state_trie, storage_tries) = db.state_proof().await?;
147            ensure!(
148                header.state_root() == &state_trie.hash_slow(),
149                "accountProof root does not match header's stateRoot"
150            );
151
152            // collect the bytecode of all referenced contracts
153            let contracts: Vec<_> = db.contracts().values().cloned().collect();
154
155            // retrieve ancestor block headers
156            let mut ancestors = Vec::new();
157            for rlp_header in db.ancestor_proof(header.number()).await? {
158                let header: F::Header = rlp_header
159                    .try_into()
160                    .map_err(|err| anyhow!("header invalid: {}", err))?;
161                ancestors.push(header);
162            }
163
164            let receipts = db.receipt_proof().await?;
165            // wrap the receipts so that they can be serialized
166            let receipts =
167                receipts.map(|receipts| receipts.into_iter().map(Eip2718Wrapper::new).collect());
168
169            debug!("state size: {}", state_trie.size());
170            debug!("storage tries: {}", storage_tries.len());
171            debug!(
172                "total storage size: {}",
173                storage_tries.iter().map(|t| t.size()).sum::<usize>()
174            );
175            debug!("contracts: {}", contracts.len());
176            debug!("ancestor blocks: {}", ancestors.len());
177            debug!("receipts: {:?}", receipts.as_ref().map(Vec::len));
178
179            let input = BlockInput {
180                header: header.into_inner(),
181                state_trie,
182                storage_tries,
183                contracts,
184                ancestors,
185                receipts,
186                phantom: PhantomData,
187            };
188
189            Ok(input)
190        }
191    }
192}