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