From 8ab299405fa7a468169645e33ddbde6d6294b30e Mon Sep 17 00:00:00 2001 From: Mablr <59505383+mablr@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:05:29 +0100 Subject: [PATCH] chore(anvil): deprecate `getBlobSidecars` Beacon API endpoint - Removed useless `BeaconResponse`, rely instead on `alloy_rpc_types_beacon` crate response structs - Move `BeaconError` in `anvil/src/server` mod, to mirror perfectly the Beacon/JSON-RPC handler/error submodules structure - updated the test accordingly --- crates/anvil/src/eth/beacon/mod.rs | 10 -- crates/anvil/src/eth/beacon/response.rs | 71 -------- crates/anvil/src/eth/mod.rs | 1 - .../error.rs => server/beacon_error.rs} | 8 +- crates/anvil/src/server/beacon_handler.rs | 53 ++---- crates/anvil/src/server/mod.rs | 1 + crates/anvil/tests/it/beacon_api.rs | 154 +----------------- 7 files changed, 22 insertions(+), 276 deletions(-) delete mode 100644 crates/anvil/src/eth/beacon/mod.rs delete mode 100644 crates/anvil/src/eth/beacon/response.rs rename crates/anvil/src/{eth/beacon/error.rs => server/beacon_error.rs} (92%) diff --git a/crates/anvil/src/eth/beacon/mod.rs b/crates/anvil/src/eth/beacon/mod.rs deleted file mode 100644 index 7585a14d1df0b..0000000000000 --- a/crates/anvil/src/eth/beacon/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! 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; diff --git a/crates/anvil/src/eth/beacon/response.rs b/crates/anvil/src/eth/beacon/response.rs deleted file mode 100644 index f308b373d52cb..0000000000000 --- a/crates/anvil/src/eth/beacon/response.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Beacon API response types - -use axum::{ - Json, - http::StatusCode, - response::{IntoResponse, Response}, -}; -use serde::{Deserialize, Serialize}; - -/// 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 { - /// 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, - /// 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, -} - -impl BeaconResponse { - /// 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 IntoResponse for BeaconResponse { - 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); - } -} diff --git a/crates/anvil/src/eth/mod.rs b/crates/anvil/src/eth/mod.rs index f8d1509d64b83..393a9ff213306 100644 --- a/crates/anvil/src/eth/mod.rs +++ b/crates/anvil/src/eth/mod.rs @@ -1,5 +1,4 @@ pub mod api; -pub mod beacon; pub mod otterscan; pub mod sign; pub use api::EthApi; diff --git a/crates/anvil/src/eth/beacon/error.rs b/crates/anvil/src/server/beacon_error.rs similarity index 92% rename from crates/anvil/src/eth/beacon/error.rs rename to crates/anvil/src/server/beacon_error.rs index 6476db7d064bf..0a08e870ad70e 100644 --- a/crates/anvil/src/eth/beacon/error.rs +++ b/crates/anvil/src/server/beacon_error.rs @@ -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 @@ -86,6 +86,7 @@ impl IntoResponse for BeaconError { pub enum BeaconErrorCode { BadRequest = 400, NotFound = 404, + Gone = 410, InternalError = 500, } @@ -100,6 +101,7 @@ impl BeaconErrorCode { match self { Self::BadRequest => "Bad Request", Self::NotFound => "Not Found", + Self::Gone => "Gone", Self::InternalError => "Internal Server Error", } } diff --git a/crates/anvil/src/server/beacon_handler.rs b/crates/anvil/src/server/beacon_handler.rs index 753e3ebd50dd5..75aca4b2c16df 100644 --- a/crates/anvil/src/server/beacon_handler.rs +++ b/crates/anvil/src/server/beacon_handler.rs @@ -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, @@ -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, - Path(block_id): Path, - Query(params): Query>, + State(_api): State, + Path(_block_id): Path, + Query(_params): Query>, ) -> 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 = params - .get("indices") - .map(|s| s.split(',').filter_map(|idx| idx.trim().parse::().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::>(), - 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 diff --git a/crates/anvil/src/server/mod.rs b/crates/anvil/src/server/mod.rs index 72ecb8fe49992..52f025e6e2ea5 100644 --- a/crates/anvil/src/server/mod.rs +++ b/crates/anvil/src/server/mod.rs @@ -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; diff --git a/crates/anvil/tests/it/beacon_api.rs b/crates/anvil/tests/it/beacon_api.rs index bce15f8017b15..abcd2986724c5 100644 --- a/crates/anvil/tests/it/beacon_api.rs +++ b/crates/anvil/tests/it/beacon_api.rs @@ -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::>(); - 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 = 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 = - 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")]