From 85643d636eded4dd7fc263fc2bdcd5141257a1c6 Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Fri, 9 Jan 2026 16:29:12 -0300 Subject: [PATCH 01/35] feat(aggregation-mode): Bump fee when proof verification times out --- .../proof_aggregator/src/backend/mod.rs | 240 ++++++++++++------ 1 file changed, 160 insertions(+), 80 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index f43398595..916425fb3 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -22,7 +22,7 @@ use alloy::{ consensus::{BlobTransactionSidecar, EnvKzgSettings, EthereumTxEnvelope, TxEip4844WithSidecar}, eips::{eip4844::BYTES_PER_BLOB, eip7594::BlobTransactionSidecarEip7594, Encodable2718}, hex, - network::EthereumWallet, + network::{EthereumWallet, TransactionBuilder}, primitives::{utils::parse_ether, Address, U256}, providers::{PendingTransactionError, Provider, ProviderBuilder}, rpc::types::TransactionReceipt, @@ -334,90 +334,170 @@ impl ProofAggregator { info!("Sending proof to ProofAggregationService contract..."); - 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(), - ) - .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)?; - self.proof_aggregation_service - .verifyAggregationRisc0( + // TODO: Read from config + let max_retries = 5; + let retry_interval = Duration::from_secs(120); + let fee_multiplier: f64 = 1.2; + + for attempt in 0..max_retries { + let mut tx_req = match aggregated_proof { + AlignedProof::SP1(proof) => self + .proof_aggregation_service + .verifyAggregationSP1( blob_versioned_hash.into(), - encoded_seal.into(), - proof.receipt.journal.bytes.clone().into(), - self.risc0_chunk_aggregator_image_id_bytes.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(), ) - .sidecar(blob) - .into_transaction_request() - } - }; - - let provider = self.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)?; + .sidecar(blob.clone()) + .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)?; + self.proof_aggregation_service + .verifyAggregationRisc0( + blob_versioned_hash.into(), + encoded_seal.into(), + proof.receipt.journal.bytes.clone().into(), + self.risc0_chunk_aggregator_image_id_bytes.into(), + ) + .sidecar(blob.clone()) + .into_transaction_request() + } + }; - 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 provider = self.proof_aggregation_service.provider(); + + // TODO: Move this to a separate method reset_gas_fees() + // Increase gas price/fees for retries before filling + if attempt > 0 { + let multiplier = fee_multiplier.powi(attempt as i32); + + info!( + "Retry attempt {} with increased fee ({}x)", + attempt, multiplier + ); + + // Obtain the current gas price if not set + if tx_req.max_fee_per_gas.is_none() { + let current_gas_price = provider.get_gas_price().await.map_err(|e| { + RetryError::Transient(AggregatedProofSubmissionError::GasPriceError( + e.to_string(), + )) + })?; + + let new_max_fee = (current_gas_price as f64 * multiplier) as u128; + let new_priority_fee = (current_gas_price as f64 * multiplier * 0.1) as u128; + + tx_req = tx_req + .with_max_fee_per_gas(new_max_fee) + .with_max_priority_fee_per_gas(new_priority_fee); + } else { + // If set, multiplicate the current ones + if let Some(max_fee) = tx_req.max_fee_per_gas { + let new_max_fee = (max_fee as f64 * multiplier) as u128; + tx_req = tx_req.with_max_fee_per_gas(new_max_fee); + } + if let Some(priority_fee) = tx_req.max_priority_fee_per_gas { + let new_priority_fee = (priority_fee as f64 * multiplier * 0.1) as u128; + tx_req = tx_req.with_max_priority_fee_per_gas(new_priority_fee); + } + } + } - let receipt = pending_tx - .get_receipt() - .await - .map_err(|err| { - AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( - err.to_string(), - ) - }) - .map_err(RetryError::Transient)?; + 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)?; + + // Wait for the receipt with timeout + let receipt_result = + tokio::time::timeout(retry_interval, pending_tx.get_receipt()).await; + + match receipt_result { + Ok(Ok(receipt)) => { + info!( + "Transaction confirmed successfully on attempt {}", + attempt + 1 + ); + return Ok(receipt); + } + Ok(Err(err)) => { + warn!("Error getting receipt on attempt {}: {}", attempt + 1, err); + if attempt == max_retries - 1 { + return Err(RetryError::Transient( + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( + err.to_string(), + ), + )); + } + } + Err(_) => { + warn!("Transaction not confirmed after {} seconds on attempt {}, retrying with higher fee...", + retry_interval.as_secs(), attempt + 1); + if attempt == max_retries - 1 { + return Err(RetryError::Transient( + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( + "Transaction timeout after all retries".to_string(), + ), + )); + } + } + } + } - Ok(receipt) + Err(RetryError::Transient( + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( + "Max retries exceeded".to_string(), + ), + )) } async fn wait_until_can_submit_aggregated_proof( From e377dda8d1390733beea5ebbbc415e21e074781b Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Fri, 9 Jan 2026 16:46:48 -0300 Subject: [PATCH 02/35] fix clippy lint removing unnecessary cast --- aggregation_mode/proof_aggregator/src/backend/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 916425fb3..ea5868861 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -374,7 +374,7 @@ impl ProofAggregator { // TODO: Move this to a separate method reset_gas_fees() // Increase gas price/fees for retries before filling if attempt > 0 { - let multiplier = fee_multiplier.powi(attempt as i32); + let multiplier = fee_multiplier.powi(attempt); info!( "Retry attempt {} with increased fee ({}x)", From cc96a7ecbd73412d66a37283324483681962cdbd Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Fri, 9 Jan 2026 18:04:55 -0300 Subject: [PATCH 03/35] refactor: move the gas fees update to a separate method --- .../proof_aggregator/src/backend/mod.rs | 103 ++++++++++++------ 1 file changed, 68 insertions(+), 35 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index ea5868861..891d18e45 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -369,45 +369,15 @@ impl ProofAggregator { } }; - let provider = self.proof_aggregation_service.provider(); - - // TODO: Move this to a separate method reset_gas_fees() // Increase gas price/fees for retries before filling if attempt > 0 { - let multiplier = fee_multiplier.powi(attempt); - - info!( - "Retry attempt {} with increased fee ({}x)", - attempt, multiplier - ); - - // Obtain the current gas price if not set - if tx_req.max_fee_per_gas.is_none() { - let current_gas_price = provider.get_gas_price().await.map_err(|e| { - RetryError::Transient(AggregatedProofSubmissionError::GasPriceError( - e.to_string(), - )) - })?; - - let new_max_fee = (current_gas_price as f64 * multiplier) as u128; - let new_priority_fee = (current_gas_price as f64 * multiplier * 0.1) as u128; - - tx_req = tx_req - .with_max_fee_per_gas(new_max_fee) - .with_max_priority_fee_per_gas(new_priority_fee); - } else { - // If set, multiplicate the current ones - if let Some(max_fee) = tx_req.max_fee_per_gas { - let new_max_fee = (max_fee as f64 * multiplier) as u128; - tx_req = tx_req.with_max_fee_per_gas(new_max_fee); - } - if let Some(priority_fee) = tx_req.max_priority_fee_per_gas { - let new_priority_fee = (priority_fee as f64 * multiplier * 0.1) as u128; - tx_req = tx_req.with_max_priority_fee_per_gas(new_priority_fee); - } - } + tx_req = self + .update_gas_fees(fee_multiplier, attempt, tx_req) + .await?; } + let provider = self.proof_aggregation_service.provider(); + let envelope = provider .fill(tx_req) .await @@ -500,6 +470,69 @@ impl ProofAggregator { )) } + // Updates the gas fees of a `TransactionRequest` for retry attempts by applying an exponential fee + // multiplier based on the retry number. This method is intended to be used when a previous transaction + // attempt was not confirmed (e.g. receipt timeout or transient failure). By increasing the gas fees + // on each retry, it improves the likelihood that the transaction will be included + // on the next block. + // + // Fee strategy: + // An exponential multiplier is computed on each iteration. For example, with `fee_multiplier = 1.2`: + // - `attempt = 1` → `1.2x` + // - `attempt = 2` → `1.44x` + // - `attempt = 3` → `1.728x` + // + // If the transaction request doesn't have `max_fee_per_gas` set, the current network gas price is fetched + // from the provider, the max fee per gas is set to `current_gas_price * multiplier` and the max priority + // fee per gas is set to `current_gas_price * multiplier * 0.1`. + // + // If the transaction request already contains the fee fields, the existing max fee per gas is multiplied + // by `multiplier`, and the existing max priority fee per gas is multiplied by multiplier * 0.1`. + async fn update_gas_fees( + &self, + fee_multiplier: f64, + attempt: i32, + tx_req: alloy::rpc::types::TransactionRequest, + ) -> Result> + { + let provider = self.proof_aggregation_service.provider(); + + let multiplier = fee_multiplier.powi(attempt); + + info!( + "Retry attempt {} with increased fee ({}x)", + attempt, multiplier + ); + + let mut current_tx_req = tx_req.clone(); + + // Obtain the current gas price if not set + if tx_req.max_fee_per_gas.is_none() { + let current_gas_price = provider.get_gas_price().await.map_err(|e| { + RetryError::Transient(AggregatedProofSubmissionError::GasPriceError(e.to_string())) + })?; + + let new_max_fee = (current_gas_price as f64 * multiplier) as u128; + let new_priority_fee = (current_gas_price as f64 * multiplier * 0.1) as u128; + + current_tx_req = current_tx_req + .with_max_fee_per_gas(new_max_fee) + .with_max_priority_fee_per_gas(new_priority_fee); + } else { + // If set, multiplicate the current ones + if let Some(max_fee) = tx_req.max_fee_per_gas { + let new_max_fee = (max_fee as f64 * multiplier) as u128; + current_tx_req = tx_req.clone().with_max_fee_per_gas(new_max_fee); + } + if let Some(priority_fee) = tx_req.max_priority_fee_per_gas { + let new_priority_fee = (priority_fee as f64 * multiplier * 0.1) as u128; + current_tx_req = tx_req.with_max_priority_fee_per_gas(new_priority_fee); + } + } + + Ok(current_tx_req) + } + async fn wait_until_can_submit_aggregated_proof( &self, ) -> Result<(), RetryError> { From 31807fb37108ef0767e25c28f955b36e2c34a395 Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Fri, 9 Jan 2026 18:37:50 -0300 Subject: [PATCH 04/35] Move the bump behavior config values to the proof aggregator config files --- aggregation_mode/proof_aggregator/src/backend/config.rs | 3 +++ aggregation_mode/proof_aggregator/src/backend/mod.rs | 9 ++++----- .../config-proof-aggregator-ethereum-package.yaml | 5 +++++ config-files/config-proof-aggregator.yaml | 5 +++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/config.rs b/aggregation_mode/proof_aggregator/src/backend/config.rs index 41081afc7..eff20fe14 100644 --- a/aggregation_mode/proof_aggregator/src/backend/config.rs +++ b/aggregation_mode/proof_aggregator/src/backend/config.rs @@ -21,6 +21,9 @@ pub struct Config { pub sp1_chunk_aggregator_vk_hash: String, pub monthly_budget_eth: f64, pub db_connection_urls: Vec, + pub max_bump_retries: u16, + pub bump_retry_interval_seconds: u64, + pub bump_increase_fee_multiplier: f64, } impl Config { diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 891d18e45..766fd665a 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -334,10 +334,9 @@ impl ProofAggregator { info!("Sending proof to ProofAggregationService contract..."); - // TODO: Read from config - let max_retries = 5; - let retry_interval = Duration::from_secs(120); - let fee_multiplier: f64 = 1.2; + let max_retries = self.config.max_bump_retries; + let retry_interval = Duration::from_secs(self.config.bump_retry_interval_seconds); + let fee_multiplier: f64 = self.config.bump_increase_fee_multiplier; for attempt in 0..max_retries { let mut tx_req = match aggregated_proof { @@ -372,7 +371,7 @@ impl ProofAggregator { // Increase gas price/fees for retries before filling if attempt > 0 { tx_req = self - .update_gas_fees(fee_multiplier, attempt, tx_req) + .update_gas_fees(fee_multiplier, attempt as i32, tx_req) .await?; } diff --git a/config-files/config-proof-aggregator-ethereum-package.yaml b/config-files/config-proof-aggregator-ethereum-package.yaml index 23034743c..2942a7658 100644 --- a/config-files/config-proof-aggregator-ethereum-package.yaml +++ b/config-files/config-proof-aggregator-ethereum-package.yaml @@ -27,6 +27,11 @@ monthly_budget_eth: 15.0 sp1_chunk_aggregator_vk_hash: "00d6e32a34f68ea643362b96615591c94ee0bf99ee871740ab2337966a4f77af" risc0_chunk_aggregator_image_id: "8908f01022827e80a5de71908c16ee44f4a467236df20f62e7c994491629d74c" +# These values modify the bumping behavior after the aggregated proof on-chain submission times out. +max_bump_retries: 5 +bump_retry_interval_seconds: 120 +bump_increase_fee_multiplier: 1.2 + ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" private_key_store_password: "" diff --git a/config-files/config-proof-aggregator.yaml b/config-files/config-proof-aggregator.yaml index 61a4f982a..804962bf2 100644 --- a/config-files/config-proof-aggregator.yaml +++ b/config-files/config-proof-aggregator.yaml @@ -27,6 +27,11 @@ monthly_budget_eth: 15.0 sp1_chunk_aggregator_vk_hash: "00ba19eed0aaeb0151f07b8d3ee7c659bcd29f3021e48fb42766882f55b84509" risc0_chunk_aggregator_image_id: "d8cfdd5410c70395c0a1af1842a0148428cc46e353355faccfba694dd4862dbf" +# These values modify the bumping behavior after the aggregated proof on-chain submission times out. +max_bump_retries: 5 +bump_retry_interval_seconds: 120 +bump_increase_fee_multiplier: 1.2 + ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" private_key_store_password: "" From 6e99953679faa820f4ac3adae874e402c2d98788 Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Mon, 12 Jan 2026 11:07:54 -0300 Subject: [PATCH 05/35] fix: use modified tx_req in update_gas_fees --- .../proof_aggregator/src/backend/mod.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 766fd665a..15fc18596 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -505,8 +505,7 @@ impl ProofAggregator { let mut current_tx_req = tx_req.clone(); - // Obtain the current gas price if not set - if tx_req.max_fee_per_gas.is_none() { + if current_tx_req.max_fee_per_gas.is_none() { let current_gas_price = provider.get_gas_price().await.map_err(|e| { RetryError::Transient(AggregatedProofSubmissionError::GasPriceError(e.to_string())) })?; @@ -518,14 +517,13 @@ impl ProofAggregator { .with_max_fee_per_gas(new_max_fee) .with_max_priority_fee_per_gas(new_priority_fee); } else { - // If set, multiplicate the current ones - if let Some(max_fee) = tx_req.max_fee_per_gas { + if let Some(max_fee) = current_tx_req.max_fee_per_gas { let new_max_fee = (max_fee as f64 * multiplier) as u128; - current_tx_req = tx_req.clone().with_max_fee_per_gas(new_max_fee); + current_tx_req = current_tx_req.with_max_fee_per_gas(new_max_fee); } - if let Some(priority_fee) = tx_req.max_priority_fee_per_gas { - let new_priority_fee = (priority_fee as f64 * multiplier * 0.1) as u128; - current_tx_req = tx_req.with_max_priority_fee_per_gas(new_priority_fee); + if let Some(priority_fee) = current_tx_req.max_priority_fee_per_gas { + let new_priority_fee = (priority_fee as f64 * multiplier) as u128; + current_tx_req = current_tx_req.with_max_priority_fee_per_gas(new_priority_fee); } } From f475f08d14e4fd8e1f3955335f311a47619d5c83 Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Mon, 12 Jan 2026 11:26:58 -0300 Subject: [PATCH 06/35] rework the bump logic to use a linear bump instead of an exponential one --- .../proof_aggregator/src/backend/config.rs | 3 +- .../proof_aggregator/src/backend/mod.rs | 65 ++++++++++--------- ...fig-proof-aggregator-ethereum-package.yaml | 3 +- config-files/config-proof-aggregator.yaml | 3 +- 4 files changed, 41 insertions(+), 33 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/config.rs b/aggregation_mode/proof_aggregator/src/backend/config.rs index eff20fe14..94788f01a 100644 --- a/aggregation_mode/proof_aggregator/src/backend/config.rs +++ b/aggregation_mode/proof_aggregator/src/backend/config.rs @@ -23,7 +23,8 @@ pub struct Config { pub db_connection_urls: Vec, pub max_bump_retries: u16, pub bump_retry_interval_seconds: u64, - pub bump_increase_fee_multiplier: f64, + pub base_bump_percentage: u64, + pub retry_attempt_percentage: u64, } impl Config { diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 15fc18596..630b347be 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -336,7 +336,8 @@ impl ProofAggregator { let max_retries = self.config.max_bump_retries; let retry_interval = Duration::from_secs(self.config.bump_retry_interval_seconds); - let fee_multiplier: f64 = self.config.bump_increase_fee_multiplier; + let base_bump_percentage = self.config.base_bump_percentage; // e.g., 10 + let retry_attempt_percentage = self.config.retry_attempt_percentage; // e.g., 5 for attempt in 0..max_retries { let mut tx_req = match aggregated_proof { @@ -371,7 +372,12 @@ impl ProofAggregator { // Increase gas price/fees for retries before filling if attempt > 0 { tx_req = self - .update_gas_fees(fee_multiplier, attempt as i32, tx_req) + .update_gas_fees( + base_bump_percentage, + retry_attempt_percentage, + attempt as u64, + tx_req, + ) .await?; } @@ -469,39 +475,31 @@ impl ProofAggregator { )) } - // Updates the gas fees of a `TransactionRequest` for retry attempts by applying an exponential fee - // multiplier based on the retry number. This method is intended to be used when a previous transaction - // attempt was not confirmed (e.g. receipt timeout or transient failure). By increasing the gas fees - // on each retry, it improves the likelihood that the transaction will be included - // on the next block. - // - // Fee strategy: - // An exponential multiplier is computed on each iteration. For example, with `fee_multiplier = 1.2`: - // - `attempt = 1` → `1.2x` - // - `attempt = 2` → `1.44x` - // - `attempt = 3` → `1.728x` + // Updates the gas fees of a `TransactionRequest` for retry attempts by applying a linear fee + // bump based on the retry number. This method is intended to be used when a previous transaction + // attempt was not confirmed (e.g. receipt timeout or transient failure). // - // If the transaction request doesn't have `max_fee_per_gas` set, the current network gas price is fetched - // from the provider, the max fee per gas is set to `current_gas_price * multiplier` and the max priority - // fee per gas is set to `current_gas_price * multiplier * 0.1`. + // Fee strategy (similar to Go implementation): + // The bump is calculated as: base_bump_percentage + (retry_count * retry_attempt_percentage) + // For example, with `base_bump_percentage = 10` and `retry_attempt_percentage = 5`: + // - `attempt = 1` → 10% + (1 * 5%) = 15% bump + // - `attempt = 2` → 10% + (2 * 5%) = 20% bump + // - `attempt = 3` → 10% + (3 * 5%) = 25% bump // - // If the transaction request already contains the fee fields, the existing max fee per gas is multiplied - // by `multiplier`, and the existing max priority fee per gas is multiplied by multiplier * 0.1`. + // The bumped price is: current_gas_price * (1 + total_bump_percentage / 100) async fn update_gas_fees( &self, - fee_multiplier: f64, - attempt: i32, + base_bump_percentage: u64, + retry_attempt_percentage: u64, + attempt: u64, tx_req: alloy::rpc::types::TransactionRequest, ) -> Result> { let provider = self.proof_aggregation_service.provider(); - let multiplier = fee_multiplier.powi(attempt); - - info!( - "Retry attempt {} with increased fee ({}x)", - attempt, multiplier - ); + // Calculate total bump percentage: base + (retry_count * retry_attempt) + let incremental_retry_percentage = retry_attempt_percentage * attempt; + let total_bump_percentage = base_bump_percentage + incremental_retry_percentage; let mut current_tx_req = tx_req.clone(); @@ -510,19 +508,21 @@ impl ProofAggregator { RetryError::Transient(AggregatedProofSubmissionError::GasPriceError(e.to_string())) })?; - let new_max_fee = (current_gas_price as f64 * multiplier) as u128; - let new_priority_fee = (current_gas_price as f64 * multiplier * 0.1) as u128; + let new_max_fee = + Self::calculate_bumped_price(current_gas_price, total_bump_percentage); + let new_priority_fee = new_max_fee / 10; // 10% of max fee current_tx_req = current_tx_req .with_max_fee_per_gas(new_max_fee) .with_max_priority_fee_per_gas(new_priority_fee); } else { if let Some(max_fee) = current_tx_req.max_fee_per_gas { - let new_max_fee = (max_fee as f64 * multiplier) as u128; + let new_max_fee = Self::calculate_bumped_price(max_fee, total_bump_percentage); current_tx_req = current_tx_req.with_max_fee_per_gas(new_max_fee); } if let Some(priority_fee) = current_tx_req.max_priority_fee_per_gas { - let new_priority_fee = (priority_fee as f64 * multiplier) as u128; + let new_priority_fee = + Self::calculate_bumped_price(priority_fee, total_bump_percentage); current_tx_req = current_tx_req.with_max_priority_fee_per_gas(new_priority_fee); } } @@ -530,6 +530,11 @@ impl ProofAggregator { Ok(current_tx_req) } + fn calculate_bumped_price(current_price: u128, total_bump_percentage: u64) -> u128 { + let bump_amount = (current_price * total_bump_percentage as u128) / 100; + current_price + bump_amount + } + async fn wait_until_can_submit_aggregated_proof( &self, ) -> Result<(), RetryError> { diff --git a/config-files/config-proof-aggregator-ethereum-package.yaml b/config-files/config-proof-aggregator-ethereum-package.yaml index 2942a7658..3f594d7d8 100644 --- a/config-files/config-proof-aggregator-ethereum-package.yaml +++ b/config-files/config-proof-aggregator-ethereum-package.yaml @@ -30,7 +30,8 @@ risc0_chunk_aggregator_image_id: "8908f01022827e80a5de71908c16ee44f4a467236df20f # These values modify the bumping behavior after the aggregated proof on-chain submission times out. max_bump_retries: 5 bump_retry_interval_seconds: 120 -bump_increase_fee_multiplier: 1.2 +base_bump_percentage: 10 +retry_attempt_percentage: 2 ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" diff --git a/config-files/config-proof-aggregator.yaml b/config-files/config-proof-aggregator.yaml index 804962bf2..b3fd9af67 100644 --- a/config-files/config-proof-aggregator.yaml +++ b/config-files/config-proof-aggregator.yaml @@ -30,7 +30,8 @@ risc0_chunk_aggregator_image_id: "d8cfdd5410c70395c0a1af1842a0148428cc46e353355f # These values modify the bumping behavior after the aggregated proof on-chain submission times out. max_bump_retries: 5 bump_retry_interval_seconds: 120 -bump_increase_fee_multiplier: 1.2 +base_bump_percentage: 10 +retry_attempt_percentage: 2 ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" From c47fffa5c18ae57bceab0fcd578a1100d16baa9f Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Mon, 12 Jan 2026 11:40:22 -0300 Subject: [PATCH 07/35] Wrap the entire proof submission in a result to catch all errors --- .../proof_aggregator/src/backend/mod.rs | 296 ++++++++++-------- 1 file changed, 169 insertions(+), 127 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 630b347be..ca8c0f41c 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -336,143 +336,179 @@ impl ProofAggregator { let max_retries = self.config.max_bump_retries; let retry_interval = Duration::from_secs(self.config.bump_retry_interval_seconds); - let base_bump_percentage = self.config.base_bump_percentage; // e.g., 10 - let retry_attempt_percentage = self.config.retry_attempt_percentage; // e.g., 5 + let base_bump_percentage = self.config.base_bump_percentage; + let retry_attempt_percentage = self.config.retry_attempt_percentage; - for attempt in 0..max_retries { - let mut 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(), - ) - .sidecar(blob.clone()) - .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)?; - self.proof_aggregation_service - .verifyAggregationRisc0( - blob_versioned_hash.into(), - encoded_seal.into(), - proof.receipt.journal.bytes.clone().into(), - self.risc0_chunk_aggregator_image_id_bytes.into(), - ) - .sidecar(blob.clone()) - .into_transaction_request() - } - }; - - // Increase gas price/fees for retries before filling - if attempt > 0 { - tx_req = self - .update_gas_fees( - base_bump_percentage, - retry_attempt_percentage, - attempt as u64, - tx_req, - ) - .await?; - } - - let provider = self.proof_aggregation_service.provider(); + let mut last_error: Option = None; - 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)?; + for attempt in 0..max_retries { + info!("Transaction attempt {} of {}", attempt + 1, max_retries); - // Wait for the receipt with timeout - let receipt_result = - tokio::time::timeout(retry_interval, pending_tx.get_receipt()).await; + // Wrap the entire transaction submission in a result to catch all errors + let attempt_result = self + .try_submit_transaction( + &blob, + blob_versioned_hash, + aggregated_proof, + base_bump_percentage, + retry_attempt_percentage, + attempt as u64, + retry_interval, + ) + .await; - match receipt_result { - Ok(Ok(receipt)) => { + match attempt_result { + Ok(receipt) => { info!( "Transaction confirmed successfully on attempt {}", attempt + 1 ); return Ok(receipt); } - Ok(Err(err)) => { - warn!("Error getting receipt on attempt {}: {}", attempt + 1, err); - if attempt == max_retries - 1 { - return Err(RetryError::Transient( - AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( - err.to_string(), - ), - )); - } - } - Err(_) => { - warn!("Transaction not confirmed after {} seconds on attempt {}, retrying with higher fee...", - retry_interval.as_secs(), attempt + 1); - if attempt == max_retries - 1 { - return Err(RetryError::Transient( - AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( - "Transaction timeout after all retries".to_string(), - ), - )); + Err(err) => { + warn!("Attempt {} failed: {:?}", attempt + 1, err); + last_error = Some(err); + + if attempt < max_retries - 1 { + info!("Retrying with bumped gas fees..."); + + tokio::time::sleep(Duration::from_millis(500)).await; + } else { + warn!("Max retries ({}) exceeded", max_retries); } } } } - Err(RetryError::Transient( + // If we exhausted all retries, return the last error + Err(RetryError::Transient(last_error.unwrap_or_else(|| { AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( - "Max retries exceeded".to_string(), + "Max retries exceeded with no error details".to_string(), + ) + }))) + } + + async fn try_submit_transaction( + &self, + blob: &BlobTransactionSidecar, + blob_versioned_hash: [u8; 32], + aggregated_proof: &AlignedProof, + base_bump_percentage: u64, + retry_attempt_percentage: u64, + attempt: u64, + retry_interval: Duration, + ) -> Result { + // Build the transaction request + let mut 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(), + ) + .sidecar(blob.clone()) + .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.clone().into(), + self.risc0_chunk_aggregator_image_id_bytes.into(), + ) + .sidecar(blob.clone()) + .into_transaction_request() + } + }; + + // Apply gas fee bump for retries + if attempt > 0 { + tx_req = self + .apply_gas_fee_bump( + base_bump_percentage, + retry_attempt_percentage, + attempt, + tx_req, + ) + .await?; + } + + let provider = self.proof_aggregation_service.provider(); + + // Fill the transaction + let envelope = provider + .fill(tx_req) + .await + .map_err(|err| { + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( + "Failed to fill transaction: {}", + err + )) + })? + .try_into_envelope() + .map_err(|err| { + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( + "Failed to convert to envelope: {}", + err + )) + })?; + + // Convert to EIP-4844 transaction + let tx: EthereumTxEnvelope> = envelope + .try_into_pooled() + .map_err(|err| { + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( + "Failed to pool transaction: {}", + err + )) + })? + .try_map_eip4844(|tx| { + tx.try_map_sidecar(|sidecar| sidecar.try_into_7594(EnvKzgSettings::Default.get())) + }) + .map_err(|err| { + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( + "Failed to convert to EIP-7594: {}", + err + )) + })?; + + // Send the transaction + let encoded_tx = tx.encoded_2718(); + let pending_tx = provider + .send_raw_transaction(&encoded_tx) + .await + .map_err(|err| { + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( + "Failed to send raw transaction: {}", + err + )) + })?; + + info!("Transaction sent, waiting for confirmation..."); + + // Wait for the receipt with timeout + let receipt_result = tokio::time::timeout(retry_interval, pending_tx.get_receipt()).await; + + match receipt_result { + Ok(Ok(receipt)) => Ok(receipt), + Ok(Err(err)) => Err( + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( + "Error getting receipt: {}", + err + )), + ), + Err(_) => Err( + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( + "Transaction timeout after {} seconds", + retry_interval.as_secs() + )), ), - )) + } } // Updates the gas fees of a `TransactionRequest` for retry attempts by applying a linear fee @@ -487,30 +523,36 @@ impl ProofAggregator { // - `attempt = 3` → 10% + (3 * 5%) = 25% bump // // The bumped price is: current_gas_price * (1 + total_bump_percentage / 100) - async fn update_gas_fees( + async fn apply_gas_fee_bump( &self, base_bump_percentage: u64, retry_attempt_percentage: u64, attempt: u64, tx_req: alloy::rpc::types::TransactionRequest, - ) -> Result> - { + ) -> Result { let provider = self.proof_aggregation_service.provider(); // Calculate total bump percentage: base + (retry_count * retry_attempt) let incremental_retry_percentage = retry_attempt_percentage * attempt; let total_bump_percentage = base_bump_percentage + incremental_retry_percentage; + info!( + "Applying {}% gas fee bump for attempt {}", + total_bump_percentage, + attempt + 1 + ); + let mut current_tx_req = tx_req.clone(); if current_tx_req.max_fee_per_gas.is_none() { - let current_gas_price = provider.get_gas_price().await.map_err(|e| { - RetryError::Transient(AggregatedProofSubmissionError::GasPriceError(e.to_string())) - })?; + let current_gas_price = provider + .get_gas_price() + .await + .map_err(|e| AggregatedProofSubmissionError::GasPriceError(e.to_string()))?; let new_max_fee = Self::calculate_bumped_price(current_gas_price, total_bump_percentage); - let new_priority_fee = new_max_fee / 10; // 10% of max fee + let new_priority_fee = new_max_fee / 10; current_tx_req = current_tx_req .with_max_fee_per_gas(new_max_fee) From 8bed204b7f73098f62a1bbde384696263af9e4e5 Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Mon, 12 Jan 2026 12:14:09 -0300 Subject: [PATCH 08/35] fix clippy lints --- .../proof_aggregator/src/backend/mod.rs | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index ca8c0f41c..2dc2f495c 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -335,9 +335,6 @@ impl ProofAggregator { info!("Sending proof to ProofAggregationService contract..."); let max_retries = self.config.max_bump_retries; - let retry_interval = Duration::from_secs(self.config.bump_retry_interval_seconds); - let base_bump_percentage = self.config.base_bump_percentage; - let retry_attempt_percentage = self.config.retry_attempt_percentage; let mut last_error: Option = None; @@ -350,10 +347,7 @@ impl ProofAggregator { &blob, blob_versioned_hash, aggregated_proof, - base_bump_percentage, - retry_attempt_percentage, attempt as u64, - retry_interval, ) .await; @@ -393,11 +387,12 @@ impl ProofAggregator { blob: &BlobTransactionSidecar, blob_versioned_hash: [u8; 32], aggregated_proof: &AlignedProof, - base_bump_percentage: u64, - retry_attempt_percentage: u64, attempt: u64, - retry_interval: Duration, ) -> Result { + let retry_interval = Duration::from_secs(self.config.bump_retry_interval_seconds); + let base_bump_percentage = self.config.base_bump_percentage; + let retry_attempt_percentage = self.config.retry_attempt_percentage; + // Build the transaction request let mut tx_req = match aggregated_proof { AlignedProof::SP1(proof) => self @@ -446,15 +441,13 @@ impl ProofAggregator { .await .map_err(|err| { AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( - "Failed to fill transaction: {}", - err + "Failed to fill transaction: {err}" )) })? .try_into_envelope() .map_err(|err| { AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( - "Failed to convert to envelope: {}", - err + "Failed to convert to envelope: {err}" )) })?; @@ -463,8 +456,7 @@ impl ProofAggregator { .try_into_pooled() .map_err(|err| { AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( - "Failed to pool transaction: {}", - err + "Failed to pool transaction: {err}" )) })? .try_map_eip4844(|tx| { @@ -472,8 +464,7 @@ impl ProofAggregator { }) .map_err(|err| { AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( - "Failed to convert to EIP-7594: {}", - err + "Failed to convert to EIP-7594: {err}" )) })?; @@ -484,8 +475,7 @@ impl ProofAggregator { .await .map_err(|err| { AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( - "Failed to send raw transaction: {}", - err + "Failed to send raw transaction: {err}" )) })?; @@ -498,8 +488,7 @@ impl ProofAggregator { Ok(Ok(receipt)) => Ok(receipt), Ok(Err(err)) => Err( AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( - "Error getting receipt: {}", - err + "Error getting receipt: {err}" )), ), Err(_) => Err( From 01d25d6262a143d535ef43e7a4bae204e1893f88 Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Mon, 12 Jan 2026 14:51:52 -0300 Subject: [PATCH 09/35] Update the vk hash and image id at proof aggregator config file --- config-files/config-proof-aggregator.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config-files/config-proof-aggregator.yaml b/config-files/config-proof-aggregator.yaml index b3fd9af67..5feb9e232 100644 --- a/config-files/config-proof-aggregator.yaml +++ b/config-files/config-proof-aggregator.yaml @@ -24,8 +24,8 @@ monthly_budget_eth: 15.0 # These program ids are the ones from the chunk aggregator programs # Can be found in the Proof Aggregation Service deployment config # (remember to trim the 0x prefix) -sp1_chunk_aggregator_vk_hash: "00ba19eed0aaeb0151f07b8d3ee7c659bcd29f3021e48fb42766882f55b84509" -risc0_chunk_aggregator_image_id: "d8cfdd5410c70395c0a1af1842a0148428cc46e353355faccfba694dd4862dbf" +sp1_chunk_aggregator_vk_hash: "00d6e32a34f68ea643362b96615591c94ee0bf99ee871740ab2337966a4f77af" +risc0_chunk_aggregator_image_id: "8908f01022827e80a5de71908c16ee44f4a467236df20f62e7c994491629d74c" # These values modify the bumping behavior after the aggregated proof on-chain submission times out. max_bump_retries: 5 From 019e4bbdc653d6f835af69447355b8e30138938b Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Tue, 13 Jan 2026 14:11:47 -0300 Subject: [PATCH 10/35] change the logic to have a fixed priority fee in gwei by config --- .../proof_aggregator/src/backend/config.rs | 3 +- .../proof_aggregator/src/backend/mod.rs | 87 ++++++------------- ...fig-proof-aggregator-ethereum-package.yaml | 3 +- config-files/config-proof-aggregator.yaml | 3 +- 4 files changed, 34 insertions(+), 62 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/config.rs b/aggregation_mode/proof_aggregator/src/backend/config.rs index 94788f01a..deaa39a67 100644 --- a/aggregation_mode/proof_aggregator/src/backend/config.rs +++ b/aggregation_mode/proof_aggregator/src/backend/config.rs @@ -24,7 +24,8 @@ pub struct Config { pub max_bump_retries: u16, pub bump_retry_interval_seconds: u64, pub base_bump_percentage: u64, - pub retry_attempt_percentage: u64, + pub max_fee_bump_percentage: u64, + pub priority_fee_gwei: u128, } impl Config { diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 2dc2f495c..674a87537 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -25,7 +25,7 @@ use alloy::{ network::{EthereumWallet, TransactionBuilder}, primitives::{utils::parse_ether, Address, U256}, providers::{PendingTransactionError, Provider, ProviderBuilder}, - rpc::types::TransactionReceipt, + rpc::types::{TransactionReceipt, TransactionRequest}, signers::local::LocalSigner, }; use config::Config; @@ -391,7 +391,8 @@ impl ProofAggregator { ) -> Result { let retry_interval = Duration::from_secs(self.config.bump_retry_interval_seconds); let base_bump_percentage = self.config.base_bump_percentage; - let retry_attempt_percentage = self.config.retry_attempt_percentage; + let max_fee_bump_percentage = self.config.max_fee_bump_percentage; + let priority_fee_gwei = self.config.priority_fee_gwei; // Build the transaction request let mut tx_req = match aggregated_proof { @@ -426,8 +427,8 @@ impl ProofAggregator { tx_req = self .apply_gas_fee_bump( base_bump_percentage, - retry_attempt_percentage, - attempt, + max_fee_bump_percentage, + priority_fee_gwei, tx_req, ) .await?; @@ -500,70 +501,38 @@ impl ProofAggregator { } } - // Updates the gas fees of a `TransactionRequest` for retry attempts by applying a linear fee - // bump based on the retry number. This method is intended to be used when a previous transaction - // attempt was not confirmed (e.g. receipt timeout or transient failure). + // Updates the gas fees of a `TransactionRequest` using a fixed bump strategy. + // Intended for retrying an on-chain submission after a timeout. // - // Fee strategy (similar to Go implementation): - // The bump is calculated as: base_bump_percentage + (retry_count * retry_attempt_percentage) - // For example, with `base_bump_percentage = 10` and `retry_attempt_percentage = 5`: - // - `attempt = 1` → 10% + (1 * 5%) = 15% bump - // - `attempt = 2` → 10% + (2 * 5%) = 20% bump - // - `attempt = 3` → 10% + (3 * 5%) = 25% bump + // Strategy: + // - Fetch the current network gas price. + // - Apply `base_bump_percentage` to compute a bumped base fee. + // - Apply `max_fee_bump_percentage` on top of the bumped base fee to set `max_fee_per_gas`. + // - Set `max_priority_fee_per_gas` to a fixed value derived from `priority_fee_gwei`. // - // The bumped price is: current_gas_price * (1 + total_bump_percentage / 100) + // Fees are recomputed on each retry using the latest gas price (no incremental per-attempt bump). + async fn apply_gas_fee_bump( &self, base_bump_percentage: u64, - retry_attempt_percentage: u64, - attempt: u64, - tx_req: alloy::rpc::types::TransactionRequest, - ) -> Result { + max_fee_bump_percentage: u64, + priority_fee_gwei: u128, + tx_req: TransactionRequest, + ) -> Result { let provider = self.proof_aggregation_service.provider(); - // Calculate total bump percentage: base + (retry_count * retry_attempt) - let incremental_retry_percentage = retry_attempt_percentage * attempt; - let total_bump_percentage = base_bump_percentage + incremental_retry_percentage; - - info!( - "Applying {}% gas fee bump for attempt {}", - total_bump_percentage, - attempt + 1 - ); - - let mut current_tx_req = tx_req.clone(); - - if current_tx_req.max_fee_per_gas.is_none() { - let current_gas_price = provider - .get_gas_price() - .await - .map_err(|e| AggregatedProofSubmissionError::GasPriceError(e.to_string()))?; - - let new_max_fee = - Self::calculate_bumped_price(current_gas_price, total_bump_percentage); - let new_priority_fee = new_max_fee / 10; - - current_tx_req = current_tx_req - .with_max_fee_per_gas(new_max_fee) - .with_max_priority_fee_per_gas(new_priority_fee); - } else { - if let Some(max_fee) = current_tx_req.max_fee_per_gas { - let new_max_fee = Self::calculate_bumped_price(max_fee, total_bump_percentage); - current_tx_req = current_tx_req.with_max_fee_per_gas(new_max_fee); - } - if let Some(priority_fee) = current_tx_req.max_priority_fee_per_gas { - let new_priority_fee = - Self::calculate_bumped_price(priority_fee, total_bump_percentage); - current_tx_req = current_tx_req.with_max_priority_fee_per_gas(new_priority_fee); - } - } + let current_gas_price = provider + .get_gas_price() + .await + .map_err(|e| AggregatedProofSubmissionError::GasPriceError(e.to_string()))?; - Ok(current_tx_req) - } + let new_base_fee = current_gas_price * (1 + base_bump_percentage as u128 / 100); + let new_max_fee = new_base_fee * (1 + max_fee_bump_percentage as u128 / 100); + let new_priority_fee = priority_fee_gwei * 1000000000; // Convert to wei - fn calculate_bumped_price(current_price: u128, total_bump_percentage: u64) -> u128 { - let bump_amount = (current_price * total_bump_percentage as u128) / 100; - current_price + bump_amount + Ok(tx_req + .with_max_fee_per_gas(new_max_fee) + .with_max_priority_fee_per_gas(new_priority_fee)) } async fn wait_until_can_submit_aggregated_proof( diff --git a/config-files/config-proof-aggregator-ethereum-package.yaml b/config-files/config-proof-aggregator-ethereum-package.yaml index 3f594d7d8..0ec4f9b78 100644 --- a/config-files/config-proof-aggregator-ethereum-package.yaml +++ b/config-files/config-proof-aggregator-ethereum-package.yaml @@ -31,7 +31,8 @@ risc0_chunk_aggregator_image_id: "8908f01022827e80a5de71908c16ee44f4a467236df20f max_bump_retries: 5 bump_retry_interval_seconds: 120 base_bump_percentage: 10 -retry_attempt_percentage: 2 +max_fee_bump_percentage: 100 +priority_fee_gwei: 2 ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" diff --git a/config-files/config-proof-aggregator.yaml b/config-files/config-proof-aggregator.yaml index 5feb9e232..86a080034 100644 --- a/config-files/config-proof-aggregator.yaml +++ b/config-files/config-proof-aggregator.yaml @@ -31,7 +31,8 @@ risc0_chunk_aggregator_image_id: "8908f01022827e80a5de71908c16ee44f4a467236df20f max_bump_retries: 5 bump_retry_interval_seconds: 120 base_bump_percentage: 10 -retry_attempt_percentage: 2 +max_fee_bump_percentage: 100 +priority_fee_gwei: 2 ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" From 2e1dfcecf065f4d0e7199ebe63e2893ad5d3e565 Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Tue, 13 Jan 2026 15:10:56 -0300 Subject: [PATCH 11/35] handle the same nonce for the transaction on bumps --- .../proof_aggregator/src/backend/mod.rs | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 674a87537..4c97fcb8d 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -338,16 +338,34 @@ impl ProofAggregator { let mut last_error: Option = None; + // Get the nonce once at the beginning and reuse it for all retries + let nonce = self + .proof_aggregation_service + .provider() + .get_transaction_count(*self.proof_aggregation_service.address()) + .await + .map_err(|e| { + RetryError::Transient( + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( + "Failed to get nonce: {e}" + )), + ) + })?; + + info!("Using nonce {} for all retry attempts", nonce); + for attempt in 0..max_retries { info!("Transaction attempt {} of {}", attempt + 1, max_retries); - // Wrap the entire transaction submission in a result to catch all errors + // Wrap the entire transaction submission in a result to catch all errors, passing + // the same nonce to all attempts let attempt_result = self .try_submit_transaction( &blob, blob_versioned_hash, aggregated_proof, attempt as u64, + nonce, ) .await; @@ -364,7 +382,7 @@ impl ProofAggregator { last_error = Some(err); if attempt < max_retries - 1 { - info!("Retrying with bumped gas fees..."); + info!("Retrying with bumped gas fees and same nonce {}...", nonce); tokio::time::sleep(Duration::from_millis(500)).await; } else { @@ -388,6 +406,7 @@ impl ProofAggregator { blob_versioned_hash: [u8; 32], aggregated_proof: &AlignedProof, attempt: u64, + nonce: u64, ) -> Result { let retry_interval = Duration::from_secs(self.config.bump_retry_interval_seconds); let base_bump_percentage = self.config.base_bump_percentage; @@ -422,6 +441,9 @@ impl ProofAggregator { } }; + // Set the nonce explicitly + tx_req = tx_req.with_nonce(nonce); + // Apply gas fee bump for retries if attempt > 0 { tx_req = self @@ -480,7 +502,10 @@ impl ProofAggregator { )) })?; - info!("Transaction sent, waiting for confirmation..."); + info!( + "Transaction sent with nonce {}, waiting for confirmation...", + nonce + ); // Wait for the receipt with timeout let receipt_result = tokio::time::timeout(retry_interval, pending_tx.get_receipt()).await; From c5c06c5dad00bb9177eecec26bc2116cc3e2cfae Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Tue, 13 Jan 2026 15:34:58 -0300 Subject: [PATCH 12/35] change the priority fee value to be a number in wei as it can be represented in u128 entirely --- .../proof_aggregator/src/backend/config.rs | 2 +- aggregation_mode/proof_aggregator/src/backend/mod.rs | 10 +++++----- .../config-proof-aggregator-ethereum-package.yaml | 2 +- config-files/config-proof-aggregator.yaml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/config.rs b/aggregation_mode/proof_aggregator/src/backend/config.rs index deaa39a67..3e6e1b11c 100644 --- a/aggregation_mode/proof_aggregator/src/backend/config.rs +++ b/aggregation_mode/proof_aggregator/src/backend/config.rs @@ -25,7 +25,7 @@ pub struct Config { pub bump_retry_interval_seconds: u64, pub base_bump_percentage: u64, pub max_fee_bump_percentage: u64, - pub priority_fee_gwei: u128, + pub priority_fee_wei: u128, } impl Config { diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 4c97fcb8d..51bc9dd0a 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -411,7 +411,7 @@ impl ProofAggregator { let retry_interval = Duration::from_secs(self.config.bump_retry_interval_seconds); let base_bump_percentage = self.config.base_bump_percentage; let max_fee_bump_percentage = self.config.max_fee_bump_percentage; - let priority_fee_gwei = self.config.priority_fee_gwei; + let priority_fee_wei = self.config.priority_fee_wei; // Build the transaction request let mut tx_req = match aggregated_proof { @@ -450,7 +450,7 @@ impl ProofAggregator { .apply_gas_fee_bump( base_bump_percentage, max_fee_bump_percentage, - priority_fee_gwei, + priority_fee_wei, tx_req, ) .await?; @@ -533,7 +533,7 @@ impl ProofAggregator { // - Fetch the current network gas price. // - Apply `base_bump_percentage` to compute a bumped base fee. // - Apply `max_fee_bump_percentage` on top of the bumped base fee to set `max_fee_per_gas`. - // - Set `max_priority_fee_per_gas` to a fixed value derived from `priority_fee_gwei`. + // - Set `max_priority_fee_per_gas` to a fixed value derived from `priority_fee_wei`. // // Fees are recomputed on each retry using the latest gas price (no incremental per-attempt bump). @@ -541,7 +541,7 @@ impl ProofAggregator { &self, base_bump_percentage: u64, max_fee_bump_percentage: u64, - priority_fee_gwei: u128, + priority_fee_wei: u128, tx_req: TransactionRequest, ) -> Result { let provider = self.proof_aggregation_service.provider(); @@ -553,7 +553,7 @@ impl ProofAggregator { let new_base_fee = current_gas_price * (1 + base_bump_percentage as u128 / 100); let new_max_fee = new_base_fee * (1 + max_fee_bump_percentage as u128 / 100); - let new_priority_fee = priority_fee_gwei * 1000000000; // Convert to wei + let new_priority_fee = priority_fee_wei; Ok(tx_req .with_max_fee_per_gas(new_max_fee) diff --git a/config-files/config-proof-aggregator-ethereum-package.yaml b/config-files/config-proof-aggregator-ethereum-package.yaml index 0ec4f9b78..124a3cae3 100644 --- a/config-files/config-proof-aggregator-ethereum-package.yaml +++ b/config-files/config-proof-aggregator-ethereum-package.yaml @@ -32,7 +32,7 @@ max_bump_retries: 5 bump_retry_interval_seconds: 120 base_bump_percentage: 10 max_fee_bump_percentage: 100 -priority_fee_gwei: 2 +priority_fee_wei: 2000000000 ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" diff --git a/config-files/config-proof-aggregator.yaml b/config-files/config-proof-aggregator.yaml index 86a080034..9eba16206 100644 --- a/config-files/config-proof-aggregator.yaml +++ b/config-files/config-proof-aggregator.yaml @@ -32,7 +32,7 @@ max_bump_retries: 5 bump_retry_interval_seconds: 120 base_bump_percentage: 10 max_fee_bump_percentage: 100 -priority_fee_gwei: 2 +priority_fee_wei: 2000000000 ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" From c2fdc66378517094d2fadeb563b1c2c37346fc1d Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Tue, 13 Jan 2026 18:16:31 -0300 Subject: [PATCH 13/35] fix: use float values to avoid lossing presicion on operation --- aggregation_mode/proof_aggregator/src/backend/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 51bc9dd0a..cbf885f21 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -551,13 +551,13 @@ impl ProofAggregator { .await .map_err(|e| AggregatedProofSubmissionError::GasPriceError(e.to_string()))?; - let new_base_fee = current_gas_price * (1 + base_bump_percentage as u128 / 100); - let new_max_fee = new_base_fee * (1 + max_fee_bump_percentage as u128 / 100); + let new_base_fee = current_gas_price as f64 * (1.0 + base_bump_percentage as f64 / 100.0); + let new_max_fee = new_base_fee as f64 * (1.0 + max_fee_bump_percentage as f64 / 100.0); let new_priority_fee = priority_fee_wei; Ok(tx_req - .with_max_fee_per_gas(new_max_fee) - .with_max_priority_fee_per_gas(new_priority_fee)) + .with_max_fee_per_gas(new_max_fee as u128) + .with_max_priority_fee_per_gas(new_priority_fee as u128)) } async fn wait_until_can_submit_aggregated_proof( From ea3779f7b7493dd8ee049eeef8744ae51cf2722f Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Tue, 13 Jan 2026 18:17:09 -0300 Subject: [PATCH 14/35] fix: use the right address when obtaining the tx nonce --- aggregation_mode/proof_aggregator/src/backend/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index cbf885f21..abbe405e8 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -24,7 +24,7 @@ use alloy::{ hex, network::{EthereumWallet, TransactionBuilder}, primitives::{utils::parse_ether, Address, U256}, - providers::{PendingTransactionError, Provider, ProviderBuilder}, + providers::{PendingTransactionError, Provider, ProviderBuilder, WalletProvider}, rpc::types::{TransactionReceipt, TransactionRequest}, signers::local::LocalSigner, }; @@ -342,7 +342,11 @@ impl ProofAggregator { let nonce = self .proof_aggregation_service .provider() - .get_transaction_count(*self.proof_aggregation_service.address()) + .get_transaction_count( + self.proof_aggregation_service + .provider() + .default_signer_address(), + ) .await .map_err(|e| { RetryError::Transient( From c4914e2c522402d38f5dba9833f2bac2a48b8a49 Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Tue, 13 Jan 2026 18:17:34 -0300 Subject: [PATCH 15/35] also set the base fee to the tx request (gas_price field) --- aggregation_mode/proof_aggregator/src/backend/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index abbe405e8..0b9f5e47a 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -560,6 +560,9 @@ impl ProofAggregator { let new_priority_fee = priority_fee_wei; Ok(tx_req + // In TransactionRequest docs the gas_price field is defined as + // "The max base fee per gas the sender is willing to pay." + .with_gas_price(new_base_fee as u128) .with_max_fee_per_gas(new_max_fee as u128) .with_max_priority_fee_per_gas(new_priority_fee as u128)) } From bb309a41f02de7dbd40c6af49c615ce893d68e71 Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Tue, 13 Jan 2026 18:45:06 -0300 Subject: [PATCH 16/35] fix clippy lints --- aggregation_mode/proof_aggregator/src/backend/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 0b9f5e47a..8f452123a 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -556,7 +556,7 @@ impl ProofAggregator { .map_err(|e| AggregatedProofSubmissionError::GasPriceError(e.to_string()))?; let new_base_fee = current_gas_price as f64 * (1.0 + base_bump_percentage as f64 / 100.0); - let new_max_fee = new_base_fee as f64 * (1.0 + max_fee_bump_percentage as f64 / 100.0); + let new_max_fee = new_base_fee * (1.0 + max_fee_bump_percentage as f64 / 100.0); let new_priority_fee = priority_fee_wei; Ok(tx_req @@ -564,7 +564,7 @@ impl ProofAggregator { // "The max base fee per gas the sender is willing to pay." .with_gas_price(new_base_fee as u128) .with_max_fee_per_gas(new_max_fee as u128) - .with_max_priority_fee_per_gas(new_priority_fee as u128)) + .with_max_priority_fee_per_gas(new_priority_fee)) } async fn wait_until_can_submit_aggregated_proof( From 5e724b360cd073b29c359f9dd22ec4f70e28442e Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Wed, 14 Jan 2026 11:49:03 -0300 Subject: [PATCH 17/35] save the signer address on init to avoid getting it from provider on each iteration --- aggregation_mode/proof_aggregator/src/backend/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 8f452123a..250f795cc 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -62,6 +62,7 @@ pub struct ProofAggregator { sp1_chunk_aggregator_vk_hash_bytes: [u8; 32], risc0_chunk_aggregator_image_id_bytes: [u8; 32], db: Db, + signer_address: Address, } impl ProofAggregator { @@ -74,6 +75,8 @@ impl ProofAggregator { .expect("Keystore signer should be `cast wallet` compliant"); let wallet = EthereumWallet::from(signer); + let signer_address = signer.address(); + // Check if the monthly budget is non-negative to avoid runtime errors later let _monthly_budget_in_wei = parse_ether(&config.monthly_budget_eth.to_string()) .expect("Monthly budget must be a non-negative value"); @@ -117,6 +120,7 @@ impl ProofAggregator { sp1_chunk_aggregator_vk_hash_bytes, risc0_chunk_aggregator_image_id_bytes, db, + signer_address, } } @@ -342,11 +346,7 @@ impl ProofAggregator { let nonce = self .proof_aggregation_service .provider() - .get_transaction_count( - self.proof_aggregation_service - .provider() - .default_signer_address(), - ) + .get_transaction_count(self.signer_address) .await .map_err(|e| { RetryError::Transient( From ad01ed06a0aab7e0107a1a8e9e9432c03aef2fd6 Mon Sep 17 00:00:00 2001 From: Maximo Palopoli <96491141+maximopalopoli@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:50:13 -0300 Subject: [PATCH 18/35] Update aggregation_mode/proof_aggregator/src/backend/mod.rs Co-authored-by: Marcos Nicolau <76252340+MarcosNicolau@users.noreply.github.com> --- aggregation_mode/proof_aggregator/src/backend/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 250f795cc..a2f9a88e2 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -356,7 +356,7 @@ impl ProofAggregator { ) })?; - info!("Using nonce {} for all retry attempts", nonce); + info!("Using nonce {}", nonce); for attempt in 0..max_retries { info!("Transaction attempt {} of {}", attempt + 1, max_retries); From c3c0b91f6df4a88da53a4c5def0ce23c01f92720 Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Wed, 14 Jan 2026 11:51:23 -0300 Subject: [PATCH 19/35] apply the gas fee bump in all attempts (no exceptions) --- .../proof_aggregator/src/backend/mod.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index a2f9a88e2..ef4fa1f4f 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -449,16 +449,14 @@ impl ProofAggregator { tx_req = tx_req.with_nonce(nonce); // Apply gas fee bump for retries - if attempt > 0 { - tx_req = self - .apply_gas_fee_bump( - base_bump_percentage, - max_fee_bump_percentage, - priority_fee_wei, - tx_req, - ) - .await?; - } + tx_req = self + .apply_gas_fee_bump( + base_bump_percentage, + max_fee_bump_percentage, + priority_fee_wei, + tx_req, + ) + .await?; let provider = self.proof_aggregation_service.provider(); From d53681073ff716d65eb977aaebfd6d44090f3dbe Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Wed, 14 Jan 2026 11:52:00 -0300 Subject: [PATCH 20/35] Avoid updating the tx base fee on bump --- aggregation_mode/proof_aggregator/src/backend/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index ef4fa1f4f..14ca19783 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -558,9 +558,6 @@ impl ProofAggregator { let new_priority_fee = priority_fee_wei; Ok(tx_req - // In TransactionRequest docs the gas_price field is defined as - // "The max base fee per gas the sender is willing to pay." - .with_gas_price(new_base_fee as u128) .with_max_fee_per_gas(new_max_fee as u128) .with_max_priority_fee_per_gas(new_priority_fee)) } From 72e586c0c658724327f28bf52c8389c0dabf364d Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Wed, 14 Jan 2026 12:09:12 -0300 Subject: [PATCH 21/35] fix clippy lints --- aggregation_mode/proof_aggregator/src/backend/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 14ca19783..f8acd0a48 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -24,7 +24,7 @@ use alloy::{ hex, network::{EthereumWallet, TransactionBuilder}, primitives::{utils::parse_ether, Address, U256}, - providers::{PendingTransactionError, Provider, ProviderBuilder, WalletProvider}, + providers::{PendingTransactionError, Provider, ProviderBuilder}, rpc::types::{TransactionReceipt, TransactionRequest}, signers::local::LocalSigner, }; @@ -73,7 +73,7 @@ impl ProofAggregator { config.ecdsa.private_key_store_password.clone(), ) .expect("Keystore signer should be `cast wallet` compliant"); - let wallet = EthereumWallet::from(signer); + let wallet = EthereumWallet::from(signer.clone()); let signer_address = signer.address(); @@ -409,7 +409,7 @@ impl ProofAggregator { blob: &BlobTransactionSidecar, blob_versioned_hash: [u8; 32], aggregated_proof: &AlignedProof, - attempt: u64, + _attempt: u64, // Check if this param is useful nonce: u64, ) -> Result { let retry_interval = Duration::from_secs(self.config.bump_retry_interval_seconds); From ce697ca138bf95f44a9cd060ba0c15e705766f63 Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Wed, 14 Jan 2026 13:26:35 -0300 Subject: [PATCH 22/35] Save the tx hash if the tx is pending and check pending ones after all attempts --- .../proof_aggregator/src/backend/mod.rs | 86 +++++++++++++------ 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index f8acd0a48..51b36be35 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -23,7 +23,7 @@ use alloy::{ eips::{eip4844::BYTES_PER_BLOB, eip7594::BlobTransactionSidecarEip7594, Encodable2718}, hex, network::{EthereumWallet, TransactionBuilder}, - primitives::{utils::parse_ether, Address, U256}, + primitives::{utils::parse_ether, Address, TxHash, U256}, providers::{PendingTransactionError, Provider, ProviderBuilder}, rpc::types::{TransactionReceipt, TransactionRequest}, signers::local::LocalSigner, @@ -54,6 +54,11 @@ pub enum AggregatedProofSubmissionError { GasPriceError(String), } +enum SubmitOutcome { + Confirmed(TransactionReceipt), + Pending(TxHash), +} + pub struct ProofAggregator { engine: ZKVMEngine, proof_aggregation_service: AlignedProofAggregationServiceContract, @@ -342,6 +347,8 @@ impl ProofAggregator { let mut last_error: Option = None; + let mut pending_hashes: Vec = Vec::with_capacity(max_retries as usize); + // Get the nonce once at the beginning and reuse it for all retries let nonce = self .proof_aggregation_service @@ -364,23 +371,35 @@ impl ProofAggregator { // Wrap the entire transaction submission in a result to catch all errors, passing // the same nonce to all attempts let attempt_result = self - .try_submit_transaction( - &blob, - blob_versioned_hash, - aggregated_proof, - attempt as u64, - nonce, - ) + .try_submit_transaction(&blob, blob_versioned_hash, aggregated_proof, nonce) .await; match attempt_result { - Ok(receipt) => { + Ok(SubmitOutcome::Confirmed(receipt)) => { info!( "Transaction confirmed successfully on attempt {}", attempt + 1 ); return Ok(receipt); } + Ok(SubmitOutcome::Pending(tx_hash)) => { + warn!( + "Attempt {} timed out waiting for receipt; storing pending tx and continuing", + attempt + 1 + ); + pending_hashes.push(tx_hash); + + last_error = Some( + AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( + "Timed out waiting for receipt".to_string(), + ), + ); + + if attempt < max_retries - 1 { + info!("Retrying with bumped gas fees and same nonce {}...", nonce); + tokio::time::sleep(Duration::from_millis(500)).await; + } + } Err(err) => { warn!("Attempt {} failed: {:?}", attempt + 1, err); last_error = Some(err); @@ -396,7 +415,36 @@ impl ProofAggregator { } } - // If we exhausted all retries, return the last error + // After exhausting all retry attempts, we iterate over every pending transaction hash + // that was previously submitted with the same nonce but different gas parameters. + // One of these transactions may have been included in a block while we were still + // retrying and waiting on others. By explicitly checking the receipt for each hash, + // we ensure we don't "lose" a transaction that was actually mined but whose receipt + // we never observed due to timeouts during earlier attempts. + for (i, tx_hash) in pending_hashes.into_iter().enumerate() { + match self + .proof_aggregation_service + .provider() + .get_transaction_receipt(tx_hash) + .await + { + Ok(Some(receipt)) => { + info!("Pending tx #{} confirmed; returning receipt", i + 1); + return Ok(receipt); + } + Ok(None) => { + warn!( + "Pending tx #{} still no receipt yet (hash {})", + i + 1, + tx_hash + ); + } + Err(err) => { + warn!("Pending tx #{} receipt query failed: {:?}", i + 1, err); + } + } + } + Err(RetryError::Transient(last_error.unwrap_or_else(|| { AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( "Max retries exceeded with no error details".to_string(), @@ -409,9 +457,8 @@ impl ProofAggregator { blob: &BlobTransactionSidecar, blob_versioned_hash: [u8; 32], aggregated_proof: &AlignedProof, - _attempt: u64, // Check if this param is useful nonce: u64, - ) -> Result { + ) -> Result { let retry_interval = Duration::from_secs(self.config.bump_retry_interval_seconds); let base_bump_percentage = self.config.base_bump_percentage; let max_fee_bump_percentage = self.config.max_fee_bump_percentage; @@ -504,27 +551,18 @@ impl ProofAggregator { )) })?; - info!( - "Transaction sent with nonce {}, waiting for confirmation...", - nonce - ); + let tx_hash = *pending_tx.tx_hash(); - // Wait for the receipt with timeout let receipt_result = tokio::time::timeout(retry_interval, pending_tx.get_receipt()).await; match receipt_result { - Ok(Ok(receipt)) => Ok(receipt), + Ok(Ok(receipt)) => Ok(SubmitOutcome::Confirmed(receipt)), Ok(Err(err)) => Err( AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( "Error getting receipt: {err}" )), ), - Err(_) => Err( - AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( - "Transaction timeout after {} seconds", - retry_interval.as_secs() - )), - ), + Err(_) => Ok(SubmitOutcome::Pending(tx_hash)), } } From ae34d73dfee11c99819435c4d5c10cb2aeae81c2 Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Wed, 14 Jan 2026 13:33:33 -0300 Subject: [PATCH 23/35] fix clippy lint about boxing an enum variant --- aggregation_mode/proof_aggregator/src/backend/mod.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 51b36be35..eaafadd08 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -55,7 +55,10 @@ pub enum AggregatedProofSubmissionError { } enum SubmitOutcome { - Confirmed(TransactionReceipt), + // NOTE: Boxed because enums are sized to their largest variant; without boxing, + // every `SubmitOutcome` would reserve space for a full `TransactionReceipt`, + // even in the `Pending` case (see clippy::large_enum_variant). + Confirmed(Box), Pending(TxHash), } @@ -380,7 +383,7 @@ impl ProofAggregator { "Transaction confirmed successfully on attempt {}", attempt + 1 ); - return Ok(receipt); + return Ok(*receipt); } Ok(SubmitOutcome::Pending(tx_hash)) => { warn!( @@ -556,7 +559,7 @@ impl ProofAggregator { let receipt_result = tokio::time::timeout(retry_interval, pending_tx.get_receipt()).await; match receipt_result { - Ok(Ok(receipt)) => Ok(SubmitOutcome::Confirmed(receipt)), + Ok(Ok(receipt)) => Ok(SubmitOutcome::Confirmed(Box::new(receipt))), Ok(Err(err)) => Err( AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!( "Error getting receipt: {err}" From 5e1dac50d27e522ae13cae38551ccb53e1c0c274 Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Wed, 14 Jan 2026 17:20:35 -0300 Subject: [PATCH 24/35] Get the current base fee fro the last block instead of from provider --- .../proof_aggregator/src/backend/mod.rs | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index eaafadd08..87332a7f8 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -20,7 +20,10 @@ use crate::{ use alloy::{ consensus::{BlobTransactionSidecar, EnvKzgSettings, EthereumTxEnvelope, TxEip4844WithSidecar}, - eips::{eip4844::BYTES_PER_BLOB, eip7594::BlobTransactionSidecarEip7594, Encodable2718}, + eips::{ + eip4844::BYTES_PER_BLOB, eip7594::BlobTransactionSidecarEip7594, BlockNumberOrTag, + Encodable2718, + }, hex, network::{EthereumWallet, TransactionBuilder}, primitives::{utils::parse_ether, Address, TxHash, U256}, @@ -52,6 +55,8 @@ pub enum AggregatedProofSubmissionError { MerkleRootMisMatch, StoringMerklePaths(DbError), GasPriceError(String), + LatestBlockNotFound, + BaseFeePerGasMissing, } enum SubmitOutcome { @@ -589,18 +594,23 @@ impl ProofAggregator { ) -> Result { let provider = self.proof_aggregation_service.provider(); - let current_gas_price = provider - .get_gas_price() + let latest_block = provider + .get_block_by_number(BlockNumberOrTag::Latest) .await - .map_err(|e| AggregatedProofSubmissionError::GasPriceError(e.to_string()))?; + .map_err(|e| AggregatedProofSubmissionError::GasPriceError(e.to_string()))? + .ok_or(AggregatedProofSubmissionError::LatestBlockNotFound)?; + + let current_base_fee = latest_block + .header + .base_fee_per_gas + .ok_or(AggregatedProofSubmissionError::BaseFeePerGasMissing)?; - let new_base_fee = current_gas_price as f64 * (1.0 + base_bump_percentage as f64 / 100.0); + let new_base_fee = current_base_fee as f64 * (1.0 + base_bump_percentage as f64 / 100.0); let new_max_fee = new_base_fee * (1.0 + max_fee_bump_percentage as f64 / 100.0); - let new_priority_fee = priority_fee_wei; Ok(tx_req .with_max_fee_per_gas(new_max_fee as u128) - .with_max_priority_fee_per_gas(new_priority_fee)) + .with_max_priority_fee_per_gas(priority_fee_wei)) } async fn wait_until_can_submit_aggregated_proof( From e0013192388caa9e9d941d7816e921b0ff4d0b7e Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Wed, 14 Jan 2026 17:43:18 -0300 Subject: [PATCH 25/35] move the bump variables declaration to inside of apply_gas_fee_bump --- .../proof_aggregator/src/backend/mod.rs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 87332a7f8..99d58c1d8 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -468,9 +468,6 @@ impl ProofAggregator { nonce: u64, ) -> Result { let retry_interval = Duration::from_secs(self.config.bump_retry_interval_seconds); - let base_bump_percentage = self.config.base_bump_percentage; - let max_fee_bump_percentage = self.config.max_fee_bump_percentage; - let priority_fee_wei = self.config.priority_fee_wei; // Build the transaction request let mut tx_req = match aggregated_proof { @@ -504,14 +501,7 @@ impl ProofAggregator { tx_req = tx_req.with_nonce(nonce); // Apply gas fee bump for retries - tx_req = self - .apply_gas_fee_bump( - base_bump_percentage, - max_fee_bump_percentage, - priority_fee_wei, - tx_req, - ) - .await?; + tx_req = self.apply_gas_fee_bump(tx_req).await?; let provider = self.proof_aggregation_service.provider(); @@ -587,13 +577,14 @@ impl ProofAggregator { async fn apply_gas_fee_bump( &self, - base_bump_percentage: u64, - max_fee_bump_percentage: u64, - priority_fee_wei: u128, tx_req: TransactionRequest, ) -> Result { let provider = self.proof_aggregation_service.provider(); + let base_bump_percentage = self.config.base_bump_percentage; + let max_fee_bump_percentage = self.config.max_fee_bump_percentage; + let priority_fee_wei = self.config.priority_fee_wei; + let latest_block = provider .get_block_by_number(BlockNumberOrTag::Latest) .await From 96e69f964219d453c2e459940bd1b65fc83af4c9 Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Wed, 14 Jan 2026 17:44:40 -0300 Subject: [PATCH 26/35] Change the priority fee to 3 gwei in proof aggregator config files --- config-files/config-proof-aggregator-ethereum-package.yaml | 2 +- config-files/config-proof-aggregator.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config-files/config-proof-aggregator-ethereum-package.yaml b/config-files/config-proof-aggregator-ethereum-package.yaml index 124a3cae3..f9cb19d56 100644 --- a/config-files/config-proof-aggregator-ethereum-package.yaml +++ b/config-files/config-proof-aggregator-ethereum-package.yaml @@ -32,7 +32,7 @@ max_bump_retries: 5 bump_retry_interval_seconds: 120 base_bump_percentage: 10 max_fee_bump_percentage: 100 -priority_fee_wei: 2000000000 +priority_fee_wei: 3000000000 // 3 Gwei ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" diff --git a/config-files/config-proof-aggregator.yaml b/config-files/config-proof-aggregator.yaml index 9eba16206..630e0af97 100644 --- a/config-files/config-proof-aggregator.yaml +++ b/config-files/config-proof-aggregator.yaml @@ -32,7 +32,7 @@ max_bump_retries: 5 bump_retry_interval_seconds: 120 base_bump_percentage: 10 max_fee_bump_percentage: 100 -priority_fee_wei: 2000000000 +priority_fee_wei: 3000000000 // 3 Gwei ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" From c0aafb2caac31525283dd1200ea62d6d7f3e08aa Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Wed, 14 Jan 2026 18:36:43 -0300 Subject: [PATCH 27/35] Add a timeout for the get receipt final calls as alloy does not provide one --- .../proof_aggregator/src/backend/config.rs | 1 + .../proof_aggregator/src/backend/mod.rs | 29 ++++++++++++++----- ...fig-proof-aggregator-ethereum-package.yaml | 3 +- config-files/config-proof-aggregator.yaml | 3 +- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/config.rs b/aggregation_mode/proof_aggregator/src/backend/config.rs index 3e6e1b11c..5fcb09971 100644 --- a/aggregation_mode/proof_aggregator/src/backend/config.rs +++ b/aggregation_mode/proof_aggregator/src/backend/config.rs @@ -26,6 +26,7 @@ pub struct Config { pub base_bump_percentage: u64, pub max_fee_bump_percentage: u64, pub priority_fee_wei: u128, + pub final_receipt_check_timeout_seconds: u64, } impl Config { diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 99d58c1d8..59268fcee 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -423,6 +423,8 @@ impl ProofAggregator { } } + let receipt_timeout = Duration::from_secs(self.config.final_receipt_check_timeout_seconds); + // After exhausting all retry attempts, we iterate over every pending transaction hash // that was previously submitted with the same nonce but different gas parameters. // One of these transactions may have been included in a block while we were still @@ -430,26 +432,37 @@ impl ProofAggregator { // we ensure we don't "lose" a transaction that was actually mined but whose receipt // we never observed due to timeouts during earlier attempts. for (i, tx_hash) in pending_hashes.into_iter().enumerate() { - match self - .proof_aggregation_service - .provider() - .get_transaction_receipt(tx_hash) - .await + // NOTE: `get_transaction_receipt` has no built-in timeout, so we guard it to + // avoid hanging the aggregator on a stuck RPC call. + match tokio::time::timeout( + receipt_timeout, + self.proof_aggregation_service + .provider() + .get_transaction_receipt(tx_hash), + ) + .await { - Ok(Some(receipt)) => { + Ok(Ok(Some(receipt))) => { info!("Pending tx #{} confirmed; returning receipt", i + 1); return Ok(receipt); } - Ok(None) => { + Ok(Ok(None)) => { warn!( "Pending tx #{} still no receipt yet (hash {})", i + 1, tx_hash ); } - Err(err) => { + Ok(Err(err)) => { warn!("Pending tx #{} receipt query failed: {:?}", i + 1, err); } + Err(_) => { + warn!( + "Pending tx #{} receipt query timed out after {:?}", + i + 1, + receipt_timeout + ); + } } } diff --git a/config-files/config-proof-aggregator-ethereum-package.yaml b/config-files/config-proof-aggregator-ethereum-package.yaml index f9cb19d56..d116e134b 100644 --- a/config-files/config-proof-aggregator-ethereum-package.yaml +++ b/config-files/config-proof-aggregator-ethereum-package.yaml @@ -32,7 +32,8 @@ max_bump_retries: 5 bump_retry_interval_seconds: 120 base_bump_percentage: 10 max_fee_bump_percentage: 100 -priority_fee_wei: 3000000000 // 3 Gwei +priority_fee_wei: 3000000000 # 3 Gwei +final_receipt_check_timeout_seconds: 5 ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" diff --git a/config-files/config-proof-aggregator.yaml b/config-files/config-proof-aggregator.yaml index 630e0af97..707815c26 100644 --- a/config-files/config-proof-aggregator.yaml +++ b/config-files/config-proof-aggregator.yaml @@ -32,7 +32,8 @@ max_bump_retries: 5 bump_retry_interval_seconds: 120 base_bump_percentage: 10 max_fee_bump_percentage: 100 -priority_fee_wei: 3000000000 // 3 Gwei +priority_fee_wei: 3000000000 # 3 Gwei +final_receipt_check_timeout_seconds: 5 ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" From 3db3479e9b2eb5c222f892c6970912270ba91b68 Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Thu, 15 Jan 2026 11:23:02 -0300 Subject: [PATCH 28/35] Revert "Add a timeout for the get receipt final calls as alloy does not provide one" This reverts commit c0aafb2caac31525283dd1200ea62d6d7f3e08aa. --- .../proof_aggregator/src/backend/config.rs | 1 - .../proof_aggregator/src/backend/mod.rs | 29 +++++-------------- ...fig-proof-aggregator-ethereum-package.yaml | 3 +- config-files/config-proof-aggregator.yaml | 3 +- 4 files changed, 10 insertions(+), 26 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/config.rs b/aggregation_mode/proof_aggregator/src/backend/config.rs index 5fcb09971..3e6e1b11c 100644 --- a/aggregation_mode/proof_aggregator/src/backend/config.rs +++ b/aggregation_mode/proof_aggregator/src/backend/config.rs @@ -26,7 +26,6 @@ pub struct Config { pub base_bump_percentage: u64, pub max_fee_bump_percentage: u64, pub priority_fee_wei: u128, - pub final_receipt_check_timeout_seconds: u64, } impl Config { diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 59268fcee..99d58c1d8 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -423,8 +423,6 @@ impl ProofAggregator { } } - let receipt_timeout = Duration::from_secs(self.config.final_receipt_check_timeout_seconds); - // After exhausting all retry attempts, we iterate over every pending transaction hash // that was previously submitted with the same nonce but different gas parameters. // One of these transactions may have been included in a block while we were still @@ -432,37 +430,26 @@ impl ProofAggregator { // we ensure we don't "lose" a transaction that was actually mined but whose receipt // we never observed due to timeouts during earlier attempts. for (i, tx_hash) in pending_hashes.into_iter().enumerate() { - // NOTE: `get_transaction_receipt` has no built-in timeout, so we guard it to - // avoid hanging the aggregator on a stuck RPC call. - match tokio::time::timeout( - receipt_timeout, - self.proof_aggregation_service - .provider() - .get_transaction_receipt(tx_hash), - ) - .await + match self + .proof_aggregation_service + .provider() + .get_transaction_receipt(tx_hash) + .await { - Ok(Ok(Some(receipt))) => { + Ok(Some(receipt)) => { info!("Pending tx #{} confirmed; returning receipt", i + 1); return Ok(receipt); } - Ok(Ok(None)) => { + Ok(None) => { warn!( "Pending tx #{} still no receipt yet (hash {})", i + 1, tx_hash ); } - Ok(Err(err)) => { + Err(err) => { warn!("Pending tx #{} receipt query failed: {:?}", i + 1, err); } - Err(_) => { - warn!( - "Pending tx #{} receipt query timed out after {:?}", - i + 1, - receipt_timeout - ); - } } } diff --git a/config-files/config-proof-aggregator-ethereum-package.yaml b/config-files/config-proof-aggregator-ethereum-package.yaml index d116e134b..f9cb19d56 100644 --- a/config-files/config-proof-aggregator-ethereum-package.yaml +++ b/config-files/config-proof-aggregator-ethereum-package.yaml @@ -32,8 +32,7 @@ max_bump_retries: 5 bump_retry_interval_seconds: 120 base_bump_percentage: 10 max_fee_bump_percentage: 100 -priority_fee_wei: 3000000000 # 3 Gwei -final_receipt_check_timeout_seconds: 5 +priority_fee_wei: 3000000000 // 3 Gwei ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" diff --git a/config-files/config-proof-aggregator.yaml b/config-files/config-proof-aggregator.yaml index 707815c26..630e0af97 100644 --- a/config-files/config-proof-aggregator.yaml +++ b/config-files/config-proof-aggregator.yaml @@ -32,8 +32,7 @@ max_bump_retries: 5 bump_retry_interval_seconds: 120 base_bump_percentage: 10 max_fee_bump_percentage: 100 -priority_fee_wei: 3000000000 # 3 Gwei -final_receipt_check_timeout_seconds: 5 +priority_fee_wei: 3000000000 // 3 Gwei ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" From e9454dd4a3a5df8d4e34a289d14f82852c663b0e Mon Sep 17 00:00:00 2001 From: maximopalopoli Date: Thu, 15 Jan 2026 11:24:07 -0300 Subject: [PATCH 29/35] fix the way comment was done in proof agg config files --- config-files/config-proof-aggregator-ethereum-package.yaml | 2 +- config-files/config-proof-aggregator.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config-files/config-proof-aggregator-ethereum-package.yaml b/config-files/config-proof-aggregator-ethereum-package.yaml index f9cb19d56..67e04a1c6 100644 --- a/config-files/config-proof-aggregator-ethereum-package.yaml +++ b/config-files/config-proof-aggregator-ethereum-package.yaml @@ -32,7 +32,7 @@ max_bump_retries: 5 bump_retry_interval_seconds: 120 base_bump_percentage: 10 max_fee_bump_percentage: 100 -priority_fee_wei: 3000000000 // 3 Gwei +priority_fee_wei: 3000000000 # 3 Gwei ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" diff --git a/config-files/config-proof-aggregator.yaml b/config-files/config-proof-aggregator.yaml index 630e0af97..e7a491940 100644 --- a/config-files/config-proof-aggregator.yaml +++ b/config-files/config-proof-aggregator.yaml @@ -32,7 +32,7 @@ max_bump_retries: 5 bump_retry_interval_seconds: 120 base_bump_percentage: 10 max_fee_bump_percentage: 100 -priority_fee_wei: 3000000000 // 3 Gwei +priority_fee_wei: 3000000000 # 3 Gwei ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" From 7d9f380bfbb197e8c9cd841085f066002d10c657 Mon Sep 17 00:00:00 2001 From: JuArce <52429267+JuArce@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:03:40 -0300 Subject: [PATCH 30/35] fix fee calculation --- .../proof_aggregator/src/backend/config.rs | 1 - .../proof_aggregator/src/backend/mod.rs | 16 +++++++--------- ...config-proof-aggregator-ethereum-package.yaml | 1 - config-files/config-proof-aggregator.yaml | 1 - 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/config.rs b/aggregation_mode/proof_aggregator/src/backend/config.rs index 3e6e1b11c..bb3979a63 100644 --- a/aggregation_mode/proof_aggregator/src/backend/config.rs +++ b/aggregation_mode/proof_aggregator/src/backend/config.rs @@ -23,7 +23,6 @@ pub struct Config { pub db_connection_urls: Vec, pub max_bump_retries: u16, pub bump_retry_interval_seconds: u64, - pub base_bump_percentage: u64, pub max_fee_bump_percentage: u64, pub priority_fee_wei: u128, } diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 99d58c1d8..05204c0e6 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -564,16 +564,15 @@ impl ProofAggregator { } } - // Updates the gas fees of a `TransactionRequest` using a fixed bump strategy. + // Updates the gas fees of a `TransactionRequest` using EIP-1559 fee parameters. // Intended for retrying an on-chain submission after a timeout. // // Strategy: - // - Fetch the current network gas price. - // - Apply `base_bump_percentage` to compute a bumped base fee. - // - Apply `max_fee_bump_percentage` on top of the bumped base fee to set `max_fee_per_gas`. - // - Set `max_priority_fee_per_gas` to a fixed value derived from `priority_fee_wei`. + // - Fetch the current base fee from the latest block. + // - Set `max_priority_fee_per_gas` to a fixed value from `priority_fee_wei`. + // - Compute `max_fee_per_gas` as: (1 + max_fee_bump_percentage/100) * base_fee + priority_fee. // - // Fees are recomputed on each retry using the latest gas price (no incremental per-attempt bump). + // Fees are recomputed on each retry using the latest base fee (no incremental per-attempt bump). async fn apply_gas_fee_bump( &self, @@ -581,7 +580,6 @@ impl ProofAggregator { ) -> Result { let provider = self.proof_aggregation_service.provider(); - let base_bump_percentage = self.config.base_bump_percentage; let max_fee_bump_percentage = self.config.max_fee_bump_percentage; let priority_fee_wei = self.config.priority_fee_wei; @@ -596,8 +594,8 @@ impl ProofAggregator { .base_fee_per_gas .ok_or(AggregatedProofSubmissionError::BaseFeePerGasMissing)?; - let new_base_fee = current_base_fee as f64 * (1.0 + base_bump_percentage as f64 / 100.0); - let new_max_fee = new_base_fee * (1.0 + max_fee_bump_percentage as f64 / 100.0); + let max_fee_multiplier = 1.0 + max_fee_bump_percentage as f64 / 100.0; + let new_max_fee = max_fee_multiplier * current_base_fee as f64 + priority_fee_wei as f64; Ok(tx_req .with_max_fee_per_gas(new_max_fee as u128) diff --git a/config-files/config-proof-aggregator-ethereum-package.yaml b/config-files/config-proof-aggregator-ethereum-package.yaml index 67e04a1c6..1bca0f11a 100644 --- a/config-files/config-proof-aggregator-ethereum-package.yaml +++ b/config-files/config-proof-aggregator-ethereum-package.yaml @@ -30,7 +30,6 @@ risc0_chunk_aggregator_image_id: "8908f01022827e80a5de71908c16ee44f4a467236df20f # These values modify the bumping behavior after the aggregated proof on-chain submission times out. max_bump_retries: 5 bump_retry_interval_seconds: 120 -base_bump_percentage: 10 max_fee_bump_percentage: 100 priority_fee_wei: 3000000000 # 3 Gwei diff --git a/config-files/config-proof-aggregator.yaml b/config-files/config-proof-aggregator.yaml index e7a491940..aa600060e 100644 --- a/config-files/config-proof-aggregator.yaml +++ b/config-files/config-proof-aggregator.yaml @@ -30,7 +30,6 @@ risc0_chunk_aggregator_image_id: "8908f01022827e80a5de71908c16ee44f4a467236df20f # These values modify the bumping behavior after the aggregated proof on-chain submission times out. max_bump_retries: 5 bump_retry_interval_seconds: 120 -base_bump_percentage: 10 max_fee_bump_percentage: 100 priority_fee_wei: 3000000000 # 3 Gwei From d9f8c5ba717e5926257484a0ef5951557557eaa4 Mon Sep 17 00:00:00 2001 From: JuArce <52429267+JuArce@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:09:10 -0300 Subject: [PATCH 31/35] improve fee calculation --- .../proof_aggregator/src/backend/config.rs | 2 +- .../proof_aggregator/src/backend/mod.rs | 47 +++++++++++++++---- ...fig-proof-aggregator-ethereum-package.yaml | 2 +- config-files/config-proof-aggregator.yaml | 2 +- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/config.rs b/aggregation_mode/proof_aggregator/src/backend/config.rs index bb3979a63..34d748f17 100644 --- a/aggregation_mode/proof_aggregator/src/backend/config.rs +++ b/aggregation_mode/proof_aggregator/src/backend/config.rs @@ -24,7 +24,7 @@ pub struct Config { pub max_bump_retries: u16, pub bump_retry_interval_seconds: u64, pub max_fee_bump_percentage: u64, - pub priority_fee_wei: u128, + pub max_priority_fee_upper_limit: u128, } impl Config { diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 05204c0e6..0d2a2d3f2 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -379,7 +379,13 @@ impl ProofAggregator { // Wrap the entire transaction submission in a result to catch all errors, passing // the same nonce to all attempts let attempt_result = self - .try_submit_transaction(&blob, blob_versioned_hash, aggregated_proof, nonce) + .try_submit_transaction( + &blob, + blob_versioned_hash, + aggregated_proof, + nonce, + attempt, + ) .await; match attempt_result { @@ -466,6 +472,7 @@ impl ProofAggregator { blob_versioned_hash: [u8; 32], aggregated_proof: &AlignedProof, nonce: u64, + attempt: u16, ) -> Result { let retry_interval = Duration::from_secs(self.config.bump_retry_interval_seconds); @@ -501,7 +508,7 @@ impl ProofAggregator { tx_req = tx_req.with_nonce(nonce); // Apply gas fee bump for retries - tx_req = self.apply_gas_fee_bump(tx_req).await?; + tx_req = self.apply_gas_fee_bump(tx_req, attempt).await?; let provider = self.proof_aggregation_service.provider(); @@ -569,19 +576,21 @@ impl ProofAggregator { // // Strategy: // - Fetch the current base fee from the latest block. - // - Set `max_priority_fee_per_gas` to a fixed value from `priority_fee_wei`. + // - Fetch the suggested priority fee from the network (eth_maxPriorityFeePerGas). + // - Compute priority fee as: suggested * (1 + (attempt + 1) * 0.1), capped at `max_priority_fee_upper_limit`. // - Compute `max_fee_per_gas` as: (1 + max_fee_bump_percentage/100) * base_fee + priority_fee. // - // Fees are recomputed on each retry using the latest base fee (no incremental per-attempt bump). + // Fees are recomputed on each retry using the latest base fee. async fn apply_gas_fee_bump( &self, tx_req: TransactionRequest, + attempt: u16, ) -> Result { let provider = self.proof_aggregation_service.provider(); let max_fee_bump_percentage = self.config.max_fee_bump_percentage; - let priority_fee_wei = self.config.priority_fee_wei; + let max_priority_fee_upper_limit = self.config.max_priority_fee_upper_limit; let latest_block = provider .get_block_by_number(BlockNumberOrTag::Latest) @@ -592,14 +601,34 @@ impl ProofAggregator { let current_base_fee = latest_block .header .base_fee_per_gas - .ok_or(AggregatedProofSubmissionError::BaseFeePerGasMissing)?; + .ok_or(AggregatedProofSubmissionError::BaseFeePerGasMissing)? + as f64; + + // Fetch suggested priority fee from the network + let suggested_priority_fee = provider + .get_max_priority_fee_per_gas() + .await + .map_err(|e| AggregatedProofSubmissionError::GasPriceError(e.to_string()))? + as f64; + + // Calculate priority fee: suggested * (1 + (attempt + 1) * 0.1), capped at max + let priority_fee_multiplier = 1.0 + (attempt + 1) as f64 * 0.1; + let max_priority_fee_per_gas = (suggested_priority_fee * priority_fee_multiplier) + .min(max_priority_fee_upper_limit as f64); let max_fee_multiplier = 1.0 + max_fee_bump_percentage as f64 / 100.0; - let new_max_fee = max_fee_multiplier * current_base_fee as f64 + priority_fee_wei as f64; + let max_fee_per_gas = max_fee_multiplier * current_base_fee + max_priority_fee_per_gas; + + info!( + "Base fee: {:.4} Gwei. Applying max_fee_per_gas: {:.4} Gwei and max_priority_fee_per_gas: {:.4} Gwei to tx", + current_base_fee / 1e9, + max_fee_per_gas / 1e9, + max_priority_fee_per_gas / 1e9 + ); Ok(tx_req - .with_max_fee_per_gas(new_max_fee as u128) - .with_max_priority_fee_per_gas(priority_fee_wei)) + .with_max_fee_per_gas(max_fee_per_gas as u128) + .with_max_priority_fee_per_gas(max_priority_fee_per_gas as u128)) } async fn wait_until_can_submit_aggregated_proof( diff --git a/config-files/config-proof-aggregator-ethereum-package.yaml b/config-files/config-proof-aggregator-ethereum-package.yaml index 1bca0f11a..e99e240d0 100644 --- a/config-files/config-proof-aggregator-ethereum-package.yaml +++ b/config-files/config-proof-aggregator-ethereum-package.yaml @@ -31,7 +31,7 @@ risc0_chunk_aggregator_image_id: "8908f01022827e80a5de71908c16ee44f4a467236df20f max_bump_retries: 5 bump_retry_interval_seconds: 120 max_fee_bump_percentage: 100 -priority_fee_wei: 3000000000 # 3 Gwei +max_priority_fee_upper_limit: 3000000000 # 3 Gwei ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" diff --git a/config-files/config-proof-aggregator.yaml b/config-files/config-proof-aggregator.yaml index aa600060e..e4642b8c7 100644 --- a/config-files/config-proof-aggregator.yaml +++ b/config-files/config-proof-aggregator.yaml @@ -31,7 +31,7 @@ risc0_chunk_aggregator_image_id: "8908f01022827e80a5de71908c16ee44f4a467236df20f max_bump_retries: 5 bump_retry_interval_seconds: 120 max_fee_bump_percentage: 100 -priority_fee_wei: 3000000000 # 3 Gwei +max_priority_fee_upper_limit: 3000000000 # 3 Gwei ecdsa: private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json" From a1434ff2199ec3dfce77592b8c276505db46e903 Mon Sep 17 00:00:00 2001 From: JuArce <52429267+JuArce@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:53:45 -0300 Subject: [PATCH 32/35] fee tweaks --- aggregation_mode/proof_aggregator/src/backend/mod.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 0d2a2d3f2..8986041f3 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -609,13 +609,14 @@ impl ProofAggregator { .get_max_priority_fee_per_gas() .await .map_err(|e| AggregatedProofSubmissionError::GasPriceError(e.to_string()))? - as f64; + as u128; - // Calculate priority fee: suggested * (1 + (attempt + 1) * 0.1), capped at max - let priority_fee_multiplier = 1.0 + (attempt + 1) as f64 * 0.1; + // Calculate priority fee: suggested * (1 + (attempt + 1), capped at max + let priority_fee_multiplier = attempt + 1; let max_priority_fee_per_gas = (suggested_priority_fee * priority_fee_multiplier) - .min(max_priority_fee_upper_limit as f64); + .min(max_priority_fee_upper_limit); + // Calculate max fee with cumulative bump per attempt to ensure replacement tx is accepted let max_fee_multiplier = 1.0 + max_fee_bump_percentage as f64 / 100.0; let max_fee_per_gas = max_fee_multiplier * current_base_fee + max_priority_fee_per_gas; @@ -628,7 +629,7 @@ impl ProofAggregator { Ok(tx_req .with_max_fee_per_gas(max_fee_per_gas as u128) - .with_max_priority_fee_per_gas(max_priority_fee_per_gas as u128)) + .with_max_priority_fee_per_gas(max_priority_fee_per_gas)) } async fn wait_until_can_submit_aggregated_proof( From 996320baa9fd31a1a1deea2ad2cfd286f79b6da8 Mon Sep 17 00:00:00 2001 From: JuArce <52429267+JuArce@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:59:18 -0300 Subject: [PATCH 33/35] fix types --- .../proof_aggregator/src/backend/mod.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 8986041f3..da3143c8b 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -611,24 +611,25 @@ impl ProofAggregator { .map_err(|e| AggregatedProofSubmissionError::GasPriceError(e.to_string()))? as u128; - // Calculate priority fee: suggested * (1 + (attempt + 1), capped at max - let priority_fee_multiplier = attempt + 1; + // Calculate priority fee: suggested * (attempt + 1), capped at max + let priority_fee_multiplier = (attempt + 1) as u128; let max_priority_fee_per_gas = (suggested_priority_fee * priority_fee_multiplier) .min(max_priority_fee_upper_limit); // Calculate max fee with cumulative bump per attempt to ensure replacement tx is accepted let max_fee_multiplier = 1.0 + max_fee_bump_percentage as f64 / 100.0; - let max_fee_per_gas = max_fee_multiplier * current_base_fee + max_priority_fee_per_gas; + let max_fee_per_gas = + (max_fee_multiplier * current_base_fee) as u128 + max_priority_fee_per_gas; info!( "Base fee: {:.4} Gwei. Applying max_fee_per_gas: {:.4} Gwei and max_priority_fee_per_gas: {:.4} Gwei to tx", current_base_fee / 1e9, - max_fee_per_gas / 1e9, - max_priority_fee_per_gas / 1e9 + max_fee_per_gas as f64 / 1e9, + max_priority_fee_per_gas as f64 / 1e9 ); Ok(tx_req - .with_max_fee_per_gas(max_fee_per_gas as u128) + .with_max_fee_per_gas(max_fee_per_gas) .with_max_priority_fee_per_gas(max_priority_fee_per_gas)) } From 4bb5c915392021a1ebcb783cd8648292971d7d3e Mon Sep 17 00:00:00 2001 From: JuArce <52429267+JuArce@users.noreply.github.com> Date: Fri, 16 Jan 2026 17:26:55 -0300 Subject: [PATCH 34/35] check pending tx on each iteration --- .../proof_aggregator/src/backend/mod.rs | 79 ++++++++----------- 1 file changed, 32 insertions(+), 47 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index da3143c8b..3e26a5246 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -398,67 +398,32 @@ impl ProofAggregator { } Ok(SubmitOutcome::Pending(tx_hash)) => { warn!( - "Attempt {} timed out waiting for receipt; storing pending tx and continuing", - attempt + 1 - ); + "Attempt {} timed out waiting for receipt; storing pending tx", + attempt + 1 + ); pending_hashes.push(tx_hash); - last_error = Some( AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( "Timed out waiting for receipt".to_string(), ), ); - - if attempt < max_retries - 1 { - info!("Retrying with bumped gas fees and same nonce {}...", nonce); - tokio::time::sleep(Duration::from_millis(500)).await; - } } Err(err) => { warn!("Attempt {} failed: {:?}", attempt + 1, err); last_error = Some(err); - - if attempt < max_retries - 1 { - info!("Retrying with bumped gas fees and same nonce {}...", nonce); - - tokio::time::sleep(Duration::from_millis(500)).await; - } else { - warn!("Max retries ({}) exceeded", max_retries); - } } } - } - // After exhausting all retry attempts, we iterate over every pending transaction hash - // that was previously submitted with the same nonce but different gas parameters. - // One of these transactions may have been included in a block while we were still - // retrying and waiting on others. By explicitly checking the receipt for each hash, - // we ensure we don't "lose" a transaction that was actually mined but whose receipt - // we never observed due to timeouts during earlier attempts. - for (i, tx_hash) in pending_hashes.into_iter().enumerate() { - match self - .proof_aggregation_service - .provider() - .get_transaction_receipt(tx_hash) - .await - { - Ok(Some(receipt)) => { - info!("Pending tx #{} confirmed; returning receipt", i + 1); - return Ok(receipt); - } - Ok(None) => { - warn!( - "Pending tx #{} still no receipt yet (hash {})", - i + 1, - tx_hash - ); - } - Err(err) => { - warn!("Pending tx #{} receipt query failed: {:?}", i + 1, err); - } + // Check if any pending tx was confirmed before retrying + if let Some(receipt) = self.check_pending_txs_confirmed(&pending_hashes).await { + return Ok(receipt); } + + info!("Retrying with bumped gas fees and same nonce {}...", nonce); + tokio::time::sleep(Duration::from_millis(500)).await; } + warn!("Max retries ({}) exceeded", max_retries); Err(RetryError::Transient(last_error.unwrap_or_else(|| { AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction( "Max retries exceeded with no error details".to_string(), @@ -571,6 +536,26 @@ impl ProofAggregator { } } + // Checks if any of the pending transactions have been confirmed. + // Returns the receipt if one is found, otherwise None. + async fn check_pending_txs_confirmed( + &self, + pending_hashes: &[TxHash], + ) -> Option { + for tx_hash in pending_hashes { + if let Ok(Some(receipt)) = self + .proof_aggregation_service + .provider() + .get_transaction_receipt(*tx_hash) + .await + { + info!("Pending tx {} confirmed before retry", tx_hash); + return Some(receipt); + } + } + None + } + // Updates the gas fees of a `TransactionRequest` using EIP-1559 fee parameters. // Intended for retrying an on-chain submission after a timeout. // @@ -613,8 +598,8 @@ impl ProofAggregator { // Calculate priority fee: suggested * (attempt + 1), capped at max let priority_fee_multiplier = (attempt + 1) as u128; - let max_priority_fee_per_gas = (suggested_priority_fee * priority_fee_multiplier) - .min(max_priority_fee_upper_limit); + let max_priority_fee_per_gas = + (suggested_priority_fee * priority_fee_multiplier).min(max_priority_fee_upper_limit); // Calculate max fee with cumulative bump per attempt to ensure replacement tx is accepted let max_fee_multiplier = 1.0 + max_fee_bump_percentage as f64 / 100.0; From 4ccecf6b574e113fbb329c1f443ad2ffe3eeeeb4 Mon Sep 17 00:00:00 2001 From: JuArce <52429267+JuArce@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:17:12 -0300 Subject: [PATCH 35/35] clippy --- aggregation_mode/proof_aggregator/src/backend/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aggregation_mode/proof_aggregator/src/backend/mod.rs b/aggregation_mode/proof_aggregator/src/backend/mod.rs index 3e26a5246..7f2b83bfb 100644 --- a/aggregation_mode/proof_aggregator/src/backend/mod.rs +++ b/aggregation_mode/proof_aggregator/src/backend/mod.rs @@ -593,8 +593,7 @@ impl ProofAggregator { let suggested_priority_fee = provider .get_max_priority_fee_per_gas() .await - .map_err(|e| AggregatedProofSubmissionError::GasPriceError(e.to_string()))? - as u128; + .map_err(|e| AggregatedProofSubmissionError::GasPriceError(e.to_string()))?; // Calculate priority fee: suggested * (attempt + 1), capped at max let priority_fee_multiplier = (attempt + 1) as u128;