mirror of
https://github.com/matter-labs/teepot.git
synced 2025-07-22 15:34:48 +02:00
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:
parent
1e853f653a
commit
2605e2ae3a
34 changed files with 2918 additions and 2304 deletions
139
bin/verify-era-proof-attestation/src/proof/fetcher.rs
Normal file
139
bin/verify-era-proof-attestation/src/proof/fetcher.rs
Normal 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
|
||||
}
|
||||
}
|
9
bin/verify-era-proof-attestation/src/proof/mod.rs
Normal file
9
bin/verify-era-proof-attestation/src/proof/mod.rs
Normal 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;
|
277
bin/verify-era-proof-attestation/src/proof/parsing.rs
Normal file
277
bin/verify-era-proof-attestation/src/proof/parsing.rs
Normal 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);
|
||||
}
|
||||
}
|
83
bin/verify-era-proof-attestation/src/proof/types.rs
Normal file
83
bin/verify-era-proof-attestation/src/proof/types.rs
Normal 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()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue