Skip to content

anvil --fork-transaction-hash deadlocks when replaying same-sender transactions #13630

@ritzdorf

Description

@ritzdorf

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

  1. Fork Ethereum mainnet at transaction 0xd839527f0b64a0f51f98a5e9e26639095ad78c099cd352cf2bd932e569b09a85 (block 24,285,092, index 18). This transaction is from sender 0xd7E1236C08731C3632519DCd1A581bFe6876a3B2 with nonce 258009.

  2. Start Anvil:

    anvil --fork-url $ETH_RPC_URL \
          --fork-transaction-hash 0xd839...
  3. 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
  4. The transaction is accepted into the pool but never mined. evm_mine produces empty blocks. txpool_status shows queued: 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_transactions in the Miner struct.
  • The transaction pool is empty. The ReadyTransactionMiner has no notifications and returns Poll::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_nonce reads on-chain nonce from the fork state (block N-1) = 258009
  • required_marker(258010, 258009, sender) returns [to_marker(258009, sender)]
  • PoolInner::add_transaction checks the ready pool's provided_markers — empty, since force_transactions were never added to the pool
  • missing_markers is non-empty → tx goes to the pending queue
  • AddedTransaction::Pendingnotify_ready does 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

No one assigned

    Labels

    Type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions