Skip to main content

anvil/server/beacon/
handlers.rs

1use super::{error::BeaconError, utils::must_be_ssz};
2use crate::eth::EthApi;
3use alloy_eips::BlockId;
4use alloy_primitives::{B256, aliases::B32};
5use alloy_rpc_types_beacon::{
6    genesis::{GenesisData, GenesisResponse},
7    sidecar::GetBlobsResponse,
8};
9use axum::{
10    Json,
11    extract::{Path, Query, State},
12    http::HeaderMap,
13    response::{IntoResponse, Response},
14};
15use foundry_primitives::FoundryNetwork;
16use ssz::Encode;
17use std::{collections::HashMap, str::FromStr as _};
18
19/// Handles incoming Beacon API requests for blob sidecars
20///
21/// This endpoint is deprecated. Use `GET /eth/v1/beacon/blobs/{block_id}` instead.
22///
23/// GET /eth/v1/beacon/blob_sidecars/{block_id}
24pub async fn handle_get_blob_sidecars(
25    State(_api): State<EthApi<FoundryNetwork>>,
26    Path(_block_id): Path<String>,
27    Query(_params): Query<HashMap<String, String>>,
28) -> Response {
29    BeaconError::deprecated_endpoint_with_hint("Use `GET /eth/v1/beacon/blobs/{block_id}` instead.")
30        .into_response()
31}
32
33/// Handles incoming Beacon API requests for blobs
34///
35/// GET /eth/v1/beacon/blobs/{block_id}
36pub async fn handle_get_blobs(
37    headers: HeaderMap,
38    State(api): State<EthApi<FoundryNetwork>>,
39    Path(block_id): Path<String>,
40    Query(versioned_hashes): Query<HashMap<String, String>>,
41) -> Response {
42    // Parse block_id from path parameter
43    let Ok(block_id) = BlockId::from_str(&block_id) else {
44        return BeaconError::invalid_block_id(block_id).into_response();
45    };
46
47    // Parse versioned hashes from query parameters
48    // Supports comma-separated format: ?versioned_hashes=0x...,0x...
49    let versioned_hashes: Vec<B256> = match versioned_hashes.get("versioned_hashes") {
50        Some(s) => {
51            let mut hashes = Vec::new();
52            for hash in s.split(',') {
53                let hash = hash.trim();
54                if hash.is_empty() {
55                    continue;
56                }
57                match B256::from_str(hash) {
58                    Ok(h) => hashes.push(h),
59                    Err(_) => {
60                        return BeaconError::new(
61                            super::error::BeaconErrorCode::BadRequest,
62                            format!("Invalid versioned hash: {hash}"),
63                        )
64                        .into_response();
65                    }
66                }
67            }
68            hashes
69        }
70        None => Vec::new(),
71    };
72
73    // Get the blob sidecars using existing EthApi logic
74    match api.anvil_get_blobs_by_block_id(block_id, versioned_hashes) {
75        Ok(Some(blobs)) => {
76            if must_be_ssz(&headers) {
77                blobs.as_ssz_bytes().into_response()
78            } else {
79                Json(GetBlobsResponse {
80                    execution_optimistic: false,
81                    finalized: false,
82                    data: blobs,
83                })
84                .into_response()
85            }
86        }
87        Ok(None) => BeaconError::block_not_found().into_response(),
88        Err(_) => BeaconError::internal_error().into_response(),
89    }
90}
91
92/// Handles incoming Beacon API requests for genesis details
93///
94/// Only returns the `genesis_time`, other fields are set to zero.
95///
96/// GET /eth/v1/beacon/genesis
97pub async fn handle_get_genesis(State(api): State<EthApi<FoundryNetwork>>) -> Response {
98    match api.anvil_get_genesis_time() {
99        Ok(genesis_time) => Json(GenesisResponse {
100            data: GenesisData {
101                genesis_time,
102                genesis_validators_root: B256::ZERO,
103                genesis_fork_version: B32::ZERO,
104            },
105        })
106        .into_response(),
107        Err(_) => BeaconError::internal_error().into_response(),
108    }
109}
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use axum::http::HeaderValue;
114
115    fn header_map_with_accept(accept: &str) -> HeaderMap {
116        let mut headers = HeaderMap::new();
117        headers.insert(axum::http::header::ACCEPT, HeaderValue::from_str(accept).unwrap());
118        headers
119    }
120
121    #[test]
122    fn test_must_be_ssz() {
123        let test_cases = vec![
124            (None, false, "no Accept header"),
125            (Some("application/json"), false, "JSON only"),
126            (Some("application/octet-stream"), true, "octet-stream only"),
127            (Some("application/octet-stream;q=1.0,application/json;q=0.9"), true, "SSZ preferred"),
128            (
129                Some("application/json;q=1.0,application/octet-stream;q=0.9"),
130                false,
131                "JSON preferred",
132            ),
133            (Some("application/octet-stream;q=0.5,application/json;q=0.5"), false, "equal quality"),
134            (
135                Some("text/html;q=0.9, application/octet-stream;q=1.0, application/json;q=0.8"),
136                true,
137                "multiple types",
138            ),
139            (
140                Some("application/octet-stream ; q=1.0 , application/json ; q=0.9"),
141                true,
142                "whitespace handling",
143            ),
144            (Some("application/octet-stream, application/json;q=0.9"), true, "default quality"),
145        ];
146
147        for (accept_header, expected, description) in test_cases {
148            let headers = match accept_header {
149                None => HeaderMap::new(),
150                Some(header) => header_map_with_accept(header),
151            };
152            assert_eq!(
153                must_be_ssz(&headers),
154                expected,
155                "Test case '{}' failed: expected {}, got {}",
156                description,
157                expected,
158                !expected
159            );
160        }
161    }
162}