risc0_steel/history/
mod.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//! Types related to commitments to a historical state.
16use crate::{
17    beacon, beacon::BeaconBlockId, BlockHeaderCommit, Commitment, CommitmentVersion, ComposeInput,
18};
19use alloy_primitives::{Sealed, B256, U256};
20use beacon::{BeaconCommit, GeneralizedBeaconCommit, STATE_ROOT_LEAF_INDEX};
21use beacon_roots::{BeaconRootsContract, BeaconRootsState};
22use serde::{Deserialize, Serialize};
23
24pub(crate) mod beacon_roots;
25
26/// Input committing a previous block hash to the corresponding Beacon Chain block root.
27pub type HistoryInput<F> = ComposeInput<F, HistoryCommit>;
28
29/// A commitment that an execution block is included as an ancestor of a specific beacon block on
30/// the Ethereum blockchain.
31///
32/// This struct encapsulates the necessary data to prove that a given execution block is part of the
33/// canonical chain according to the Beacon Chain.
34#[derive(Clone, Serialize, Deserialize)]
35pub struct HistoryCommit {
36    /// Commit of the Steel EVM execution block hash to its beacon block hash.
37    evm_commit: BeaconCommit,
38    /// Iterative commits for verifying `evm_commit` as an ancestor of some valid Beacon block.
39    state_commits: Vec<StateCommit>,
40}
41
42/// Represents a commitment of a beacon roots contract state to a Beacon Chain block root.
43#[derive(Clone, Serialize, Deserialize)]
44struct StateCommit {
45    /// State for verifying `evm_commit`.
46    state: BeaconRootsState,
47    /// Commitment for `state` to a Beacon Chain block root.
48    state_commit: GeneralizedBeaconCommit<STATE_ROOT_LEAF_INDEX>,
49}
50
51impl<H> BlockHeaderCommit<H> for HistoryCommit {
52    /// Generates a commitment that proves the given block header is included in the Beacon Chain's
53    /// history. Panics if the provided [HistoryCommit] data is invalid or inconsistent.
54    #[inline]
55    fn commit(self, header: &Sealed<H>, config_id: B256) -> Commitment {
56        // first, compute the beacon commit of the EVM execution
57        let evm_commitment = self.evm_commit.commit(header, config_id);
58        let (id, version) = evm_commitment.decode_id();
59        // just a sanity check, a BeaconCommit will always have this version
60        assert_eq!(version, CommitmentVersion::Beacon as u16);
61
62        let mut beacon_block_id = BeaconBlockId::Eip4788(id.to());
63        let mut beacon_root = evm_commitment.digest;
64
65        // starting from evm_commit, "walk forward" along state_commits to reach a later beacon root
66        for mut state_commit in self.state_commits {
67            // verify that the previous commitment is valid wrt the current state
68            let state_root = state_commit.state.root();
69            let timestamp = match beacon_block_id {
70                BeaconBlockId::Eip4788(ts) => U256::from(ts),
71                BeaconBlockId::Slot(_) => panic!("Invalid state commitment: wrong version"),
72            };
73            let commitment_root =
74                BeaconRootsContract::get_from_db(&mut state_commit.state, timestamp)
75                    .expect("Beacon roots contract failed");
76            assert_eq!(commitment_root, beacon_root, "Beacon root does not match");
77
78            // compute the beacon commitment of the current state
79            (beacon_block_id, beacon_root) = state_commit.state_commit.into_commit(state_root);
80        }
81
82        Commitment::new(
83            beacon_block_id.as_version(),
84            beacon_block_id.as_id(),
85            beacon_root,
86            evm_commitment.configID,
87        )
88    }
89}
90
91#[cfg(feature = "host")]
92mod host {
93    use super::*;
94    use crate::{
95        beacon::{
96            host::{client::BeaconClient, create_beacon_commit},
97            BeaconBlockId,
98        },
99        ethereum::EthBlockHeader,
100        history::beacon_roots::BeaconRootsState,
101        EvmBlockHeader,
102    };
103    use alloy::{network::Ethereum, providers::Provider};
104    use anyhow::{ensure, Context};
105    use url::Url;
106
107    impl HistoryCommit {
108        /// Creates a `HistoryCommit` from an EVM execution block header and a later commitment
109        /// header.
110        ///
111        /// This method constructs a chain of proofs to link the `execution_header` to the
112        /// `commitment_header` via the Beacon Chain and the EIP-4788 beacon roots contract.
113        /// It effectively proves that the `execution_header` is an ancestor of a state verifiable
114        /// by the `commitment_header`.
115        pub(crate) async fn from_headers<P>(
116            execution_header: &Sealed<EthBlockHeader>,
117            commitment_header: &Sealed<EthBlockHeader>,
118            commitment_version: CommitmentVersion,
119            rpc_provider: P,
120            beacon_url: Url,
121        ) -> anyhow::Result<Self>
122        where
123            P: Provider<Ethereum>,
124        {
125            ensure!(
126                execution_header.number() < commitment_header.number(),
127                "EVM execution block not before commitment block"
128            );
129            let client = BeaconClient::new(beacon_url.clone()).context("invalid URL")?;
130
131            // 1. Create a beacon commitment for the execution_header.
132            // This establishes the target beacon root we need to eventually verify.
133            let evm_commit = BeaconCommit::from_header(
134                execution_header,
135                CommitmentVersion::Beacon,
136                &rpc_provider,
137                beacon_url,
138            )
139            .await
140            .context("failed to create beacon commit for the execution header")?;
141            let execution_commit = match evm_commit.clone().into_commit(execution_header.seal()) {
142                (BeaconBlockId::Eip4788(ts), beacon_root) => (U256::from(ts), beacon_root),
143                // CommitmentVersion::Beacon should always yield Eip4788
144                _ => unreachable!(),
145            };
146
147            // 2. Initialize the backward chaining process starting from the commitment_header.
148            // current_state_block_hash is the block hash whose state we are currently inspecting
149            // current_state_commit is the beacon commit for current_state_block_hash's state
150            let mut current_state_block_hash = commitment_header.seal();
151            let (mut current_state_commit, _) = create_beacon_commit(
152                commitment_header,
153                "state_root".into(), // we need to prove the state_root of the commitment_header
154                commitment_version,
155                &rpc_provider,
156                &client,
157            )
158            .await
159            .context("failed to create beacon commit for the commitment header")?;
160
161            let mut state_commits: Vec<StateCommit> = Vec::new();
162
163            // loop backwards until we link to `execution_header`'s beacon root
164            loop {
165                log::debug!("Processing state for block: {current_state_block_hash}");
166
167                // 2a. Query the beacon roots contract *within the current state* for the timestamp
168                // in the slot that the execution commit will eventually occupy,
169                let timestamp = beacon_roots::get_timestamp(
170                    execution_commit.0,
171                    &rpc_provider,
172                    current_state_block_hash.into(),
173                )
174                .await
175                .context("failed to get timestamp from beacon roots contract")?;
176                // 2b. Preflight the beacon roots contract call for timestamp. This gives us the
177                // BeaconRootsState and the parent_beacon_root of that particular call.
178                let (parent_beacon_root, state_proof) = BeaconRootsState::preflight_get(
179                    timestamp,
180                    &rpc_provider,
181                    current_state_block_hash.into(),
182                )
183                .await
184                .context("failed to preflight beacon roots contract")?;
185
186                // 2c. Store the fetched BeaconRootsState and its beacon commitment
187                // These are inserted at the beginning as we are building the chain in reverse.
188                state_commits.insert(
189                    0,
190                    StateCommit {
191                        state: state_proof,
192                        state_commit: current_state_commit,
193                    },
194                );
195
196                // 2d. Check if the chain is complete. This happens if the beacon roots contract
197                // actually contained the execution commit.
198                if timestamp == execution_commit.0 {
199                    // if timestamps match, the parent beacon root must also match
200                    ensure!(
201                        parent_beacon_root == execution_commit.1,
202                        "failed to verify final beacon commit"
203                    );
204                    break; // chain successfully linked
205                }
206
207                // 2e. If not yet linked, prepare for the next iteration. The parent_beacon_root is
208                // the beacon root of an *earlier* block's state, and we need to find that
209                // execution block and repeat the process with its state.
210                current_state_block_hash = client
211                    .get_execution_payload_block_hash(parent_beacon_root)
212                    .await
213                    .with_context(|| {
214                        format!(
215                            "Failed to get execution payload block hash for beacon block {parent_beacon_root}"
216                        )
217                    })?;
218                // create the beacon commitment for the next state
219                current_state_commit = GeneralizedBeaconCommit::from_beacon_root(
220                    "state_root".into(),
221                    parent_beacon_root,
222                    &client,
223                    // in the current state, timestamp can be used to look up parent_beacon_root
224                    BeaconBlockId::Eip4788(timestamp.to()),
225                )
226                .await
227                .with_context(|| {
228                    format!(
229                        "Failed to create beacon commit for new state block hash {current_state_block_hash}"
230                    )
231                })?;
232            }
233
234            log::debug!("Generated {} state commitments", state_commits.len());
235
236            Ok(HistoryCommit {
237                evm_commit,
238                state_commits,
239            })
240        }
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use crate::{
248        ethereum::EthBlockHeader,
249        test_utils::{get_cl_url, get_el_url},
250    };
251    use alloy::providers::{Provider, ProviderBuilder};
252    use alloy_primitives::Sealable;
253
254    #[tokio::test]
255    #[cfg_attr(
256        any(not(feature = "rpc-tests"), no_auth),
257        ignore = "RPC tests are disabled"
258    )]
259    async fn from_beacon_commit_and_header() {
260        let el = ProviderBuilder::default().connect_http(get_el_url());
261
262        // get the latest 4 headers
263        let headers = get_headers(4).await.unwrap();
264
265        // create a history commitment executing on header[0] and committing to header[2]
266        let mut commit = HistoryCommit::from_headers(
267            &headers[0],
268            &headers[2],
269            CommitmentVersion::Beacon,
270            &el,
271            get_cl_url(),
272        )
273        .await
274        .unwrap();
275
276        let [StateCommit {
277            state,
278            state_commit,
279        }] = &mut commit.state_commits[..]
280        else {
281            panic!("invalid state_commits")
282        };
283
284        // the state commit should verify against the beacon block root of headers[2]<
285        state_commit
286            .verify(state.root(), headers[3].parent_beacon_block_root.unwrap())
287            .unwrap();
288        // the beacon roots contract should return the beacon block root of headers[0]
289        assert_eq!(
290            BeaconRootsContract::get_from_db(
291                state,
292                U256::from(commit.evm_commit.block_id().as_id())
293            )
294            .unwrap(),
295            headers[1].parent_beacon_block_root.unwrap(),
296        );
297        // the resulting commitment should correspond to the beacon block root of headers[2]
298        assert_eq!(
299            commit.commit(&headers[0], B256::ZERO).digest,
300            headers[3].parent_beacon_block_root.unwrap()
301        );
302    }
303
304    // get the latest n headers, with header[0] being the oldest and header[n-1] being the newest.
305    async fn get_headers(n: usize) -> anyhow::Result<Vec<Sealed<EthBlockHeader>>> {
306        let el = ProviderBuilder::new().connect_http(get_el_url());
307        let latest = el.get_block_number().await?;
308
309        let mut headers = Vec::with_capacity(n);
310        for number in latest + 1 - (n as u64)..=latest {
311            let block = el.get_block_by_number(number.into()).await?.unwrap();
312            let header: EthBlockHeader = block.header.try_into()?;
313            headers.push(header.seal_slow());
314        }
315
316        Ok(headers)
317    }
318}