diff --git a/anvil/anvil.go b/anvil/anvil.go index 536c5083..8bab1168 100644 --- a/anvil/anvil.go +++ b/anvil/anvil.go @@ -19,9 +19,10 @@ import ( ) type Config struct { - ChainId uint64 - Port uint64 - Genesis []byte + ChainID uint64 + SourceChainID uint64 + Port uint64 + Genesis []byte } type Anvil struct { @@ -63,7 +64,7 @@ func (a *Anvil) Start(ctx context.Context) error { args := []string{ "--host", host, - "--chain-id", fmt.Sprintf("%d", a.cfg.ChainId), + "--chain-id", fmt.Sprintf("%d", a.cfg.ChainID), "--port", fmt.Sprintf("%d", a.cfg.Port), } @@ -79,7 +80,7 @@ func (a *Anvil) Start(ctx context.Context) error { args = append(args, "--init", tempFile.Name()) } - anvilLog := a.log.New("role", "anvil", "chain.id", a.cfg.ChainId) + anvilLog := a.log.New("role", "anvil", "chain.id", a.cfg.ChainID) anvilLog.Info("starting anvil", "args", args) a.cmd = exec.CommandContext(a.resourceCtx, "anvil", args...) go func() { @@ -92,7 +93,7 @@ func (a *Anvil) Start(ctx context.Context) error { anvilPortCh := make(chan uint64) // Handle stdout/stderr - logFile, err := os.CreateTemp("", fmt.Sprintf("anvil-chain-%d-", a.cfg.ChainId)) + logFile, err := os.CreateTemp("", fmt.Sprintf("anvil-chain-%d-", a.cfg.ChainID)) if err != nil { return fmt.Errorf("failed to create temp log file: %w", err) } @@ -187,8 +188,12 @@ func (a *Anvil) Endpoint() string { return fmt.Sprintf("http://%s:%d", host, a.cfg.Port) } -func (a *Anvil) ChainId() uint64 { - return a.cfg.ChainId +func (a *Anvil) ChainID() uint64 { + return a.cfg.ChainID +} + +func (a *Anvil) SourceChainID() uint64 { + return a.cfg.SourceChainID } func (a *Anvil) LogPath() string { @@ -221,3 +226,9 @@ func (a *Anvil) WaitUntilReady(ctx context.Context) error { } } } + +func (a *Anvil) String() string { + var b strings.Builder + fmt.Fprintf(&b, "Chain ID: %d RPC: %s LogPath: %s", a.ChainID(), a.Endpoint(), a.LogPath()) + return b.String() +} diff --git a/anvil/anvil_test.go b/anvil/anvil_test.go index a19ef1ed..a4bd8d12 100644 --- a/anvil/anvil_test.go +++ b/anvil/anvil_test.go @@ -13,7 +13,7 @@ import ( ) func TestAnvil(t *testing.T) { - cfg := Config{ChainId: 10, Port: 0} + cfg := Config{ChainID: 10, Port: 0} testlog := testlog.Logger(t, log.LevelInfo) anvil := New(testlog, &cfg) @@ -28,5 +28,5 @@ func TestAnvil(t *testing.T) { // query chainId var chainId math.HexOrDecimal64 require.NoError(t, client.CallContext(context.Background(), &chainId, "eth_chainId")) - require.Equal(t, uint64(chainId), cfg.ChainId) + require.Equal(t, uint64(chainId), cfg.ChainID) } diff --git a/cmd/main.go b/cmd/main.go index 5dda1c92..37843200 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -52,5 +52,10 @@ func SupersimMain(ctx *cli.Context, closeApp context.CancelCauseFunc) (cliapp.Li } // use config and setup supersim - return supersim.NewSupersim(log, &supersim.DefaultConfig), nil + s, err := supersim.NewSupersim(log, &supersim.DefaultConfig) + if err != nil { + return nil, fmt.Errorf("failed to create supersim") + } + + return s, nil } diff --git a/op-simulator/op_simulator.go b/op-simulator/op_simulator.go index 54cf11a4..4f4f2a07 100644 --- a/op-simulator/op_simulator.go +++ b/op-simulator/op_simulator.go @@ -22,7 +22,8 @@ const ( ) type Config struct { - Port uint64 + Port uint64 + SourceChainID uint64 } type OpSimulator struct { @@ -57,7 +58,7 @@ func (opSim *OpSimulator) Start(ctx context.Context) error { return fmt.Errorf("failed to start HTTP RPC server: %w", err) } - opSim.log.Info("started op-simulator", "chain.id", opSim.ChainId(), "addr", hs.Addr()) + opSim.log.Info("started op-simulator", "chain.id", opSim.ChainID(), "addr", hs.Addr()) opSim.httpServer = hs if opSim.cfg.Port == 0 { @@ -104,6 +105,16 @@ func (opSim *OpSimulator) Endpoint() string { return fmt.Sprintf("http://%s:%d", host, opSim.cfg.Port) } -func (opSim *OpSimulator) ChainId() uint64 { - return opSim.anvil.ChainId() +func (opSim *OpSimulator) ChainID() uint64 { + return opSim.anvil.ChainID() +} + +func (opSim *OpSimulator) SourceChainID() uint64 { + return opSim.cfg.SourceChainID +} + +func (opSim *OpSimulator) String() string { + var b strings.Builder + fmt.Fprintf(&b, "Chain ID: %d RPC: %s LogPath: %s", opSim.ChainID(), opSim.Endpoint(), opSim.anvil.LogPath()) + return b.String() } diff --git a/genesis/genesis-l1.json b/orchestrator/genesisstates/genesis-l1.json similarity index 100% rename from genesis/genesis-l1.json rename to orchestrator/genesisstates/genesis-l1.json diff --git a/genesis/genesis-l2.json b/orchestrator/genesisstates/genesis-l2.json similarity index 100% rename from genesis/genesis-l2.json rename to orchestrator/genesisstates/genesis-l2.json diff --git a/orchestrator/orchestrator.go b/orchestrator/orchestrator.go new file mode 100644 index 00000000..704fffd5 --- /dev/null +++ b/orchestrator/orchestrator.go @@ -0,0 +1,198 @@ +package orchestrator + +import ( + "context" + _ "embed" + "fmt" + "strings" + "sync" + + "github.com/ethereum-optimism/supersim/anvil" + op_simulator "github.com/ethereum-optimism/supersim/op-simulator" + + "github.com/ethereum/go-ethereum/log" +) + +type ChainConfig struct { + Port uint64 + ChainID uint64 + // The base chain ID when the chain is a rollup. + // If set to 0, the chain is considered an L1 chain + SourceChainID uint64 +} + +type OrchestratorConfig struct { + ChainConfigs []ChainConfig +} + +type Orchestrator struct { + log log.Logger + + OpSimInstances []*op_simulator.OpSimulator + anvilInstances []*anvil.Anvil +} + +//go:embed genesisstates/genesis-l1.json +var genesisL1JSON []byte + +//go:embed genesisstates/genesis-l2.json +var genesisL2JSON []byte + +func NewOrchestrator(log log.Logger, config *OrchestratorConfig) (*Orchestrator, error) { + var opSimInstances []*op_simulator.OpSimulator + var anvilInstances []*anvil.Anvil + + l1Count := 0 + for _, config := range config.ChainConfigs { + if config.SourceChainID == 0 { + l1Count++ + } + } + + if l1Count > 1 { + return nil, fmt.Errorf("supersim does not support more than one l1") + } + + for _, chainConfig := range config.ChainConfigs { + genesis := genesisL2JSON + if chainConfig.SourceChainID == 0 { + genesis = genesisL1JSON + } + anvil := anvil.New(log, &anvil.Config{ChainID: chainConfig.ChainID, SourceChainID: chainConfig.SourceChainID, Genesis: genesis}) + anvilInstances = append(anvilInstances, anvil) + // Only create Op Simulators for L2 chains. + if chainConfig.SourceChainID != 0 { + opSimInstances = append(opSimInstances, op_simulator.New(log, &op_simulator.Config{Port: chainConfig.Port, SourceChainID: chainConfig.SourceChainID}, anvil)) + } + } + + return &Orchestrator{log, opSimInstances, anvilInstances}, nil +} + +func (o *Orchestrator) Start(ctx context.Context) error { + o.log.Info("starting orchestrator") + + for _, anvilInstance := range o.anvilInstances { + if err := anvilInstance.Start(ctx); err != nil { + return fmt.Errorf("anvil instance chain.id=%v failed to start: %w", anvilInstance.ChainID(), err) + } + } + for _, opSimInstance := range o.OpSimInstances { + if err := opSimInstance.Start(ctx); err != nil { + return fmt.Errorf("op simulator instance chain.id=%v failed to start: %w", opSimInstance.ChainID(), err) + } + } + + if err := o.WaitUntilReady(); err != nil { + return fmt.Errorf("orchestrator failed to get ready: %w", err) + } + + o.log.Info("orchestrator is ready") + + return nil +} + +func (o *Orchestrator) Stop(ctx context.Context) error { + o.log.Info("stopping orchestrator") + + for _, opSim := range o.OpSimInstances { + if err := opSim.Stop(ctx); err != nil { + return fmt.Errorf("op simulator chain.id=%v failed to stop: %w", opSim.ChainID(), err) + } + o.log.Info("stopped op simulator", "chain.id", opSim.ChainID()) + } + for _, anvil := range o.anvilInstances { + if err := anvil.Stop(); err != nil { + return fmt.Errorf("anvil chain.id=%v failed to stop: %w", anvil.ChainID(), err) + } + o.log.Info("stopped anvil", "chain.id", anvil.ChainID()) + } + + o.log.Info("stopped orchestrator") + + return nil +} + +func (o *Orchestrator) Stopped() bool { + for _, anvil := range o.anvilInstances { + if stopped := anvil.Stopped(); !stopped { + return stopped + } + } + for _, opSim := range o.OpSimInstances { + if stopped := opSim.Stopped(); !stopped { + return stopped + } + } + + return true +} + +func (o *Orchestrator) WaitUntilReady() error { + var once sync.Once + var err error + ctx, cancel := context.WithCancel(context.Background()) + + handleErr := func(e error) { + if e == nil { + return + } + + once.Do(func() { + err = e + cancel() + }) + } + + var wg sync.WaitGroup + + waitForAnvil := func(anvil *anvil.Anvil) { + defer wg.Done() + handleErr(anvil.WaitUntilReady(ctx)) + } + + o.iterateAnvilInstances(func(chain *anvil.Anvil) { + wg.Add(1) + go waitForAnvil(chain) + }) + + wg.Wait() + + return err +} + +func (o *Orchestrator) iterateAnvilInstances(fn func(anvil *anvil.Anvil)) { + for _, anvilInstance := range o.anvilInstances { + fn(anvilInstance) + } +} + +func (o *Orchestrator) L1Anvil() *anvil.Anvil { + var result *anvil.Anvil + for _, anvil := range o.anvilInstances { + if anvil.SourceChainID() == 0 { + result = anvil + } + } + + return result +} + +func (o *Orchestrator) ConfigAsString() string { + var b strings.Builder + + fmt.Fprintf(&b, "\nSupersim Config:\n") + if o.L1Anvil() != nil { + fmt.Fprintf(&b, "L1:\n") + fmt.Fprintf(&b, " %s\n", o.L1Anvil().String()) + } + + if len(o.OpSimInstances) > 0 { + fmt.Fprintf(&b, "L2:\n") + for _, opSim := range o.OpSimInstances { + fmt.Fprintf(&b, " %s\n", opSim.String()) + } + } + + return b.String() +} diff --git a/orchestrator/orchestrator_test.go b/orchestrator/orchestrator_test.go new file mode 100644 index 00000000..5d73b9c6 --- /dev/null +++ b/orchestrator/orchestrator_test.go @@ -0,0 +1,70 @@ +package orchestrator + +import ( + "context" + "testing" + + "github.com/ethereum-optimism/optimism/op-service/testlog" + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/log" +) + +type TestSuite struct { + t *testing.T + + Cfg *OrchestratorConfig + orchestrator *Orchestrator +} + +func createTestSuite(t *testing.T) *TestSuite { + cfg := &OrchestratorConfig{ + ChainConfigs: []ChainConfig{ + {ChainID: 1, Port: 0}, + {ChainID: 10, SourceChainID: 1, Port: 0}, + {ChainID: 30, SourceChainID: 1, Port: 0}, + }, + } + testlog := testlog.Logger(t, log.LevelInfo) + orchestrator, _ := NewOrchestrator(testlog, cfg) + t.Cleanup(func() { + if err := orchestrator.Stop(context.Background()); err != nil { + t.Errorf("failed to stop orchestrator: %s", err) + } + }) + + if err := orchestrator.Start(context.Background()); err != nil { + t.Fatalf("unable to start orchestrator: %s", err) + return nil + } + + return &TestSuite{ + t: t, + Cfg: cfg, + orchestrator: orchestrator, + } +} + +func TestStartup(t *testing.T) { + testSuite := createTestSuite(t) + + require.Equal(t, testSuite.orchestrator.L1Anvil().ChainID(), uint64(1)) + require.Equal(t, len(testSuite.orchestrator.OpSimInstances), 2) + require.Equal(t, testSuite.orchestrator.OpSimInstances[0].ChainID(), uint64(10)) + require.Equal(t, testSuite.orchestrator.OpSimInstances[0].SourceChainID(), uint64(1)) + require.Equal(t, testSuite.orchestrator.OpSimInstances[1].ChainID(), uint64(30)) + require.Equal(t, testSuite.orchestrator.OpSimInstances[1].SourceChainID(), uint64(1)) +} + +func TestTooManyL1sError(t *testing.T) { + cfg := &OrchestratorConfig{ + ChainConfigs: []ChainConfig{ + {ChainID: 1, Port: 0}, + {ChainID: 10, Port: 0}, + {ChainID: 30, SourceChainID: 1, Port: 0}, + }, + } + testlog := testlog.Logger(t, log.LevelInfo) + _, err := NewOrchestrator(testlog, cfg) + require.Error(t, err) +} diff --git a/supersim.go b/supersim.go index 55e2fa4e..9491003b 100644 --- a/supersim.go +++ b/supersim.go @@ -1,48 +1,26 @@ package supersim import ( - _ "embed" "fmt" "strings" - "sync" "context" - "github.com/ethereum-optimism/supersim/anvil" - opsim "github.com/ethereum-optimism/supersim/op-simulator" + "github.com/ethereum-optimism/supersim/orchestrator" "github.com/ethereum/go-ethereum/log" ) type Config struct { - l1Chain ChainConfig - l2Chains []ChainConfig + orchestratorConfig orchestrator.OrchestratorConfig } -type ChainConfig struct { - anvilConfig anvil.Config - opSimConfig opsim.Config -} - -//go:embed genesis/genesis-l1.json -var genesisL1JSON []byte - -//go:embed genesis/genesis-l2.json -var genesisL2JSON []byte - var DefaultConfig = Config{ - l1Chain: ChainConfig{ - anvilConfig: anvil.Config{ChainId: 1, Port: 0, Genesis: genesisL1JSON}, - opSimConfig: opsim.Config{Port: 0}, - }, - l2Chains: []ChainConfig{ - { - anvilConfig: anvil.Config{ChainId: 10, Port: 0, Genesis: genesisL2JSON}, - opSimConfig: opsim.Config{Port: 0}, - }, - { - anvilConfig: anvil.Config{ChainId: 30, Port: 0, Genesis: genesisL2JSON}, - opSimConfig: opsim.Config{Port: 0}, + orchestratorConfig: orchestrator.OrchestratorConfig{ + ChainConfigs: []orchestrator.ChainConfig{ + {ChainID: 1, Port: 0}, + {ChainID: 10, SourceChainID: 1, Port: 0}, + {ChainID: 30, SourceChainID: 1, Port: 0}, }, }, } @@ -50,52 +28,23 @@ var DefaultConfig = Config{ type Supersim struct { log log.Logger - l1Anvil *anvil.Anvil - l1OpSim *opsim.OpSimulator - - l2Anvils map[uint64]*anvil.Anvil - l2OpSims map[uint64]*opsim.OpSimulator + Orchestrator *orchestrator.Orchestrator } -func NewSupersim(log log.Logger, config *Config) *Supersim { - l1Anvil := anvil.New(log, &config.l1Chain.anvilConfig) - l1OpSim := opsim.New(log, &config.l1Chain.opSimConfig, l1Anvil) - - l2Anvils := make(map[uint64]*anvil.Anvil) - l2OpSims := make(map[uint64]*opsim.OpSimulator) - for i := range config.l2Chains { - l2ChainConfig := config.l2Chains[i] - l2Anvil := anvil.New(log, &l2ChainConfig.anvilConfig) - l2Anvils[l2ChainConfig.anvilConfig.ChainId] = l2Anvil - l2OpSims[l2ChainConfig.anvilConfig.ChainId] = opsim.New(log, &l2ChainConfig.opSimConfig, l2Anvil) +func NewSupersim(log log.Logger, config *Config) (*Supersim, error) { + o, err := orchestrator.NewOrchestrator(log, &DefaultConfig.orchestratorConfig) + if err != nil { + return nil, fmt.Errorf("failed to create orchestrator") } - return &Supersim{log, l1Anvil, l1OpSim, l2Anvils, l2OpSims} + return &Supersim{log, o}, nil } func (s *Supersim) Start(ctx context.Context) error { s.log.Info("starting supersim") - if err := s.l1Anvil.Start(ctx); err != nil { - return fmt.Errorf("l1 anvil failed to start: %w", err) - } - if err := s.l1OpSim.Start(ctx); err != nil { - return fmt.Errorf("l1 op simulator failed to start: %w", err) - } - - for _, l2Anvil := range s.l2Anvils { - if err := l2Anvil.Start(ctx); err != nil { - return fmt.Errorf("l2 anvil failed to start: %w", err) - } - } - for _, l2OpSim := range s.l2OpSims { - if err := l2OpSim.Start(ctx); err != nil { - return fmt.Errorf("l2 op simulator failed to start: %w", err) - } - } - - if err := s.WaitUntilReady(); err != nil { - return fmt.Errorf("supersim failed to get ready: %w", err) + if err := s.Orchestrator.Start(ctx); err != nil { + return fmt.Errorf("orchestrator failed to start: %w", err) } s.log.Info("supersim is ready") @@ -107,101 +56,22 @@ func (s *Supersim) Start(ctx context.Context) error { func (s *Supersim) Stop(ctx context.Context) error { s.log.Info("stopping supersim") - for _, l2OpSim := range s.l2OpSims { - if err := l2OpSim.Stop(ctx); err != nil { - return fmt.Errorf("l2 op simulator failed to stop: %w", err) - } - s.log.Info("stopped op simulator", "chain.id", l2OpSim.ChainId()) - } - for _, l2Anvil := range s.l2Anvils { - if err := l2Anvil.Stop(); err != nil { - return fmt.Errorf("l2 anvil failed to stop: %w", err) - } + if err := s.Orchestrator.Stop(ctx); err != nil { + return fmt.Errorf("orchestrator failed to stop: %w", err) } - if err := s.l1OpSim.Stop(ctx); err != nil { - return fmt.Errorf("l1 op simulator failed to stop: %w", err) - } - if err := s.l1Anvil.Stop(); err != nil { - return fmt.Errorf("l1 anvil failed to stop: %w", err) - } - s.log.Info("stopped op simulator", "chain.id", s.l1OpSim.ChainId()) - return nil } func (s *Supersim) Stopped() bool { - for _, l2OpSim := range s.l2OpSims { - if stopped := l2OpSim.Stopped(); !stopped { - return stopped - } - } - for _, l2Anvil := range s.l2Anvils { - if stopped := l2Anvil.Stopped(); !stopped { - return stopped - } - } - - if stopped := s.l1Anvil.Stopped(); !stopped { - return stopped - } - if stopped := s.l1OpSim.Stopped(); !stopped { - return stopped - } - - return true -} - -func (s *Supersim) WaitUntilReady() error { - var once sync.Once - var err error - ctx, cancel := context.WithCancel(context.Background()) - - handleErr := func(e error) { - if e != nil { - once.Do(func() { - err = e - cancel() - }) - } - } - - var wg sync.WaitGroup - - waitForAnvil := func(anvil *anvil.Anvil) { - defer wg.Done() - handleErr(anvil.WaitUntilReady(ctx)) - } - - s.IterateChains(func(chain *anvil.Anvil) { - wg.Add(1) - go waitForAnvil(chain) - }) - - wg.Wait() - - return err -} - -func (s *Supersim) IterateChains(fn func(anvil *anvil.Anvil)) { - fn(s.l1Anvil) - - for _, l2Anvil := range s.l2Anvils { - fn(l2Anvil) - } + return s.Orchestrator.Stopped() } func (s *Supersim) ConfigAsString() string { var b strings.Builder fmt.Fprintf(&b, "\nSupersim Config:\n") - fmt.Fprintf(&b, "L1:\n") - fmt.Fprintf(&b, " Chain ID: %d RPC: %s LogPath: %s\n", s.l1OpSim.ChainId(), s.l1OpSim.Endpoint(), s.l1Anvil.LogPath()) - - fmt.Fprintf(&b, "L2:\n") - for id, l2OpSim := range s.l2OpSims { - fmt.Fprintf(&b, " Chain ID: %d RPC: %s LogPath: %s\n", l2OpSim.ChainId(), l2OpSim.Endpoint(), s.l2Anvils[id].LogPath()) - } + fmt.Fprint(&b, s.Orchestrator.ConfigAsString()) return b.String() } diff --git a/supersim_test.go b/supersim_test.go index c738548c..8e1192d4 100644 --- a/supersim_test.go +++ b/supersim_test.go @@ -31,7 +31,7 @@ type TestSuite struct { func createTestSuite(t *testing.T) *TestSuite { cfg := &DefaultConfig testlog := testlog.Logger(t, log.LevelInfo) - supersim := NewSupersim(testlog, cfg) + supersim, _ := NewSupersim(testlog, cfg) t.Cleanup(func() { if err := supersim.Stop(context.Background()); err != nil { t.Errorf("failed to stop supersim: %s", err) @@ -53,20 +53,12 @@ func createTestSuite(t *testing.T) *TestSuite { func TestStartup(t *testing.T) { testSuite := createTestSuite(t) - var chainId math.HexOrDecimal64 - - // test that all chains can be queried - l1Client, err := rpc.Dial(testSuite.Supersim.l1OpSim.Endpoint()) - require.NoError(t, err) - require.NoError(t, l1Client.CallContext(context.Background(), &chainId, "eth_chainId")) - require.Equal(t, uint64(chainId), testSuite.Supersim.l1OpSim.ChainId()) - l1Client.Close() - - for id, l2Chain := range testSuite.Supersim.l2OpSims { - require.Equal(t, id, l2Chain.ChainId()) - - l2Client, err := rpc.Dial(l2Chain.Endpoint()) + require.True(t, len(testSuite.Supersim.Orchestrator.OpSimInstances) > 0) + // test that all op simulators can be queried + for _, opSim := range testSuite.Supersim.Orchestrator.OpSimInstances { + l2Client, err := rpc.Dial(opSim.Endpoint()) require.NoError(t, err) + var chainId math.HexOrDecimal64 require.NoError(t, l2Client.CallContext(context.Background(), &chainId, "eth_chainId")) // Commented out due to a bug in foundry that sets the chain id to 1 whenever genesis.json file is supplied @@ -79,9 +71,10 @@ func TestStartup(t *testing.T) { func TestGenesisState(t *testing.T) { testSuite := createTestSuite(t) + require.True(t, len(testSuite.Supersim.Orchestrator.OpSimInstances) > 0) // assert that the predeploys exists on the l2 anvil instances - for _, l2Chain := range testSuite.Supersim.l2OpSims { - client, err := rpc.Dial(l2Chain.Endpoint()) + for _, l2OpSim := range testSuite.Supersim.Orchestrator.OpSimInstances { + client, err := rpc.Dial(l2OpSim.Endpoint()) require.NoError(t, err) defer client.Close()