diff --git a/aggregation_mode/Cargo.lock b/aggregation_mode/Cargo.lock index 6e424297a..97fc2fe4e 100644 --- a/aggregation_mode/Cargo.lock +++ b/aggregation_mode/Cargo.lock @@ -2017,17 +2017,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "backon" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" -dependencies = [ - "fastrand", - "gloo-timers 0.3.0", - "tokio", -] - [[package]] name = "backtrace" version = "0.3.76" @@ -4448,7 +4437,7 @@ version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" dependencies = [ - "gloo-timers 0.2.6", + "gloo-timers", "send_wrapper 0.4.0", ] @@ -4634,18 +4623,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "group" version = "0.12.1" @@ -7156,7 +7133,6 @@ version = "0.1.0" dependencies = [ "aligned-sdk", "alloy", - "backon", "bincode", "c-kzg", "ciborium", diff --git a/aggregation_mode/proof_aggregator/Cargo.toml b/aggregation_mode/proof_aggregator/Cargo.toml index 2c367a144..294973f80 100644 --- a/aggregation_mode/proof_aggregator/Cargo.toml +++ b/aggregation_mode/proof_aggregator/Cargo.toml @@ -21,7 +21,6 @@ reqwest = { version = "0.12" } ciborium = "=0.2.2" lambdaworks-crypto = { git = "https://github.com/lambdaclass/lambdaworks.git", rev = "5f8f2cfcc8a1a22f77e8dff2d581f1166eefb80b", features = ["serde"]} rayon = "1.10.0" -backon = "1.2.0" sqlx = { version = "0.8", features = [ "runtime-tokio", "postgres", "uuid", "bigdecimal" ] } # zkvms diff --git a/aggregation_mode/proof_aggregator/src/backend/eth.rs b/aggregation_mode/proof_aggregator/src/backend/eth.rs new file mode 100644 index 000000000..097863764 --- /dev/null +++ b/aggregation_mode/proof_aggregator/src/backend/eth.rs @@ -0,0 +1,144 @@ +use alloy::primitives::{utils::parse_ether, U256}; +use std::time::Duration; + +// We assume a fixed gas cost of 300,000 for each of the 2 transactions +const ON_CHAIN_COST_IN_GAS_UNITS: u64 = 600_000u64; + +/// Decides whether to send the aggregated proof to be verified on-chain based on +/// time elapsed since last submission and monthly ETH budget. +/// We make a linear function with the eth to spend this month and the time elapsed since last submission. +/// If eth to spend / elapsed time is over the linear function, we skip the submission. +pub fn should_send_proof_to_verify_on_chain( + time_elapsed: Duration, + monthly_eth_budget: f64, + network_gas_price: U256, +) -> bool { + let on_chain_cost_in_gas: U256 = U256::from(ON_CHAIN_COST_IN_GAS_UNITS); + let max_to_spend_wei = max_to_spend_in_wei(time_elapsed, monthly_eth_budget); + + let expected_cost_in_wei = network_gas_price * on_chain_cost_in_gas; + + expected_cost_in_wei <= max_to_spend_wei +} + +fn max_to_spend_in_wei(time_elapsed: Duration, monthly_eth_budget: f64) -> U256 { + const SECONDS_PER_MONTH: u64 = 30 * 24 * 60 * 60; + + // Note: this expect is safe because in case it was invalid, should have been caught at startup + let monthly_budget_in_wei = parse_ether(&monthly_eth_budget.to_string()) + .expect("The monthly budget should be a non-negative value"); + + let elapsed_seconds = U256::from(time_elapsed.as_secs()); + + let budget_available_per_second_in_wei = monthly_budget_in_wei / U256::from(SECONDS_PER_MONTH); + + budget_available_per_second_in_wei * elapsed_seconds +} + +#[cfg(test)] +mod tests { + use super::should_send_proof_to_verify_on_chain; + use alloy::primitives::U256; + use std::time::Duration; + + #[test] + fn test_should_send_proof_to_verify_on_chain_updated_cases() { + // The should_send_proof_to_verify_on_chain function returns true when: + // gas_price * 600_000 <= (seconds_elapsed) * (monthly_eth_budget / (30 * 24 * 60 * 60)) + + const BUDGET_PER_MONTH_IN_ETH: f64 = 0.15; + const ONE_DAY_SECONDS: u64 = 24 * 60 * 60; + let gas_price = U256::from(1_000_000_000u64); // 1 Gwei + + // Case 1: Base case -> should return true + // Monthly Budget: 0.15 ETH -> 0.005 ETH per day -> 0.000000058 ETH per hour + // Elapsed Time: 24 hours + // Gas Price: 1 Gwei + // Max to spend: 0.000000058 ETH/hour * 24 hours = 0.005 ETH + // Expected cost: 600,000 * 1 Gwei = 0.0006 ETH + // Expected cost < Max to spend, so we can send the proof + assert!(should_send_proof_to_verify_on_chain( + Duration::from_secs(ONE_DAY_SECONDS), // 24 hours + BUDGET_PER_MONTH_IN_ETH, // 0.15 ETH monthly budget + gas_price, // 1 Gwei gas price + )); + + // Case 2: Slightly Increased Gas Price -> should return false + // Monthly Budget: 0.15 ETH -> 0.005 ETH per day -> 0.000000058 ETH per hour + // Elapsed Time: 24 hours + // Gas Price: 8 Gwei + // Max to spend: 0.000000058 ETH/hour * 24 hours = 0.005 ETH + // Expected cost: 600,000 * 8 Gwei = 0.0048 ETH + // Expected cost < Max to spend, so we can send the proof + assert!(should_send_proof_to_verify_on_chain( + Duration::from_secs(ONE_DAY_SECONDS), // 24 hours + BUDGET_PER_MONTH_IN_ETH, // 0.15 ETH monthly budget + U256::from(8_000_000_000u64), // 8 Gwei gas price + )); + + // Case 3: Increased Gas Price -> should return false + // Monthly Budget: 0.15 ETH -> 0.005 ETH per day -> 0.000000058 ETH per hour + // Elapsed Time: 24 hours + // Gas Price: 10 Gwei + // Max to spend: 0.000000058 ETH/hour * 24 hours = 0.005 ETH + // Expected cost: 600,000 * 10 Gwei = 0.006 ETH + // Expected cost > Max to spend, so we cannot send the proof + assert!(!should_send_proof_to_verify_on_chain( + Duration::from_secs(ONE_DAY_SECONDS), // 24 hours + BUDGET_PER_MONTH_IN_ETH, // 0.15 ETH monthly budget + U256::from(10_000_000_000u64), // 10 Gwei gas price + )); + + // Case 4: Slightly Reduced Time Elapsed -> should return true + // Monthly Budget: 0.15 ETH -> 0.005 ETH per day -> 0.000000058 ETH per hour + // Elapsed Time: 2 hours + // Gas Price: 1 Gwei + // Max to spend: 0.000000058 ETH/hour * 3 hours = 0.000625 ETH + // Expected cost: 600,000 * 1 Gwei = 0.0006 ETH + // Expected cost < Max to spend, so we can send the proof + assert!(should_send_proof_to_verify_on_chain( + Duration::from_secs(3 * 3600), // 3 hours + BUDGET_PER_MONTH_IN_ETH, // 0.15 ETH monthly budget + gas_price, // 1 Gwei gas price + )); + + // Case 5: Reduced Time Elapsed -> should return false + // Monthly Budget: 0.15 ETH -> 0.005 ETH per day -> 0.000000058 ETH per hour + // Elapsed Time: 1.2 hours + // Gas Price: 1 Gwei + // Max to spend: 0.000000058 ETH/hour * 1.2 hours = 0.00025 ETH + // Expected cost: 600,000 * 1 Gwei = 0.0006 ETH + // Expected cost > Max to spend, so we cannot send the proof + assert!(!should_send_proof_to_verify_on_chain( + Duration::from_secs_f64(1.2 * 3600.0), // 1.2 hours + BUDGET_PER_MONTH_IN_ETH, // 0.15 ETH monthly budget + gas_price, // 1 Gwei gas price + )); + + // Case 6: Slightly Reduced Monthly Budget -> should return true + // Monthly Budget: 0.1 ETH -> 0.0033 ETH per day -> 0.000000038 ETH per hour + // Elapsed Time: 24 hours + // Gas Price: 1 Gwei + // Max to spend: 0.000000038 ETH/hour * 24 hours = 0.0032832 ETH + // Expected cost: 600,000 * 1 Gwei = 0.0006 ETH + // Expected cost < Max to spend, so we can send the proof + assert!(should_send_proof_to_verify_on_chain( + Duration::from_secs(ONE_DAY_SECONDS), // 24 hours + 0.1, // 0.1 ETH monthly budget + gas_price, // 1 Gwei gas price + )); + + // Case 7: Decreased Monthly Budget -> should return false + // Monthly Budget: 0.01 ETH -> 0.00033 ETH per day -> 0.0000000038 ETH per hour + // Elapsed Time: 24 hours + // Gas Price: 1 Gwei + // Max to spend: 0.0000000038 ETH/hour * 24 hours = 0.00032832 ETH + // Expected cost: 600,000 * 1 Gwei = 0.0006 ETH + // Expected cost > Max to spend, so we cannot send the proof + assert!(!should_send_proof_to_verify_on_chain( + Duration::from_secs(ONE_DAY_SECONDS), // 24 hours + 0.01, // 0.01 ETH monthly budget + gas_price, // 1 Gwei gas price + )); + } +} diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 9dee2164c..7cfee0d83 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -1,14 +1,24 @@ pub mod config; mod db; +mod eth; pub mod fetcher; mod merkle_tree; +mod retry; mod types; use crate::{ aggregators::{AlignedProof, ProofAggregationError, ZKVMEngine}, - backend::db::{Db, DbError}, + backend::{ + db::{Db, DbError}, + retry::{retry_function, RetryError}, + types::{AlignedProofAggregationService, AlignedProofAggregationServiceContract}, + }, }; +use aligned_sdk::common::constants::{ + ETHEREUM_CALL_BACKOFF_FACTOR, ETHEREUM_CALL_MAX_RETRIES, ETHEREUM_CALL_MAX_RETRY_DELAY, + ETHEREUM_CALL_MIN_RETRY_DELAY, +}; use alloy::{ consensus::{BlobTransactionSidecar, EnvKzgSettings, EthereumTxEnvelope, TxEip4844WithSidecar}, eips::{eip4844::BYTES_PER_BLOB, eip7594::BlobTransactionSidecarEip7594, Encodable2718}, @@ -24,10 +34,10 @@ use fetcher::{ProofsFetcher, ProofsFetcherError}; use merkle_tree::compute_proofs_merkle_root; use risc0_ethereum_contracts::encode_seal; use sqlx::types::Uuid; -use std::thread::sleep; use std::{str::FromStr, time::Duration}; -use tracing::{error, info, warn}; -use types::{AlignedProofAggregationService, AlignedProofAggregationServiceContract, RPCProvider}; +use tokio::time::sleep; +use tracing::info; +use tracing::{error, warn}; #[derive(Debug)] pub enum AggregatedProofSubmissionError { @@ -50,7 +60,6 @@ pub struct ProofAggregator { proof_aggregation_service: AlignedProofAggregationServiceContract, fetcher: ProofsFetcher, config: Config, - rpc_provider: RPCProvider, sp1_chunk_aggregator_vk_hash_bytes: [u8; 32], risc0_chunk_aggregator_image_id_bytes: [u8; 32], db: Db, @@ -72,8 +81,6 @@ impl ProofAggregator { info!("Monthly budget set to {} eth", config.monthly_budget_eth); - let rpc_provider = ProviderBuilder::new().connect_http(rpc_url.clone()); - let signed_rpc_provider = ProviderBuilder::new().wallet(wallet).connect_http(rpc_url); let proof_aggregation_service = AlignedProofAggregationService::new( @@ -108,7 +115,6 @@ impl ProofAggregator { proof_aggregation_service, fetcher, config, - rpc_provider, sp1_chunk_aggregator_vk_hash_bytes, risc0_chunk_aggregator_image_id_bytes, db, @@ -175,38 +181,8 @@ impl ProofAggregator { hex::encode(blob_versioned_hash) ); - // Iterate until we can send the proof on-chain - let mut time_elapsed = Duration::from_secs(24 * 3600); - - loop { - // We add 24 hours because the proof aggregator runs once a day, so the time elapsed - // should be considered over a 24h period. - - let gas_price = self - .rpc_provider - .get_gas_price() - .await - .map_err(|e| AggregatedProofSubmissionError::GasPriceError(e.to_string()))?; - - if Self::should_send_proof_to_verify_on_chain( - time_elapsed, - self.config.monthly_budget_eth, - U256::from(gas_price), - ) { - break; - } else { - info!("Skipping sending proof to ProofAggregationService contract due to budget/time constraints."); - } - - // Sleep for 3 minutes (15 blocks) before re-evaluating - let time_to_sleep = Duration::from_secs(180); - time_elapsed += time_to_sleep; - sleep(time_to_sleep); - } - - info!("Sending proof to ProofAggregationService contract..."); let receipt = self - .send_proof_to_verify_on_chain(blob, blob_versioned_hash, aggregated_proof) + .wait_and_send_proof_on_chain_retryable(blob, blob_versioned_hash, aggregated_proof) .await?; info!( "Proof sent and verified, tx hash {:?}", @@ -237,105 +213,34 @@ impl ProofAggregator { Ok(()) } - fn max_to_spend_in_wei(time_elapsed: Duration, monthly_eth_budget: f64) -> U256 { - const SECONDS_PER_MONTH: u64 = 30 * 24 * 60 * 60; - - // Note: this expect is safe because in case it was invalid, should have been caught at startup - let monthly_budget_in_wei = parse_ether(&monthly_eth_budget.to_string()) - .expect("The monthly budget should be a non-negative value"); - - let elapsed_seconds = U256::from(time_elapsed.as_secs()); - - let budget_available_per_second_in_wei = - monthly_budget_in_wei / U256::from(SECONDS_PER_MONTH); - - budget_available_per_second_in_wei * elapsed_seconds - } - - /// Decides whether to send the aggregated proof to be verified on-chain based on - /// time elapsed since last submission and monthly ETH budget. - /// We make a linear function with the eth to spend this month and the time elapsed since last submission. - /// If eth to spend / elapsed time is over the linear function, we skip the submission. - fn should_send_proof_to_verify_on_chain( - time_elapsed: Duration, - monthly_eth_budget: f64, - network_gas_price: U256, - ) -> bool { - // We assume a fixed gas cost of 300,000 for each of the 2 transactions - const ON_CHAIN_COST_IN_GAS_UNITS: u64 = 600_000u64; - - let on_chain_cost_in_gas: U256 = U256::from(ON_CHAIN_COST_IN_GAS_UNITS); - let max_to_spend_in_wei = Self::max_to_spend_in_wei(time_elapsed, monthly_eth_budget); - - let expected_cost_in_wei = network_gas_price * on_chain_cost_in_gas; - - expected_cost_in_wei <= max_to_spend_in_wei - } - - async fn send_proof_to_verify_on_chain( + async fn wait_and_send_proof_on_chain_retryable( &self, blob: BlobTransactionSidecar, blob_versioned_hash: [u8; 32], aggregated_proof: AlignedProof, ) -> Result { - let tx_req = match aggregated_proof { - AlignedProof::SP1(proof) => self - .proof_aggregation_service - .verifyAggregationSP1( - blob_versioned_hash.into(), - proof.proof_with_pub_values.public_values.to_vec().into(), - proof.proof_with_pub_values.bytes().into(), - self.sp1_chunk_aggregator_vk_hash_bytes.into(), + retry_function( + || { + Self::wait_and_send_proof_to_verify_on_chain( + blob.clone(), + blob_versioned_hash, + &aggregated_proof, + self.proof_aggregation_service.clone(), + self.sp1_chunk_aggregator_vk_hash_bytes, + self.risc0_chunk_aggregator_image_id_bytes, + self.config.monthly_budget_eth, ) - .sidecar(blob) - .into_transaction_request(), - AlignedProof::Risc0(proof) => { - let encoded_seal = encode_seal(&proof.receipt).map_err(|e| { - AggregatedProofSubmissionError::Risc0EncodingSeal(e.to_string()) - })?; - self.proof_aggregation_service - .verifyAggregationRisc0( - blob_versioned_hash.into(), - encoded_seal.into(), - proof.receipt.journal.bytes.into(), - self.risc0_chunk_aggregator_image_id_bytes.into(), - ) - .sidecar(blob) - .into_transaction_request() - } - }; - - let provider = self.proof_aggregation_service.provider(); - let envelope = provider - .fill(tx_req) - .await - .map_err(Self::send_verify_aggregated_proof_err)? - .try_into_envelope() - .map_err(Self::send_verify_aggregated_proof_err)?; - let tx: EthereumTxEnvelope> = envelope - .try_into_pooled() - .map_err(Self::send_verify_aggregated_proof_err)? - .try_map_eip4844(|tx| { - tx.try_map_sidecar(|sidecar| sidecar.try_into_7594(EnvKzgSettings::Default.get())) - }) - .map_err(Self::send_verify_aggregated_proof_err)?; - - let encoded_tx = tx.encoded_2718(); - let pending_tx = provider - .send_raw_transaction(&encoded_tx) - .await - .map_err(Self::send_verify_aggregated_proof_err)?; - - let receipt = pending_tx - .get_receipt() - .await - .map_err(Self::send_verify_aggregated_proof_err)?; - - Ok(receipt) - } - - fn send_verify_aggregated_proof_err(err: E) -> AggregatedProofSubmissionError { - AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(err.to_string()) + }, + ETHEREUM_CALL_MIN_RETRY_DELAY, + ETHEREUM_CALL_BACKOFF_FACTOR, + ETHEREUM_CALL_MAX_RETRIES, + ETHEREUM_CALL_MAX_RETRY_DELAY, + ) + .await + .map_err(|e| { + error!("Couldn't get nonce: {:?}", e); + e.inner() + }) } /// ### Blob capacity @@ -409,110 +314,151 @@ impl ProofAggregator { Ok((blob, blob_versioned_hash)) } -} -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_should_send_proof_to_verify_on_chain_updated_cases() { - // The should_send_proof_to_verify_on_chain function returns true when: - // gas_price * 600_000 <= (seconds_elapsed) * (monthly_eth_budget / (30 * 24 * 60 * 60)) - - const BUDGET_PER_MONTH_IN_ETH: f64 = 0.15; - const ONE_DAY_SECONDS: u64 = 24 * 60 * 60; - let gas_price = U256::from(1_000_000_000u64); // 1 Gwei - - // Case 1: Base case -> should return true - // Monthly Budget: 0.15 ETH -> 0.005 ETH per day -> 0.000000058 ETH per hour - // Elapsed Time: 24 hours - // Gas Price: 1 Gwei - // Max to spend: 0.000000058 ETH/hour * 24 hours = 0.005 ETH - // Expected cost: 600,000 * 1 Gwei = 0.0006 ETH - // Expected cost < Max to spend, so we can send the proof - assert!(ProofAggregator::should_send_proof_to_verify_on_chain( - Duration::from_secs(ONE_DAY_SECONDS), // 24 hours - BUDGET_PER_MONTH_IN_ETH, // 0.15 ETH monthly budget - gas_price, // 1 Gwei gas price - )); - - // Case 2: Slightly Increased Gas Price -> should return false - // Monthly Budget: 0.15 ETH -> 0.005 ETH per day -> 0.000000058 ETH per hour - // Elapsed Time: 24 hours - // Gas Price: 8 Gwei - // Max to spend: 0.000000058 ETH/hour * 24 hours = 0.005 ETH - // Expected cost: 600,000 * 8 Gwei = 0.0048 ETH - // Expected cost < Max to spend, so we can send the proof - assert!(ProofAggregator::should_send_proof_to_verify_on_chain( - Duration::from_secs(ONE_DAY_SECONDS), // 24 hours - BUDGET_PER_MONTH_IN_ETH, // 0.15 ETH monthly budget - U256::from(8_000_000_000u64), // 8 Gwei gas price - )); - - // Case 3: Increased Gas Price -> should return false - // Monthly Budget: 0.15 ETH -> 0.005 ETH per day -> 0.000000058 ETH per hour - // Elapsed Time: 24 hours - // Gas Price: 10 Gwei - // Max to spend: 0.000000058 ETH/hour * 24 hours = 0.005 ETH - // Expected cost: 600,000 * 10 Gwei = 0.006 ETH - // Expected cost > Max to spend, so we cannot send the proof - assert!(!ProofAggregator::should_send_proof_to_verify_on_chain( - Duration::from_secs(ONE_DAY_SECONDS), // 24 hours - BUDGET_PER_MONTH_IN_ETH, // 0.15 ETH monthly budget - U256::from(10_000_000_000u64), // 10 Gwei gas price - )); - - // Case 4: Slightly Reduced Time Elapsed -> should return true - // Monthly Budget: 0.15 ETH -> 0.005 ETH per day -> 0.000000058 ETH per hour - // Elapsed Time: 2 hours - // Gas Price: 1 Gwei - // Max to spend: 0.000000058 ETH/hour * 3 hours = 0.000625 ETH - // Expected cost: 600,000 * 1 Gwei = 0.0006 ETH - // Expected cost < Max to spend, so we can send the proof - assert!(ProofAggregator::should_send_proof_to_verify_on_chain( - Duration::from_secs(3 * 3600), // 3 hours - BUDGET_PER_MONTH_IN_ETH, // 0.15 ETH monthly budget - gas_price, // 1 Gwei gas price - )); - - // Case 5: Reduced Time Elapsed -> should return false - // Monthly Budget: 0.15 ETH -> 0.005 ETH per day -> 0.000000058 ETH per hour - // Elapsed Time: 1.2 hours - // Gas Price: 1 Gwei - // Max to spend: 0.000000058 ETH/hour * 1.2 hours = 0.00025 ETH - // Expected cost: 600,000 * 1 Gwei = 0.0006 ETH - // Expected cost > Max to spend, so we cannot send the proof - assert!(!ProofAggregator::should_send_proof_to_verify_on_chain( - Duration::from_secs_f64(1.2 * 3600.0), // 1.2 hours - BUDGET_PER_MONTH_IN_ETH, // 0.15 ETH monthly budget - gas_price, // 1 Gwei gas price - )); - - // Case 6: Slightly Reduced Monthly Budget -> should return true - // Monthly Budget: 0.1 ETH -> 0.0033 ETH per day -> 0.000000038 ETH per hour - // Elapsed Time: 24 hours - // Gas Price: 1 Gwei - // Max to spend: 0.000000038 ETH/hour * 24 hours = 0.0032832 ETH - // Expected cost: 600,000 * 1 Gwei = 0.0006 ETH - // Expected cost < Max to spend, so we can send the proof - assert!(ProofAggregator::should_send_proof_to_verify_on_chain( - Duration::from_secs(ONE_DAY_SECONDS), // 24 hours - 0.1, // 0.1 ETH monthly budget - gas_price, // 1 Gwei gas price - )); - - // Case 7: Decreased Monthly Budget -> should return false - // Monthly Budget: 0.01 ETH -> 0.00033 ETH per day -> 0.0000000038 ETH per hour - // Elapsed Time: 24 hours - // Gas Price: 1 Gwei - // Max to spend: 0.0000000038 ETH/hour * 24 hours = 0.00032832 ETH - // Expected cost: 600,000 * 1 Gwei = 0.0006 ETH - // Expected cost > Max to spend, so we cannot send the proof - assert!(!ProofAggregator::should_send_proof_to_verify_on_chain( - Duration::from_secs(ONE_DAY_SECONDS), // 24 hours - 0.01, // 0.01 ETH monthly budget - gas_price, // 1 Gwei gas price - )); + async fn wait_until_can_submit_aggregated_proof( + proof_aggregation_service: AlignedProofAggregationServiceContract, + monthly_budget_eth: f64, + ) -> Result<(), RetryError> { + info!("Started waiting until we can submit the aggregated proof."); + + // We start on 24 hours because the proof aggregator runs once a day, so the time elapsed + // should be considered over a 24h period. + let mut time_elapsed = Duration::from_secs(24 * 3600); + + // Sleep for 3 minutes (15 blocks) before re-evaluating on each iteration + let time_to_sleep = Duration::from_secs(180); + + // Iterate until we can send the proof on-chain + loop { + // Fetch gas price from network + let gas_price = proof_aggregation_service + .provider() + .get_gas_price() + .await + .map_err(|e| { + RetryError::Transient(AggregatedProofSubmissionError::GasPriceError( + e.to_string(), + )) + })?; + + info!("Fetched gas price from network: {gas_price}"); + + if eth::should_send_proof_to_verify_on_chain( + time_elapsed, + monthly_budget_eth, + U256::from(gas_price), + ) { + break; + } else { + info!("Skipping sending proof to ProofAggregationService contract due to budget/time constraints."); + } + + time_elapsed += time_to_sleep; + sleep(time_to_sleep).await; + } + + Ok(()) + } + + pub async fn wait_and_send_proof_to_verify_on_chain( + blob: BlobTransactionSidecar, + blob_versioned_hash: [u8; 32], + aggregated_proof: &AlignedProof, + proof_aggregation_service: AlignedProofAggregationServiceContract, + sp1_chunk_aggregator_vk_hash_bytes: [u8; 32], + risc0_chunk_aggregator_image_id_bytes: [u8; 32], + monthly_budget_eth: f64, + ) -> Result> { + Self::wait_until_can_submit_aggregated_proof( + proof_aggregation_service.clone(), + monthly_budget_eth, + ) + .await?; + + info!("Sending proof to ProofAggregationService contract..."); + + let tx_req = match aggregated_proof { + AlignedProof::SP1(proof) => proof_aggregation_service + .verifyAggregationSP1( + blob_versioned_hash.into(), + proof.proof_with_pub_values.public_values.to_vec().into(), + proof.proof_with_pub_values.bytes().into(), + sp1_chunk_aggregator_vk_hash_bytes.into(), + ) + .sidecar(blob) + .into_transaction_request(), + AlignedProof::Risc0(proof) => { + let encoded_seal = encode_seal(&proof.receipt) + .map_err(|e| AggregatedProofSubmissionError::Risc0EncodingSeal(e.to_string())) + .map_err(RetryError::Permanent)?; + proof_aggregation_service + .verifyAggregationRisc0( + blob_versioned_hash.into(), + encoded_seal.into(), + proof.receipt.journal.bytes.clone().into(), + risc0_chunk_aggregator_image_id_bytes.into(), + ) + .sidecar(blob) + .into_transaction_request() + } + }; + + let provider = proof_aggregation_service.provider(); + let envelope = provider + .fill(tx_req) + .await + .map_err(|err| { + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( + err.to_string(), + ) + }) + .map_err(RetryError::Transient)? + .try_into_envelope() + .map_err(|err| { + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( + err.to_string(), + ) + }) + .map_err(RetryError::Transient)?; + let tx: EthereumTxEnvelope> = envelope + .try_into_pooled() + .map_err(|err| { + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( + err.to_string(), + ) + }) + .map_err(RetryError::Transient)? + .try_map_eip4844(|tx| { + tx.try_map_sidecar(|sidecar| sidecar.try_into_7594(EnvKzgSettings::Default.get())) + }) + .map_err(|err| { + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( + err.to_string(), + ) + }) + .map_err(RetryError::Transient)?; + + let encoded_tx = tx.encoded_2718(); + let pending_tx = provider + .send_raw_transaction(&encoded_tx) + .await + .map_err(|err| { + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( + err.to_string(), + ) + }) + .map_err(RetryError::Transient)?; + + let receipt = pending_tx + .get_receipt() + .await + .map_err(|err| { + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( + err.to_string(), + ) + }) + .map_err(RetryError::Transient)?; + + Ok(receipt) } } diff --git a/aggregation_mode/proof_aggregator/src/backend/retry.rs b/aggregation_mode/proof_aggregator/src/backend/retry.rs new file mode 100644 index 000000000..cc1a567b3 --- /dev/null +++ b/aggregation_mode/proof_aggregator/src/backend/retry.rs @@ -0,0 +1,84 @@ +use std::future::Future; +use std::time::Duration; + +#[derive(Debug)] +pub enum RetryError { + Transient(E), + Permanent(E), +} + +impl std::fmt::Display for RetryError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + RetryError::Transient(e) => write!(f, "{e}"), + RetryError::Permanent(e) => write!(f, "{e}"), + } + } +} + +impl RetryError { + pub fn inner(self) -> E { + match self { + RetryError::Transient(e) => e, + RetryError::Permanent(e) => e, + } + } +} + +impl std::error::Error for RetryError where E: std::fmt::Debug {} + +pub async fn retry_function( + mut function: FutureFn, + min_delay_ms: u64, + factor: f32, + max_times: usize, + max_delay_seconds: u64, +) -> Result> +where + Fut: Future>>, + FutureFn: FnMut() -> Fut, +{ + let mut delay = Duration::from_millis(min_delay_ms); + + let factor = (factor as f64).max(1.0); + + let mut attempt: usize = 0; + + loop { + match function().await { + Ok(v) => return Ok(v), + Err(RetryError::Permanent(e)) => return Err(RetryError::Permanent(e)), + Err(RetryError::Transient(e)) => { + if attempt >= max_times { + return Err(RetryError::Transient(e)); + } + + tokio::time::sleep(delay).await; + + delay = next_backoff_delay(delay, max_delay_seconds, factor); + + attempt += 1; + } + } + } +} + +/// TODO: Replace with the one in aggregation_mode/db/src/orchestrator.rs, or use a common method. +fn next_backoff_delay(current_delay: Duration, max_delay_seconds: u64, factor: f64) -> Duration { + let max: Duration = Duration::from_secs(max_delay_seconds); + // Defensive: factor should be >= 1.0 for backoff, we clamp it to avoid shrinking/NaN. + + let scaled_secs = current_delay.as_secs_f64() * factor; + let scaled_secs = if scaled_secs.is_finite() { + scaled_secs + } else { + max.as_secs_f64() + }; + + let scaled = Duration::from_secs_f64(scaled_secs); + if scaled > max { + max + } else { + scaled + } +} diff --git a/aggregation_mode/proof_aggregator/src/backend/types.rs b/aggregation_mode/proof_aggregator/src/backend/types.rs index 8d6a92d9e..7f10e803d 100644 --- a/aggregation_mode/proof_aggregator/src/backend/types.rs +++ b/aggregation_mode/proof_aggregator/src/backend/types.rs @@ -30,20 +30,3 @@ pub type AlignedProofAggregationServiceContract = AlignedProofAggregationService RootProvider, >, >; - -pub type RPCProvider = alloy::providers::fillers::FillProvider< - alloy::providers::fillers::JoinFill< - alloy::providers::Identity, - alloy::providers::fillers::JoinFill< - alloy::providers::fillers::GasFiller, - alloy::providers::fillers::JoinFill< - alloy::providers::fillers::BlobGasFiller, - alloy::providers::fillers::JoinFill< - alloy::providers::fillers::NonceFiller, - alloy::providers::fillers::ChainIdFiller, - >, - >, - >, - >, - alloy::providers::RootProvider, ->;