diff --git a/anvil/anvil.go b/anvil/anvil.go index dd2700c9..59dd899b 100644 --- a/anvil/anvil.go +++ b/anvil/anvil.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "os/exec" + "strconv" "strings" "sync/atomic" "time" @@ -36,7 +37,8 @@ type Anvil struct { } const ( - host = "127.0.0.1" + host = "127.0.0.1" + anvilListeningLogStr = "Listening on" ) func New(log log.Logger, cfg *Config) *Anvil { @@ -55,22 +57,22 @@ func (a *Anvil) Start(ctx context.Context) error { return errors.New("anvil already started") } - tempFile, err := os.CreateTemp("", "genesis-*.json") - if err != nil { - return fmt.Errorf("error creating temporary genesis file: %w", err) - } - - _, err = tempFile.Write(a.cfg.Genesis) - if err != nil { - return fmt.Errorf("error writing to genesis file: %w", err) - } - - // Prep args args := []string{ "--host", host, "--chain-id", fmt.Sprintf("%d", a.cfg.ChainId), "--port", fmt.Sprintf("%d", a.cfg.Port), - "--init", tempFile.Name(), + } + + if len(a.cfg.Genesis) > 0 { + tempFile, err := os.CreateTemp("", "genesis-*.json") + if err != nil { + return fmt.Errorf("error creating temporary genesis file: %w", err) + } + if _, err = tempFile.Write(a.cfg.Genesis); err != nil { + return fmt.Errorf("error writing to genesis file: %w", err) + } + + args = append(args, "--init", tempFile.Name()) } anvilLog := a.log.New("role", "anvil", "chain.id", a.cfg.ChainId) @@ -81,6 +83,10 @@ func (a *Anvil) Start(ctx context.Context) error { a.resourceCancel() }() + // In the event anvil is started with port 0, we'll need to block + // and see what port anvil eventually binds to when started + anvilPortCh := make(chan uint64) + // Handle stdout/stderr // - TODO: Figure out best way to dump into logger. Some hex isn't showing appropriately stdout, err := a.cmd.StdoutPipe() @@ -94,7 +100,17 @@ func (a *Anvil) Start(ctx context.Context) error { go func() { scanner := bufio.NewScanner(stdout) for scanner.Scan() { - anvilLog.Info(scanner.Text()) + txt := scanner.Text() + anvilLog.Info(txt) + + // scan for port if applicable + if a.cfg.Port == 0 && strings.HasPrefix(txt, anvilListeningLogStr) { + port, err := strconv.ParseInt(strings.Split(txt, ":")[1], 10, 64) + if err != nil { + panic(fmt.Errorf("unexpected anvil listening port log: %w", err)) + } + anvilPortCh <- uint64(port) + } } }() go func() { @@ -110,8 +126,6 @@ func (a *Anvil) Start(ctx context.Context) error { } go func() { - defer os.Remove(tempFile.Name()) - if err := a.cmd.Wait(); err != nil { anvilLog.Error("anvil terminated with an error", "error", err) } else { @@ -120,6 +134,17 @@ func (a *Anvil) Start(ctx context.Context) error { a.stoppedCh <- struct{}{} }() + // wait & update the port if applicable. Since we're in the same routine to which `Start` + // is called, we're safe to overrwrite the `Port` field which the caller can observe + if a.cfg.Port == 0 { + done := ctx.Done() + select { + case a.cfg.Port = <-anvilPortCh: + case <-done: + return ctx.Err() + } + } + return nil } @@ -182,12 +207,9 @@ func waitForAnvilClientToBeReady(ctx context.Context, client *rpc.Client, timeou return fmt.Errorf("timed out waiting for response from client") case <-ticker.C: var result string - callErr := client.Call(&result, "web3_clientVersion") - - if callErr != nil { + if err := client.Call(&result, "web3_clientVersion"); err != nil { continue } - if strings.HasPrefix(result, "anvil") { return nil } diff --git a/anvil/anvil_test.go b/anvil/anvil_test.go new file mode 100644 index 00000000..90e213cf --- /dev/null +++ b/anvil/anvil_test.go @@ -0,0 +1,32 @@ +package anvil + +import ( + "context" + "testing" + + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" +) + +func TestAnvil(t *testing.T) { + cfg := Config{ChainId: 10, Port: 0} + testlog := testlog.Logger(t, log.LevelInfo) + anvil := New(testlog, &cfg) + + require.NoError(t, anvil.Start(context.Background())) + + // port overriden on startup + require.NotEqual(t, cfg.Port, 0) + + client, err := rpc.Dial(anvil.Endpoint()) + require.NoError(t, err) + + // query chainId + var chainId math.HexOrDecimal64 + require.NoError(t, client.CallContext(context.Background(), &chainId, "eth_chainId")) + require.Equal(t, uint64(chainId), cfg.ChainId) +} diff --git a/op-simulator/op_simulator.go b/op-simulator/op_simulator.go index bab439a1..54cf11a4 100644 --- a/op-simulator/op_simulator.go +++ b/op-simulator/op_simulator.go @@ -9,6 +9,7 @@ import ( "net/http/httputil" "net/url" "strconv" + "strings" "sync/atomic" ophttp "github.com/ethereum-optimism/optimism/op-service/httputil" @@ -16,6 +17,10 @@ import ( "github.com/ethereum/go-ethereum/log" ) +const ( + host = "127.0.0.1" +) + type Config struct { Port uint64 } @@ -30,10 +35,6 @@ type OpSimulator struct { cfg *Config } -const ( - host = "127.0.0.1" -) - func New(log log.Logger, cfg *Config, anvil *anvil.Anvil) *OpSimulator { return &OpSimulator{ log: log, @@ -47,18 +48,27 @@ func (opSim *OpSimulator) Start(ctx context.Context) error { if err != nil { return fmt.Errorf("error creating reverse proxy: %w", err) } + mux := http.NewServeMux() mux.Handle("/", proxy) - endpoint := net.JoinHostPort(host, strconv.Itoa(int(opSim.cfg.Port))) - hs, err := ophttp.StartHTTPServer(endpoint, mux) + hs, err := ophttp.StartHTTPServer(net.JoinHostPort(host, fmt.Sprintf("%d", opSim.cfg.Port)), mux) if err != nil { return fmt.Errorf("failed to start HTTP RPC server: %w", err) } - opSim.log.Info(fmt.Sprintf("listening on %v", endpoint), "chain.id", opSim.ChainId()) + opSim.log.Info("started op-simulator", "chain.id", opSim.ChainId(), "addr", hs.Addr()) opSim.httpServer = hs + if opSim.cfg.Port == 0 { + port, err := strconv.ParseInt(strings.Split(hs.Addr().String(), ":")[1], 10, 64) + if err != nil { + panic(fmt.Errorf("unexpected op-simulator listening port: %w", err)) + } + + opSim.cfg.Port = uint64(port) + } + return nil } diff --git a/supersim.go b/supersim.go index eecc999a..c8a9a392 100644 --- a/supersim.go +++ b/supersim.go @@ -32,17 +32,17 @@ var genesisL2JSON []byte var DefaultConfig = Config{ l1Chain: ChainConfig{ - anvilConfig: anvil.Config{ChainId: 1, Port: 8545, Genesis: genesisL1JSON}, - opSimConfig: opsim.Config{Port: 8546}, + anvilConfig: anvil.Config{ChainId: 1, Port: 0, Genesis: genesisL1JSON}, + opSimConfig: opsim.Config{Port: 0}, }, l2Chains: []ChainConfig{ { - anvilConfig: anvil.Config{ChainId: 10, Port: 9545, Genesis: genesisL2JSON}, - opSimConfig: opsim.Config{Port: 9546}, + anvilConfig: anvil.Config{ChainId: 10, Port: 0, Genesis: genesisL2JSON}, + opSimConfig: opsim.Config{Port: 0}, }, { - anvilConfig: anvil.Config{ChainId: 30, Port: 9555, Genesis: genesisL2JSON}, - opSimConfig: opsim.Config{Port: 9556}, + anvilConfig: anvil.Config{ChainId: 30, Port: 0, Genesis: genesisL2JSON}, + opSimConfig: opsim.Config{Port: 0}, }, }, } diff --git a/supersim_test.go b/supersim_test.go index 2be05b72..c738548c 100644 --- a/supersim_test.go +++ b/supersim_test.go @@ -30,7 +30,6 @@ type TestSuite struct { func createTestSuite(t *testing.T) *TestSuite { cfg := &DefaultConfig - testlog := testlog.Logger(t, log.LevelInfo) supersim := NewSupersim(testlog, cfg) t.Cleanup(func() {