Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
85643d6
feat(aggregation-mode): Bump fee when proof verification times out
maximopalopoli Jan 9, 2026
e377dda
fix clippy lint removing unnecessary cast
maximopalopoli Jan 9, 2026
cc96a7e
refactor: move the gas fees update to a separate method
maximopalopoli Jan 9, 2026
31807fb
Move the bump behavior config values to the proof aggregator config f…
maximopalopoli Jan 9, 2026
6e99953
fix: use modified tx_req in update_gas_fees
maximopalopoli Jan 12, 2026
f475f08
rework the bump logic to use a linear bump instead of an exponential one
maximopalopoli Jan 12, 2026
c47fffa
Wrap the entire proof submission in a result to catch all errors
maximopalopoli Jan 12, 2026
8bed204
fix clippy lints
maximopalopoli Jan 12, 2026
01d25d6
Update the vk hash and image id at proof aggregator config file
maximopalopoli Jan 12, 2026
019e4bb
change the logic to have a fixed priority fee in gwei by config
maximopalopoli Jan 13, 2026
2e1dfce
handle the same nonce for the transaction on bumps
maximopalopoli Jan 13, 2026
c5c06c5
change the priority fee value to be a number in wei as it can be repr…
maximopalopoli Jan 13, 2026
c2fdc66
fix: use float values to avoid lossing presicion on operation
maximopalopoli Jan 13, 2026
ea3779f
fix: use the right address when obtaining the tx nonce
maximopalopoli Jan 13, 2026
c4914e2
also set the base fee to the tx request (gas_price field)
maximopalopoli Jan 13, 2026
bb309a4
fix clippy lints
maximopalopoli Jan 13, 2026
5e724b3
save the signer address on init to avoid getting it from provider on …
maximopalopoli Jan 14, 2026
ad01ed0
Update aggregation_mode/proof_aggregator/src/backend/mod.rs
maximopalopoli Jan 14, 2026
c3c0b91
apply the gas fee bump in all attempts (no exceptions)
maximopalopoli Jan 14, 2026
d536810
Avoid updating the tx base fee on bump
maximopalopoli Jan 14, 2026
72e586c
fix clippy lints
maximopalopoli Jan 14, 2026
ce697ca
Save the tx hash if the tx is pending and check pending ones after al…
maximopalopoli Jan 14, 2026
ae34d73
fix clippy lint about boxing an enum variant
maximopalopoli Jan 14, 2026
5e1dac5
Get the current base fee fro the last block instead of from provider
maximopalopoli Jan 14, 2026
e001319
move the bump variables declaration to inside of apply_gas_fee_bump
maximopalopoli Jan 14, 2026
96e69f9
Change the priority fee to 3 gwei in proof aggregator config files
maximopalopoli Jan 14, 2026
c0aafb2
Add a timeout for the get receipt final calls as alloy does not provi…
maximopalopoli Jan 14, 2026
3db3479
Revert "Add a timeout for the get receipt final calls as alloy does n…
maximopalopoli Jan 15, 2026
e9454dd
fix the way comment was done in proof agg config files
maximopalopoli Jan 15, 2026
7d9f380
fix fee calculation
JuArce Jan 16, 2026
d9f8c5b
improve fee calculation
JuArce Jan 16, 2026
a1434ff
fee tweaks
JuArce Jan 16, 2026
996320b
fix types
JuArce Jan 16, 2026
4bb5c91
check pending tx on each iteration
JuArce Jan 16, 2026
4ccecf6
clippy
JuArce Jan 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions aggregation_mode/proof_aggregator/src/backend/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ pub struct Config {
pub sp1_chunk_aggregator_vk_hash: String,
pub monthly_budget_eth: f64,
pub db_connection_urls: Vec<String>,
pub max_bump_retries: u16,
pub bump_retry_interval_seconds: u64,
pub base_bump_percentage: u64,
pub retry_attempt_percentage: u64,
}

impl Config {
Expand Down
230 changes: 188 additions & 42 deletions aggregation_mode/proof_aggregator/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -334,7 +334,67 @@ impl ProofAggregator {

info!("Sending proof to ProofAggregationService contract...");

let tx_req = match aggregated_proof {
let max_retries = self.config.max_bump_retries;

let mut last_error: Option<AggregatedProofSubmissionError> = None;

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
let attempt_result = self
.try_submit_transaction(
&blob,
blob_versioned_hash,
aggregated_proof,
attempt as u64,
)
.await;

match attempt_result {
Ok(receipt) => {
info!(
"Transaction confirmed successfully on attempt {}",
attempt + 1
);
return Ok(receipt);
}
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);
}
}
}
}

// If we exhausted all retries, return the last error
Err(RetryError::Transient(last_error.unwrap_or_else(|| {
AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(
"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,
attempt: u64,
) -> Result<TransactionReceipt, AggregatedProofSubmissionError> {
Comment thread
MarcosNicolau marked this conversation as resolved.
Outdated
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
.proof_aggregation_service
.verifyAggregationSP1(
Expand All @@ -343,81 +403,167 @@ impl ProofAggregator {
proof.proof_with_pub_values.bytes().into(),
self.sp1_chunk_aggregator_vk_hash_bytes.into(),
)
.sidecar(blob)
.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)?;
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)
.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(
err.to_string(),
)
})
.map_err(RetryError::Transient)?
AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!(
"Failed to fill transaction: {err}"
))
})?
.try_into_envelope()
.map_err(|err| {
AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(
err.to_string(),
)
})
.map_err(RetryError::Transient)?;
AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(format!(
"Failed to convert to envelope: {err}"
))
})?;

// Convert to EIP-4844 transaction
let tx: EthereumTxEnvelope<TxEip4844WithSidecar<BlobTransactionSidecarEip7594>> = envelope
.try_into_pooled()
.map_err(|err| {
AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(
err.to_string(),
)
})
.map_err(RetryError::Transient)?
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(
err.to_string(),
)
})
.map_err(RetryError::Transient)?;
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(
err.to_string(),
)
})
.map_err(RetryError::Transient)?;
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()
)),
),
}
}

let receipt = pending_tx
.get_receipt()
.await
.map_err(|err| {
AggregatedProofSubmissionError::SendVerifyAggregatedProofTransaction(
err.to_string(),
)
})
.map_err(RetryError::Transient)?;
// 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).
//
// 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
//
// The bumped price is: current_gas_price * (1 + total_bump_percentage / 100)
async fn apply_gas_fee_bump(
&self,
base_bump_percentage: u64,
retry_attempt_percentage: u64,
attempt: u64,
tx_req: alloy::rpc::types::TransactionRequest,
) -> Result<alloy::rpc::types::TransactionRequest, AggregatedProofSubmissionError> {
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);
}
}

Ok(current_tx_req)
}

Ok(receipt)
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(
Expand Down
6 changes: 6 additions & 0 deletions config-files/config-proof-aggregator-ethereum-package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ 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
base_bump_percentage: 10
retry_attempt_percentage: 2

ecdsa:
private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json"
private_key_store_password: ""
10 changes: 8 additions & 2 deletions config-files/config-proof-aggregator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ 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
bump_retry_interval_seconds: 120
base_bump_percentage: 10
retry_attempt_percentage: 2

ecdsa:
private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json"
Expand Down
Loading