Skip to content

Commit

Permalink
WEB3-88: feat: implement minimal beacon API client (#214)
Browse files Browse the repository at this point in the history
A simple reimplementation using reqwest and only supporting the two
required methods. This gets rid of the `beacon-api-client` dependency.

closes WEB3-97
  • Loading branch information
Wollac authored Sep 12, 2024
1 parent 33b96e2 commit 055dc1c
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 74 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ alloy = { version = "0.3" }
alloy-trie = { version = "0.5" }

# Beacon chain support
beacon-api-client = { git = "https://github.com/ralexstokes/ethereum-consensus.git", rev = "cf3c404043230559660810bc0c9d6d5a8498d819" }
ethereum-consensus = { git = "https://github.com/ralexstokes/ethereum-consensus.git", rev = "cf3c404043230559660810bc0c9d6d5a8498d819" }

anyhow = { version = "1.0" }
Expand All @@ -43,10 +42,12 @@ log = "0.4"
nybbles = { version = "0.2.1" }
once_cell = "1.19"
revm = { version = "14.0", default-features = false, features = ["std"] }
reqwest = "0.12"
serde = "1.0"
serde_json = "1.0"
sha2 = { version = "0.10" }
test-log = "0.2.15"
thiserror = "1.0"
tokio = { version = "1.35" }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
url = { version = "2.5" }
7 changes: 5 additions & 2 deletions steel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ alloy-rlp = { workspace = true }
alloy-rlp-derive = { workspace = true }
alloy-sol-types = { workspace = true }
anyhow = { workspace = true }
beacon-api-client = { workspace = true, optional = true }
ethereum-consensus = { workspace = true, optional = true }
log = { workspace = true, optional = true }
nybbles = { workspace = true, features = ["serde"] }
once_cell = { workspace = true }
reqwest = { workspace = true, optional = true }
revm = { workspace = true, features = ["serde"] }
serde = { workspace = true }
serde_json = { workspace = true, optional = true }
sha2 = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, optional = true }
url = { workspace = true, optional = true }

Expand All @@ -42,9 +44,10 @@ test-log = { workspace = true }
default = []
host = [
"dep:alloy",
"dep:beacon-api-client",
"dep:ethereum-consensus",
"dep:log",
"dep:reqwest",
"dep:serde_json",
"dep:tokio",
"dep:url",
]
241 changes: 184 additions & 57 deletions steel/src/beacon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub struct BeaconInput<H> {
}

impl<H: EvmBlockHeader> BeaconInput<H> {
/// Converts the input into a [EvmEnv] for a verifiable state access in the guest.
/// Converts the input into a [EvmEnv] for verifiable state access in the guest.
///
/// [EvmEnv]: crate::EvmEnv
pub fn into_env(self) -> GuestEvmEnv<H> {
Expand Down Expand Up @@ -89,9 +89,9 @@ mod host {
EvmBlockHeader,
};
use alloy::{network::Ethereum, providers::Provider, transports::Transport};
use alloy_primitives::Sealable;
use alloy_primitives::{Sealable, B256};
use anyhow::{bail, ensure, Context};
use beacon_api_client::{mainnet::Client as BeaconClient, BeaconHeaderSummary, BlockId};
use client::{BeaconClient, GetBlockHeaderResponse};
use ethereum_consensus::{ssz::prelude::*, types::SignedBeaconBlock, Fork};
use log::info;
use proofs::{Proof, ProofAndWitness};
Expand All @@ -118,47 +118,11 @@ mod host {
let input = BlockInput::from_env(env)
.await
.context("failed to derive block input")?;
let client = BeaconClient::new(url);

// first get the header of the parent and then the actual block header
let parent_beacon_header = client
.get_beacon_header(BlockId::Root(parent_beacon_block_root.0.into()))
.await
.with_context(|| {
format!("failed to get block header {}", parent_beacon_block_root)
})?;
let beacon_header = get_child_beacon_header(&client, parent_beacon_header)
.await
.with_context(|| {
format!("failed to get child of block {}", parent_beacon_block_root)
})?;

// get the entire block
let signed_beacon_block = client
.get_beacon_block(BlockId::Root(beacon_header.root))
.await
.with_context(|| format!("failed to get block {}", beacon_header.root))?;
// create the inclusion proof of the execution block hash depending on the fork version
let (proof, beacon_root) = match signed_beacon_block {
SignedBeaconBlock::Deneb(signed_block) => {
prove_block_hash_inclusion(signed_block.message)?
}
_ => {
bail!(
"invalid version of block {}: expected {}; got {}",
beacon_header.root,
Fork::Deneb,
signed_beacon_block.version()
);
}
};

// convert and verify the proof
let proof: MerkleProof = proof
.try_into()
.context("proof derived from API is invalid")?;
let client = BeaconClient::new(url).context("invalid URL")?;
let (proof, beacon_root) = create_proof(parent_beacon_block_root, client).await?;
ensure!(
proof.process(block_hash).0 == beacon_root.0,
proof.process(block_hash) == beacon_root,
"proof derived from API does not verify",
);

Expand All @@ -171,30 +135,153 @@ mod host {
}
}

/// Returns the inclusion proof of `block_hash` in the given `BeaconBlock`.
fn prove_block_hash_inclusion<T: SimpleSerialize>(
beacon_block: T,
) -> Result<ProofAndWitness, MerkleizationError> {
// the `block_hash` is in the ExecutionPayload in the BeaconBlockBody in the BeaconBlock
beacon_block.prove(&[
"body".into(),
"execution_payload".into(),
"block_hash".into(),
])
mod client {
use ethereum_consensus::{
phase0::SignedBeaconBlockHeader, primitives::Root, types::mainnet::SignedBeaconBlock,
Fork,
};
use reqwest::IntoUrl;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt::Display};
use url::Url;

/// Errors returned by the [BeaconClient].
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("could not parse URL: {0}")]
Url(#[from] url::ParseError),
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("version field does not match data version")]
VersionMismatch,
}

/// Response returned by the `get_block_header` API.
#[derive(Debug, Serialize, Deserialize)]
pub struct GetBlockHeaderResponse {
pub root: Root,
pub canonical: bool,
pub header: SignedBeaconBlockHeader,
}

/// Wrapper returned by the API calls.
#[derive(Serialize, Deserialize)]
struct Response<T> {
data: T,
#[serde(flatten)]
meta: HashMap<String, serde_json::Value>,
}

/// Wrapper returned by the API calls that includes a version.
#[derive(Serialize, Deserialize)]
struct VersionedResponse<T> {
version: Fork,
#[serde(flatten)]
inner: Response<T>,
}

/// Simple beacon API client for the `mainnet` preset that can query headers and blocks.
pub struct BeaconClient {
http: reqwest::Client,
endpoint: Url,
}

impl BeaconClient {
/// Creates a new beacon endpoint API client.
pub fn new<U: IntoUrl>(endpoint: U) -> Result<Self, Error> {
let client = reqwest::Client::new();
Ok(Self {
http: client,
endpoint: endpoint.into_url()?,
})
}

async fn http_get<T: serde::de::DeserializeOwned>(
&self,
path: &str,
) -> Result<T, Error> {
let target = self.endpoint.join(path)?;
let resp = self.http.get(target).send().await?;
let value = resp.error_for_status()?.json().await?;
Ok(value)
}

/// Retrieves block header for given block id.
pub async fn get_block_header(
&self,
block_id: impl Display,
) -> Result<GetBlockHeaderResponse, Error> {
let path = format!("eth/v1/beacon/headers/{block_id}");
let result: Response<GetBlockHeaderResponse> = self.http_get(&path).await?;
Ok(result.data)
}

/// Retrieves block details for given block id.
pub async fn get_block(
&self,
block_id: impl Display,
) -> Result<SignedBeaconBlock, Error> {
let path = format!("eth/v2/beacon/blocks/{block_id}");
let result: VersionedResponse<SignedBeaconBlock> = self.http_get(&path).await?;
if result.version.to_string() != result.inner.data.version().to_string() {
return Err(Error::VersionMismatch);
}
Ok(result.inner.data)
}
}
}

/// Creates the [MerkleProof] of `block_hash` in the `BeaconBlock` with the given
/// `parent_beacon_block_root`.
async fn create_proof(
parent_root: B256,
client: BeaconClient,
) -> anyhow::Result<(MerkleProof, B256)> {
// first get the header of the parent and then the actual block header
let parent_beacon_header = client
.get_block_header(parent_root)
.await
.with_context(|| format!("failed to get block header {}", parent_root))?;
let beacon_header = get_child_beacon_header(&client, parent_beacon_header)
.await
.with_context(|| format!("failed to get child of block {}", parent_root))?;

// get the entire block
let signed_beacon_block = client
.get_block(beacon_header.root)
.await
.with_context(|| format!("failed to get block {}", beacon_header.root))?;
// create the inclusion proof of the execution block hash depending on the fork version
let (proof, beacon_root) = match signed_beacon_block {
SignedBeaconBlock::Deneb(signed_block) => prove_block_hash(signed_block.message)?,
_ => {
bail!(
"invalid version of block {}: expected {}; got {}",
beacon_header.root,
Fork::Deneb,
signed_beacon_block.version()
);
}
};
let proof: MerkleProof = proof
.try_into()
.context("proof derived from API is invalid")?;

Ok((proof, beacon_root.0.into()))
}

/// Returns the header, with `parent_root` equal to `parent.root`.
///
/// It iteratively tries to fetch headers of successive slots until success.
/// TODO: use [BeaconClient::get_beacon_header_for_parent_root], once the nodes add support.
/// TODO: use `eth/v1/beacon/headers?parent_root`, once all the nodes support it.
async fn get_child_beacon_header(
client: &BeaconClient,
parent: BeaconHeaderSummary,
) -> anyhow::Result<BeaconHeaderSummary> {
parent: GetBlockHeaderResponse,
) -> anyhow::Result<GetBlockHeaderResponse> {
let parent_slot = parent.header.message.slot;
let mut request_error = None;
for slot in (parent_slot + 1)..=(parent_slot + 32) {
match client.get_beacon_header(BlockId::Slot(slot)).await {
match client.get_block_header(slot).await {
Err(err) => request_error = Some(err),
Ok(resp) => {
let header = &resp.header.message;
Expand All @@ -214,6 +301,18 @@ mod host {
Err(err.context("no valid response received for the 32 consecutive slots"))
}

/// Returns the inclusion proof of `block_hash` in the given `BeaconBlock`.
fn prove_block_hash<T: SimpleSerialize>(
beacon_block: T,
) -> Result<ProofAndWitness, MerkleizationError> {
// the `block_hash` is in the ExecutionPayload in the BeaconBlockBody in the BeaconBlock
beacon_block.prove(&[
"body".into(),
"execution_payload".into(),
"block_hash".into(),
])
}

impl TryFrom<Proof> for MerkleProof {
type Error = anyhow::Error;

Expand All @@ -228,10 +327,38 @@ mod host {
})
}
}

#[cfg(test)]
mod tests {
use super::*;
use alloy::{eips::BlockNumberOrTag, network::BlockResponse, providers::ProviderBuilder};

#[tokio::test]
#[ignore] // This queries actual RPC nodes, running only on demand.
async fn eth_mainnet_proof() {
const EL_URL: &str = "https://ethereum-rpc.publicnode.com";
const CL_URL: &str = "https://ethereum-beacon-api.publicnode.com";

let el = ProviderBuilder::new().on_builtin(EL_URL).await.unwrap();
let cl = BeaconClient::new(CL_URL).unwrap();

let block = el
.get_block_by_number(BlockNumberOrTag::Finalized, false)
.await
.expect("eth_getBlockByNumber failed")
.unwrap();
let header = block.header();

let (proof, beacon_root) = create_proof(header.parent_beacon_block_root.unwrap(), cl)
.await
.expect("proving failed");
assert_eq!(proof.process(header.hash), beacon_root);
}
}
}

#[cfg(test)]
pub(crate) mod tests {
mod tests {
use super::*;
use alloy_primitives::b256;

Expand Down
7 changes: 7 additions & 0 deletions steel/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ pub struct ChainSpec {

impl ChainSpec {
/// Creates a new configuration consisting of only one specification ID.
///
/// For example, this can be used to create a [ChainSpec] for an anvil instance:
/// ```rust
/// # use revm::primitives::SpecId;
/// # use risc0_steel::config::ChainSpec;
/// let spec = ChainSpec::new_single(31337, SpecId::CANCUN);
/// ```
pub fn new_single(chain_id: ChainId, spec_id: SpecId) -> Self {
ChainSpec {
chain_id,
Expand Down
Loading

0 comments on commit 055dc1c

Please sign in to comment.