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}