diff --git a/fault-proof/src/config.rs b/fault-proof/src/config.rs index 6606b09a8..308c515fd 100644 --- a/fault-proof/src/config.rs +++ b/fault-proof/src/config.rs @@ -78,6 +78,13 @@ pub struct ProposerConfig { /// Optional path to backup file for persisting proposer state across restarts. pub backup_path: Option, + + /// Maximum time (in seconds) to wait for an L1 transaction submitted by the proposer to + /// reach the required number of confirmations before the watcher gives up. Setting this + /// too low risks declaring "confirmation timeout" on transactions that actually land on + /// chain, which can produce duplicate sibling games on retry. Defaults to 180 to leave + /// comfortable headroom for mempool inclusion plus the configured confirmation depth. + pub tx_confirmation_timeout: u64, } /// Helper function to parse a comma-separated list of addresses @@ -139,6 +146,9 @@ impl ProposerConfig { .parse()?, proof_provider: ProofProviderConfig::from_env()?, backup_path: env::var("BACKUP_PATH").ok().map(PathBuf::from), + tx_confirmation_timeout: env::var("TX_CONFIRMATION_TIMEOUT") + .unwrap_or("180".to_string()) + .parse()?, }) } @@ -175,6 +185,7 @@ impl ProposerConfig { min_auction_period = self.proof_provider.min_auction_period, whitelist = ?self.proof_provider.whitelist, backup_path = ?self.backup_path, + tx_confirmation_timeout = self.tx_confirmation_timeout, "Proposer configuration loaded" ); } diff --git a/fault-proof/src/proposer.rs b/fault-proof/src/proposer.rs index 407ac7583..3fc6b3214 100644 --- a/fault-proof/src/proposer.rs +++ b/fault-proof/src/proposer.rs @@ -1118,7 +1118,11 @@ where let transaction_request = game.prove(agg_proof.bytes().into()).into_transaction_request(); let receipt = self .signer - .send_transaction_request(self.config.l1_rpc.clone(), transaction_request) + .send_transaction_request_with_timeout( + self.config.l1_rpc.clone(), + transaction_request, + self.config.tx_confirmation_timeout, + ) .await?; if !receipt.status() { @@ -1181,7 +1185,11 @@ where let receipt = self .signer - .send_transaction_request(self.config.l1_rpc.clone(), transaction_request) + .send_transaction_request_with_timeout( + self.config.l1_rpc.clone(), + transaction_request, + self.config.tx_confirmation_timeout, + ) .await?; if !receipt.status() { @@ -1311,7 +1319,11 @@ where let transaction_request = contract.resolve().into_transaction_request(); let receipt = self .signer - .send_transaction_request(self.config.l1_rpc.clone(), transaction_request) + .send_transaction_request_with_timeout( + self.config.l1_rpc.clone(), + transaction_request, + self.config.tx_confirmation_timeout, + ) .await?; if !receipt.status() { @@ -1337,7 +1349,11 @@ where contract.claimCredit(self.signer.address()).gas(300_000).into_transaction_request(); let receipt = self .signer - .send_transaction_request(self.config.l1_rpc.clone(), transaction_request) + .send_transaction_request_with_timeout( + self.config.l1_rpc.clone(), + transaction_request, + self.config.tx_confirmation_timeout, + ) .await?; if !receipt.status() { diff --git a/fault-proof/tests/common/process.rs b/fault-proof/tests/common/process.rs index 8817df925..d468911bc 100644 --- a/fault-proof/tests/common/process.rs +++ b/fault-proof/tests/common/process.rs @@ -49,6 +49,7 @@ pub async fn new_proposer( range_split_count: RangeSplitCount::one(), max_concurrent_range_proofs: NonZero::::MIN, backup_path, + tx_confirmation_timeout: 180, proof_provider: ProofProviderConfig { timeout: 14400, // 4 hours network_calls_timeout: 15, diff --git a/utils/signer/src/lib.rs b/utils/signer/src/lib.rs index b7508e093..be47de524 100644 --- a/utils/signer/src/lib.rs +++ b/utils/signer/src/lib.rs @@ -93,11 +93,24 @@ impl Signer { } } - /// Sends a transaction request, signed by the configured `signer`. + /// Sends a transaction request, signed by the configured `signer`, using the default + /// confirmation timeout of [`TIMEOUT_SECONDS`]. pub async fn send_transaction_request( + &self, + l1_rpc: Url, + transaction_request: TransactionRequest, + ) -> Result { + self.send_transaction_request_with_timeout(l1_rpc, transaction_request, TIMEOUT_SECONDS) + .await + } + + /// Sends a transaction request, signed by the configured `signer`, with a caller-supplied + /// confirmation timeout (in seconds). + pub async fn send_transaction_request_with_timeout( &self, l1_rpc: Url, mut transaction_request: TransactionRequest, + timeout_secs: u64, ) -> Result { match self { Signer::Web3Signer(signer_url, signer_address) => { @@ -126,7 +139,7 @@ impl Signer { .await .context("Failed to send transaction")? .with_required_confirmations(NUM_CONFIRMATIONS) - .with_timeout(Some(Duration::from_secs(TIMEOUT_SECONDS))) + .with_timeout(Some(Duration::from_secs(timeout_secs))) .get_receipt() .await?; @@ -151,7 +164,7 @@ impl Signer { .await .context("Failed to send transaction")? .with_required_confirmations(NUM_CONFIRMATIONS) - .with_timeout(Some(Duration::from_secs(TIMEOUT_SECONDS))) + .with_timeout(Some(Duration::from_secs(timeout_secs))) .get_receipt() .await?; @@ -177,7 +190,7 @@ impl Signer { .await .context("Failed to send KMS-signed transaction")? .with_required_confirmations(NUM_CONFIRMATIONS) - .with_timeout(Some(Duration::from_secs(TIMEOUT_SECONDS))) + .with_timeout(Some(Duration::from_secs(timeout_secs))) .get_receipt() .await?; @@ -212,8 +225,9 @@ impl SignerLock { self.cached_address } - /// Sends a transaction request, signed by the configured signer. - /// Transactions are serialized via a Mutex to prevent nonce conflicts. + /// Sends a transaction request, signed by the configured signer, using the default + /// confirmation timeout of [`TIMEOUT_SECONDS`]. Transactions are serialized via a Mutex + /// to prevent nonce conflicts. pub async fn send_transaction_request( &self, l1_rpc: Url, @@ -222,6 +236,20 @@ impl SignerLock { let signer = self.inner.lock().await; signer.send_transaction_request(l1_rpc, transaction_request).await } + + /// Sends a transaction request with a caller-supplied confirmation timeout (in seconds). + /// Transactions are serialized via a Mutex to prevent nonce conflicts. + pub async fn send_transaction_request_with_timeout( + &self, + l1_rpc: Url, + transaction_request: TransactionRequest, + timeout_secs: u64, + ) -> Result { + let signer = self.inner.lock().await; + signer + .send_transaction_request_with_timeout(l1_rpc, transaction_request, timeout_secs) + .await + } } #[cfg(test)]