Skip to content

Commit 40e7612

Browse files
authored
refactor: add attestation middleware (#506)
1 parent cffaa22 commit 40e7612

File tree

13 files changed

+407
-196
lines changed

13 files changed

+407
-196
lines changed

crates/service/src/error.rs

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

4-
use alloy::primitives::Address;
54
use anyhow::Error;
65
use axum::{
76
response::{IntoResponse, Response},
@@ -27,12 +26,6 @@ pub enum IndexerServiceError {
2726

2827
#[error("Issues with provided receipt: {0}")]
2928
ReceiptError(#[from] tap_core::Error),
30-
#[error("No attestation signer found for allocation `{0}`")]
31-
NoSignerForAllocation(Address),
32-
#[error("Error while processing the request: {0}")]
33-
ProcessingError(SubgraphServiceError),
34-
#[error("Failed to sign attestation")]
35-
FailedToSignAttestation,
3629

3730
#[error("There was an error while accessing escrow account: {0}")]
3831
EscrowAccount(#[from] EscrowAccountsError),
@@ -48,12 +41,10 @@ impl IntoResponse for IndexerServiceError {
4841
}
4942

5043
let status = match self {
51-
NoSignerForAllocation(_) | FailedToSignAttestation => StatusCode::INTERNAL_SERVER_ERROR,
52-
53-
ReceiptError(_) | EscrowAccount(_) | ProcessingError(_) => StatusCode::BAD_REQUEST,
44+
ReceiptError(_) | EscrowAccount(_) => StatusCode::BAD_REQUEST,
5445
ReceiptNotFound => StatusCode::PAYMENT_REQUIRED,
5546
DeploymentIdNotFound => StatusCode::INTERNAL_SERVER_ERROR,
56-
AxumError(_) => StatusCode::BAD_REQUEST,
47+
AxumError(_) => StatusCode::INTERNAL_SERVER_ERROR,
5748
SerializationError(_) => StatusCode::BAD_REQUEST,
5849
};
5950
tracing::error!(%self, "An IndexerServiceError occoured.");

crates/service/src/middleware.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
// Copyright 2023-, Edge & Node, GraphOps, and Semiotic Labs.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
mod allocation;
5+
mod attestation;
6+
mod attestation_signer;
47
pub mod auth;
5-
mod inject_allocation;
6-
mod inject_context;
7-
mod inject_deployment;
8-
mod inject_labels;
9-
mod inject_receipt;
10-
mod inject_sender;
8+
mod deployment;
9+
mod labels;
1110
mod prometheus_metrics;
11+
mod sender;
12+
mod tap_context;
13+
mod tap_receipt;
1214

13-
pub use inject_allocation::{allocation_middleware, Allocation, AllocationState};
14-
pub use inject_context::context_middleware;
15-
pub use inject_deployment::deployment_middleware;
16-
pub use inject_labels::labels_middleware;
17-
pub use inject_receipt::receipt_middleware;
18-
pub use inject_sender::{sender_middleware, SenderState};
15+
pub use allocation::{allocation_middleware, Allocation, AllocationState};
16+
pub use attestation::{attestation_middleware, AttestationInput};
17+
pub use attestation_signer::{signer_middleware, AttestationState};
18+
pub use deployment::deployment_middleware;
19+
pub use labels::labels_middleware;
1920
pub use prometheus_metrics::PrometheusMetricsMiddlewareLayer;
21+
pub use sender::{sender_middleware, SenderState};
22+
pub use tap_context::context_middleware;
23+
pub use tap_receipt::receipt_middleware;

crates/service/src/middleware/inject_allocation.rs renamed to crates/service/src/middleware/allocation.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub async fn allocation_middleware(
5858

5959
#[cfg(test)]
6060
mod tests {
61-
use crate::middleware::inject_allocation::Allocation;
61+
use crate::middleware::allocation::Allocation;
6262

6363
use super::{allocation_middleware, AllocationState};
6464

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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 is attestable and 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+
async fn send_request(app: Router, signer: Option<AttestationSigner>) -> Response<Body> {
141+
let mut request = Request::builder().uri("/");
142+
143+
if let Some(signer) = signer {
144+
request = request.extension(signer);
145+
}
146+
147+
app.oneshot(request.body(Body::empty()).unwrap())
148+
.await
149+
.unwrap()
150+
}
151+
152+
#[tokio::test]
153+
async fn test_create_attestation() {
154+
let (allocation, signer) = allocation_signer();
155+
let middleware = from_fn(attestation_middleware);
156+
157+
let handle = move |_: Request<Body>| async move {
158+
let mut res = Response::new(RESPONSE.to_string());
159+
res.extensions_mut().insert(AttestationInput::Attestable {
160+
req: REQUEST.to_string(),
161+
});
162+
res
163+
};
164+
165+
let app = Router::new().route("/", get(handle)).layer(middleware);
166+
167+
// with signer
168+
let res = send_request(app, Some(signer.clone())).await;
169+
assert_eq!(res.status(), StatusCode::OK);
170+
171+
let response = payload_from_response(res).await;
172+
assert_eq!(response.graphql_response, RESPONSE.to_string());
173+
174+
let attestation = response.attestation.unwrap();
175+
assert!(signer
176+
.verify(&attestation, REQUEST, RESPONSE, &allocation.id)
177+
.is_ok());
178+
}
179+
180+
#[tokio::test]
181+
async fn test_non_assignable() {
182+
let (_, signer) = allocation_signer();
183+
let handle = move |_: Request<Body>| async move { Response::new(RESPONSE.to_string()) };
184+
185+
let middleware = from_fn(attestation_middleware);
186+
let app = Router::new().route("/", get(handle)).layer(middleware);
187+
188+
let res = send_request(app, Some(signer.clone())).await;
189+
assert_eq!(res.status(), StatusCode::OK);
190+
191+
let response = payload_from_response(res).await;
192+
assert_eq!(response.graphql_response, RESPONSE.to_string());
193+
assert!(response.attestation.is_none());
194+
}
195+
196+
#[tokio::test]
197+
async fn test_no_signer() {
198+
let handle = move |_: Request<Body>| async move {
199+
Response::new(RESPONSE.to_string());
200+
};
201+
202+
let middleware = from_fn(attestation_middleware);
203+
let app = Router::new().route("/", get(handle)).layer(middleware);
204+
205+
let res = send_request(app, None).await;
206+
assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
207+
}
208+
}

0 commit comments

Comments
 (0)