Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions op-devstack/sysgo/l2_challenger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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()),
Expand Down
272 changes: 272 additions & 0 deletions op-devstack/sysgo/l2_challenger_faultproof.go
Original file line number Diff line number Diff line change
@@ -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{})
Copy link
Member

Choose a reason for hiding this comment

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

The lock is not held before c.sub.Start() is called. I think this could cause race when concurrent Start()/Stop() calls are made. Maybe low priority if we don't need such tests to be covered.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch. A proper fix isn't trivial though: holding the lock through Start() risks deadlocks since sub.Start() can block and OnExit runs async. Given current usage (single Start/Stop, no concurrency), I'll leave as-is for now.

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
})
}
5 changes: 3 additions & 2 deletions op-devstack/sysgo/l2_proposer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 16 additions & 12 deletions op-devstack/sysgo/l2_proposer_faultproof.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -25,21 +24,21 @@ import (
type L2SuccinctFaultProofProposer struct {
mu sync.Mutex
id stack.L2ProposerID
service *ps.ProposerService
userRPC string
execPath string
args []string
p devtest.P
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()
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")

Expand All @@ -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()
Expand Down
Loading