Skip to content

Commit 520b580

Browse files
committed
feat(anvil): add basic Beacon API support
- New Beacon response and error types - Updated the server router to handle Beacon REST API calls. - Implemented tests for the Beacon API endpoint to validate blob sidecar retrieval.
1 parent fcdf5b1 commit 520b580

File tree

9 files changed

+475
-4
lines changed

9 files changed

+475
-4
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/anvil/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ alloy-signer-local = { workspace = true, features = ["mnemonic"] }
4444
alloy-sol-types = { workspace = true, features = ["std"] }
4545
alloy-dyn-abi = { workspace = true, features = ["std", "eip712"] }
4646
alloy-rpc-types = { workspace = true, features = ["anvil", "trace", "txpool"] }
47+
alloy-rpc-types-beacon.workspace = true
4748
alloy-serde.workspace = true
4849
alloy-provider = { workspace = true, features = [
4950
"reqwest",
@@ -111,6 +112,7 @@ alloy-provider = { workspace = true, features = ["txpool-api"] }
111112
alloy-pubsub.workspace = true
112113
alloy-eip5792.workspace = true
113114
rand.workspace = true
115+
reqwest.workspace = true
114116
foundry-test-utils.workspace = true
115117
tokio = { workspace = true, features = ["full"] }
116118

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//! Beacon API error types
2+
3+
use axum::{
4+
Json,
5+
http::StatusCode,
6+
response::{IntoResponse, Response},
7+
};
8+
use serde::{Deserialize, Serialize};
9+
use std::{
10+
borrow::Cow,
11+
fmt::{self, Display},
12+
};
13+
14+
/// Represents a Beacon API error response
15+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
16+
pub struct BeaconError {
17+
/// HTTP status code
18+
#[serde(skip)]
19+
pub status_code: u16,
20+
/// Error code
21+
pub code: BeaconErrorCode,
22+
/// Error message
23+
pub message: Cow<'static, str>,
24+
}
25+
26+
impl BeaconError {
27+
/// Creates a new beacon error with the given code
28+
pub fn new(code: BeaconErrorCode, message: impl Into<Cow<'static, str>>) -> Self {
29+
let status_code = code.status_code();
30+
Self { status_code, code, message: message.into() }
31+
}
32+
33+
/// Helper function to create a 400 Bad Request error for invalid block ID
34+
pub fn invalid_block_id(block_id: impl Display) -> Self {
35+
Self::new(BeaconErrorCode::BadRequest, format!("Invalid block ID: {block_id}"))
36+
}
37+
38+
/// Helper function to create a 404 Not Found error for block not found
39+
pub fn block_not_found() -> Self {
40+
Self::new(BeaconErrorCode::NotFound, "Block not found")
41+
}
42+
43+
/// Helper function to create a 500 Internal Server Error
44+
pub fn internal_error() -> Self {
45+
Self::new(BeaconErrorCode::InternalError, "Internal server error")
46+
}
47+
48+
/// Helper function to create a 500 Internal Server Error with the given details
49+
pub fn internal_error_with_details(error: impl Display) -> Self {
50+
Self::new(BeaconErrorCode::InternalError, format!("Internal server error: {error}"))
51+
}
52+
53+
/// Converts to an Axum response
54+
pub fn into_response(self) -> Response {
55+
let status =
56+
StatusCode::from_u16(self.status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
57+
58+
(
59+
status,
60+
Json(serde_json::json!({
61+
"code": self.code as u16,
62+
"message": self.message,
63+
})),
64+
)
65+
.into_response()
66+
}
67+
}
68+
69+
impl fmt::Display for BeaconError {
70+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71+
write!(f, "{}: {}", self.code.as_str(), self.message)
72+
}
73+
}
74+
75+
impl std::error::Error for BeaconError {}
76+
77+
impl IntoResponse for BeaconError {
78+
fn into_response(self) -> Response {
79+
Self::into_response(self)
80+
}
81+
}
82+
83+
/// Beacon API error codes following the beacon chain specification
84+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
85+
#[repr(u16)]
86+
pub enum BeaconErrorCode {
87+
BadRequest = 400,
88+
NotFound = 404,
89+
InternalError = 500,
90+
}
91+
92+
impl BeaconErrorCode {
93+
/// Returns the HTTP status code for this error
94+
pub const fn status_code(&self) -> u16 {
95+
*self as u16
96+
}
97+
98+
/// Returns a string representation of the error code
99+
pub const fn as_str(&self) -> &'static str {
100+
match self {
101+
Self::BadRequest => "Bad Request",
102+
Self::NotFound => "Not Found",
103+
Self::InternalError => "Internal Server Error",
104+
}
105+
}
106+
}
107+
108+
impl fmt::Display for BeaconErrorCode {
109+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110+
write!(f, "{}", self.as_str())
111+
}
112+
}
113+
114+
#[cfg(test)]
115+
mod tests {
116+
use super::*;
117+
118+
#[test]
119+
fn test_beacon_error_codes() {
120+
assert_eq!(BeaconErrorCode::BadRequest.status_code(), 400);
121+
assert_eq!(BeaconErrorCode::NotFound.status_code(), 404);
122+
assert_eq!(BeaconErrorCode::InternalError.status_code(), 500);
123+
}
124+
125+
#[test]
126+
fn test_beacon_error_display() {
127+
let err = BeaconError::invalid_block_id("current");
128+
assert_eq!(err.to_string(), "Bad Request: Invalid block ID: current");
129+
}
130+
}

crates/anvil/src/eth/beacon/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//! Beacon API types and utilities for Anvil
2+
//!
3+
//! This module provides types and utilities for implementing Beacon API endpoints
4+
//! in Anvil, allowing testing of blob-based transactions with standard beacon chain APIs.
5+
6+
pub mod error;
7+
pub mod response;
8+
9+
pub use error::BeaconError;
10+
pub use response::{BeaconResponse, BeaconResult};
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//! Beacon API response types
2+
3+
use super::error::BeaconError;
4+
use axum::{
5+
Json,
6+
http::StatusCode,
7+
response::{IntoResponse, Response},
8+
};
9+
use serde::{Deserialize, Serialize};
10+
11+
/// Result type for beacon API operations
12+
pub type BeaconResult<T> = Result<T, BeaconError>;
13+
14+
/// Generic Beacon API response wrapper
15+
///
16+
/// This follows the beacon chain API specification where responses include
17+
/// the actual data plus metadata about execution optimism and finalization.
18+
#[derive(Debug, Clone, Serialize, Deserialize)]
19+
pub struct BeaconResponse<T> {
20+
/// The response data
21+
pub data: T,
22+
/// Whether the response references an unverified execution payload
23+
///
24+
/// For Anvil, this is always `false` since there's no real consensus layer
25+
#[serde(default, skip_serializing_if = "Option::is_none")]
26+
pub execution_optimistic: Option<bool>,
27+
/// Whether the response references finalized history
28+
///
29+
/// For Anvil, this is always `false` since there's no real consensus layer
30+
#[serde(default, skip_serializing_if = "Option::is_none")]
31+
pub finalized: Option<bool>,
32+
}
33+
34+
impl<T> BeaconResponse<T> {
35+
/// Creates a new beacon response with the given data
36+
///
37+
/// For Anvil context, `execution_optimistic` and `finalized` are always `false`
38+
pub fn new(data: T) -> Self {
39+
Self { data, execution_optimistic: None, finalized: None }
40+
}
41+
42+
/// Creates a beacon response with custom execution_optimistic and finalized flags
43+
pub fn with_flags(data: T, execution_optimistic: bool, finalized: bool) -> Self {
44+
Self { data, execution_optimistic: Some(execution_optimistic), finalized: Some(finalized) }
45+
}
46+
}
47+
48+
impl<T: Serialize> IntoResponse for BeaconResponse<T> {
49+
fn into_response(self) -> Response {
50+
(StatusCode::OK, Json(self)).into_response()
51+
}
52+
}
53+
54+
#[cfg(test)]
55+
mod tests {
56+
use super::*;
57+
58+
#[test]
59+
fn test_beacon_response_defaults() {
60+
let response = BeaconResponse::new("test data");
61+
assert_eq!(response.data, "test data");
62+
assert!(response.execution_optimistic.is_none());
63+
assert!(response.finalized.is_none());
64+
}
65+
66+
#[test]
67+
fn test_beacon_response_serialization() {
68+
let response = BeaconResponse::with_flags(vec![1, 2, 3], false, false);
69+
let json = serde_json::to_value(&response).unwrap();
70+
71+
assert_eq!(json["data"], serde_json::json!([1, 2, 3]));
72+
assert_eq!(json["execution_optimistic"], false);
73+
assert_eq!(json["finalized"], false);
74+
}
75+
}

crates/anvil/src/eth/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod api;
2+
pub mod beacon;
23
pub mod otterscan;
34
pub mod sign;
45
pub use api::EthApi;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use crate::eth::{
2+
EthApi,
3+
beacon::{BeaconError, BeaconResponse},
4+
};
5+
use alloy_eips::BlockId;
6+
use alloy_rpc_types_beacon::{header::Header, sidecar::BlobData};
7+
use axum::{
8+
extract::{Path, Query, State},
9+
response::{IntoResponse, Response},
10+
};
11+
use std::{collections::HashMap, str::FromStr as _};
12+
13+
/// Handles incoming Beacon API requests for blob sidecars
14+
///
15+
/// GET /eth/v1/beacon/blob_sidecars/{block_id}
16+
pub async fn handle_get_blob_sidecars(
17+
State(api): State<EthApi>,
18+
Path(block_id): Path<String>,
19+
Query(params): Query<HashMap<String, String>>,
20+
) -> Response {
21+
// Parse block_id from path parameter
22+
let Ok(block_id) = BlockId::from_str(&block_id) else {
23+
return BeaconError::invalid_block_id(block_id).into_response();
24+
};
25+
26+
// Parse indices from query parameters
27+
// Supports both comma-separated (?indices=1,2,3) and repeated parameters (?indices=1&indices=2)
28+
let indices: Vec<u64> = params
29+
.get("indices")
30+
.map(|s| s.split(',').filter_map(|idx| idx.trim().parse::<u64>().ok()).collect())
31+
.unwrap_or_default();
32+
33+
// Get the blob sidecars using existing EthApi logic
34+
match api.anvil_get_blob_sidecars_by_block_id(block_id) {
35+
Ok(Some(sidecar)) => BeaconResponse::with_flags(
36+
sidecar
37+
.into_iter()
38+
.filter(|blob_item| indices.is_empty() || indices.contains(&blob_item.index))
39+
.map(|blob_item| BlobData {
40+
index: blob_item.index,
41+
blob: blob_item.blob,
42+
kzg_commitment: blob_item.kzg_commitment,
43+
kzg_proof: blob_item.kzg_proof,
44+
signed_block_header: Header::default(), // Not available in Anvil
45+
kzg_commitment_inclusion_proof: vec![], // Not available in Anvil
46+
})
47+
.collect::<Vec<_>>(),
48+
false, // Not available in Anvil
49+
false, // Not available in Anvil
50+
)
51+
.into_response(),
52+
Ok(None) => BeaconError::block_not_found().into_response(),
53+
Err(_) => BeaconError::internal_error().into_response(),
54+
}
55+
}

crates/anvil/src/server/mod.rs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
33
use crate::{EthApi, IpcTask};
44
use anvil_server::{ServerConfig, ipc::IpcEndpoint};
5-
use axum::Router;
5+
use axum::{Router, routing::get};
66
use futures::StreamExt;
77
use handler::{HttpEthRpcHandler, PubSubEthRpcHandler};
88
use std::{io, net::SocketAddr, pin::pin};
99
use tokio::net::TcpListener;
1010

11+
mod beacon_handler;
1112
pub mod error;
1213
mod handler;
1314

@@ -33,11 +34,30 @@ pub async fn serve_on(
3334
axum::serve(tcp_listener, router(api, config).into_make_service()).await
3435
}
3536

36-
/// Configures an [`axum::Router`] that handles [`EthApi`] related JSON-RPC calls via HTTP and WS.
37+
/// Configures an [`axum::Router`] that handles [`EthApi`] related JSON-RPC calls via HTTP and WS,
38+
/// and Beacon REST API calls.
3739
pub fn router(api: EthApi, config: ServerConfig) -> Router {
3840
let http = HttpEthRpcHandler::new(api.clone());
39-
let ws = PubSubEthRpcHandler::new(api);
40-
anvil_server::http_ws_router(config, http, ws)
41+
let ws = PubSubEthRpcHandler::new(api.clone());
42+
43+
// JSON-RPC router
44+
let rpc_router = anvil_server::http_ws_router(config, http, ws);
45+
46+
// Beacon REST API router
47+
let beacon_router = beacon_router(api);
48+
49+
// Merge the routers
50+
rpc_router.merge(beacon_router)
51+
}
52+
53+
/// Configures an [`axum::Router`] that handles Beacon REST API calls.
54+
fn beacon_router(api: EthApi) -> Router {
55+
Router::new()
56+
.route(
57+
"/eth/v1/beacon/blob_sidecars/{block_id}",
58+
get(beacon_handler::handle_get_blob_sidecars),
59+
)
60+
.with_state(api)
4161
}
4262

4363
/// Launches an ipc server at the given path in a new task

0 commit comments

Comments
 (0)