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,139 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
use crate::{
client::{HttpClient, RetryConfig, RetryHelper},
error::{Error, Result},
proof::{
parsing::ProofResponseParser,
types::{GetProofsRequest, GetProofsResponse, Proof},
},
};
use std::time::Duration;
use tokio::sync::watch;
use url::Url;
use zksync_basic_types::{tee_types::TeeType, L1BatchNumber};
/// Handles fetching proofs from the server with retry logic
pub struct ProofFetcher {
http_client: HttpClient,
rpc_url: Url,
retry_config: RetryConfig,
}
impl ProofFetcher {
/// Create a new proof fetcher
pub fn new(http_client: HttpClient, rpc_url: Url, retry_config: RetryConfig) -> Self {
Self {
http_client,
rpc_url,
retry_config,
}
}
/// Get proofs for a batch number with retry logic
pub async fn get_proofs(
&self,
stop_receiver: &mut watch::Receiver<bool>,
batch_number: L1BatchNumber,
tee_type: &TeeType,
) -> Result<Vec<Proof>> {
let mut proofs_request = GetProofsRequest::new(batch_number, tee_type);
let mut backoff = Duration::from_secs(1);
let max_backoff = Duration::from_secs(128);
let retry_backoff_multiplier: f32 = 2.0;
while !*stop_receiver.borrow() {
match self.send_request(&proofs_request, stop_receiver).await {
Ok(response) => {
// Parse the response using the ProofResponseParser
match ProofResponseParser::parse_response(response) {
Ok(proofs) => {
// Filter valid proofs
let valid_proofs = ProofResponseParser::filter_valid_proofs(&proofs);
if !valid_proofs.is_empty() {
return Ok(valid_proofs);
}
// No valid proofs found, retry
let error_msg = format!(
"No valid TEE proofs found for batch #{}. They may not be ready yet. Retrying in {} milliseconds.",
batch_number.0,
backoff.as_millis()
);
tracing::warn!(batch_no = batch_number.0, "{}", error_msg);
// Here we could use the ProofFetching error if we needed to return immediately
// return Err(Error::ProofFetching(error_msg));
}
Err(e) => {
// Handle specific error for Sgx variant
if let Error::JsonRpc(msg) = &e {
if msg.contains("RPC requires 'Sgx' variant") {
tracing::debug!("Switching to 'Sgx' variant for RPC");
proofs_request.params.1 = "Sgx".to_string();
continue;
}
}
return Err(e);
}
}
}
Err(e) => {
return Err(e);
}
}
tokio::time::timeout(backoff, stop_receiver.changed())
.await
.ok();
backoff = std::cmp::min(
Duration::from_millis(
(backoff.as_millis() as f32 * retry_backoff_multiplier) as u64,
),
max_backoff,
);
if *stop_receiver.borrow() {
break;
}
}
// If we've reached this point, we've either been stopped or exhausted retries
if *stop_receiver.borrow() {
// Return empty vector if stopped
Ok(vec![])
} else {
// Use the ProofFetching error variant if we've exhausted retries
Err(Error::proof_fetch(batch_number, "exhausted retries"))
}
}
/// Send a request to the server with retry logic
async fn send_request(
&self,
request: &GetProofsRequest,
stop_receiver: &mut watch::Receiver<bool>,
) -> Result<GetProofsResponse> {
let retry_helper = RetryHelper::new(self.retry_config.clone());
let request_clone = request.clone();
let http_client = self.http_client.clone();
let rpc_url = self.rpc_url.clone();
retry_helper
.execute(&format!("get_proofs_{}", request.params.0), || async {
let result = http_client
.send_json::<_, GetProofsResponse>(&rpc_url, &request_clone)
.await;
// Check if we need to abort due to stop signal
if *stop_receiver.borrow() {
return Err(Error::Interrupted);
}
result
})
.await
}
}

View file

@ -0,0 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
mod fetcher;
mod parsing;
mod types;
pub use fetcher::ProofFetcher;
pub use types::Proof;

View file

@ -0,0 +1,277 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
use super::types::{GetProofsResponse, Proof};
use crate::error;
/// Handles parsing of proof responses and error handling
pub struct ProofResponseParser;
impl ProofResponseParser {
/// Parse a response and extract the proofs
pub fn parse_response(response: GetProofsResponse) -> error::Result<Vec<Proof>> {
// Handle JSON-RPC errors
if let Some(error) = response.error {
// Special case for handling the old RPC interface
if let Some(data) = error.data() {
if data.get().contains("unknown variant `sgx`, expected `Sgx`") {
return Err(error::Error::JsonRpc(
"RPC requires 'Sgx' variant instead of 'sgx'".to_string(),
));
}
}
return Err(error::Error::JsonRpc(format!("JSONRPC error: {:?}", error)));
}
// Extract proofs from the result
Ok(response.result.unwrap_or_default())
}
/// Filter proofs to find valid ones
pub fn filter_valid_proofs(proofs: &[Proof]) -> Vec<Proof> {
proofs
.iter()
.filter(|proof| !proof.is_failed_or_picked())
.cloned()
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use jsonrpsee_types::error::ErrorObject;
#[test]
fn test_proof_is_permanently_ignored() {
let proof = Proof {
l1_batch_number: 123,
tee_type: "TDX".to_string(),
pubkey: None,
signature: None,
proof: None,
proved_at: "2023-01-01T00:00:00Z".to_string(),
status: Some("permanently_ignored".to_string()),
attestation: None,
};
assert!(proof.is_permanently_ignored());
let proof = Proof {
l1_batch_number: 123,
tee_type: "TDX".to_string(),
pubkey: None,
signature: None,
proof: None,
proved_at: "2023-01-01T00:00:00Z".to_string(),
status: Some("PERMANENTLY_IGNORED".to_string()),
attestation: None,
};
assert!(proof.is_permanently_ignored());
let proof = Proof {
l1_batch_number: 123,
tee_type: "TDX".to_string(),
pubkey: None,
signature: None,
proof: None,
proved_at: "2023-01-01T00:00:00Z".to_string(),
status: Some("other".to_string()),
attestation: None,
};
assert!(!proof.is_permanently_ignored());
let proof = Proof {
l1_batch_number: 123,
tee_type: "TDX".to_string(),
pubkey: None,
signature: None,
proof: None,
proved_at: "2023-01-01T00:00:00Z".to_string(),
status: None,
attestation: None,
};
assert!(!proof.is_permanently_ignored());
}
#[test]
fn test_proof_is_failed_or_picked() {
let proof = Proof {
l1_batch_number: 123,
tee_type: "TDX".to_string(),
pubkey: None,
signature: None,
proof: None,
proved_at: "2023-01-01T00:00:00Z".to_string(),
status: Some("failed".to_string()),
attestation: None,
};
assert!(proof.is_failed_or_picked());
let proof = Proof {
l1_batch_number: 123,
tee_type: "TDX".to_string(),
pubkey: None,
signature: None,
proof: None,
proved_at: "2023-01-01T00:00:00Z".to_string(),
status: Some("picked_by_prover".to_string()),
attestation: None,
};
assert!(proof.is_failed_or_picked());
let proof = Proof {
l1_batch_number: 123,
tee_type: "TDX".to_string(),
pubkey: None,
signature: None,
proof: None,
proved_at: "2023-01-01T00:00:00Z".to_string(),
status: Some("FAILED".to_string()),
attestation: None,
};
assert!(proof.is_failed_or_picked());
let proof = Proof {
l1_batch_number: 123,
tee_type: "TDX".to_string(),
pubkey: None,
signature: None,
proof: None,
proved_at: "2023-01-01T00:00:00Z".to_string(),
status: Some("other".to_string()),
attestation: None,
};
assert!(!proof.is_failed_or_picked());
let proof = Proof {
l1_batch_number: 123,
tee_type: "TDX".to_string(),
pubkey: None,
signature: None,
proof: None,
proved_at: "2023-01-01T00:00:00Z".to_string(),
status: None,
attestation: None,
};
assert!(!proof.is_failed_or_picked());
}
#[test]
fn test_parse_response_success() {
let response = GetProofsResponse {
jsonrpc: "2.0".to_string(),
result: Some(vec![Proof {
l1_batch_number: 123,
tee_type: "TDX".to_string(),
pubkey: None,
signature: None,
proof: None,
proved_at: "2023-01-01T00:00:00Z".to_string(),
status: None,
attestation: None,
}]),
id: 1,
error: None,
};
let proofs = ProofResponseParser::parse_response(response).unwrap();
assert_eq!(proofs.len(), 1);
assert_eq!(proofs[0].l1_batch_number, 123);
}
#[test]
fn test_parse_response_error() {
let response = GetProofsResponse {
jsonrpc: "2.0".to_string(),
result: None,
id: 1,
error: Some(ErrorObject::owned(1, "Error", None::<()>)),
};
let error = ProofResponseParser::parse_response(response).unwrap_err();
match error {
error::Error::JsonRpc(msg) => {
assert!(msg.contains("JSONRPC error"));
}
_ => panic!("Expected JsonRpc error"),
}
}
#[test]
fn test_parse_response_sgx_variant_error() {
let error_obj = ErrorObject::owned(
1,
"Error",
Some(
serde_json::to_value("unknown variant `sgx`, expected `Sgx`")
.unwrap()
.to_string(),
),
);
let response = GetProofsResponse {
jsonrpc: "2.0".to_string(),
result: None,
id: 1,
error: Some(error_obj),
};
let error = ProofResponseParser::parse_response(response).unwrap_err();
match error {
error::Error::JsonRpc(msg) => {
assert!(msg.contains("RPC requires 'Sgx' variant"));
}
_ => panic!("Expected JsonRpc error about Sgx variant"),
}
}
#[test]
fn test_filter_valid_proofs() {
let proofs = vec![
Proof {
l1_batch_number: 123,
tee_type: "TDX".to_string(),
pubkey: None,
signature: None,
proof: None,
proved_at: "2023-01-01T00:00:00Z".to_string(),
status: None,
attestation: None,
},
Proof {
l1_batch_number: 124,
tee_type: "TDX".to_string(),
pubkey: None,
signature: None,
proof: None,
proved_at: "2023-01-01T00:00:00Z".to_string(),
status: Some("failed".to_string()),
attestation: None,
},
Proof {
l1_batch_number: 125,
tee_type: "TDX".to_string(),
pubkey: None,
signature: None,
proof: None,
proved_at: "2023-01-01T00:00:00Z".to_string(),
status: Some("picked_by_prover".to_string()),
attestation: None,
},
];
let valid_proofs = ProofResponseParser::filter_valid_proofs(&proofs);
assert_eq!(valid_proofs.len(), 1);
assert_eq!(valid_proofs[0].l1_batch_number, 123);
}
}

View file

@ -0,0 +1,83 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2025 Matter Labs
use jsonrpsee_types::error::ErrorObject;
use serde::{Deserialize, Serialize};
use serde_with::{hex::Hex, serde_as};
use zksync_basic_types::{tee_types::TeeType, L1BatchNumber};
/// Request structure for fetching proofs
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GetProofsRequest {
pub jsonrpc: String,
pub id: u32,
pub method: String,
pub params: (L1BatchNumber, String),
}
impl GetProofsRequest {
/// Create a new request for the given batch number
pub fn new(batch_number: L1BatchNumber, tee_type: &TeeType) -> Self {
GetProofsRequest {
jsonrpc: "2.0".to_string(),
id: 1,
method: "unstable_getTeeProofs".to_string(),
params: (batch_number, tee_type.to_string()),
}
}
}
/// Response structure for proof requests
#[derive(Debug, Serialize, Deserialize)]
pub struct GetProofsResponse {
pub jsonrpc: String,
pub result: Option<Vec<Proof>>,
pub id: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ErrorObject<'static>>,
}
/// Proof structure containing attestation and signature data
#[serde_as]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Proof {
pub l1_batch_number: u32,
pub tee_type: String,
#[serde_as(as = "Option<Hex>")]
pub pubkey: Option<Vec<u8>>,
#[serde_as(as = "Option<Hex>")]
pub signature: Option<Vec<u8>>,
#[serde_as(as = "Option<Hex>")]
pub proof: Option<Vec<u8>>,
pub proved_at: String,
pub status: Option<String>,
#[serde_as(as = "Option<Hex>")]
pub attestation: Option<Vec<u8>>,
}
impl Proof {
/// Check if the proof is marked as permanently ignored
pub fn is_permanently_ignored(&self) -> bool {
self.status
.as_ref()
.map_or(false, |s| s.eq_ignore_ascii_case("permanently_ignored"))
}
/// Check if the proof is failed or picked by a prover
pub fn is_failed_or_picked(&self) -> bool {
self.status.as_ref().map_or(false, |s| {
s.eq_ignore_ascii_case("failed") || s.eq_ignore_ascii_case("picked_by_prover")
})
}
/// Get the attestation bytes or an empty vector if not present
pub fn attestation_bytes(&self) -> Vec<u8> {
self.attestation.clone().unwrap_or_default()
}
/// Get the signature bytes or an empty vector if not present
pub fn signature_bytes(&self) -> Vec<u8> {
self.signature.clone().unwrap_or_default()
}
}