alloy-rs / alloy-rs/alloy

[Bug] PendingTransactionBuilder::get_receipt() can time out for >1 confirmations even when the receipt already exists

Đang mở
#3,916 0 bình luận 0 reaction 0 người được giao Xem trên GitHub
bug
Ngôn ngữ chính
Rust
Star
1.3k
Fork
668
Merge trung bình
2 ngày 1 giờ
Pull request đã merge (30 ngày)
29

Mô tả

### Component

provider, pubsub

### What version of Alloy are you on?

alloy v1.8.3

### Operating System

macOS (Intel)

### Describe the bug

## Summary

`PendingTransactionBuilder::with_required_confirmations(...).get_receipt()` can return a timeout from the tx watcher even though:

- the transaction was already mined successfully
- `eth_getTransactionReceipt(tx_hash)` returns a receipt
- enough blocks have already passed to satisfy the requested confirmation count

I reproduced this against local Anvil with HTTP polling.

## Expected behavior

If the receipt exists and the latest block is already at or past:

`receipt.block_number + required_confirmations - 1`

then `get_receipt()` should resolve successfully.

## Actual behavior

`get_receipt()` sometimes returns:

- `Err(PendingTransactionError::TxWatcher(_))`
- with message similar to `transaction was not confirmed within the timeout`

but a direct receipt lookup right after that returns a valid successful receipt.

## Minimal repro

### Cargo.toml

```toml
[package]
name = "alloy-heartbeat-repro"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
alloy = { version = "1.0", features = ["node-bindings", "provider-anvil-api", "signer-local"] }

### src/main.rs

use alloy::network::{ReceiptResponse, TransactionBuilder};
use alloy::node_bindings::Anvil;
use alloy::primitives::U256;
use alloy::providers::ext::AnvilApi;
use alloy::providers::{PendingTransactionError, Provider, ProviderBuilder, WalletProvider};
use alloy::rpc::client::RpcClient;
use alloy::rpc::types::TransactionRequest;
use std::time::Duration;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let anvil = Anvil::new().block_time(1).spawn();
let wallet = anvil.wallet().expect("dev wallet");

let client = RpcClient::builder()
.connect(&anvil.endpoint())
.await?
.with_poll_interval(Duration::from_millis(100));

let provider = ProviderBuilder::new().wallet(wallet).connect_client(client);

provider.anvil_set_auto_mine(false).await?;

for attempt in 1..=50 {
let tx = TransactionRequest::default()
.with_to(provider.default_signer_address())
.with_value(U256::from(1_u64));

let pending = provider.send_transaction(tx).await?;
let tx_hash = *pending.tx_hash();

let watcher = tokio::spawn(async move {
pending
.with_required_confirmations(3)
.with_timeout(Some(Duration::from_secs(10)))
.get_receipt()
.await
});

tokio::time::sleep(Duration::from_millis(10)).await;
provider.anvil_mine(Some(3), None).await?;

let watcher_result = watcher.await.expect("watch task panicked");
let direct_receipt = provider.get_transaction_receipt(tx_hash).await?;
let latest_block = provider.get_block_number().await?;

match (&watcher_result, &direct_receipt) {
(Err(PendingTransactionError::TxWatcher(_)), Some(receipt)) => {
println!(
"BUG reproduced on attempt {attempt}: tx_hash={tx_hash:?}, latest_block={latest_block}, receipt_block={:?}, receipt_status={}",
receipt.block_number(),
receipt.status()
);
return Ok(());
}
_ => {
println!(
"attempt {attempt}: watcher_ok={}, receipt_present={}, receipt_block={:?}",
watcher_result.is_ok(),
direct_receipt.is_some(),
direct_receipt.as_ref().and_then(|r| r.block_number())
);
}
}
}

anyhow::bail!("did not reproduce")
}
```
Run:

cargo run

## Reproduced output

Example failure:

BUG reproduced on attempt 2: tx_hash=..., latest_block=16, receipt_block=Some(4), receipt_status=true

So in that failing run:

- the receipt already existed
- the tx was mined in block 4
- latest block was 16
- required confirmations was 3

That should already be well past the confirmation target.

## Notes

From tracing, the timeout appears to come from the internal watcher path rather than receipt absence on the RPC side.

This looks like a race in the pending transaction confirmation tracking path for required_confirmations > 1.

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.