Skip to content

feat: add fork block number and selector on fork exit#61

Open
Mouradif wants to merge 1 commit into
mainfrom
feat/fork-block-number
Open

feat: add fork block number and selector on fork exit#61
Mouradif wants to merge 1 commit into
mainfrom
feat/fork-block-number

Conversation

@Mouradif
Copy link
Copy Markdown
Contributor

@Mouradif Mouradif commented Mar 17, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for forking at a specific block number via the --fork-block-number flag in the fork enter command.
    • Improved fork exit experience with interactive fork selection when multiple forks are active.
  • Tests

    • Added integration tests for forking at specific block numbers and exiting multiple forks.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 17, 2026

📝 Walkthrough

Walkthrough

This pull request introduces fork block number specification for Anvil forks and adds interactive selection when exiting multiple active forks. The ForkBlockNumber parameter is propagated from CLI flag through use case parameters, domain models, and the Anvil adapter. Fork exit logic now detects and handles multiple active forks with optional user prompting.

Changes

Cohort / File(s) Summary
Fork Block Number Support
internal/adapters/anvil/manager.go, internal/cli/fork.go, internal/domain/anvil.go, internal/usecase/enter_fork.go
Added ForkBlockNumber field and CLI flag (--fork-block-number) to enable forking at a specific block number. Parameter threaded through CLI, use case, domain models, and Anvil adapter command generation.
Multiple Fork Exit Handling
internal/cli/fork.go, internal/domain/fork.go
Refactored fork exit logic to support multiple active forks with interactive selection. Added ActiveNetworks() helper method to ForkState and selectForkNetwork() picker using promptui for user prompting when multiple forks are active.
Integration Tests
test/integration/fork_test.go
Added TestForkEnterBlockNumber for block number forking validation, TestForkExitMultipleForks covering multiple fork scenarios, and helper functions (mineBlocks, ethBlockNumber) for test support. Extended TestForkExternalStatus coverage.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI: fork enter
    participant UC as EnterFork<br/>UseCase
    participant Domain as AnvilInstance<br/>(domain)
    participant Adapter as Anvil<br/>Adapter
    participant Anvil as Anvil<br/>Process

    CLI->>UC: EnterForkParams{ForkBlockNumber: N}
    activate UC
    UC->>Domain: Create AnvilInstance<br/>with ForkBlockNumber
    activate Domain
    Domain->>Domain: Store ForkBlockNumber field
    deactivate Domain
    UC->>Adapter: Build startup args<br/>from AnvilInstance
    activate Adapter
    Adapter->>Adapter: If ForkBlockNumber > 0:<br/>append --fork-block-number N
    deactivate Adapter
    UC->>Anvil: Execute anvil<br/>--fork-url ... --fork-block-number N
    activate Anvil
    Anvil->>Anvil: Fork at specified<br/>block number
    deactivate Anvil
    deactivate UC
Loading
sequenceDiagram
    participant CLI as CLI: fork exit
    participant UC as ExitFork<br/>UseCase
    participant Repo as Fork<br/>Repository
    participant State as ForkState
    participant Picker as Interactive<br/>Picker

    CLI->>Repo: Load fork state
    activate Repo
    Repo->>State: Load active forks
    activate State
    State->>State: Deserialize fork-state.json
    deactivate State
    deactivate Repo
    
    alt Multiple forks active
        Repo->>State: ActiveNetworks()
        State-->>Repo: []string{networks...}
        Repo->>Picker: Prompt user to select
        activate Picker
        Picker-->>Picker: Display fork options
        Picker-->>CLI: Return selected network
        deactivate Picker
    else Single fork active
        Repo->>Repo: Auto-select fork
    else No forks active
        Repo-->>CLI: Error
    end
    
    CLI->>UC: Exit selected fork
    activate UC
    UC->>UC: Cleanup fork state
    deactivate UC
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 A bunker's burrow goes deep, block by block we creep,
Fork at 99, exit with a line,
Pick your fork when many align,
Hop through states both old and new,
Anvil dreams in every hue!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding fork block number support and a selector mechanism for fork exit.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/fork-block-number
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can customize the high-level summary generated by CodeRabbit.

Configure the reviews.high_level_summary_instructions setting to provide custom instructions for generating the high-level summary.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
test/integration/fork_test.go (1)

1688-1734: Extend this block-pin coverage through fork restart.

This case only exercises the initial fork enter path. The risky part of this feature is rehydrating the fork from saved state, so without a restart step the suite won't catch a pinned fork restarting at latest.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/fork_test.go` around lines 1688 - 1734, The test
TestForkEnterBlockNumber only checks the initial fork enter; add a step to run
the restart path and rehydrate from saved state: after asserting the fork was
entered and before cleanupForkAnvil, invoke the CLI equivalent of "fork restart
anvil-31337" via ctx.TrebContext.Treb, wait for the restarted process, re-read
state with readForkState and verify the fork entry still exists, the restarted
anvil is alive (isProcessAlive) and ethBlockNumber(fork.ForkURL) still equals
forkBlock; also assert the PID changed if you want to ensure a true restart.
Ensure you handle errors with require.NoError and reuse existing helpers like
readForkState, ethBlockNumber, cleanupForkAnvil.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/cli/fork.go`:
- Around line 180-205: The help text and code diverge: the no-arg fork exit used
to fall back to the currently configured network but this branch always selects
from state.Forks; fix by restoring the old fallback or updating help.
Concretely, before loading/inspecting state.Forks (ForkStateStore.Load) check
the configured network value (the CLI/app config field that holds the current
network) and if it is non-empty and corresponds to an active fork in
state.Forks, set network = configuredNetwork and skip the interactive selection;
otherwise keep the existing switch that uses selectForkNetwork and
app.Config.NonInteractive. Alternatively, if you prefer the new behavior, update
the command help to say the no-arg form chooses from active forks instead of
using the configured network.

In `@internal/usecase/enter_fork.go`:
- Around line 148-154: The code only sets ForkBlockNumber on the transient
AnvilInstance but doesn't persist it to domain.ForkEntry, so a restarted fork
loses the pinned block; update the logic in the enter/fork creation path (the
function that builds the domain.ForkEntry and calls NewAnvilInstance) to copy
params.ForkBlockNumber into the persisted domain.ForkEntry (e.g., set
ForkEntry.ForkBlockNumber = params.ForkBlockNumber) and ensure when rebuilding
the instance from a saved entry (the code that constructs AnvilInstance from
domain.ForkEntry on restart) you restore that value onto the reconstructed
AnvilInstance.ForkBlockNumber so pinned-block restarts correctly.
- Around line 49-55: The EnterFork use case must reject the combination of an
ExternalURL with a non-zero ForkBlockNumber because executeExternal ignores
ForkBlockNumber; add a validation early in the use case (e.g., in the method
that orchestrates entering a fork such as Execute/Enter or the public Run method
that consumes the struct with fields Network, ExternalURL, ForkBlockNumber) that
returns an explicit error when ExternalURL is non-empty and ForkBlockNumber !=
0. Reference the ForkBlockNumber field and the executeExternal path in the error
message (e.g., "cannot pin fork block when attaching to external Anvil") so
callers get a clear failure instead of silently dropping the block pin. Ensure
unit tests or callers that expect this validation are updated accordingly.

---

Nitpick comments:
In `@test/integration/fork_test.go`:
- Around line 1688-1734: The test TestForkEnterBlockNumber only checks the
initial fork enter; add a step to run the restart path and rehydrate from saved
state: after asserting the fork was entered and before cleanupForkAnvil, invoke
the CLI equivalent of "fork restart anvil-31337" via ctx.TrebContext.Treb, wait
for the restarted process, re-read state with readForkState and verify the fork
entry still exists, the restarted anvil is alive (isProcessAlive) and
ethBlockNumber(fork.ForkURL) still equals forkBlock; also assert the PID changed
if you want to ensure a true restart. Ensure you handle errors with
require.NoError and reuse existing helpers like readForkState, ethBlockNumber,
cleanupForkAnvil.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cf068acf-7b9a-44c6-8f93-2ef22e470f64

📥 Commits

Reviewing files that changed from the base of the PR and between 8e396cb and dfc77ba.

📒 Files selected for processing (6)
  • internal/adapters/anvil/manager.go
  • internal/cli/fork.go
  • internal/domain/anvil.go
  • internal/domain/fork.go
  • internal/usecase/enter_fork.go
  • test/integration/fork_test.go

Comment thread internal/cli/fork.go
Comment on lines +180 to 205
// No network specified and not --all: check if multiple forks are active
state, err := app.ForkStateStore.Load(ctx)
if err != nil {
return fmt.Errorf("failed to load fork state: %w", err)
}

switch len(state.Forks) {
case 0:
return fmt.Errorf("no active forks")
case 1:
// Only one fork — use it directly
for name := range state.Forks {
network = name
}
default:
// Multiple forks — prompt user to select
if app.Config.NonInteractive {
return fmt.Errorf("multiple active forks. Specify a network or use --all")
}

selected, err := selectForkNetwork(state.ActiveNetworks())
if err != nil {
return err
}
network = selected
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep the no-arg fork exit help text in sync with this behavior.

Lines 155-156 still say the no-arg form uses the currently configured network, but this branch now ignores that and chooses from the active-fork set instead. Either preserve the old fallback before this switch or update the command help in the same change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cli/fork.go` around lines 180 - 205, The help text and code diverge:
the no-arg fork exit used to fall back to the currently configured network but
this branch always selects from state.Forks; fix by restoring the old fallback
or updating help. Concretely, before loading/inspecting state.Forks
(ForkStateStore.Load) check the configured network value (the CLI/app config
field that holds the current network) and if it is non-empty and corresponds to
an active fork in state.Forks, set network = configuredNetwork and skip the
interactive selection; otherwise keep the existing switch that uses
selectForkNetwork and app.Config.NonInteractive. Alternatively, if you prefer
the new behavior, update the command help to say the no-arg form chooses from
active forks instead of using the configured network.

Comment on lines +49 to 55
Network string // network name from foundry.toml
RPCURL string // resolved RPC URL (after env var expansion)
ChainID uint64 // chain ID
EnvVarName string // env var name that foundry.toml uses for the RPC endpoint
ExternalURL string // optional external Anvil endpoint URL (skips local Anvil startup)
ForkBlockNumber uint64 // optional block number to fork at (0 = latest)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject ForkBlockNumber when attaching to an external Anvil.

executeExternal ignores this field, so callers can pass ExternalURL and ForkBlockNumber together and silently attach to whatever head the external endpoint is already on. Validate that combination in the use case and return an explicit error instead of dropping the block pin.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/usecase/enter_fork.go` around lines 49 - 55, The EnterFork use case
must reject the combination of an ExternalURL with a non-zero ForkBlockNumber
because executeExternal ignores ForkBlockNumber; add a validation early in the
use case (e.g., in the method that orchestrates entering a fork such as
Execute/Enter or the public Run method that consumes the struct with fields
Network, ExternalURL, ForkBlockNumber) that returns an explicit error when
ExternalURL is non-empty and ForkBlockNumber != 0. Reference the ForkBlockNumber
field and the executeExternal path in the error message (e.g., "cannot pin fork
block when attaching to external Anvil") so callers get a clear failure instead
of silently dropping the block pin. Ensure unit tests or callers that expect
this validation are updated accordingly.

Comment on lines +148 to +154
Name: fmt.Sprintf("fork-%s", params.Network),
Port: fmt.Sprintf("%d", port),
ChainID: fmt.Sprintf("%d", params.ChainID),
ForkURL: params.RPCURL,
ForkBlockNumber: params.ForkBlockNumber,
PidFile: filepath.Join(privDir, fmt.Sprintf("fork-%s.pid", params.Network)),
LogFile: filepath.Join(privDir, fmt.Sprintf("fork-%s.log", params.Network)),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Persist the pinned block number in fork state.

This only sets ForkBlockNumber on the transient AnvilInstance. fork restart later reconstructs a fresh instance from persisted domain.ForkEntry data without this field, so a fork entered at a specific block comes back at latest. Save the value on ForkEntry and restore it when rebuilding the instance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/usecase/enter_fork.go` around lines 148 - 154, The code only sets
ForkBlockNumber on the transient AnvilInstance but doesn't persist it to
domain.ForkEntry, so a restarted fork loses the pinned block; update the logic
in the enter/fork creation path (the function that builds the domain.ForkEntry
and calls NewAnvilInstance) to copy params.ForkBlockNumber into the persisted
domain.ForkEntry (e.g., set ForkEntry.ForkBlockNumber = params.ForkBlockNumber)
and ensure when rebuilding the instance from a saved entry (the code that
constructs AnvilInstance from domain.ForkEntry on restart) you restore that
value onto the reconstructed AnvilInstance.ForkBlockNumber so pinned-block
restarts correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant