Skip to content

Commit 8fe93b9

Browse files
feat(aggregation-mode): receipt endpoint in agg mode batcher (#2188)
Co-authored-by: Marcos Nicolau <[email protected]> Co-authored-by: Marcos Nicolau <[email protected]>
1 parent c80a869 commit 8fe93b9

File tree

9 files changed

+175
-72
lines changed

9 files changed

+175
-72
lines changed

aggregation_mode/batcher/abi/AggregationModePaymentService.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

aggregation_mode/batcher/src/db.rs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,23 @@ pub enum DbError {
1414
ConnectError(String),
1515
}
1616

17+
#[derive(Debug, Clone, sqlx::Type, serde::Serialize)]
18+
#[sqlx(type_name = "task_status")]
19+
#[sqlx(rename_all = "lowercase")]
20+
pub enum TaskStatus {
21+
Pending,
22+
Processing,
23+
Verified,
24+
}
25+
26+
#[derive(Debug, Clone, sqlx::FromRow, sqlx::Type, serde::Serialize)]
27+
pub struct Receipt {
28+
pub status: TaskStatus,
29+
pub merkle_path: Option<Vec<u8>>,
30+
pub nonce: i64,
31+
pub address: String,
32+
}
33+
1734
impl Db {
1835
pub async fn try_new(connection_url: &str) -> Result<Self, DbError> {
1936
let pool = PgPoolOptions::new()
@@ -45,29 +62,66 @@ impl Db {
4562
.map(|res| res.flatten())
4663
}
4764

65+
pub async fn get_tasks_by_address_and_nonce(
66+
&self,
67+
address: &str,
68+
nonce: i64,
69+
) -> Result<Vec<Receipt>, sqlx::Error> {
70+
sqlx::query_as::<_, Receipt>(
71+
"SELECT status,merkle_path,nonce,address FROM tasks
72+
WHERE address = $1
73+
AND nonce = $2
74+
ORDER BY nonce DESC",
75+
)
76+
.bind(address.to_lowercase())
77+
.bind(nonce)
78+
.fetch_all(&self.pool)
79+
.await
80+
}
81+
82+
pub async fn get_tasks_by_address_with_limit(
83+
&self,
84+
address: &str,
85+
limit: i64,
86+
) -> Result<Vec<Receipt>, sqlx::Error> {
87+
sqlx::query_as::<_, Receipt>(
88+
"SELECT status,merkle_path,nonce,address FROM tasks
89+
WHERE address = $1
90+
ORDER BY nonce DESC
91+
LIMIT $2",
92+
)
93+
.bind(address.to_lowercase())
94+
.bind(limit)
95+
.fetch_all(&self.pool)
96+
.await
97+
}
98+
4899
pub async fn insert_task(
49100
&self,
50101
address: &str,
51102
proving_system_id: i32,
52103
proof: &[u8],
53104
program_commitment: &[u8],
54105
merkle_path: Option<&[u8]>,
106+
nonce: i64,
55107
) -> Result<Uuid, sqlx::Error> {
56108
sqlx::query_scalar::<_, Uuid>(
57109
"INSERT INTO tasks (
58110
address,
59111
proving_system_id,
60112
proof,
61113
program_commitment,
62-
merkle_path
63-
) VALUES ($1, $2, $3, $4, $5)
114+
merkle_path,
115+
nonce
116+
) VALUES ($1, $2, $3, $4, $5, $6)
64117
RETURNING task_id",
65118
)
66119
.bind(address.to_lowercase())
67120
.bind(proving_system_id)
68121
.bind(proof)
69122
.bind(program_commitment)
70123
.bind(merkle_path)
124+
.bind(nonce)
71125
.fetch_one(&self.pool)
72126
.await
73127
}

aggregation_mode/batcher/src/server/http.rs

Lines changed: 79 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ use sqlx::types::BigDecimal;
1414

1515
use super::{
1616
helpers::format_merkle_path,
17-
types::{AppResponse, GetProofMerklePathQueryParams},
17+
types::{AppResponse, GetReceiptsQueryParams},
1818
};
1919

2020
use crate::{
2121
config::Config,
2222
db::Db,
23-
server::types::{SubmitProofRequestRisc0, SubmitProofRequestSP1},
23+
server::types::{GetReceiptsResponse, SubmitProofRequestRisc0, SubmitProofRequestSP1},
2424
verifiers::{verify_sp1_proof, VerificationError},
2525
};
2626

@@ -45,7 +45,7 @@ impl BatcherServer {
4545
App::new()
4646
.app_data(Data::new(state.clone()))
4747
.route("/nonce/{address}", web::get().to(Self::get_nonce))
48-
.route("/proof/merkle", web::get().to(Self::get_proof_merkle_path))
48+
.route("/receipts", web::get().to(Self::get_receipts))
4949
.route("/proof/sp1", web::post().to(Self::post_proof_sp1))
5050
.route("/proof/risc0", web::post().to(Self::post_proof_risc0))
5151
})
@@ -56,20 +56,28 @@ impl BatcherServer {
5656
.expect("Server to never end");
5757
}
5858

59+
// Returns the nonce (number of submitted tasks) for a given address
5960
async fn get_nonce(req: HttpRequest) -> impl Responder {
60-
let Some(address) = req.match_info().get("address") else {
61+
let Some(address_raw) = req.match_info().get("address") else {
6162
return HttpResponse::BadRequest()
6263
.json(AppResponse::new_unsucessfull("Missing address", 400));
6364
};
6465

65-
// TODO: validate valid ethereum address
66+
// Check that the address is a valid ethereum address
67+
if alloy::primitives::Address::from_str(address_raw.trim()).is_err() {
68+
return HttpResponse::BadRequest()
69+
.json(AppResponse::new_unsucessfull("Invalid address", 400));
70+
}
71+
72+
let address = address_raw.to_lowercase();
73+
6674
let Some(state) = req.app_data::<Data<BatcherServer>>() else {
6775
return HttpResponse::InternalServerError()
6876
.json(AppResponse::new_unsucessfull("Internal server error", 500));
6977
};
7078

7179
let state = state.get_ref();
72-
match state.db.count_tasks_by_address(address).await {
80+
match state.db.count_tasks_by_address(&address).await {
7381
Ok(count) => HttpResponse::Ok().json(AppResponse::new_sucessfull(serde_json::json!(
7482
{
7583
"nonce": count
@@ -80,6 +88,7 @@ impl BatcherServer {
8088
}
8189
}
8290

91+
// Posts an SP1 proof to the batcher, recovering the address from the signature
8392
async fn post_proof_sp1(
8493
req: HttpRequest,
8594
MultipartForm(data): MultipartForm<SubmitProofRequestSP1>,
@@ -172,6 +181,7 @@ impl BatcherServer {
172181
&proof_content,
173182
&vk_content,
174183
None,
184+
data.nonce.0 as i64,
175185
)
176186
.await
177187
{
@@ -184,65 +194,89 @@ impl BatcherServer {
184194
}
185195

186196
/// TODO: complete for risc0 (see `post_proof_sp1`)
197+
// Posts a Risc0 proof to the batcher, recovering the address from the signature
187198
async fn post_proof_risc0(
188199
_req: HttpRequest,
189200
MultipartForm(_): MultipartForm<SubmitProofRequestRisc0>,
190201
) -> impl Responder {
191202
HttpResponse::Ok().json(AppResponse::new_sucessfull(serde_json::json!({})))
192203
}
193204

194-
async fn get_proof_merkle_path(
205+
// Returns the last 100 receipt merkle proofs for the address received in the URL.
206+
// In case of also receiving a nonce on the query param, it returns only the merkle proof for that nonce.
207+
async fn get_receipts(
195208
req: HttpRequest,
196-
params: web::Query<GetProofMerklePathQueryParams>,
209+
params: web::Query<GetReceiptsQueryParams>,
197210
) -> impl Responder {
198211
let Some(state) = req.app_data::<Data<BatcherServer>>() else {
199-
return HttpResponse::InternalServerError()
200-
.json(AppResponse::new_unsucessfull("Internal server error", 500));
212+
return HttpResponse::InternalServerError().json(AppResponse::new_unsucessfull(
213+
"Internal server error: Failed to get app data",
214+
500,
215+
));
201216
};
202217

203218
let state = state.get_ref();
204219

205-
// TODO: maybe also accept proof commitment in query param
206-
let Some(id) = params.id.clone() else {
207-
return HttpResponse::BadRequest().json(AppResponse::new_unsucessfull(
208-
"Provide task `id` query param",
209-
400,
210-
));
211-
};
212-
213-
if id.is_empty() {
214-
return HttpResponse::BadRequest().json(AppResponse::new_unsucessfull(
215-
"Proof id cannot be empty",
216-
400,
217-
));
220+
if alloy::primitives::Address::from_str(params.address.clone().trim()).is_err() {
221+
return HttpResponse::BadRequest()
222+
.json(AppResponse::new_unsucessfull("Invalid address", 400));
218223
}
219224

220-
let Ok(proof_id) = sqlx::types::Uuid::parse_str(&id) else {
221-
return HttpResponse::BadRequest()
222-
.json(AppResponse::new_unsucessfull("Proof id invalid uuid", 400));
225+
let limit = match params.limit {
226+
Some(received_limit) => received_limit.min(100),
227+
None => 100,
223228
};
224229

225-
let db_result = state.db.get_merkle_path_by_task_id(proof_id).await;
226-
let merkle_path = match db_result {
227-
Ok(Some(merkle_path)) => merkle_path,
228-
Ok(None) => {
229-
return HttpResponse::NotFound().json(AppResponse::new_unsucessfull(
230-
"Proof merkle path not found",
231-
404,
232-
))
233-
}
234-
Err(_) => {
235-
return HttpResponse::InternalServerError()
236-
.json(AppResponse::new_unsucessfull("Internal server error", 500));
237-
}
230+
let address = params.address.to_lowercase();
231+
232+
let query = if let Some(nonce) = params.nonce {
233+
state
234+
.db
235+
.get_tasks_by_address_and_nonce(&address, nonce)
236+
.await
237+
} else {
238+
state
239+
.db
240+
.get_tasks_by_address_with_limit(&address, limit)
241+
.await
238242
};
239243

240-
match format_merkle_path(&merkle_path) {
241-
Ok(merkle_path) => {
242-
HttpResponse::Ok().json(AppResponse::new_sucessfull(serde_json::json!({
243-
"merkle_path": merkle_path
244-
})))
245-
}
244+
let Ok(receipts) = query else {
245+
return HttpResponse::InternalServerError().json(AppResponse::new_unsucessfull(
246+
"Internal server error: Failed to get tasks by address and nonce",
247+
500,
248+
));
249+
};
250+
251+
let responses: Result<Vec<GetReceiptsResponse>, String> = receipts
252+
.into_iter()
253+
.map(|receipt| {
254+
let Some(merkle_path) = receipt.merkle_path else {
255+
return Ok(GetReceiptsResponse {
256+
status: receipt.status,
257+
merkle_path: Vec::new(),
258+
nonce: receipt.nonce,
259+
address: receipt.address,
260+
});
261+
};
262+
263+
let Ok(formatted) = format_merkle_path(&merkle_path) else {
264+
return Err("Error formatting merkle path".into());
265+
};
266+
267+
Ok(GetReceiptsResponse {
268+
status: receipt.status,
269+
merkle_path: formatted,
270+
nonce: receipt.nonce,
271+
address: receipt.address,
272+
})
273+
})
274+
.collect();
275+
276+
match responses {
277+
Ok(resp) => HttpResponse::Ok().json(AppResponse::new_sucessfull(serde_json::json!({
278+
"receipts": resp
279+
}))),
246280
Err(_) => HttpResponse::InternalServerError()
247281
.json(AppResponse::new_unsucessfull("Internal server error", 500)),
248282
}

aggregation_mode/batcher/src/server/types.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
22
use serde::{Deserialize, Serialize};
33
use serde_json::Value;
44

5+
use crate::db::TaskStatus;
6+
57
#[derive(Serialize, Deserialize)]
68
pub(super) struct AppResponse {
79
status: u16,
@@ -27,10 +29,14 @@ impl AppResponse {
2729
}
2830
}
2931

30-
/// Query parameters accepted by `GET /proof/merkle`, containing an optional proof id.
32+
/// Query parameters accepted by `GET /receipts`. Requires an address, and accepts a nonce
33+
/// and a limit for the amount of tasks included in the query (the maximum value is 100).
34+
/// Note: The limit value will only be taken into account if nonce is None.
3135
#[derive(Deserialize, Clone)]
32-
pub(super) struct GetProofMerklePathQueryParams {
33-
pub id: Option<String>,
36+
pub(super) struct GetReceiptsQueryParams {
37+
pub address: String,
38+
pub nonce: Option<i64>,
39+
pub limit: Option<i64>,
3440
}
3541

3642
#[derive(Debug, MultipartForm)]
@@ -48,3 +54,11 @@ pub(super) struct SubmitProofRequestRisc0 {
4854
pub _program_image_id_hex: Text<String>,
4955
pub _signature_hex: Text<String>,
5056
}
57+
58+
#[derive(Debug, Clone, sqlx::FromRow, sqlx::Type, serde::Serialize)]
59+
pub struct GetReceiptsResponse {
60+
pub status: TaskStatus,
61+
pub merkle_path: Vec<String>,
62+
pub nonce: i64,
63+
pub address: String,
64+
}

aggregation_mode/db/migrations/001_init.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ CREATE TABLE tasks (
77
proof BYTEA,
88
program_commitment BYTEA,
99
merkle_path BYTEA,
10-
status task_status DEFAULT 'pending'
10+
status task_status DEFAULT 'pending',
11+
nonce BIGINT NOT NULL
1112
);
1213

1314
CREATE TABLE payment_events (

contracts/scripts/anvil/state/alignedlayer-deployed-anvil-state.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

contracts/scripts/anvil/state/eigenlayer-deployed-anvil-state.json

Lines changed: 17 additions & 17 deletions
Large diffs are not rendered by default.

contracts/scripts/anvil/state/risc0-deployed-anvil-state.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

contracts/scripts/anvil/state/sp1-deployed-anvil-state.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)