Skip to content

Commit 755fc4a

Browse files
authored
refactor(external-prover-api): Polish the API implementation (#2774)
## What ❔ - Use `L1BatchProofForL1` instead of `VerifyProofRequest`. - Rework errors: do not use "branching" variants that are handled separately in `IntoResponse`; instead use one variant per possible error. - Use `thiserror` to improve ergonomics of errors. - Do not use `Multipart` directly, instead use a dedicated type that implements `FromRequest`. - Introduce `Api` structure to implement axum API (instead of procedural approach) -- aligns better with the framework design. - Better separate `Processor` and `Api` in a way that `Processor` is backend-agnostic (e.g. know nothing about `axum`). - Remove dependency on `zksync_config`. - Improve framework integration. - Other minor things. ## Why ❔ Ergonomics, maintainability, and readability.
1 parent 05c940e commit 755fc4a

File tree

7 files changed

+294
-290
lines changed

7 files changed

+294
-290
lines changed

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/node/external_proof_integration_api/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ categories.workspace = true
1212

1313
[dependencies]
1414
axum = { workspace = true, features = ["multipart"] }
15+
async-trait.workspace = true
1516
tracing.workspace = true
17+
thiserror.workspace = true
1618
zksync_prover_interface.workspace = true
1719
zksync_basic_types.workspace = true
18-
zksync_config.workspace = true
1920
zksync_object_store.workspace = true
2021
zksync_dal.workspace = true
2122
tokio.workspace = true

core/node/external_proof_integration_api/src/error.rs

Lines changed: 55 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -6,81 +6,74 @@ use zksync_basic_types::L1BatchNumber;
66
use zksync_dal::DalError;
77
use zksync_object_store::ObjectStoreError;
88

9+
#[derive(Debug, thiserror::Error)]
910
pub(crate) enum ProcessorError {
10-
ObjectStore(ObjectStoreError),
11-
Dal(DalError),
12-
Serialization(bincode::Error),
11+
#[error("Failed to deserialize proof data")]
12+
Serialization(#[from] bincode::Error),
13+
#[error("Invalid proof submitted")]
1314
InvalidProof,
15+
#[error("Batch {0} is not yet ready for proving. Most likely our proof for this batch is not generated yet, try again later")]
1416
BatchNotReady(L1BatchNumber),
17+
#[error("Invalid file: {0}")]
18+
InvalidFile(#[from] FileError),
19+
#[error("Internal error")]
20+
Internal,
21+
#[error("Proof verification not possible anymore, batch is too old")]
22+
ProofIsGone,
1523
}
1624

17-
impl From<ObjectStoreError> for ProcessorError {
18-
fn from(err: ObjectStoreError) -> Self {
19-
Self::ObjectStore(err)
25+
impl ProcessorError {
26+
fn status_code(&self) -> StatusCode {
27+
match self {
28+
Self::Internal => StatusCode::INTERNAL_SERVER_ERROR,
29+
Self::Serialization(_) => StatusCode::BAD_REQUEST,
30+
Self::InvalidProof => StatusCode::BAD_REQUEST,
31+
Self::InvalidFile(_) => StatusCode::BAD_REQUEST,
32+
Self::BatchNotReady(_) => StatusCode::NOT_FOUND,
33+
Self::ProofIsGone => StatusCode::GONE,
34+
}
2035
}
2136
}
2237

23-
impl From<DalError> for ProcessorError {
24-
fn from(err: DalError) -> Self {
25-
Self::Dal(err)
38+
impl IntoResponse for ProcessorError {
39+
fn into_response(self) -> Response {
40+
(self.status_code(), self.to_string()).into_response()
2641
}
2742
}
2843

29-
impl From<bincode::Error> for ProcessorError {
30-
fn from(err: bincode::Error) -> Self {
31-
Self::Serialization(err)
44+
impl From<ObjectStoreError> for ProcessorError {
45+
fn from(err: ObjectStoreError) -> Self {
46+
match err {
47+
ObjectStoreError::KeyNotFound(_) => {
48+
tracing::debug!("Too old proof was requested: {:?}", err);
49+
Self::ProofIsGone
50+
}
51+
_ => {
52+
tracing::warn!("GCS error: {:?}", err);
53+
Self::Internal
54+
}
55+
}
3256
}
3357
}
3458

35-
impl IntoResponse for ProcessorError {
36-
fn into_response(self) -> Response {
37-
let (status_code, message) = match self {
38-
ProcessorError::ObjectStore(err) => {
39-
tracing::error!("GCS error: {:?}", err);
40-
match err {
41-
ObjectStoreError::KeyNotFound(_) => (
42-
StatusCode::NOT_FOUND,
43-
"Proof verification not possible anymore, batch is too old.".to_owned(),
44-
),
45-
_ => (
46-
StatusCode::INTERNAL_SERVER_ERROR,
47-
"Failed fetching from GCS".to_owned(),
48-
),
49-
}
50-
}
51-
ProcessorError::Dal(err) => {
52-
tracing::error!("Sqlx error: {:?}", err);
53-
match err.inner() {
54-
zksync_dal::SqlxError::RowNotFound => {
55-
(StatusCode::NOT_FOUND, "Non existing L1 batch".to_owned())
56-
}
57-
_ => (
58-
StatusCode::INTERNAL_SERVER_ERROR,
59-
"Failed fetching/saving from db".to_owned(),
60-
),
61-
}
62-
}
63-
ProcessorError::Serialization(err) => {
64-
tracing::error!("Serialization error: {:?}", err);
65-
(
66-
StatusCode::BAD_REQUEST,
67-
"Failed to deserialize proof data".to_owned(),
68-
)
69-
}
70-
ProcessorError::BatchNotReady(l1_batch_number) => {
71-
tracing::error!(
72-
"Batch {l1_batch_number:?} is not yet ready for proving. Most likely our proof for this batch is not generated yet"
73-
);
74-
(
75-
StatusCode::INTERNAL_SERVER_ERROR,
76-
format!("Batch {l1_batch_number:?} is not yet ready for proving. Most likely our proof for this batch is not generated yet, try again later"),
77-
)
78-
}
79-
ProcessorError::InvalidProof => {
80-
tracing::error!("Invalid proof data");
81-
(StatusCode::BAD_REQUEST, "Invalid proof data".to_owned())
82-
}
83-
};
84-
(status_code, message).into_response()
59+
impl From<DalError> for ProcessorError {
60+
fn from(_err: DalError) -> Self {
61+
// We don't want to check if the error is `RowNotFound`: we check that batch exists before
62+
// processing a request, so it's handled separately.
63+
// Thus, any unhandled error from DAL is an internal error.
64+
Self::Internal
8565
}
8666
}
67+
68+
#[derive(Debug, thiserror::Error)]
69+
pub(crate) enum FileError {
70+
#[error("Multipart error: {0}")]
71+
MultipartRejection(#[from] axum::extract::multipart::MultipartRejection),
72+
#[error("Multipart error: {0}")]
73+
Multipart(#[from] axum::extract::multipart::MultipartError),
74+
#[error("File not found in request. It was expected to be in the field {field_name} with the content type {content_type}")]
75+
FileNotFound {
76+
field_name: &'static str,
77+
content_type: &'static str,
78+
},
79+
}

core/node/external_proof_integration_api/src/lib.rs

Lines changed: 84 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,81 @@ mod error;
22
mod metrics;
33
mod middleware;
44
mod processor;
5+
mod types;
56

6-
use std::{net::SocketAddr, sync::Arc};
7+
pub use crate::processor::Processor;
8+
9+
use std::net::SocketAddr;
710

811
use anyhow::Context;
912
use axum::{
10-
extract::{Multipart, Path, Request},
13+
extract::{Path, Request, State},
1114
middleware::Next,
1215
routing::{get, post},
1316
Router,
1417
};
18+
use error::ProcessorError;
1519
use tokio::sync::watch;
16-
use zksync_basic_types::commitment::L1BatchCommitmentMode;
17-
use zksync_config::configs::external_proof_integration_api::ExternalProofIntegrationApiConfig;
18-
use zksync_dal::{ConnectionPool, Core};
19-
use zksync_object_store::ObjectStore;
20+
use types::{ExternalProof, ProofGenerationDataResponse};
21+
use zksync_basic_types::L1BatchNumber;
2022

2123
use crate::{
2224
metrics::{CallOutcome, Method},
2325
middleware::MetricsMiddleware,
24-
processor::Processor,
2526
};
2627

27-
pub async fn run_server(
28-
config: ExternalProofIntegrationApiConfig,
29-
blob_store: Arc<dyn ObjectStore>,
30-
connection_pool: ConnectionPool<Core>,
31-
commitment_mode: L1BatchCommitmentMode,
32-
mut stop_receiver: watch::Receiver<bool>,
33-
) -> anyhow::Result<()> {
34-
let bind_address = SocketAddr::from(([0, 0, 0, 0], config.http_port));
35-
tracing::info!("Starting external prover API server on {bind_address}");
36-
let app = create_router(blob_store, connection_pool, commitment_mode).await;
28+
/// External API implementation.
29+
#[derive(Debug)]
30+
pub struct Api {
31+
router: Router,
32+
port: u16,
33+
}
3734

38-
let listener = tokio::net::TcpListener::bind(bind_address)
39-
.await
40-
.with_context(|| format!("Failed binding external prover API server to {bind_address}"))?;
41-
axum::serve(listener, app)
35+
impl Api {
36+
pub fn new(processor: Processor, port: u16) -> Self {
37+
let middleware_factory = |method: Method| {
38+
axum::middleware::from_fn(move |req: Request, next: Next| async move {
39+
let middleware = MetricsMiddleware::new(method);
40+
let response = next.run(req).await;
41+
let outcome = match response.status().is_success() {
42+
true => CallOutcome::Success,
43+
false => CallOutcome::Failure,
44+
};
45+
middleware.observe(outcome);
46+
response
47+
})
48+
};
49+
50+
let router = Router::new()
51+
.route(
52+
"/proof_generation_data",
53+
get(Api::latest_generation_data)
54+
.layer(middleware_factory(Method::GetLatestProofGenerationData)),
55+
)
56+
.route(
57+
"/proof_generation_data/:l1_batch_number",
58+
get(Api::generation_data_for_existing_batch)
59+
.layer(middleware_factory(Method::GetSpecificProofGenerationData)),
60+
)
61+
.route(
62+
"/verify_proof/:l1_batch_number",
63+
post(Api::verify_proof).layer(middleware_factory(Method::VerifyProof)),
64+
)
65+
.with_state(processor);
66+
67+
Self { router, port }
68+
}
69+
70+
pub async fn run(self, mut stop_receiver: watch::Receiver<bool>) -> anyhow::Result<()> {
71+
let bind_address = SocketAddr::from(([0, 0, 0, 0], self.port));
72+
tracing::info!("Starting external prover API server on {bind_address}");
73+
74+
let listener = tokio::net::TcpListener::bind(bind_address)
75+
.await
76+
.with_context(|| {
77+
format!("Failed binding external prover API server to {bind_address}")
78+
})?;
79+
axum::serve(listener, self.router)
4280
.with_graceful_shutdown(async move {
4381
if stop_receiver.changed().await.is_err() {
4482
tracing::warn!("Stop signal sender for external prover API server was dropped without sending a signal");
@@ -47,57 +85,32 @@ pub async fn run_server(
4785
})
4886
.await
4987
.context("External prover API server failed")?;
50-
tracing::info!("External prover API server shut down");
51-
Ok(())
52-
}
88+
tracing::info!("External prover API server shut down");
89+
Ok(())
90+
}
5391

54-
async fn create_router(
55-
blob_store: Arc<dyn ObjectStore>,
56-
connection_pool: ConnectionPool<Core>,
57-
commitment_mode: L1BatchCommitmentMode,
58-
) -> Router {
59-
let mut processor =
60-
Processor::new(blob_store.clone(), connection_pool.clone(), commitment_mode);
61-
let verify_proof_processor = processor.clone();
62-
let specific_proof_processor = processor.clone();
92+
async fn latest_generation_data(
93+
State(processor): State<Processor>,
94+
) -> Result<ProofGenerationDataResponse, ProcessorError> {
95+
processor.get_proof_generation_data().await
96+
}
6397

64-
let middleware_factory = |method: Method| {
65-
axum::middleware::from_fn(move |req: Request, next: Next| async move {
66-
let middleware = MetricsMiddleware::new(method);
67-
let response = next.run(req).await;
68-
let outcome = match response.status().is_success() {
69-
true => CallOutcome::Success,
70-
false => CallOutcome::Failure,
71-
};
72-
middleware.observe(outcome);
73-
response
74-
})
75-
};
98+
async fn generation_data_for_existing_batch(
99+
State(processor): State<Processor>,
100+
Path(l1_batch_number): Path<u32>,
101+
) -> Result<ProofGenerationDataResponse, ProcessorError> {
102+
processor
103+
.proof_generation_data_for_existing_batch(L1BatchNumber(l1_batch_number))
104+
.await
105+
}
76106

77-
Router::new()
78-
.route(
79-
"/proof_generation_data",
80-
get(move || async move { processor.get_proof_generation_data().await })
81-
.layer(middleware_factory(Method::GetLatestProofGenerationData)),
82-
)
83-
.route(
84-
"/proof_generation_data/:l1_batch_number",
85-
get(move |l1_batch_number: Path<u32>| async move {
86-
specific_proof_processor
87-
.proof_generation_data_for_existing_batch(l1_batch_number)
88-
.await
89-
})
90-
.layer(middleware_factory(Method::GetSpecificProofGenerationData)),
91-
)
92-
.route(
93-
"/verify_proof/:l1_batch_number",
94-
post(
95-
move |l1_batch_number: Path<u32>, multipart: Multipart| async move {
96-
verify_proof_processor
97-
.verify_proof(l1_batch_number, multipart)
98-
.await
99-
},
100-
)
101-
.layer(middleware_factory(Method::VerifyProof)),
102-
)
107+
async fn verify_proof(
108+
State(processor): State<Processor>,
109+
Path(l1_batch_number): Path<u32>,
110+
proof: ExternalProof,
111+
) -> Result<(), ProcessorError> {
112+
processor
113+
.verify_proof(L1BatchNumber(l1_batch_number), proof)
114+
.await
115+
}
103116
}

0 commit comments

Comments
 (0)