diff --git a/Cargo.lock b/Cargo.lock index f555a8ef..daaa8f54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2188,6 +2188,7 @@ dependencies = [ "tap_core", "thegraph-core", "thegraph-graphql-http", + "thegraph-headers", "thiserror 2.0.6", "tokio", "tokio-test", @@ -4725,6 +4726,19 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "thegraph-headers" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904f12db82a53c6fb87516bdbb7ad64fe82ec809afb2c85dd05b98891d3e5483" +dependencies = [ + "headers", + "http 1.2.0", + "serde", + "serde_json", + "thegraph-core", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/Cargo.toml b/Cargo.toml index e0b31140..709c2ed9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ thegraph-core = { version = "0.9.0", features = [ "serde", ] } thegraph-graphql-http = { version = "0.3.2", features = ["reqwest"] } +thegraph-headers = { version = "0.1.0", features = ["attestation"] } thiserror = "2.0.2" tokio = { version = "1.38.0", features = [ "macros", diff --git a/src/client_query.rs b/src/client_query.rs index 760cf67e..408c9a27 100644 --- a/src/client_query.rs +++ b/src/client_query.rs @@ -21,17 +21,17 @@ use rand::{thread_rng, Rng as _}; use serde::Deserialize; use serde_json::value::RawValue; use thegraph_core::{alloy::primitives::BlockNumber, AllocationId, DeploymentId, IndexerId}; +use thegraph_headers::{graph_attestation::GraphAttestation, HttpBuilderExt as _}; use tokio::sync::mpsc; use tracing::{info_span, Instrument as _}; use url::Url; -use self::{attestation_header::GraphAttestation, context::Context, query_selector::QuerySelector}; +use self::{context::Context, query_selector::QuerySelector}; use crate::{ auth::AuthSettings, block_constraints::{resolve_block_requirements, rewrite_query, BlockRequirements}, budgets::USD, errors::{Error, IndexerError, IndexerErrors, MissingBlockError, UnavailableReason}, - http_ext::HttpBuilderExt as _, indexer_client::{IndexerAuth, IndexerResponse}, indexing_performance, metrics::{with_metric, METRICS}, @@ -40,7 +40,6 @@ use crate::{ reports, }; -mod attestation_header; pub mod context; mod query_selector; @@ -104,12 +103,16 @@ pub async fn handle_query( attestation, .. }| { - Response::builder() + let mut builder = Response::builder() .status(StatusCode::OK) - .header_typed(ContentType::json()) - .header_typed(GraphAttestation(attestation)) - .body(client_response) - .unwrap() + .header_typed(ContentType::json()); + + // Add attestation header if present + if let Some(attestation) = attestation { + builder = builder.header_typed(GraphAttestation(attestation)); + } + + builder.body(client_response).expect("valid response") }, ) } @@ -769,12 +772,16 @@ pub async fn handle_indexer_query( attestation, .. }| { - Response::builder() + let mut builder = Response::builder() .status(StatusCode::OK) - .header_typed(ContentType::json()) - .header_typed(GraphAttestation(attestation)) - .body(client_response) - .unwrap() + .header_typed(ContentType::json()); + + // Add attestation header if present + if let Some(attestation) = attestation { + builder = builder.header_typed(GraphAttestation(attestation)); + } + + builder.body(client_response).expect("valid response") }, ) } diff --git a/src/client_query/attestation_header.rs b/src/client_query/attestation_header.rs deleted file mode 100644 index f2b660c7..00000000 --- a/src/client_query/attestation_header.rs +++ /dev/null @@ -1,238 +0,0 @@ -// TODO(LNSD): Move to `thegraph` crate - -use axum::http::{HeaderName, HeaderValue}; -use headers::Error; -use thegraph_core::attestation::Attestation; - -static GRAPH_ATTESTATION_HEADER_NAME: HeaderName = HeaderName::from_static("graph-attestation"); - -/// A typed header for the `graph-attestation` header. -/// -/// The `graph-attestation` header value is a JSON encoded `Attestation` struct, or empty string -/// if no attestation is provided. -/// -/// When deserializing the header value, if the value is empty, the header will be deserialized as -/// `None`. If the value is not empty, but cannot be deserialized as an `Attestation`, the header -/// is considered invalid. -#[derive(Debug, Clone)] -pub struct GraphAttestation(pub Option); - -impl GraphAttestation { - /// Create a new `GraphAttestation` header from the given attestation. - #[cfg(test)] - pub fn new(value: Attestation) -> Self { - Self(Some(value)) - } - - /// Create a new empty `GraphAttestation` typed header. - #[cfg(test)] - pub fn empty() -> Self { - Self(None) - } -} - -impl headers::Header for GraphAttestation { - fn name() -> &'static HeaderName { - &GRAPH_ATTESTATION_HEADER_NAME - } - - fn decode<'i, I>(values: &mut I) -> Result - where - Self: Sized, - I: Iterator, - { - let value = values - .next() - .ok_or_else(Error::invalid)? - .to_str() - .map_err(|_| Error::invalid())?; - - if value.is_empty() { - return Ok(Self(None)); - } - - let value = serde_json::from_str(value).map_err(|_| Error::invalid())?; - - Ok(Self(Some(value))) - } - - fn encode>(&self, values: &mut E) { - // Serialize the attestation as a JSON string, and convert it to a `HeaderValue`. - // If the attestation is `None`, serialize an empty string. - let value = self - .0 - .as_ref() - .and_then(|att| { - serde_json::to_string(att) - .map(|s| HeaderValue::from_str(&s).unwrap()) - .ok() - }) - .unwrap_or_else(|| HeaderValue::from_static("")); - - values.extend(std::iter::once(value)); - } -} - -#[cfg(test)] -mod tests { - use assert_matches::assert_matches; - use headers::{Header, HeaderValue}; - use thegraph_core::attestation::Attestation; - - use super::GraphAttestation; - - #[test] - fn encode_attestation_into_header() { - //* Given - let attestation = Attestation { - request_cid: Default::default(), - response_cid: Default::default(), - deployment: Default::default(), - r: Default::default(), - s: Default::default(), - v: 0, - }; - - let mut headers = vec![]; - - //* When - let header = GraphAttestation::new(attestation.clone()); - - header.encode(&mut headers); - - //* Then - let value = headers - .first() - .expect("header to have been encoded") - .to_str() - .expect("header to be valid utf8"); - - assert_matches!(serde_json::from_str::(value), Ok(att) => { - assert_eq!(attestation.request_cid, att.request_cid); - assert_eq!(attestation.response_cid, att.response_cid); - assert_eq!(attestation.deployment, att.deployment); - assert_eq!(attestation.r, att.r); - assert_eq!(attestation.s, att.s); - assert_eq!(attestation.v, att.v); - }); - } - - #[test] - fn encode_empty_attestation_header() { - //* Given - let mut headers = vec![]; - - //* When - let header = GraphAttestation::empty(); - - header.encode(&mut headers); - - //* Then - let value = headers - .first() - .expect("header to have been encoded") - .to_str() - .expect("header to be valid utf8"); - - assert_eq!(value, ""); - } - - #[test] - fn decode_attestation_from_valid_header() { - //* Given - let attestation = Attestation { - request_cid: Default::default(), - response_cid: Default::default(), - deployment: Default::default(), - r: Default::default(), - s: Default::default(), - v: 0, - }; - - let header = { - let value = serde_json::to_string(&attestation).unwrap(); - HeaderValue::from_str(&value).unwrap() - }; - let headers = [header]; - - //* When - let header = GraphAttestation::decode(&mut headers.iter()); - - //* Then - assert_matches!(header, Ok(GraphAttestation(Some(att))) => { - assert_eq!(attestation.request_cid, att.request_cid); - assert_eq!(attestation.response_cid, att.response_cid); - assert_eq!(attestation.deployment, att.deployment); - assert_eq!(attestation.r, att.r); - assert_eq!(attestation.s, att.s); - assert_eq!(attestation.v, att.v); - }); - } - - #[test] - fn decode_attestation_from_first_header() { - //* Given - let attestation = Attestation { - request_cid: Default::default(), - response_cid: Default::default(), - deployment: Default::default(), - r: Default::default(), - s: Default::default(), - v: 0, - }; - - let header = { - let value = serde_json::to_string(&attestation).unwrap(); - HeaderValue::from_str(&value).unwrap() - }; - let headers = [ - header, - HeaderValue::from_static("invalid"), - HeaderValue::from_static(""), - ]; - - //* When - let header = GraphAttestation::decode(&mut headers.iter()); - - //* Then - assert_matches!(header, Ok(GraphAttestation(Some(_)))); - } - - #[test] - fn decode_empty_attestation_from_valid_header() { - //* Given - let header = HeaderValue::from_static(""); - let headers = [header]; - - //* When - let header = GraphAttestation::decode(&mut headers.iter()); - - //* Then - assert_matches!(header, Ok(GraphAttestation(None))); - } - - #[test] - fn fail_decode_attestation_from_invalid_header() { - //* Given - let header = HeaderValue::from_static("invalid"); - let headers = [header]; - - //* When - let header = GraphAttestation::decode(&mut headers.iter()); - - //* Then - assert_matches!(header, Err(_)); - } - - #[test] - fn fail_decode_attestation_if_no_headers() { - //* Given - let headers = []; - - //* When - let header = GraphAttestation::decode(&mut headers.iter()); - - //* Then - assert_matches!(header, Err(_)); - } -} diff --git a/src/graphql.rs b/src/graphql.rs index 8b36a8ac..e03c2d0b 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -1,8 +1,7 @@ use axum::http::{Response, StatusCode}; use headers::ContentType; use thegraph_graphql_http::http::response::{IntoError as IntoGraphqlResponseError, ResponseBody}; - -use crate::http_ext::HttpBuilderExt as _; +use thegraph_headers::HttpBuilderExt as _; /// Serialize an error into a GraphQL error response. /// diff --git a/src/http_ext.rs b/src/http_ext.rs deleted file mode 100644 index 3e5db0fe..00000000 --- a/src/http_ext.rs +++ /dev/null @@ -1,25 +0,0 @@ -pub trait HttpBuilderExt { - fn header_typed(self, h: T) -> Self; -} - -impl HttpBuilderExt for http::response::Builder { - fn header_typed(mut self, h: T) -> Self { - let mut v = vec![]; - h.encode(&mut v); - for value in v { - self = self.header(T::name(), value); - } - self - } -} - -impl HttpBuilderExt for http::request::Builder { - fn header_typed(mut self, h: T) -> Self { - let mut v = vec![]; - h.encode(&mut v); - for value in v { - self = self.header(T::name(), value); - } - self - } -} diff --git a/src/main.rs b/src/main.rs index 6d6675b2..93c185de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,6 @@ mod config; mod errors; mod exchange_rate; mod graphql; -mod http_ext; mod indexer_client; mod indexing_performance; mod metrics;