refactor(verify-era-proof-attestation): modularize and restructure proof verification logic

- Split `verify-era-proof-attestation` into modular subcomponents for maintainability.
- Moved client, proof handling, and core types into dedicated modules.
This commit is contained in:
Harald Hoyer 2025-04-02 16:03:01 +02:00
parent 1e853f653a
commit 2605e2ae3a
Signed by: harald
GPG key ID: F519A1143B3FBE32
34 changed files with 2918 additions and 2304 deletions

View file

@ -0,0 +1,66 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
//! HTTP client for making requests to external services
use reqwest::Client;
use serde::{de::DeserializeOwned, Serialize};
use std::time::Duration;
use url::Url;
use crate::{
core::DEFAULT_HTTP_REQUEST_TIMEOUT,
error::{Error, Result},
};
/// Client for making HTTP requests
#[derive(Clone)]
pub struct HttpClient {
client: Client,
}
impl HttpClient {
/// Create a new HTTP client with default configuration
pub fn new() -> Self {
let client = Client::builder()
.timeout(Duration::from_secs(DEFAULT_HTTP_REQUEST_TIMEOUT))
.build()
.expect("Failed to create HTTP client");
Self { client }
}
/// Make a POST request to the specified URL with the provided body
pub async fn post<T: Serialize>(&self, url: &Url, body: T) -> Result<String> {
let response = self.client.post(url.clone()).json(&body).send().await?;
self.handle_response(response).await
}
/// Send a JSON request and parse the response
pub async fn send_json<T: Serialize, R: DeserializeOwned>(
&self,
url: &Url,
body: T,
) -> Result<R> {
let response_text = self.post(url, body).await?;
let response: R = serde_json::from_str(&response_text)
.map_err(|e| Error::JsonRpcInvalidResponse(e.to_string()))?;
Ok(response)
}
/// Handle the HTTP response
async fn handle_response(&self, response: reqwest::Response) -> Result<String> {
let status = response.status();
let body = response.text().await?;
if status.is_success() {
Ok(body)
} else {
Err(Error::Http {
status_code: status.as_u16(),
message: body,
})
}
}
}

View file

@ -0,0 +1,58 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
use url::Url;
use zksync_basic_types::{L1BatchNumber, H256};
use zksync_types::L2ChainId;
use zksync_web3_decl::{
client::{Client as NodeClient, L2},
error::ClientRpcContext,
namespaces::ZksNamespaceClient,
};
use crate::error;
/// Trait for interacting with the JSON-RPC API
pub trait JsonRpcClient {
/// Get the root hash for a specific batch
async fn get_root_hash(&self, batch_number: L1BatchNumber) -> error::Result<H256>;
// TODO implement get_tee_proofs(batch_number, tee_type) once https://crates.io/crates/zksync_web3_decl crate is updated
}
/// Client for interacting with the main node
pub struct MainNodeClient(NodeClient<L2>);
impl MainNodeClient {
/// Create a new client for the main node
pub fn new(rpc_url: Url, chain_id: u64) -> error::Result<Self> {
let chain_id = L2ChainId::try_from(chain_id)
.map_err(|e| error::Error::Internal(format!("Invalid chain ID: {}", e)))?;
let node_client = NodeClient::http(rpc_url.into())
.map_err(|e| {
error::Error::Internal(format!("Failed to create JSON-RPC client: {}", e))
})?
.for_network(chain_id.into())
.build();
Ok(MainNodeClient(node_client))
}
}
impl JsonRpcClient for MainNodeClient {
async fn get_root_hash(&self, batch_number: L1BatchNumber) -> error::Result<H256> {
let batch_details = self
.0
.get_l1_batch_details(batch_number)
.rpc_context("get_l1_batch_details")
.await
.map_err(|e| error::Error::JsonRpc(format!("Failed to get batch details: {}", e)))?
.ok_or_else(|| {
error::Error::JsonRpc(format!("No details found for batch #{}", batch_number))
})?;
batch_details.base.root_hash.ok_or_else(|| {
error::Error::JsonRpc(format!("No root hash found for batch #{}", batch_number))
})
}
}

View file

@ -0,0 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
//! Client modules for external API communication
mod http;
mod json_rpc;
mod retry;
pub use http::HttpClient;
pub use json_rpc::{JsonRpcClient, MainNodeClient};
pub use retry::{RetryConfig, RetryHelper};

View file

@ -0,0 +1,107 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
//! Retry mechanism for handling transient failures
use std::time::Duration;
use tokio::time::sleep;
use crate::{
core::{DEFAULT_RETRY_DELAY_MS, MAX_PROOF_FETCH_RETRIES},
error::{Error, Result},
};
/// Configuration for retry behavior
#[derive(Debug, Clone)]
pub struct RetryConfig {
/// Maximum number of retry attempts
pub max_attempts: u32,
/// Delay between retry attempts
pub delay: Duration,
/// Whether to use exponential backoff
pub use_exponential_backoff: bool,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: MAX_PROOF_FETCH_RETRIES,
delay: Duration::from_millis(DEFAULT_RETRY_DELAY_MS),
use_exponential_backoff: true,
}
}
}
/// Helper for executing operations with retries
pub struct RetryHelper {
config: RetryConfig,
}
impl RetryHelper {
/// Create a new retry helper with the given configuration
pub fn new(config: RetryConfig) -> Self {
Self { config }
}
/// Execute an operation with retries
pub async fn execute<T, F, Fut>(&self, operation_name: &str, operation: F) -> Result<T>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
let mut attempt = 0;
let mut last_error;
loop {
attempt += 1;
tracing::debug!(
"Executing operation '{}' (attempt {}/{})",
operation_name,
attempt,
self.config.max_attempts
);
match operation().await {
Ok(result) => {
tracing::debug!(
"Operation '{}' succeeded on attempt {}",
operation_name,
attempt
);
return Ok(result);
}
Err(Error::Interrupted) => return Err(Error::Interrupted),
Err(e) => {
last_error = e;
if attempt >= self.config.max_attempts {
tracing::warn!(
"Operation '{}' failed after {} attempts. Giving up.",
operation_name,
attempt
);
break;
}
let delay = if self.config.use_exponential_backoff {
self.config.delay.mul_f32(2.0_f32.powi(attempt as i32 - 1))
} else {
self.config.delay
};
tracing::warn!(
"Operation '{}' failed on attempt {}: {}. Retrying in {:?}...",
operation_name,
attempt,
last_error,
delay
);
sleep(delay).await;
}
}
}
Err(last_error)
}
}