-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Description
Component
Anvil
Have you ensured that all of these are up to date?
- Foundry
- Foundryup
What version of Foundry are you on?
forge Version: 1.5.1-stable
What version of Foundryup are you on?
foundryup: 1.5.0
What command(s) is the bug in?
anvil --fork-transaction-hash
Operating System
Linux
Describe the bug
Bug Report: --fork-transaction-hash deadlocks when replaying same-sender transactions
Summary
When using --fork-transaction-hash in automine mode, Anvil deadlocks if a user-submitted transaction has a nonce dependency on one of the force_transactions. The force_transactions are never consumed because the miner waits for pool readiness, which never comes.
Reproduction Steps
-
Fork Ethereum mainnet at transaction
0xd839527f0b64a0f51f98a5e9e26639095ad78c099cd352cf2bd932e569b09a85(block 24,285,092, index 18). This transaction is from sender0xd7E1236C08731C3632519DCd1A581bFe6876a3B2with nonce 258009. -
Start Anvil:
anvil --fork-url $ETH_RPC_URL \ --fork-transaction-hash 0xd839... -
Impersonate the sender and submit the next transaction from the same sender (nonce 258010), which is
0x0000782fc71df22b624eff9a482f2354574392583e6ecc8cebff88e7113adaf5(index 19 in the same block):cast rpc anvil_impersonateAccount "0xd7E1236C08731C3632519DCd1A581bFe6876a3B2" cast send --from 0xd7E1236C08731C3632519DCd1A581bFe6876a3B2 ... --nonce 258010 -
The transaction is accepted into the pool but never mined.
evm_mineproduces empty blocks.txpool_statusshowsqueued: 1.
Run attached script with $ETH_RPC_URL set for a reproduction.
Root Cause
Flow of the deadlock
Startup:
- Anvil forks at block N-1 (24,285,091) and stores all block N transactions up to the fork tx as
force_transactionsin theMinerstruct. - The transaction pool is empty. The
ReadyTransactionMinerhas no notifications and returnsPoll::Pending.
Miner::poll (miner.rs:86-99):
pub fn poll(&mut self, pool: &Arc<Pool>, cx: &mut Context<'_>) -> Poll<Vec<Arc<PoolTransaction>>> {
self.inner.register(cx);
let next = ready!(self.mode.write().poll(pool, cx)); // <-- BLOCKS HERE
if let Some(mut transactions) = self.force_transactions.take() {
transactions.extend(next);
Poll::Ready(transactions)
} else {
Poll::Ready(next)
}
}The ready!() macro on the mining mode blocks. If the mode returns Poll::Pending (pool empty), force_transactions.take() is never reached.
User submits tx (same sender, nonce 258010):
request_noncereads on-chain nonce from the fork state (block N-1) = 258009required_marker(258010, 258009, sender)returns[to_marker(258009, sender)]PoolInner::add_transactionchecks the ready pool'sprovided_markers— empty, since force_transactions were never added to the poolmissing_markersis non-empty → tx goes to the pending queueAddedTransaction::Pending→notify_readydoes nothing → miner never wakes up
Result: Permanent deadlock. Force transactions are stuck in the Miner, the user's tx is stuck in the pending queue, and nothing can break the cycle.
Script to reproduce
The script does not terminate as the transaction is never mined
#!/usr/bin/env bash
set -euo pipefail
# PoC: --fork-transaction-hash deadlocks when replaying same-sender transactions
#
# This script demonstrates a deadlock in Anvil's automine mode when using
# --fork-transaction-hash. Force transactions are held outside the pool and
# never consumed because the miner waits for pool readiness, which never
# comes since the user's tx depends on an unmined force_transaction.
FORK_TX=0xd839527f0b64a0f51f98a5e9e26639095ad78c099cd352cf2bd932e569b09a85
REPLAY_TX=0x0000782fc71df22b624eff9a482f2354574392583e6ecc8cebff88e7113adaf5
ANVIL_PORT=18545
ANVIL_RPC="http://127.0.0.1:$ANVIL_PORT"
if [ -z "${ETH_RPC_URL:-}" ]; then
echo "ERROR: ETH_RPC_URL must be set to an Ethereum mainnet RPC endpoint"
exit 1
fi
# 1) Get replay tx details from mainnet
echo "=== Fetching replay transaction details ==="
TX_JSON=$(cast tx $REPLAY_TX --rpc-url "$ETH_RPC_URL" --json)
SENDER=$(echo "$TX_JSON" | jq -r '.from')
TO=$(echo "$TX_JSON" | jq -r '.to')
NONCE=$(echo "$TX_JSON" | jq -r '.nonce')
DATA=$(echo "$TX_JSON" | jq -r '.input')
GAS=$(echo "$TX_JSON" | jq -r '.gas')
VALUE=$(echo "$TX_JSON" | jq -r '.value')
echo "Sender: $SENDER, Nonce: $NONCE"
# 2) Start Anvil with --fork-transaction-hash
echo "=== Starting Anvil with --fork-transaction-hash ==="
anvil --fork-url "$ETH_RPC_URL" \
--fork-transaction-hash "$FORK_TX" \
--port $ANVIL_PORT &
ANVIL_PID=$!
trap "kill $ANVIL_PID 2>/dev/null; wait $ANVIL_PID 2>/dev/null" EXIT
sleep 5
# 3) Verify Anvil is running
echo "=== Verifying Anvil ==="
BLOCK=$(cast block-number --rpc-url "$ANVIL_RPC")
echo "Block number: $BLOCK"
# 4) Impersonate sender
echo "=== Impersonating sender ==="
cast rpc anvil_impersonateAccount "$SENDER" --rpc-url "$ANVIL_RPC"
# 5) Send the replay transaction via raw JSON-RPC (don't wait for receipt)
echo "=== Sending replay transaction ==="
RESULT=$(curl -s -X POST "$ANVIL_RPC" \
-H "Content-Type: application/json" \
--data "{
\"jsonrpc\":\"2.0\",
\"method\":\"eth_sendTransaction\",
\"params\":[{
\"from\":\"$SENDER\",
\"to\":\"$TO\",
\"nonce\":\"$NONCE\",
\"data\":\"$DATA\",
\"gas\":\"$GAS\",
\"value\":\"$VALUE\"
}],
\"id\":1
}")
TX_HASH=$(echo "$RESULT" | jq -r '.result // empty')
TX_ERROR=$(echo "$RESULT" | jq -r '.error // empty')
if [ -n "$TX_HASH" ]; then
echo "TX hash: $TX_HASH"
elif [ -n "$TX_ERROR" ]; then
echo "Error: $TX_ERROR"
exit 1
else
echo "Unexpected response: $RESULT"
exit 1
fi
# 6) Check txpool status - tx should be stuck in queued (BUG) or pending (FIXED)
echo "=== Checking txpool status ==="
cast rpc txpool_status --rpc-url "$ANVIL_RPC"
# 7) Try to mine
echo "=== Attempting to mine ==="
cast rpc evm_mine --rpc-url "$ANVIL_RPC"
# 8) Check again
echo "=== Checking txpool status after mine ==="
cast rpc txpool_status --rpc-url "$ANVIL_RPC"
# 9) Check receipt
echo "=== Checking receipt - This will keep going forever ==="
if cast receipt "$TX_HASH" --rpc-url "$ANVIL_RPC" 2>/dev/null; then
echo "Transaction was mined successfully (BUG IS FIXED)"
fi
Metadata
Metadata
Assignees
Labels
Type
Projects
Status