Skip to main content

risc0_steel/history/
mod.rs

1// Copyright 2026 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                &client,
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                commitment_version,
154                &rpc_provider,
155                &client,
156            )
157            .await
158            .context("failed to create beacon commit for the commitment header")?;
159
160            let mut state_commits: Vec<StateCommit> = Vec::new();
161
162            // loop backwards until we link to `execution_header`'s beacon root
163            loop {
164                log::debug!("Processing state for block: {current_state_block_hash}");
165
166                // 2a. Query the beacon roots contract *within the current state* for the timestamp
167                // in the slot that the execution commit will eventually occupy,
168                let timestamp = beacon_roots::get_timestamp(
169                    execution_commit.0,
170                    &rpc_provider,
171                    current_state_block_hash.into(),
172                )
173                .await
174                .context("failed to get timestamp from beacon roots contract")?;
175                // 2b. Preflight the beacon roots contract call for timestamp. This gives us the
176                // BeaconRootsState and the parent_beacon_root of that particular call.
177                let (parent_beacon_root, state_proof) = BeaconRootsState::preflight_get(
178                    timestamp,
179                    &rpc_provider,
180                    current_state_block_hash.into(),
181                )
182                .await
183                .context("failed to preflight beacon roots contract")?;
184
185                // 2c. Store the fetched BeaconRootsState and its beacon commitment
186                // These are inserted at the beginning as we are building the chain in reverse.
187                state_commits.insert(
188                    0,
189                    StateCommit {
190                        state: state_proof,
191                        state_commit: current_state_commit,
192                    },
193                );
194
195                // 2d. Check if the chain is complete. This happens if the beacon roots contract
196                // actually contained the execution commit.
197                if timestamp == execution_commit.0 {
198                    // if timestamps match, the parent beacon root must also match
199                    ensure!(
200                        parent_beacon_root == execution_commit.1,
201                        "failed to verify final beacon commit"
202                    );
203                    break; // chain successfully linked
204                }
205
206                // 2e. If not yet linked, prepare for the next iteration. The parent_beacon_root is
207                // the beacon root of an *earlier* block's state, and we need to find that
208                // execution block and repeat the process with its state.
209                current_state_block_hash = client
210                    .get_execution_payload_block_hash(parent_beacon_root)
211                    .await
212                    .with_context(|| {
213                        format!(
214                            "Failed to get execution payload block hash for beacon block {parent_beacon_root}"
215                        )
216                    })?;
217                // create the beacon commitment for the next state
218                current_state_commit = GeneralizedBeaconCommit::from_beacon_root(
219                    parent_beacon_root,
220                    &client,
221                    // in the current state, timestamp can be used to look up parent_beacon_root
222                    BeaconBlockId::Eip4788(timestamp.to()),
223                )
224                .await
225                .with_context(|| {
226                    format!(
227                        "Failed to create beacon commit for new state block hash {current_state_block_hash}"
228                    )
229                })?;
230            }
231
232            log::debug!("Generated {} state commitments", state_commits.len());
233
234            Ok(HistoryCommit {
235                evm_commit,
236                state_commits,
237            })
238        }
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use crate::{
246        ethereum::EthBlockHeader,
247        test_utils::{get_cl_url, get_el_url},
248    };
249    use alloy::providers::{Provider, ProviderBuilder};
250    use alloy_primitives::Sealable;
251
252    #[tokio::test]
253    #[cfg_attr(
254        any(not(feature = "rpc-tests"), no_auth),
255        ignore = "RPC tests are disabled"
256    )]
257    async fn from_beacon_commit_and_header() {
258        let el = ProviderBuilder::default().connect_http(get_el_url());
259
260        // get the latest 4 headers
261        let headers = get_headers(4).await.unwrap();
262
263        // create a history commitment executing on header[0] and committing to header[2]
264        let mut commit = HistoryCommit::from_headers(
265            &headers[0],
266            &headers[2],
267            CommitmentVersion::Beacon,
268            &el,
269            get_cl_url(),
270        )
271        .await
272        .unwrap();
273
274        let [StateCommit {
275            state,
276            state_commit,
277        }] = &mut commit.state_commits[..]
278        else {
279            panic!("invalid state_commits")
280        };
281
282        // the state commit should verify against the beacon block root of headers[2]<
283        state_commit
284            .verify(state.root(), headers[3].parent_beacon_block_root.unwrap())
285            .unwrap();
286        // the beacon roots contract should return the beacon block root of headers[0]
287        assert_eq!(
288            BeaconRootsContract::get_from_db(
289                state,
290                U256::from(commit.evm_commit.block_id().as_id())
291            )
292            .unwrap(),
293            headers[1].parent_beacon_block_root.unwrap(),
294        );
295        // the resulting commitment should correspond to the beacon block root of headers[2]
296        assert_eq!(
297            commit.commit(&headers[0], B256::ZERO).digest,
298            headers[3].parent_beacon_block_root.unwrap()
299        );
300    }
301
302    // get the latest n headers, with header[0] being the oldest and header[n-1] being the newest.
303    async fn get_headers(n: usize) -> anyhow::Result<Vec<Sealed<EthBlockHeader>>> {
304        let el = ProviderBuilder::new().connect_http(get_el_url());
305        let latest = el.get_block_number().await?;
306
307        let mut headers = Vec::with_capacity(n);
308        for number in latest + 1 - (n as u64)..=latest {
309            let block = el.get_block_by_number(number.into()).await?.unwrap();
310            let header: EthBlockHeader = block.header.try_into()?;
311            headers.push(header.seal_slow());
312        }
313
314        Ok(headers)
315    }
316}