Skip to content

Commit 3895893

Browse files
committed
refactor: add attestation middleware
Signed-off-by: Gustavo Inacio <[email protected]>
1 parent 36fff08 commit 3895893

File tree

2 files changed

+218
-0
lines changed

2 files changed

+218
-0
lines changed

crates/service/src/middleware.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright 2023-, Edge & Node, GraphOps, and Semiotic Labs.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
mod attestation;
45
pub mod auth;
56
mod inject_allocation;
67
mod inject_attestation_signer;
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright 2023-, Edge & Node, GraphOps, and Semiotic Labs.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use std::string::FromUtf8Error;
5+
6+
use axum::{
7+
body::to_bytes,
8+
extract::Request,
9+
middleware::Next,
10+
response::{IntoResponse, Response},
11+
};
12+
use reqwest::StatusCode;
13+
use serde::Serialize;
14+
use thegraph_core::Attestation;
15+
16+
use indexer_attestation::AttestationSigner;
17+
18+
#[derive(Clone)]
19+
pub enum AttestationInput {
20+
Attestable { req: String },
21+
NotAttestable,
22+
}
23+
24+
#[derive(Debug, Serialize)]
25+
#[cfg_attr(test, derive(serde::Deserialize))]
26+
pub struct IndexerResponsePayload {
27+
#[serde(rename = "graphQLResponse")]
28+
graphql_response: String,
29+
attestation: Option<Attestation>,
30+
}
31+
32+
/// Check if the query can be attestable generates attestation
33+
///
34+
/// Executes query -> return subgraph response: (string, attestable (bool))
35+
/// if attestable && allocation id:
36+
/// - look for signer
37+
/// - create attestation
38+
/// - return response with attestation
39+
/// else:
40+
/// - return with no attestation
41+
///
42+
/// Requires AttestationSigner
43+
pub async fn attestation_middleware(
44+
request: Request,
45+
next: Next,
46+
) -> Result<Response, AttestationError> {
47+
let signer = request
48+
.extensions()
49+
.get::<AttestationSigner>()
50+
.cloned()
51+
.ok_or(AttestationError::CouldNotFindSigner)?;
52+
53+
let (parts, graphql_response) = next.run(request).await.into_parts();
54+
let attestation_response = parts.extensions.get::<AttestationInput>();
55+
let bytes = to_bytes(graphql_response, usize::MAX).await?;
56+
let res = String::from_utf8(bytes.into())?;
57+
58+
let attestation = match attestation_response {
59+
Some(AttestationInput::Attestable { req }) => Some(signer.create_attestation(req, &res)),
60+
_ => None,
61+
};
62+
63+
let response = serde_json::to_string(&IndexerResponsePayload {
64+
graphql_response: res,
65+
attestation,
66+
})?;
67+
68+
Ok(Response::new(response.into()))
69+
}
70+
71+
#[derive(thiserror::Error, Debug)]
72+
pub enum AttestationError {
73+
#[error("Could not find signer for allocation")]
74+
CouldNotFindSigner,
75+
76+
#[error("There was an AxumError: {0}")]
77+
AxumError(#[from] axum::Error),
78+
79+
#[error("There was an error converting the response to UTF-8 string: {0}")]
80+
FromUtf8Error(#[from] FromUtf8Error),
81+
82+
#[error("there was an error while serializing the response: {0}")]
83+
SerializationError(#[from] serde_json::Error),
84+
}
85+
86+
impl IntoResponse for AttestationError {
87+
fn into_response(self) -> Response {
88+
match self {
89+
AttestationError::CouldNotFindSigner
90+
| AttestationError::AxumError(_)
91+
| AttestationError::FromUtf8Error(_)
92+
| AttestationError::SerializationError(_) => StatusCode::INTERNAL_SERVER_ERROR,
93+
}
94+
.into_response()
95+
}
96+
}
97+
98+
#[cfg(test)]
99+
mod tests {
100+
use alloy::primitives::Address;
101+
use axum::{
102+
body::{to_bytes, Body},
103+
http::{Request, Response},
104+
middleware::from_fn,
105+
routing::get,
106+
Router,
107+
};
108+
use indexer_allocation::Allocation;
109+
use indexer_attestation::AttestationSigner;
110+
use reqwest::StatusCode;
111+
use test_assets::{INDEXER_ALLOCATIONS, INDEXER_MNEMONIC};
112+
use tower::ServiceExt;
113+
114+
use crate::middleware::{
115+
attestation::IndexerResponsePayload, attestation_middleware, AttestationInput,
116+
};
117+
118+
const REQUEST: &str = "request";
119+
const RESPONSE: &str = "response";
120+
121+
fn allocation_signer() -> (Allocation, AttestationSigner) {
122+
let allocation = INDEXER_ALLOCATIONS
123+
.values()
124+
.collect::<Vec<_>>()
125+
.pop()
126+
.unwrap()
127+
.clone();
128+
let signer =
129+
AttestationSigner::new(&INDEXER_MNEMONIC.to_string(), &allocation, 1, Address::ZERO)
130+
.unwrap();
131+
(allocation, signer)
132+
}
133+
134+
async fn payload_from_response(res: Response<Body>) -> IndexerResponsePayload {
135+
let bytes = to_bytes(res.into_body(), usize::MAX).await.unwrap();
136+
137+
serde_json::from_slice(&bytes).unwrap()
138+
}
139+
140+
#[tokio::test]
141+
async fn test_create_attestation() {
142+
let (allocation, signer) = allocation_signer();
143+
let middleware = from_fn(attestation_middleware);
144+
145+
let handle = move |_: Request<Body>| async move {
146+
let mut res = Response::new(RESPONSE.to_string());
147+
res.extensions_mut().insert(AttestationInput::Attestable {
148+
req: REQUEST.to_string(),
149+
});
150+
res
151+
};
152+
153+
let app = Router::new().route("/", get(handle)).layer(middleware);
154+
155+
// with signer
156+
let res = app
157+
.oneshot(
158+
Request::builder()
159+
.uri("/")
160+
.extension(signer.clone())
161+
.body(Body::empty())
162+
.unwrap(),
163+
)
164+
.await
165+
.unwrap();
166+
assert_eq!(res.status(), StatusCode::OK);
167+
168+
let response = payload_from_response(res).await;
169+
assert_eq!(response.graphql_response, RESPONSE.to_string());
170+
171+
let attestation = response.attestation.unwrap();
172+
assert!(signer
173+
.verify(&attestation, REQUEST, RESPONSE, &allocation.id)
174+
.is_ok());
175+
}
176+
177+
#[tokio::test]
178+
async fn test_non_assignable() {
179+
let (_, signer) = allocation_signer();
180+
let handle = move |_: Request<Body>| async move { Response::new(RESPONSE.to_string()) };
181+
182+
let middleware = from_fn(attestation_middleware);
183+
let app = Router::new().route("/", get(handle)).layer(middleware);
184+
185+
let res = app
186+
.oneshot(
187+
Request::builder()
188+
.uri("/")
189+
.extension(signer.clone())
190+
.body(Body::empty())
191+
.unwrap(),
192+
)
193+
.await
194+
.unwrap();
195+
assert_eq!(res.status(), StatusCode::OK);
196+
197+
let response = payload_from_response(res).await;
198+
assert_eq!(response.graphql_response, RESPONSE.to_string());
199+
assert!(response.attestation.is_none());
200+
}
201+
202+
#[tokio::test]
203+
async fn test_no_signer() {
204+
let handle = move |_: Request<Body>| async move {
205+
Response::new(RESPONSE.to_string());
206+
};
207+
208+
let middleware = from_fn(attestation_middleware);
209+
let app = Router::new().route("/", get(handle)).layer(middleware);
210+
211+
let res = app
212+
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
213+
.await
214+
.unwrap();
215+
assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
216+
}
217+
}

0 commit comments

Comments
 (0)