@@ -23,7 +23,7 @@ use alloy::{
2323 eips:: { eip4844:: BYTES_PER_BLOB , eip7594:: BlobTransactionSidecarEip7594 , Encodable2718 } ,
2424 hex,
2525 network:: { EthereumWallet , TransactionBuilder } ,
26- primitives:: { utils:: parse_ether, Address , U256 } ,
26+ primitives:: { utils:: parse_ether, Address , TxHash , U256 } ,
2727 providers:: { PendingTransactionError , Provider , ProviderBuilder } ,
2828 rpc:: types:: { TransactionReceipt , TransactionRequest } ,
2929 signers:: local:: LocalSigner ,
@@ -54,6 +54,11 @@ pub enum AggregatedProofSubmissionError {
5454 GasPriceError ( String ) ,
5555}
5656
57+ enum SubmitOutcome {
58+ Confirmed ( TransactionReceipt ) ,
59+ Pending ( TxHash ) ,
60+ }
61+
5762pub struct ProofAggregator {
5863 engine : ZKVMEngine ,
5964 proof_aggregation_service : AlignedProofAggregationServiceContract ,
@@ -342,6 +347,8 @@ impl ProofAggregator {
342347
343348 let mut last_error: Option < AggregatedProofSubmissionError > = None ;
344349
350+ let mut pending_hashes: Vec < TxHash > = Vec :: with_capacity ( max_retries as usize ) ;
351+
345352 // Get the nonce once at the beginning and reuse it for all retries
346353 let nonce = self
347354 . proof_aggregation_service
@@ -364,23 +371,35 @@ impl ProofAggregator {
364371 // Wrap the entire transaction submission in a result to catch all errors, passing
365372 // the same nonce to all attempts
366373 let attempt_result = self
367- . try_submit_transaction (
368- & blob,
369- blob_versioned_hash,
370- aggregated_proof,
371- attempt as u64 ,
372- nonce,
373- )
374+ . try_submit_transaction ( & blob, blob_versioned_hash, aggregated_proof, nonce)
374375 . await ;
375376
376377 match attempt_result {
377- Ok ( receipt) => {
378+ Ok ( SubmitOutcome :: Confirmed ( receipt) ) => {
378379 info ! (
379380 "Transaction confirmed successfully on attempt {}" ,
380381 attempt + 1
381382 ) ;
382383 return Ok ( receipt) ;
383384 }
385+ Ok ( SubmitOutcome :: Pending ( tx_hash) ) => {
386+ warn ! (
387+ "Attempt {} timed out waiting for receipt; storing pending tx and continuing" ,
388+ attempt + 1
389+ ) ;
390+ pending_hashes. push ( tx_hash) ;
391+
392+ last_error = Some (
393+ AggregatedProofSubmissionError :: SendVerifyAggregatedProofTransaction (
394+ "Timed out waiting for receipt" . to_string ( ) ,
395+ ) ,
396+ ) ;
397+
398+ if attempt < max_retries - 1 {
399+ info ! ( "Retrying with bumped gas fees and same nonce {}..." , nonce) ;
400+ tokio:: time:: sleep ( Duration :: from_millis ( 500 ) ) . await ;
401+ }
402+ }
384403 Err ( err) => {
385404 warn ! ( "Attempt {} failed: {:?}" , attempt + 1 , err) ;
386405 last_error = Some ( err) ;
@@ -396,7 +415,36 @@ impl ProofAggregator {
396415 }
397416 }
398417
399- // If we exhausted all retries, return the last error
418+ // After exhausting all retry attempts, we iterate over every pending transaction hash
419+ // that was previously submitted with the same nonce but different gas parameters.
420+ // One of these transactions may have been included in a block while we were still
421+ // retrying and waiting on others. By explicitly checking the receipt for each hash,
422+ // we ensure we don't "lose" a transaction that was actually mined but whose receipt
423+ // we never observed due to timeouts during earlier attempts.
424+ for ( i, tx_hash) in pending_hashes. into_iter ( ) . enumerate ( ) {
425+ match self
426+ . proof_aggregation_service
427+ . provider ( )
428+ . get_transaction_receipt ( tx_hash)
429+ . await
430+ {
431+ Ok ( Some ( receipt) ) => {
432+ info ! ( "Pending tx #{} confirmed; returning receipt" , i + 1 ) ;
433+ return Ok ( receipt) ;
434+ }
435+ Ok ( None ) => {
436+ warn ! (
437+ "Pending tx #{} still no receipt yet (hash {})" ,
438+ i + 1 ,
439+ tx_hash
440+ ) ;
441+ }
442+ Err ( err) => {
443+ warn ! ( "Pending tx #{} receipt query failed: {:?}" , i + 1 , err) ;
444+ }
445+ }
446+ }
447+
400448 Err ( RetryError :: Transient ( last_error. unwrap_or_else ( || {
401449 AggregatedProofSubmissionError :: SendVerifyAggregatedProofTransaction (
402450 "Max retries exceeded with no error details" . to_string ( ) ,
@@ -409,9 +457,8 @@ impl ProofAggregator {
409457 blob : & BlobTransactionSidecar ,
410458 blob_versioned_hash : [ u8 ; 32 ] ,
411459 aggregated_proof : & AlignedProof ,
412- _attempt : u64 , // Check if this param is useful
413460 nonce : u64 ,
414- ) -> Result < TransactionReceipt , AggregatedProofSubmissionError > {
461+ ) -> Result < SubmitOutcome , AggregatedProofSubmissionError > {
415462 let retry_interval = Duration :: from_secs ( self . config . bump_retry_interval_seconds ) ;
416463 let base_bump_percentage = self . config . base_bump_percentage ;
417464 let max_fee_bump_percentage = self . config . max_fee_bump_percentage ;
@@ -504,27 +551,18 @@ impl ProofAggregator {
504551 ) )
505552 } ) ?;
506553
507- info ! (
508- "Transaction sent with nonce {}, waiting for confirmation..." ,
509- nonce
510- ) ;
554+ let tx_hash = * pending_tx. tx_hash ( ) ;
511555
512- // Wait for the receipt with timeout
513556 let receipt_result = tokio:: time:: timeout ( retry_interval, pending_tx. get_receipt ( ) ) . await ;
514557
515558 match receipt_result {
516- Ok ( Ok ( receipt) ) => Ok ( receipt) ,
559+ Ok ( Ok ( receipt) ) => Ok ( SubmitOutcome :: Confirmed ( receipt) ) ,
517560 Ok ( Err ( err) ) => Err (
518561 AggregatedProofSubmissionError :: SendVerifyAggregatedProofTransaction ( format ! (
519562 "Error getting receipt: {err}"
520563 ) ) ,
521564 ) ,
522- Err ( _) => Err (
523- AggregatedProofSubmissionError :: SendVerifyAggregatedProofTransaction ( format ! (
524- "Transaction timeout after {} seconds" ,
525- retry_interval. as_secs( )
526- ) ) ,
527- ) ,
565+ Err ( _) => Ok ( SubmitOutcome :: Pending ( tx_hash) ) ,
528566 }
529567 }
530568
0 commit comments