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}