Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/anvil/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ alloy-signer-local = { workspace = true, features = ["mnemonic"] }
alloy-sol-types = { workspace = true, features = ["std"] }
alloy-dyn-abi = { workspace = true, features = ["std", "eip712"] }
alloy-rpc-types = { workspace = true, features = ["anvil", "trace", "txpool"] }
alloy-rpc-types-beacon.workspace = true
alloy-serde.workspace = true
alloy-provider = { workspace = true, features = [
"reqwest",
Expand Down Expand Up @@ -111,6 +112,7 @@ alloy-provider = { workspace = true, features = ["txpool-api"] }
alloy-pubsub.workspace = true
alloy-eip5792.workspace = true
rand.workspace = true
reqwest.workspace = true
foundry-test-utils.workspace = true
tokio = { workspace = true, features = ["full"] }

Expand Down
130 changes: 130 additions & 0 deletions crates/anvil/src/eth/beacon/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//! Beacon API error types

use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use std::{
borrow::Cow,
fmt::{self, Display},
};

/// Represents a Beacon API error response
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BeaconError {
/// HTTP status code
#[serde(skip)]
pub status_code: u16,
/// Error code
pub code: BeaconErrorCode,
/// Error message
pub message: Cow<'static, str>,
}

impl BeaconError {
/// Creates a new beacon error with the given code
pub fn new(code: BeaconErrorCode, message: impl Into<Cow<'static, str>>) -> Self {
let status_code = code.status_code();
Self { status_code, code, message: message.into() }
}

/// Helper function to create a 400 Bad Request error for invalid block ID
pub fn invalid_block_id(block_id: impl Display) -> Self {
Self::new(BeaconErrorCode::BadRequest, format!("Invalid block ID: {block_id}"))
}

/// Helper function to create a 404 Not Found error for block not found
pub fn block_not_found() -> Self {
Self::new(BeaconErrorCode::NotFound, "Block not found")
}

/// Helper function to create a 500 Internal Server Error
pub fn internal_error() -> Self {
Self::new(BeaconErrorCode::InternalError, "Internal server error")
}

/// Helper function to create a 500 Internal Server Error with the given details
pub fn internal_error_with_details(error: impl Display) -> Self {
Self::new(BeaconErrorCode::InternalError, format!("Internal server error: {error}"))
}

/// Converts to an Axum response
pub fn into_response(self) -> Response {
let status =
StatusCode::from_u16(self.status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);

(
status,
Json(serde_json::json!({
"code": self.code as u16,
"message": self.message,
})),
)
.into_response()
}
}

impl fmt::Display for BeaconError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.code.as_str(), self.message)
}
}

impl std::error::Error for BeaconError {}

impl IntoResponse for BeaconError {
fn into_response(self) -> Response {
Self::into_response(self)
}
}

/// Beacon API error codes following the beacon chain specification
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u16)]
pub enum BeaconErrorCode {
BadRequest = 400,
NotFound = 404,
InternalError = 500,
}

impl BeaconErrorCode {
/// Returns the HTTP status code for this error
pub const fn status_code(&self) -> u16 {
*self as u16
}

/// Returns a string representation of the error code
pub const fn as_str(&self) -> &'static str {
match self {
Self::BadRequest => "Bad Request",
Self::NotFound => "Not Found",
Self::InternalError => "Internal Server Error",
}
}
}

impl fmt::Display for BeaconErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_beacon_error_codes() {
assert_eq!(BeaconErrorCode::BadRequest.status_code(), 400);
assert_eq!(BeaconErrorCode::NotFound.status_code(), 404);
assert_eq!(BeaconErrorCode::InternalError.status_code(), 500);
}

#[test]
fn test_beacon_error_display() {
let err = BeaconError::invalid_block_id("current");
assert_eq!(err.to_string(), "Bad Request: Invalid block ID: current");
}
}
10 changes: 10 additions & 0 deletions crates/anvil/src/eth/beacon/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//! Beacon API types and utilities for Anvil
//!
//! This module provides types and utilities for implementing Beacon API endpoints
//! in Anvil, allowing testing of blob-based transactions with standard beacon chain APIs.

pub mod error;
pub mod response;

pub use error::BeaconError;
pub use response::{BeaconResponse, BeaconResult};
75 changes: 75 additions & 0 deletions crates/anvil/src/eth/beacon/response.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//! Beacon API response types

use super::error::BeaconError;
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};

/// Result type for beacon API operations
pub type BeaconResult<T> = Result<T, BeaconError>;

/// Generic Beacon API response wrapper
///
/// This follows the beacon chain API specification where responses include
/// the actual data plus metadata about execution optimism and finalization.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BeaconResponse<T> {
/// The response data
pub data: T,
/// Whether the response references an unverified execution payload
///
/// For Anvil, this is always `false` since there's no real consensus layer
#[serde(default, skip_serializing_if = "Option::is_none")]
pub execution_optimistic: Option<bool>,
/// Whether the response references finalized history
///
/// For Anvil, this is always `false` since there's no real consensus layer
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finalized: Option<bool>,
}

impl<T> BeaconResponse<T> {
/// Creates a new beacon response with the given data
///
/// For Anvil context, `execution_optimistic` and `finalized` are always `false`
pub fn new(data: T) -> Self {
Self { data, execution_optimistic: None, finalized: None }
}

/// Creates a beacon response with custom execution_optimistic and finalized flags
pub fn with_flags(data: T, execution_optimistic: bool, finalized: bool) -> Self {
Self { data, execution_optimistic: Some(execution_optimistic), finalized: Some(finalized) }
}
}

impl<T: Serialize> IntoResponse for BeaconResponse<T> {
fn into_response(self) -> Response {
(StatusCode::OK, Json(self)).into_response()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_beacon_response_defaults() {
let response = BeaconResponse::new("test data");
assert_eq!(response.data, "test data");
assert!(response.execution_optimistic.is_none());
assert!(response.finalized.is_none());
}

#[test]
fn test_beacon_response_serialization() {
let response = BeaconResponse::with_flags(vec![1, 2, 3], false, false);
let json = serde_json::to_value(&response).unwrap();

assert_eq!(json["data"], serde_json::json!([1, 2, 3]));
assert_eq!(json["execution_optimistic"], false);
assert_eq!(json["finalized"], false);
}
}
1 change: 1 addition & 0 deletions crates/anvil/src/eth/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod api;
pub mod beacon;
pub mod otterscan;
pub mod sign;
pub use api::EthApi;
Expand Down
55 changes: 55 additions & 0 deletions crates/anvil/src/server/beacon_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use crate::eth::{
EthApi,
beacon::{BeaconError, BeaconResponse},
};
use alloy_eips::BlockId;
use alloy_rpc_types_beacon::{header::Header, sidecar::BlobData};
use axum::{
extract::{Path, Query, State},
response::{IntoResponse, Response},
};
use std::{collections::HashMap, str::FromStr as _};

/// Handles incoming Beacon API requests for blob sidecars
///
/// GET /eth/v1/beacon/blob_sidecars/{block_id}
pub async fn handle_get_blob_sidecars(
State(api): State<EthApi>,
Path(block_id): Path<String>,
Query(params): Query<HashMap<String, String>>,
) -> Response {
// Parse block_id from path parameter
let Ok(block_id) = BlockId::from_str(&block_id) else {
return BeaconError::invalid_block_id(block_id).into_response();
};

// Parse indices from query parameters
// Supports both comma-separated (?indices=1,2,3) and repeated parameters (?indices=1&indices=2)
let indices: Vec<u64> = params
.get("indices")
.map(|s| s.split(',').filter_map(|idx| idx.trim().parse::<u64>().ok()).collect())
.unwrap_or_default();

// Get the blob sidecars using existing EthApi logic
match api.anvil_get_blob_sidecars_by_block_id(block_id) {
Ok(Some(sidecar)) => BeaconResponse::with_flags(
sidecar
.into_iter()
.filter(|blob_item| indices.is_empty() || indices.contains(&blob_item.index))
.map(|blob_item| BlobData {
index: blob_item.index,
blob: blob_item.blob,
kzg_commitment: blob_item.kzg_commitment,
kzg_proof: blob_item.kzg_proof,
signed_block_header: Header::default(), // Not available in Anvil
kzg_commitment_inclusion_proof: vec![], // Not available in Anvil
})
.collect::<Vec<_>>(),
false, // Not available in Anvil
false, // Not available in Anvil
)
.into_response(),
Ok(None) => BeaconError::block_not_found().into_response(),
Err(_) => BeaconError::internal_error().into_response(),
}
}
28 changes: 24 additions & 4 deletions crates/anvil/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

use crate::{EthApi, IpcTask};
use anvil_server::{ServerConfig, ipc::IpcEndpoint};
use axum::Router;
use axum::{Router, routing::get};
use futures::StreamExt;
use handler::{HttpEthRpcHandler, PubSubEthRpcHandler};
use std::{io, net::SocketAddr, pin::pin};
use tokio::net::TcpListener;

mod beacon_handler;
pub mod error;
mod handler;

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

/// Configures an [`axum::Router`] that handles [`EthApi`] related JSON-RPC calls via HTTP and WS.
/// Configures an [`axum::Router`] that handles [`EthApi`] related JSON-RPC calls via HTTP and WS,
/// and Beacon REST API calls.
pub fn router(api: EthApi, config: ServerConfig) -> Router {
let http = HttpEthRpcHandler::new(api.clone());
let ws = PubSubEthRpcHandler::new(api);
anvil_server::http_ws_router(config, http, ws)
let ws = PubSubEthRpcHandler::new(api.clone());

// JSON-RPC router
let rpc_router = anvil_server::http_ws_router(config, http, ws);

// Beacon REST API router
let beacon_router = beacon_router(api);

// Merge the routers
rpc_router.merge(beacon_router)
}

/// Configures an [`axum::Router`] that handles Beacon REST API calls.
fn beacon_router(api: EthApi) -> Router {
Router::new()
.route(
"/eth/v1/beacon/blob_sidecars/{block_id}",
get(beacon_handler::handle_get_blob_sidecars),
)
.with_state(api)
}

/// Launches an ipc server at the given path in a new task
Expand Down
Loading
Loading