1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
// Copyright 2025 RISC Zero, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Types related to commitments to a historical state.
use crate::{
beacon, BlockHeaderCommit, Commitment, CommitmentVersion, ComposeInput, EvmBlockHeader,
};
use alloy_primitives::{Sealed, B256, U256};
use beacon::{BeaconCommit, GeneralizedBeaconCommit, STATE_ROOT_LEAF_INDEX};
use beacon_roots::{BeaconRootsContract, BeaconRootsState};
use serde::{Deserialize, Serialize};
pub(crate) mod beacon_roots;
/// Input committing a previous block hash to the corresponding Beacon Chain block root.
pub type HistoryInput<H> = ComposeInput<H, HistoryCommit>;
/// A commitment that an execution block is included as an ancestor of a specific beacon block on
/// the Ethereum blockchain.
///
/// This struct encapsulates the necessary data to prove that a given execution block is part of the
/// canonical chain according to the Beacon Chain.
#[derive(Clone, Serialize, Deserialize)]
pub struct HistoryCommit {
/// Commit of the Steel EVM execution block hash to its beacon block hash.
evm_commit: BeaconCommit,
/// Iterative commits for verifying `evm_commit` as an ancestor of some valid Beacon block.
state_commits: Vec<StateCommit>,
}
/// Represents a commitment of a beacon roots contract state to a Beacon Chain block root.
#[derive(Clone, Serialize, Deserialize)]
struct StateCommit {
/// State for verifying `evm_commit`.
state: BeaconRootsState,
/// Commitment for `state` to a Beacon Chain block root.
state_commit: GeneralizedBeaconCommit<STATE_ROOT_LEAF_INDEX>,
}
impl<H: EvmBlockHeader> BlockHeaderCommit<H> for HistoryCommit {
/// Generates a commitment that proves the given block header is included in the Beacon Chain's
/// history. Panics if the provided [HistoryCommit] data is invalid or inconsistent.
#[inline]
fn commit(self, header: &Sealed<H>, config_id: B256) -> Commitment {
// first, compute the beacon commit of the EVM execution
let initial_commitment = self.evm_commit.commit(header, config_id);
let (mut timestamp, version) = initial_commitment.decode_id();
// just a sanity check, a BeaconCommit will always have this version
assert_eq!(version, CommitmentVersion::Beacon as u16);
// starting from evm_commit, "walk forward" along state_commits to reach a later beacon root
let mut beacon_root = initial_commitment.digest;
for mut state_commit in self.state_commits {
// verify that the previous commitment is valid wrt the current state
let state_root = state_commit.state.root();
let commitment_root =
BeaconRootsContract::get_from_db(&mut state_commit.state, timestamp)
.expect("Beacon roots contract failed");
assert_eq!(commitment_root, beacon_root, "Beacon root does not match");
// compute the beacon commitment of the current state
let (commit_ts, commit_beacon_root) = state_commit.state_commit.into_commit(state_root);
timestamp = U256::from(commit_ts);
beacon_root = commit_beacon_root;
}
Commitment::new(
CommitmentVersion::Beacon as u16,
timestamp.to(),
beacon_root,
initial_commitment.configID,
)
}
}
#[cfg(feature = "host")]
mod host {
use super::*;
use crate::{
beacon::host::{client::BeaconClient, create_beacon_commit},
ethereum::EthBlockHeader,
history::beacon_roots::{BeaconRootsState, HISTORY_BUFFER_LENGTH},
};
use alloy::{network::Ethereum, providers::Provider};
use alloy_primitives::{BlockNumber, Sealable};
use anyhow::{ensure, Context};
use url::Url;
impl HistoryCommit {
/// Creates a `HistoryCommit` from an EVM block header and a commitment header.
///
/// This method fetches the necessary data from the Ethereum and Beacon chain to construct a
/// `HistoryCommit`. It iterates through blocks from the EVM header's number up to
/// the commitment header's number, generating `StateCommit`s for each block in the range.
pub(crate) async fn from_headers<P>(
evm_header: &Sealed<EthBlockHeader>,
commitment_header: &Sealed<EthBlockHeader>,
rpc_provider: P,
beacon_url: Url,
) -> anyhow::Result<Self>
where
P: Provider<Ethereum>,
{
ensure!(
evm_header.number() < commitment_header.number(),
"EVM execution block not before commitment block"
);
let client = BeaconClient::new(beacon_url.clone()).context("invalid URL")?;
// create a regular beacon commit to the block header used for EVM execution
let evm_commit =
BeaconCommit::from_header(evm_header, &rpc_provider, beacon_url).await?;
let mut commit_ts = evm_commit.timestamp();
// safe unwrap: BeaconCommit::from_header checks that the proof can be processed
let mut commit_beacon_root = evm_commit.process_proof(evm_header.seal()).unwrap();
let mut state_commits: Vec<StateCommit> = Vec::new();
// we assume that not more than 25% of the blocks have been skipped
// TODO(#309): implement a more sophisticated way to determine the step size
let step = HISTORY_BUFFER_LENGTH.to::<BlockNumber>() * 75 / 100;
let target = commitment_header.number();
let mut state_block = evm_header.number;
while state_block < target {
state_block = std::cmp::min(state_block + step, target);
// get the header of the state block
let rpc_block = rpc_provider
.get_block_by_number(state_block.into())
.await
.context("eth_getBlockByNumber failed")?
.with_context(|| format!("block {} not found", state_block))?;
let header: EthBlockHeader = rpc_block
.header
.try_into()
.with_context(|| format!("block {} invalid", state_block))?;
let header = header.seal_slow();
log::debug!(
"chained commitment for block {} ({})",
header.number,
header.seal()
);
// derive the historic state needed to verify the previous beacon commitment
let (beacon_root, state) = BeaconRootsState::preflight_get(
U256::from(commit_ts),
&rpc_provider,
header.seal().into(),
)
.await
.with_context(|| format!("preflight failed for block {}", header.seal()))?;
// the result of the beacon roots contract must match the beacon root of the commit
ensure!(
beacon_root == commit_beacon_root,
"inconsistent state for block {}",
header.seal()
);
// create a beacon commitment to that state
let (state_commit, beacon_root) =
create_beacon_commit(&header, "state_root".into(), &rpc_provider, &client)
.await?;
state_commit
.verify(state.root(), beacon_root)
.context("proof derived from API does not verify")?;
commit_ts = state_commit.timestamp();
commit_beacon_root = beacon_root;
state_commits.push(StateCommit {
state,
state_commit,
});
}
log::debug!("state commitments: {}", state_commits.len());
Ok(HistoryCommit {
evm_commit,
state_commits,
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ethereum::EthBlockHeader;
use alloy::providers::{Provider, ProviderBuilder};
use alloy_primitives::Sealable;
const EL_URL: &str = "https://ethereum-rpc.publicnode.com";
const CL_URL: &str = "https://ethereum-beacon-api.publicnode.com";
#[tokio::test]
#[ignore = "queries actual RPC nodes"]
async fn from_beacon_commit_and_header() {
let el = ProviderBuilder::default().connect(EL_URL).await.unwrap();
// get the latest 4 headers
let headers = get_headers(4).await.unwrap();
// create a history commitment executing on header[0] and committing to header[2]
let mut commit =
HistoryCommit::from_headers(&headers[0], &headers[2], &el, CL_URL.parse().unwrap())
.await
.unwrap();
let [StateCommit {
state,
state_commit,
}] = &mut commit.state_commits[..]
else {
panic!("invalid state_commits")
};
// the state commit should verify against the beacon block root of headers[2]<
state_commit
.verify(state.root(), headers[3].parent_beacon_block_root.unwrap())
.unwrap();
// the beacon roots contract should return the beacon block root of headers[0]
assert_eq!(
BeaconRootsContract::get_from_db(state, U256::from(commit.evm_commit.timestamp()))
.unwrap(),
headers[1].parent_beacon_block_root.unwrap(),
);
// the resulting commitment should correspond to the beacon block root of headers[2]
assert_eq!(
commit.commit(&headers[0], B256::ZERO).digest,
headers[3].parent_beacon_block_root.unwrap()
);
}
// get the latest n headers, with header[0] being the oldest and header[n-1] being the newest.
async fn get_headers(n: usize) -> anyhow::Result<Vec<Sealed<EthBlockHeader>>> {
let el = ProviderBuilder::new().connect(EL_URL).await?;
let latest = el.get_block_number().await?;
let mut headers = Vec::with_capacity(n);
for number in latest + 1 - (n as u64)..=latest {
let block = el.get_block_by_number(number.into()).await?.unwrap();
let header: EthBlockHeader = block.header.try_into()?;
headers.push(header.seal_slow());
}
Ok(headers)
}
}