[Bug] PendingTransactionBuilder::get_receipt() can time out for >1 confirmations even when the receipt already exists
- 主要语言
- Rust
- 星标
- 1.3k
- 派生
- 668
- 平均合并
- 2 天 2 小时
- 30 天内合并 PR
- 29
描述
### 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.
贡献指南
评估
这个 Issue 还没有评估数据。