Skip to content

fix: add retry with exponential backoff for UTXO loading rate limits#3935

Merged
jolestar merged 6 commits into
mainfrom
fix/utxo-rate-limit-retry
Jan 22, 2026
Merged

fix: add retry with exponential backoff for UTXO loading rate limits#3935
jolestar merged 6 commits into
mainfrom
fix/utxo-rate-limit-retry

Conversation

@jolestar

Copy link
Copy Markdown
Contributor

Summary

Fixes #3934

This PR adds retry logic with exponential backoff to handle HTTP 429 (Too Many Requests) errors when loading UTXOs in the bitcoin build-tx command.

Changes

  • Modified UTXOSelector::load_utxos() to retry on rate limit errors
  • Added exponential backoff (500ms base, 30s max delay, up to 5 retries)
  • Added is_rate_limit_error() helper to detect rate limiting errors
  • Added debug logging for retry attempts

Problem

Previously, when trying to build a transaction with a large number of UTXOs (e.g., 800+), the command would fail with a misleading error message. The root cause was that rapid pagination requests triggered the RPC server's rate limiting (HTTP 429), and there was no retry mechanism.

Testing

  • Build: make build
  • Lint: make lint-rust
  • Test: cargo test --package rooch bitcoin

Verification

The fix follows existing retry patterns in the codebase:

  • BitcoinClientActor (crates/bitcoin-client/src/actor/client.rs)
  • AvailFusionAdapter (crates/rooch-da/src/backend/openda/avail.rs)

🤖 Generated with Claude Code

- Add retry logic in UTXOSelector::load_utxos() to handle HTTP 429 errors
- Use exponential backoff (500ms base, 30s max) to avoid overwhelming server
- Detect rate limit errors by checking for 'too many requests', '429', 'serverisbusy', or 'wait for'
- Log retry attempts for debugging
- Fixes #3934

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings January 19, 2026 09:42
@vercel

vercel Bot commented Jan 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
rooch-portal-v2.1 Ready Ready Preview, Comment Jan 22, 2026 1:21am
test-portal Ready Ready Preview, Comment Jan 22, 2026 1:21am
1 Skipped Deployment
Project Deployment Review Updated (UTC)
rooch Ignored Ignored Preview Jan 22, 2026 1:21am

Request Review

@github-actions

github-actions Bot commented Jan 19, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds retry logic with exponential backoff to handle HTTP 429 (Too Many Requests) rate limiting errors when loading UTXOs during Bitcoin transaction building. The fix addresses issue #3934 where building transactions with 800+ UTXOs would fail due to rapid pagination requests triggering RPC rate limits without any retry mechanism.

Changes:

  • Added retry logic with exponential backoff (500ms base delay, 30s max, 5 retries) to UTXOSelector::load_utxos()
  • Added is_rate_limit_error() helper function to detect various rate limit error patterns
  • Added debug logging for retry attempts


use std::collections::VecDeque;

use anyhow::{bail, Result};

Copilot AI Jan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Context trait from anyhow needs to be imported to use the .context() method on line 144. Add Context to the import: use anyhow::{bail, Context, Result};

Suggested change
use anyhow::{bail, Result};
use anyhow::{bail, Context, Result};

Copilot uses AI. Check for mistakes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 10 comments.

Comment thread crates/rooch/src/commands/bitcoin/utxo_selector.rs Outdated
Comment thread crates/rooch/src/commands/bitcoin/transaction_builder.rs Outdated
Comment thread crates/rooch/src/commands/bitcoin/utxo_selector.rs Outdated
Comment on lines +259 to +273
// Build transaction with auto-loading (empty inputs = auto-load)
return build_single_transaction(
&context,
client,
sender,
bitcoin_network,
vec![], // Empty inputs triggers auto-loading
self.skip_check_seal,
self.fee_rate,
self.lock_time,
self.change_address,
specific_outputs,
self.output_file,
)
.await;

Copilot AI Jan 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using :all with send_all mode, the code loads all UTXOs, estimates fees, and then passes empty inputs to build_single_transaction (line 265). This causes the TransactionBuilder to reload all UTXOs again since empty inputs trigger auto-loading. This is inefficient and results in redundant UTXO queries. Consider using TransactionBuilder::with_utxos() instead with the already-loaded all_utxos to avoid the redundant query.

Copilot uses AI. Check for mistakes.
Comment on lines +247 to +257
// Convert :all to specific amount
let specific_outputs = self
.outputs
.into_iter()
.map(|mut output| {
if output.amount == OutputAmount::All {
output.amount = OutputAmount::Specific(output_amount);
}
output
})
.collect::<Vec<_>>();

Copilot AI Jan 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When send_all is true and multiple outputs are specified (e.g., -o addr1:all -o addr2:100), all outputs with :all will be assigned the same output_amount value. This could lead to unexpected behavior since the total would be split incorrectly. Consider validating that only one output can use :all, or properly distribute the total amount across all :all outputs.

Copilot uses AI. Check for mistakes.
Comment thread crates/rooch/src/commands/bitcoin/verify_psbt.rs Outdated
Comment thread crates/rooch/src/commands/bitcoin/utxo_selector.rs Outdated
Comment thread crates/rooch/src/commands/bitcoin/build_tx.rs Outdated
Comment on lines +454 to +459
format!(
"_{}_{}{}",
&base_path[..ext_pos],
chunk_idx + 1,
&base_path[ext_pos..]
)

Copilot AI Jan 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filename generation logic for split transactions is incorrect. When base_path contains a dot (e.g., "tx.psbt"), the format string produces "tx_1.psbt" instead of the intended "tx_1.psbt". The underscore should be after the base name, not before it. Change format string from "{}{}{}" to "{}{}{}" to fix this.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +87
let utxos_objs = self
.client
.rooch
.query_utxos(UTXOFilterView::object_ids(chunk.to_vec()), None, None, None)
.await?;

Copilot AI Jan 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The load_specific_utxos function does not implement retry logic for rate limiting errors, unlike load_utxos. When loading a large number of specific UTXOs (chunked into batches of 100), this could hit rate limits. Consider wrapping the query_utxos call with the same retry logic used in load_utxos to ensure consistency across the codebase.

Copilot uses AI. Check for mistakes.
@jolestar jolestar merged commit 9048c85 into main Jan 22, 2026
29 checks passed
@jolestar jolestar deleted the fix/utxo-rate-limit-retry branch January 22, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bitcoin build-tx fails with "Too many object IDs requested" when handling large number of UTXOs

2 participants