Skip to content
Merged
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
10 changes: 0 additions & 10 deletions crates/anvil/src/eth/beacon/mod.rs

This file was deleted.

71 changes: 0 additions & 71 deletions crates/anvil/src/eth/beacon/response.rs

This file was deleted.

1 change: 0 additions & 1 deletion crates/anvil/src/eth/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
pub mod api;
pub mod beacon;
pub mod otterscan;
pub mod sign;
pub use api::EthApi;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ impl BeaconError {
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}"))
/// Helper function to create a 410 Gone error for deprecated endpoints
pub fn deprecated_endpoint_with_hint(hint: impl Display) -> Self {
Self::new(BeaconErrorCode::Gone, format!("This endpoint is deprecated. {hint}"))
}

/// Converts to an Axum response
Expand Down Expand Up @@ -86,6 +86,7 @@ impl IntoResponse for BeaconError {
pub enum BeaconErrorCode {
BadRequest = 400,
NotFound = 404,
Gone = 410,
InternalError = 500,
}

Expand All @@ -100,6 +101,7 @@ impl BeaconErrorCode {
match self {
Self::BadRequest => "Bad Request",
Self::NotFound => "Not Found",
Self::Gone => "Gone",
Self::InternalError => "Internal Server Error",
}
}
Expand Down
53 changes: 10 additions & 43 deletions crates/anvil/src/server/beacon_handler.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
use crate::eth::{
EthApi,
beacon::{BeaconError, BeaconResponse},
};
use super::beacon_error::BeaconError;
use crate::eth::EthApi;
use alloy_eips::BlockId;
use alloy_primitives::{B256, aliases::B32};
use alloy_rpc_types_beacon::{
genesis::{GenesisData, GenesisResponse},
header::Header,
sidecar::{BlobData, GetBlobsResponse},
sidecar::GetBlobsResponse,
};
use axum::{
Json,
Expand All @@ -19,46 +16,16 @@ use std::{collections::HashMap, str::FromStr as _};

/// Handles incoming Beacon API requests for blob sidecars
///
/// This endpoint is deprecated. Use `GET /eth/v1/beacon/blobs/{block_id}` instead.
///
/// 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>>,
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(),
}
BeaconError::deprecated_endpoint_with_hint("Use `GET /eth/v1/beacon/blobs/{block_id}` instead.")
.into_response()
}

/// Handles incoming Beacon API requests for blobs
Expand Down
1 change: 1 addition & 0 deletions crates/anvil/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use handler::{HttpEthRpcHandler, PubSubEthRpcHandler};
use std::{io, net::SocketAddr, pin::pin};
use tokio::net::TcpListener;

mod beacon_error;
mod beacon_handler;
pub mod error;
mod handler;
Expand Down
154 changes: 6 additions & 148 deletions crates/anvil/tests/it/beacon_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,161 +12,19 @@ use anvil::{NodeConfig, spawn};
#[tokio::test(flavor = "multi_thread")]
async fn test_beacon_api_get_blob_sidecars() {
let node_config = NodeConfig::test().with_hardfork(Some(EthereumHardfork::Cancun.into()));
let (api, handle) = spawn(node_config).await;

// Disable auto-mining so we can include multiple transactions in the same block
api.anvil_set_auto_mine(false).await.unwrap();

let wallets = handle.dev_wallets().collect::<Vec<_>>();
let from = wallets[0].address();
let to = wallets[1].address();

let provider = http_provider(&handle.http_endpoint());

let eip1559_est = provider.estimate_eip1559_fees().await.unwrap();
let gas_price = provider.get_gas_price().await.unwrap();

// Create multiple blob transactions to be included in the same block
let blob_data =
[b"Hello Beacon API - Blob 1", b"Hello Beacon API - Blob 2", b"Hello Beacon API - Blob 3"];

let mut pending_txs = Vec::new();

// Send all transactions without waiting for receipts
for (i, data) in blob_data.iter().enumerate() {
let sidecar: SidecarBuilder<SimpleCoder> = SidecarBuilder::from_slice(data.as_slice());
let sidecar = sidecar.build().unwrap();

let tx = TransactionRequest::default()
.with_from(from)
.with_to(to)
.with_nonce(i as u64)
.with_max_fee_per_blob_gas(gas_price + 1)
.with_max_fee_per_gas(eip1559_est.max_fee_per_gas)
.with_max_priority_fee_per_gas(eip1559_est.max_priority_fee_per_gas)
.with_blob_sidecar(sidecar)
.value(U256::from(100));

let mut tx = WithOtherFields::new(tx);
tx.populate_blob_hashes();

let pending = provider.send_transaction(tx).await.unwrap();
pending_txs.push(pending);
}

// Mine a block to include all transactions
api.evm_mine(None).await.unwrap();

// Get receipts for all transactions
let mut receipts = Vec::new();
for pending in pending_txs {
let receipt = pending.get_receipt().await.unwrap();
receipts.push(receipt);
}

// Verify all transactions were included in the same block
let block_number = receipts[0].block_number.unwrap();
for (i, receipt) in receipts.iter().enumerate() {
assert_eq!(
receipt.block_number.unwrap(),
block_number,
"Transaction {i} was not included in block {block_number}"
);
}
let (_api, handle) = spawn(node_config).await;

// Test Beacon API endpoint using HTTP client
let client = reqwest::Client::new();
let url = format!("{}/eth/v1/beacon/blob_sidecars/{}", handle.http_endpoint(), block_number);
let url = format!("{}/eth/v1/beacon/blob_sidecars/latest", handle.http_endpoint());

// This endpoint is deprecated, so we expect a 410 Gone response
let response = client.get(&url).send().await.unwrap();
assert_eq!(response.status(), reqwest::StatusCode::OK);

let body: serde_json::Value = response.json().await.unwrap();

// Verify response structure
assert!(body["data"].is_array());
assert!(body["execution_optimistic"].is_boolean());
assert!(body["finalized"].is_boolean());

// Verify we have blob data from all transactions
let blobs = body["data"].as_array().unwrap();
assert_eq!(blobs.len(), 3, "Expected 3 blob sidecars from 3 transactions");

// Verify blob structure for each blob
for (i, blob) in blobs.iter().enumerate() {
assert!(blob["index"].is_string(), "Blob {i} missing index");
assert!(blob["blob"].is_string(), "Blob {i} missing blob data");
assert!(blob["kzg_commitment"].is_string(), "Blob {i} missing kzg_commitment");
assert!(blob["kzg_proof"].is_string(), "Blob {i} missing kzg_proof");
}

// Test filtering with indices query parameter - single index
let url = format!(
"{}/eth/v1/beacon/blob_sidecars/{}?indices=1",
handle.http_endpoint(),
block_number
);
let response = client.get(&url).send().await.unwrap();
let status = response.status();
if status != reqwest::StatusCode::OK {
let error_body = response.text().await.unwrap();
panic!("Expected OK status, got {status}: {error_body}");
}
let body: serde_json::Value = response.json().await.unwrap();
let filtered_blobs = body["data"].as_array().unwrap();
assert_eq!(filtered_blobs.len(), 1, "Expected 1 blob sidecar when filtering by indices=1");
assert_eq!(filtered_blobs[0]["index"].as_str().unwrap(), "1");

// Test filtering with indices query parameter - multiple indices (comma-separated)
let url = format!(
"{}/eth/v1/beacon/blob_sidecars/{}?indices=0,2",
handle.http_endpoint(),
block_number
);
let response = client.get(&url).send().await.unwrap();
assert_eq!(response.status(), reqwest::StatusCode::OK);
let body: serde_json::Value = response.json().await.unwrap();
let filtered_blobs = body["data"].as_array().unwrap();
assert_eq!(filtered_blobs.len(), 2, "Expected 2 blob sidecars when filtering by indices=0,2");
let indices: Vec<String> =
filtered_blobs.iter().map(|b| b["index"].as_str().unwrap().to_string()).collect();
assert!(indices.contains(&"0".to_string()), "Expected index 0 in results");
assert!(indices.contains(&"2".to_string()), "Expected index 2 in results");

// Test filtering with non-existent index
let url = format!(
"{}/eth/v1/beacon/blob_sidecars/{}?indices=99",
handle.http_endpoint(),
block_number
);
let response = client.get(&url).send().await.unwrap();
assert_eq!(response.status(), reqwest::StatusCode::OK);
let body: serde_json::Value = response.json().await.unwrap();
let filtered_blobs = body["data"].as_array().unwrap();
assert_eq!(
filtered_blobs.len(),
0,
"Expected 0 blob sidecars when filtering by non-existent index"
response.text().await.unwrap(),
r#"{"code":410,"message":"This endpoint is deprecated. Use `GET /eth/v1/beacon/blobs/{block_id}` instead."}"#,
"Expected deprecation message for blob_sidecars endpoint"
);

// Test with special block identifiers
let test_ids = vec!["latest", "finalized", "safe", "earliest"];
for block_id in test_ids {
let url = format!("{}/eth/v1/beacon/blob_sidecars/{}", handle.http_endpoint(), block_id);
assert_eq!(client.get(&url).send().await.unwrap().status(), reqwest::StatusCode::OK);
}
let url = format!("{}/eth/v1/beacon/blob_sidecars/pending", handle.http_endpoint());
assert_eq!(client.get(&url).send().await.unwrap().status(), reqwest::StatusCode::NOT_FOUND);

// Test with hex block number
let url = format!("{}/eth/v1/beacon/blob_sidecars/0x{block_number:x}", handle.http_endpoint());
let response = client.get(&url).send().await.unwrap();
assert_eq!(response.status(), reqwest::StatusCode::OK);

// Test with non-existent block
let url = format!("{}/eth/v1/beacon/blob_sidecars/999999", handle.http_endpoint());
let response = client.get(&url).send().await.unwrap();
assert_eq!(response.status(), reqwest::StatusCode::NOT_FOUND);
}

#[tokio::test(flavor = "multi_thread")]
Expand Down
Loading