diff --git a/op-devstack/sysgo/l2_challenger.go b/op-devstack/sysgo/l2_challenger.go index 7849ffc98d..349ab11169 100644 --- a/op-devstack/sysgo/l2_challenger.go +++ b/op-devstack/sysgo/l2_challenger.go @@ -17,6 +17,11 @@ import ( "github.com/ethereum/go-ethereum/crypto" ) +// L2ChallengerBackend is the interface for L2 challengers managed by the orchestrator. +type L2ChallengerBackend interface { + hydrate(system stack.ExtensibleSystem) +} + type l2ChallengerOpts struct { useCannonKonaConfig bool } @@ -28,6 +33,8 @@ type L2Challenger struct { config *config.Config } +var _ L2ChallengerBackend = (*L2Challenger)(nil) + func (p *L2Challenger) hydrate(system stack.ExtensibleSystem) { bFrontend := shim.NewL2Challenger(shim.L2ChallengerConfig{ CommonConfig: shim.NewCommonConfig(system.T()), diff --git a/op-devstack/sysgo/l2_challenger_faultproof.go b/op-devstack/sysgo/l2_challenger_faultproof.go new file mode 100644 index 0000000000..0b6e9befac --- /dev/null +++ b/op-devstack/sysgo/l2_challenger_faultproof.go @@ -0,0 +1,272 @@ +package sysgo + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "syscall" + + "github.com/ethereum-optimism/optimism/op-chain-ops/devkeys" + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/shim" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-service/logpipe" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" +) + +// L2SuccinctFaultProofChallenger wraps the OP Succinct fault-proof challenger binary as a subprocess. +type L2SuccinctFaultProofChallenger struct { + mu sync.Mutex + id stack.L2ChallengerID + execPath string + args []string + p devtest.P + logger log.Logger + sub *SubProcess + l2MetricsRegistrar L2MetricsRegistrar + metricsPort string +} + +var _ L2ChallengerBackend = (*L2SuccinctFaultProofChallenger)(nil) + +// FaultProofChallenger extends L2ChallengerBackend with faultproof-specific methods. +type FaultProofChallenger interface { + L2ChallengerBackend + Start() + Stop() +} + +var _ FaultProofChallenger = (*L2SuccinctFaultProofChallenger)(nil) + +func (c *L2SuccinctFaultProofChallenger) hydrate(system stack.ExtensibleSystem) { + bFrontend := shim.NewL2Challenger(shim.L2ChallengerConfig{ + CommonConfig: shim.NewCommonConfig(system.T()), + ID: c.id, + Config: nil, // Succinct challenger runs as subprocess, no op-challenger config + }) + l2Net := system.L2Network(stack.L2NetworkID(c.id.ChainID())) + l2Net.(stack.ExtensibleL2Network).AddL2Challenger(bFrontend) +} + +// Start starts the fault-proof challenger subprocess. +func (c *L2SuccinctFaultProofChallenger) Start() { + c.mu.Lock() + if c.sub != nil { + c.logger.Warn("Fault Proof Challenger already started") + c.mu.Unlock() + return + } + + // We pipe sub-process logs to the test-logger. + logOut := logpipe.ToLogger(c.logger.New("src", "stdout")) + logErr := logpipe.ToLogger(c.logger.New("src", "stderr")) + + stdOutLogs := logpipe.LogProcessor(func(line []byte) { + e := logpipe.ParseRustStructuredLogs(line) + logOut(e) + }) + stdErrLogs := logpipe.LogProcessor(func(line []byte) { + e := logpipe.ParseRustStructuredLogs(line) + logErr(e) + }) + c.sub = NewSubProcess(c.p, stdOutLogs, stdErrLogs) + c.mu.Unlock() + + c.sub.OnExit(func(err error) { + if errors.Is(err, syscall.ECHILD) { + return + } + + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok { + sig := ws.Signal() + if sig == syscall.SIGINT || sig == syscall.SIGTERM { + return + } + } + } + + c.p.Require().NoError(err, "fault-proof challenger exited unexpectedly") + }) + + err := c.sub.Start(c.execPath, c.args, []string{}) + c.p.Require().NoError(err, "Must start challenger") + + if c.metricsPort != "" && c.l2MetricsRegistrar != nil { + metricsTarget := NewPrometheusMetricsTarget("localhost", c.metricsPort, false) + c.l2MetricsRegistrar.RegisterL2MetricsTargets(c.id, metricsTarget) + c.logger.Info("Registered fault-proof challenger metrics", "port", c.metricsPort) + } +} + +// Stop stops the fault-proof challenger subprocess. +func (c *L2SuccinctFaultProofChallenger) Stop() { + c.mu.Lock() + defer c.mu.Unlock() + if c.sub == nil { + c.logger.Warn("fault-proof challenger already stopped") + return + } + + err := c.sub.Stop(true) + c.p.Require().NoError(err, "Must stop challenger") + c.sub = nil +} + +// WithSuccinctFaultProofChallenger creates a fault-proof challenger after deployment. +func WithSuccinctFaultProofChallenger(challengerID stack.L2ChallengerID, l1ELID stack.L1ELNodeID, l2ELID stack.L2ELNodeID, opts ...FaultProofChallengerOption) stack.Option[*Orchestrator] { + return stack.AfterDeploy(func(orch *Orchestrator) { + WithSuccinctFaultProofChallengerPostDeploy(orch, challengerID, l1ELID, l2ELID, opts...) + }) +} + +// WithSuperSuccinctFaultProofChallenger creates a fault-proof challenger in the Finally phase. +func WithSuperSuccinctFaultProofChallenger(challengerID stack.L2ChallengerID, + l1ELID stack.L1ELNodeID, l2ELID stack.L2ELNodeID, opts ...FaultProofChallengerOption) stack.Option[*Orchestrator] { + return stack.Finally(func(orch *Orchestrator) { + WithSuccinctFaultProofChallengerPostDeploy(orch, challengerID, l1ELID, l2ELID, opts...) + }) +} + +// WithSuccinctFaultProofChallengerPostDeploy sets up and starts the OP Succinct fault-proof challenger. +func WithSuccinctFaultProofChallengerPostDeploy(orch *Orchestrator, challengerID stack.L2ChallengerID, l1ELID stack.L1ELNodeID, l2ELID stack.L2ELNodeID, opts ...FaultProofChallengerOption) { + ctx := stack.ContextWithID(orch.P().Ctx(), challengerID) + p := orch.P().WithCtx(ctx) + logger := p.Logger().New("component", "succinct-fp-challenger") + + require := p.Require() + require.False(orch.challengers.Has(challengerID), "challenger must not already exist") + + l2Net, ok := orch.l2Nets.Get(challengerID.ChainID()) + require.True(ok, "l2 network required") + + l1EL, ok := orch.GetL1EL(l1ELID) + require.True(ok, "l1 EL node required") + + l2EL, ok := orch.GetL2EL(l2ELID) + require.True(ok, "l2 EL node required") + + // Use ChallengerRole for the challenger key + challengerKey, err := orch.GetKeys().Secret(devkeys.ChallengerRole.Key(challengerID.ChainID().ToBig())) + require.NoError(err, "failed to get challenger key") + challengerKeyStr := hexutil.Encode(crypto.FromECDSA(challengerKey)) + + cfg := &FaultProofChallengerConfig{} + for _, opt := range opts { + opt(p, challengerID, cfg) + } + + l1RPC := l1EL.UserRPC() + l2RPC := strings.ReplaceAll(l2EL.UserRPC(), "ws://", "http://") + anchorStateRegistryAddr := l2Net.deployment.anchorStateRegistry + factoryAddr := l2Net.deployment.disputeGameFactoryProxy + + logger.Info("L1_RPC", "url", l1RPC) + logger.Info("L2_RPC", "url", l2RPC) + logger.Info("ANCHOR_STATE_REGISTRY_ADDRESS", "address", anchorStateRegistryAddr) + logger.Info("FACTORY_ADDRESS", "address", factoryAddr) + + envVars := map[string]string{ + "L1_RPC": l1RPC, + "L2_RPC": l2RPC, + "ANCHOR_STATE_REGISTRY_ADDRESS": anchorStateRegistryAddr.String(), + "FACTORY_ADDRESS": factoryAddr.String(), + "GAME_TYPE": "42", + "PRIVATE_KEY": challengerKeyStr, + "LOG_FORMAT": "json", + } + + // Optional parameters (override defaults if set) + setEnvIfNotNil(envVars, "FETCH_INTERVAL", cfg.fetchInterval) + setEnvIfNotNil(envVars, "MALICIOUS_CHALLENGE_PERCENTAGE", cfg.maliciousChallengePercentage) + setEnvIfNotNil(envVars, "RUST_LOG", cfg.rustLog) + + var metricsPort string + if areMetricsEnabled() { + metricsPort, err = getAvailableLocalPort() + require.NoError(err, "failed to get available port for challenger metrics") + envVars["CHALLENGER_METRICS_PORT"] = metricsPort + } + + envDir := p.TempDir() + envFile := filepath.Join(envDir, fmt.Sprintf("fp-challenger-%s.env", challengerID.String())) + err = WriteEnvFile(envFile, envVars) + p.Require().NoError(err, "must write fault proof challenger env file") + + if cfg.envFilePath != nil { + err = WriteEnvFile(*cfg.envFilePath, envVars) + p.Require().NoError(err, "must write challenger env file") + logger.Info("challenger env file written", "path", *cfg.envFilePath) + } + + execPath := os.Getenv("FAULT_PROOF_CHALLENGER_EXEC_PATH") + p.Require().NotEmpty(execPath, "FAULT_PROOF_CHALLENGER_EXEC_PATH environment variable must be set") + _, err = os.Stat(execPath) + p.Require().NotErrorIs(err, os.ErrNotExist, "challenger executable must exist") + + c := &L2SuccinctFaultProofChallenger{ + id: challengerID, + execPath: execPath, + args: []string{"--env-file", envFile}, + p: p, + logger: logger, + l2MetricsRegistrar: orch, + metricsPort: metricsPort, + } + logger.Info("Starting fault-proof challenger") + c.Start() + p.Cleanup(func() { + logger.Info("Stopping fault-proof challenger") + c.Stop() + }) + logger.Info("fault-proof challenger is running") + + // Store the challenger in the orchestrator's challengers map + require.True(orch.challengers.SetIfMissing(challengerID, c), "challenger must not already exist") +} + +// FaultProofChallengerConfig holds configuration for the OP Succinct fault-proof challenger. +type FaultProofChallengerConfig struct { + fetchInterval *uint64 + maliciousChallengePercentage *float64 + rustLog *string + envFilePath *string +} + +// FaultProofChallengerOption is a function that configures the FaultProofChallengerConfig. +type FaultProofChallengerOption func(p devtest.P, id stack.L2ChallengerID, cfg *FaultProofChallengerConfig) + +// WithFPChallengerFetchInterval sets the polling interval in seconds. +func WithFPChallengerFetchInterval(n uint64) FaultProofChallengerOption { + return FaultProofChallengerOption(func(p devtest.P, id stack.L2ChallengerID, cfg *FaultProofChallengerConfig) { + cfg.fetchInterval = &n + }) +} + +// WithFPChallengerMaliciousChallengePercentage sets the percentage of valid games to challenge maliciously (for testing). +func WithFPChallengerMaliciousChallengePercentage(pct float64) FaultProofChallengerOption { + return FaultProofChallengerOption(func(p devtest.P, id stack.L2ChallengerID, cfg *FaultProofChallengerConfig) { + cfg.maliciousChallengePercentage = &pct + }) +} + +// WithFPChallengerRustLog sets the RUST_LOG environment variable. +func WithFPChallengerRustLog(level string) FaultProofChallengerOption { + return FaultProofChallengerOption(func(p devtest.P, id stack.L2ChallengerID, cfg *FaultProofChallengerConfig) { + cfg.rustLog = &level + }) +} + +// WithFPChallengerWriteEnvFile enables writing environment variables to a file. +func WithFPChallengerWriteEnvFile(path string) FaultProofChallengerOption { + return FaultProofChallengerOption(func(p devtest.P, id stack.L2ChallengerID, cfg *FaultProofChallengerConfig) { + cfg.envFilePath = &path + }) +} diff --git a/op-devstack/sysgo/l2_proposer.go b/op-devstack/sysgo/l2_proposer.go index 0e985b0276..13703ebb50 100644 --- a/op-devstack/sysgo/l2_proposer.go +++ b/op-devstack/sysgo/l2_proposer.go @@ -21,12 +21,13 @@ import ( oprpc "github.com/ethereum-optimism/optimism/op-service/rpc" ) -type L2Prop interface { +// L2ProposerBackend is the interface for L2 proposers managed by the orchestrator. +type L2ProposerBackend interface { hydrate(system stack.ExtensibleSystem) UserRPC() string } -var _ L2Prop = (*L2Proposer)(nil) +var _ L2ProposerBackend = (*L2Proposer)(nil) type L2Proposer struct { id stack.L2ProposerID diff --git a/op-devstack/sysgo/l2_proposer_faultproof.go b/op-devstack/sysgo/l2_proposer_faultproof.go index 100a0ccfc8..77e4c37add 100644 --- a/op-devstack/sysgo/l2_proposer_faultproof.go +++ b/op-devstack/sysgo/l2_proposer_faultproof.go @@ -14,7 +14,6 @@ import ( "github.com/ethereum-optimism/optimism/op-devstack/devtest" "github.com/ethereum-optimism/optimism/op-devstack/shim" "github.com/ethereum-optimism/optimism/op-devstack/stack" - ps "github.com/ethereum-optimism/optimism/op-proposer/proposer" "github.com/ethereum-optimism/optimism/op-service/client" "github.com/ethereum-optimism/optimism/op-service/logpipe" "github.com/ethereum/go-ethereum/common/hexutil" @@ -25,7 +24,6 @@ import ( type L2SuccinctFaultProofProposer struct { mu sync.Mutex id stack.L2ProposerID - service *ps.ProposerService userRPC string execPath string args []string @@ -33,13 +31,14 @@ type L2SuccinctFaultProofProposer struct { logger log.Logger sub *SubProcess l2MetricsRegistrar L2MetricsRegistrar + metricsPort string } -var _ L2Prop = (*L2SuccinctFaultProofProposer)(nil) +var _ L2ProposerBackend = (*L2SuccinctFaultProofProposer)(nil) -// FaultProofProposer extends L2Prop with faultproof-specific methods. +// FaultProofProposer extends L2ProposerBackend with faultproof-specific methods. type FaultProofProposer interface { - L2Prop + L2ProposerBackend Start() Stop() } @@ -109,6 +108,12 @@ func (k *L2SuccinctFaultProofProposer) Start() { err := k.sub.Start(k.execPath, k.args, []string{}) k.p.Require().NoError(err, "Must start") + + if k.metricsPort != "" && k.l2MetricsRegistrar != nil { + metricsTarget := NewPrometheusMetricsTarget("localhost", k.metricsPort, false) + k.l2MetricsRegistrar.RegisterL2MetricsTargets(k.id, metricsTarget) + k.logger.Info("Registered fault-proof proposer metrics", "port", k.metricsPort) + } } // Stops the fault-proof proposer. @@ -141,7 +146,7 @@ func WithSuperSuccinctFaultProofProposer(proposerID stack.L2ProposerID, func WithSuccinctFaultProofProposerPostDeploy(orch *Orchestrator, proposerID stack.L2ProposerID, l1CLID stack.L1CLNodeID, l1ELID stack.L1ELNodeID, l2CLID stack.L2CLNodeID, l2ELID stack.L2ELNodeID, opts ...FaultProofProposerOption) { ctx := stack.ContextWithID(orch.P().Ctx(), proposerID) p := orch.P().WithCtx(ctx) - logger := p.Logger().New("component", "succinct-faultproof") + logger := p.Logger().New("component", "succinct-fp-proposer") require := p.Require() require.False(orch.proposers.Has(proposerID), "proposer must not already exist") @@ -226,17 +231,15 @@ func WithSuccinctFaultProofProposerPostDeploy(orch *Orchestrator, proposerID sta setEnvIfNotNil(envVars, "MOCK_MODE", cfg.mockMode) setEnvIfNotNil(envVars, "RUST_LOG", cfg.rustLog) + var metricsPort string if areMetricsEnabled() { - metricsPort, err := getAvailableLocalPort() - require.NoError(err, "failed to get available port for metrics") + metricsPort, err = getAvailableLocalPort() + require.NoError(err, "failed to get available port for proposer metrics") envVars["PROPOSER_METRICS_PORT"] = metricsPort - metricsTarget := NewPrometheusMetricsTarget("localhost", metricsPort, false) - orch.RegisterL2MetricsTargets(proposerID, metricsTarget) - logger.Info("Registered fault-proof proposer metrics", "port", metricsPort) } envDir := p.TempDir() - envFile := filepath.Join(envDir, fmt.Sprintf("fault-proof-proposer-%s.env", proposerID.String())) + envFile := filepath.Join(envDir, fmt.Sprintf("fp-proposer-%s.env", proposerID.String())) err = WriteEnvFile(envFile, envVars) p.Require().NoError(err, "must write fault proof proposer env file") @@ -259,6 +262,7 @@ func WithSuccinctFaultProofProposerPostDeploy(orch *Orchestrator, proposerID sta p: p, logger: logger, l2MetricsRegistrar: orch, + metricsPort: metricsPort, } logger.Info("Starting fault-proof proposer") k.Start() diff --git a/op-devstack/sysgo/l2_proposer_validity.go b/op-devstack/sysgo/l2_proposer_validity.go index 0f6171d25b..9fa16cbee0 100644 --- a/op-devstack/sysgo/l2_proposer_validity.go +++ b/op-devstack/sysgo/l2_proposer_validity.go @@ -36,13 +36,14 @@ type L2SuccinctValidityProposer struct { sub *SubProcess databaseURL string l2MetricsRegistrar L2MetricsRegistrar + metricsPort string } -var _ L2Prop = (*L2SuccinctValidityProposer)(nil) +var _ L2ProposerBackend = (*L2SuccinctValidityProposer)(nil) -// ValidityProposer extends L2Prop with validity-specific methods. +// ValidityProposer extends L2ProposerBackend with validity-specific methods. type ValidityProposer interface { - L2Prop + L2ProposerBackend Start() Stop() DatabaseURL() string @@ -217,6 +218,12 @@ func (k *L2SuccinctValidityProposer) Start() { err := k.sub.Start(k.execPath, k.args, []string{}) k.p.Require().NoError(err, "Must start") + + if k.metricsPort != "" && k.l2MetricsRegistrar != nil { + metricsTarget := NewPrometheusMetricsTarget("localhost", k.metricsPort, false) + k.l2MetricsRegistrar.RegisterL2MetricsTargets(k.id, metricsTarget) + k.logger.Info("Registered validity proposer metrics", "port", k.metricsPort) + } } // Stops the validity proposer. @@ -344,13 +351,11 @@ func WithSuccinctValidityProposerPostDeploy(orch *Orchestrator, proposerID stack setEnvIfNotNil(envVars, "OP_SUCCINCT_MOCK", cfg.mockMode) setEnvIfNotNil(envVars, "RUST_LOG", cfg.rustLog) + var metricsPort string if areMetricsEnabled() { - metricsPort, err := getAvailableLocalPort() - require.NoError(err, "failed to get available port for metrics") + metricsPort, err = getAvailableLocalPort() + require.NoError(err, "failed to get available port for proposer metrics") envVars["METRICS_PORT"] = metricsPort - metricsTarget := NewPrometheusMetricsTarget("localhost", metricsPort, false) - orch.RegisterL2MetricsTargets(proposerID, metricsTarget) - logger.Info("Registered validity proposer metrics", "port", metricsPort) } envDir := p.TempDir() @@ -378,6 +383,7 @@ func WithSuccinctValidityProposerPostDeploy(orch *Orchestrator, proposerID stack logger: logger, databaseURL: embeddedPG.URL, l2MetricsRegistrar: orch, + metricsPort: metricsPort, } logger.Info("Starting validity proposer") k.Start() diff --git a/op-devstack/sysgo/orchestrator.go b/op-devstack/sysgo/orchestrator.go index 887aef7db6..2f3a237c0c 100644 --- a/op-devstack/sysgo/orchestrator.go +++ b/op-devstack/sysgo/orchestrator.go @@ -48,8 +48,8 @@ type Orchestrator struct { supervisors locks.RWMap[stack.SupervisorID, Supervisor] testSequencers locks.RWMap[stack.TestSequencerID, *TestSequencer] batchers locks.RWMap[stack.L2BatcherID, *L2Batcher] - challengers locks.RWMap[stack.L2ChallengerID, *L2Challenger] - proposers locks.RWMap[stack.L2ProposerID, L2Prop] + challengers locks.RWMap[stack.L2ChallengerID, L2ChallengerBackend] + proposers locks.RWMap[stack.L2ProposerID, L2ProposerBackend] // service name => prometheus endpoints to scrape l2MetricsEndpoints locks.RWMap[string, []PrometheusMetricsTarget] @@ -85,11 +85,16 @@ func (o *Orchestrator) ControlPlane() stack.ControlPlane { return o.controlPlane } -// GetProposer returns the L2Prop for the given proposer ID. -func (o *Orchestrator) GetProposer(id stack.L2ProposerID) (L2Prop, bool) { +// GetProposer returns the L2ProposerBackend for the given proposer ID. +func (o *Orchestrator) GetProposer(id stack.L2ProposerID) (L2ProposerBackend, bool) { return o.proposers.Get(id) } +// GetChallenger returns the L2ChallengerBackend for the given challenger ID. +func (o *Orchestrator) GetChallenger(id stack.L2ChallengerID) (L2ChallengerBackend, bool) { + return o.challengers.Get(id) +} + func (o *Orchestrator) EnableTimeTravel() { if o.timeTravelClock == nil { o.timeTravelClock = clock.NewAdvancingClock(100 * time.Millisecond) @@ -138,8 +143,8 @@ func (o *Orchestrator) Hydrate(sys stack.ExtensibleSystem) { o.supervisors.Range(rangeHydrateFn[stack.SupervisorID, Supervisor](sys)) o.testSequencers.Range(rangeHydrateFn[stack.TestSequencerID, *TestSequencer](sys)) o.batchers.Range(rangeHydrateFn[stack.L2BatcherID, *L2Batcher](sys)) - o.challengers.Range(rangeHydrateFn[stack.L2ChallengerID, *L2Challenger](sys)) - o.proposers.Range(rangeHydrateFn[stack.L2ProposerID, L2Prop](sys)) + o.challengers.Range(rangeHydrateFn[stack.L2ChallengerID, L2ChallengerBackend](sys)) + o.proposers.Range(rangeHydrateFn[stack.L2ProposerID, L2ProposerBackend](sys)) if o.syncTester != nil { o.syncTester.hydrate(sys) }