Skip to content

fix(anvil): serialize rollback against mining to stop a mine-race panic - #16711

Open
gomesalexandre wants to merge 3 commits into
foundry-rs:masterfrom
gomesalexandre:fix_anvil_mine_rollback_race
Open

fix(anvil): serialize rollback against mining to stop a mine-race panic#16711
gomesalexandre wants to merge 3 commits into
foundry-rs:masterfrom
gomesalexandre:fix_anvil_mine_rollback_race

Conversation

@gomesalexandre

Copy link
Copy Markdown
Contributor

anvil_rollback/anvil_reorg could unwind the chain while a concurrent multi-block evm_mine_detailed call was still mining, because do_mine_block takes the mining lock per-block but rollback never took it at all.

Depending on exactly when the rollback landed, this surfaced as one of two things:

  • A confusing BlockNotFound mid-mine - an in-flight mine_one losing the parent it was building on.
  • An attempt to subtract with overflow panic in evm_mine_detailed's post-mining block lookup (crates/anvil/src/eth/api.rs), which reads best_number() once mining finishes and assumes it's still at least mined_blocks - 1 higher than the height before the call started.

Fix

  • Backend::rollback now holds the same self.mining lock do_mine_block already takes, so it can't interleave with an in-flight mine.
  • evm_mine_detailed uses checked_sub instead of a bare subtraction when re-deriving each mined block's number from the post-mining height, skipping a block number that no longer exists instead of panicking or wrapping. This is defense in depth - the chain height can still move between individual mine iterations, not just around the rollback lock - so it's kept even with the lock fix.

Verification

Added evm_mine_detailed_handles_concurrent_rollback: a multi-threaded-runtime test that races evm_mine_detailed(blocks: 50) against repeated anvil_rollback calls.

  • Against the original code: reliably fails, showing both failure modes above (BlockNotFound before the lock fix, then the arithmetic panic once only the lock fix was applied and the narrower post-loop race was exposed).
  • With both fixes applied: passes consistently across repeated runs.

cargo test -p anvil --lib for the touched modules (eth::api::tests, plus the existing rollback-related backend test) all green.

receipts

$ cargo test -p anvil --lib "eth::api::tests"
running 7 tests
test eth::api::tests::fractional_reward_percentiles_use_cache_resolution ... ok
test eth::api::tests::shortened_pre_fork_fee_history_uses_upstream_start ... ok
test eth::api::tests::fee_history_rejects_invalid_reward_percentiles ... ok
test eth::api::tests::fee_history_is_complete_when_cache_entries_are_missing ... ok
test eth::api::tests::evm_mine_detailed_handles_concurrent_rollback ... ok
test eth::api::tests::memory_reset_stages_live_fees_after_active_mining ... ok
test eth::api::tests::set_rpc_url_installs_context_equivalent_identity_with_new_instance ... ok
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 120 filtered out

$ cargo test -p anvil --lib rollback
test eth::backend::mem::storage::tests::test_remove_block_states_on_rollback ... ok
test eth::api::tests::evm_mine_detailed_handles_concurrent_rollback ... ok
test result: ok. 2 passed; 0 failed

🤖 Generated with Claude Code

anvil_rollback/anvil_reorg could unwind the chain while a concurrent
multi-block evm_mine_detailed call was still mining, since do_mine_block
takes the mining lock per-block but rollback never took it at all. Depending
on timing this surfaced as either a confusing BlockNotFound mid-mine (an
in-flight mine_one losing the parent it was building on) or an "attempt to
subtract with overflow" panic in evm_mine_detailed's post-mining block
lookup, which assumed the chain height read right after mining still covered
every block it had just mined.

rollback now holds the same mining lock do_mine_block does, and
evm_mine_detailed uses checked_sub instead of assuming that subtraction
always fits, so a block that no longer exists is skipped rather than
panicking or wrapping.

Added a regression test that races evm_mine_detailed(blocks: 50) against
anvil_rollback on a multi-threaded runtime - confirmed it reliably fails
against the old code (both failure modes) and passes consistently with the
fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lh6V2uPTUqauqq45BM7m5k
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

✅ Changelog found

The deterministic check will validate the changed entry.

decofe
decofe previously approved these changes Sep 7, 2026
Hold the mining guard across complete multi-block mining and detailed result collection. Use the same guarded operations for rollback and reorg so height selection and state transitions cannot interleave.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants