Skip to content

Commit

Permalink
feat(op-dispute-mon): initial setup (#9359)
Browse files Browse the repository at this point in the history
  • Loading branch information
refcell committed Feb 5, 2024
1 parent eac6e99 commit 256b4e4
Show file tree
Hide file tree
Showing 12 changed files with 603 additions and 0 deletions.
20 changes: 20 additions & 0 deletions op-dispute-mon/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# op-dispute-mon

The `op-dispute-mon` is an off-chain service to monitor dispute games.

## Quickstart

Clone this repo. Then run:

```shell
make op-dispute-mon
```

This will build the `op-dispute-mon` binary which can be run with
`./op-dispute-mon/bin/op-dispute-mon`.

## Usage

`op-dispute-mon` is configurable via command line flags and environment variables. The help menu
shows the available config options and can be accessed by running `./op-dispute-mon --help`.

69 changes: 69 additions & 0 deletions op-dispute-mon/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package main

import (
"context"
"os"

"github.com/urfave/cli/v2"

"github.com/ethereum/go-ethereum/log"

monitor "github.com/ethereum-optimism/optimism/op-dispute-mon"
"github.com/ethereum-optimism/optimism/op-dispute-mon/config"
"github.com/ethereum-optimism/optimism/op-dispute-mon/flags"
"github.com/ethereum-optimism/optimism/op-dispute-mon/version"
opservice "github.com/ethereum-optimism/optimism/op-service"
"github.com/ethereum-optimism/optimism/op-service/cliapp"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
"github.com/ethereum-optimism/optimism/op-service/opio"
)

var (
GitCommit = ""
GitDate = ""
)

// VersionWithMeta holds the textual version string including the metadata.
var VersionWithMeta = opservice.FormatVersion(version.Version, GitCommit, GitDate, version.Meta)

func main() {
args := os.Args
ctx := opio.WithInterruptBlocker(context.Background())
if err := run(ctx, args, monitor.Main); err != nil {
log.Crit("Application failed", "err", err)
}
}

type ConfiguredLifecycle func(ctx context.Context, log log.Logger, config *config.Config) (cliapp.Lifecycle, error)

func run(ctx context.Context, args []string, action ConfiguredLifecycle) error {
oplog.SetupDefaults()

app := cli.NewApp()
app.Version = VersionWithMeta
app.Flags = cliapp.ProtectFlags(flags.Flags)
app.Name = "op-dispute-mon"
app.Usage = "Monitor dispute games"
app.Description = "Monitors output proposals and dispute games."
app.Action = cliapp.LifecycleCmd(func(ctx *cli.Context, close context.CancelCauseFunc) (cliapp.Lifecycle, error) {
logger, err := setupLogging(ctx)
if err != nil {
return nil, err
}
logger.Info("Starting op-dispute-mon", "version", VersionWithMeta)

cfg, err := flags.NewConfigFromCLI(ctx)
if err != nil {
return nil, err
}
return action(ctx.Context, logger, cfg)
})
return app.RunContext(ctx, args)
}

func setupLogging(ctx *cli.Context) (log.Logger, error) {
logCfg := oplog.ReadCLIConfig(ctx)
logger := oplog.NewLogger(oplog.AppOut(ctx), logCfg)
oplog.SetGlobalLogHandler(logger.GetHandler())
return logger, nil
}
43 changes: 43 additions & 0 deletions op-dispute-mon/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package config

import (
"errors"
"fmt"

opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum-optimism/optimism/op-service/oppprof"
)

var (
ErrMissingL1EthRPC = errors.New("missing l1 eth rpc url")
)

// Config is a well typed config that is parsed from the CLI params.
// It also contains config options for auxiliary services.
type Config struct {
L1EthRpc string // L1 RPC Url

MetricsConfig opmetrics.CLIConfig
PprofConfig oppprof.CLIConfig
}

func NewConfig(l1EthRpc string) Config {
return Config{
L1EthRpc: l1EthRpc,
MetricsConfig: opmetrics.DefaultCLIConfig(),
PprofConfig: oppprof.DefaultCLIConfig(),
}
}

func (c Config) Check() error {
if c.L1EthRpc == "" {
return ErrMissingL1EthRPC
}
if err := c.MetricsConfig.Check(); err != nil {
return fmt.Errorf("metrics config: %w", err)
}
if err := c.PprofConfig.Check(); err != nil {
return fmt.Errorf("pprof config: %w", err)
}
return nil
}
21 changes: 21 additions & 0 deletions op-dispute-mon/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package config

import (
"testing"

"github.com/stretchr/testify/require"
)

var (
validL1EthRpc = "http://localhost:8545"
)

func validConfig() Config {
return NewConfig(validL1EthRpc)
}

func TestL1EthRpcRequired(t *testing.T) {
config := validConfig()
config.L1EthRpc = ""
require.ErrorIs(t, config.Check(), ErrMissingL1EthRPC)
}
75 changes: 75 additions & 0 deletions op-dispute-mon/flags/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package flags

import (
"fmt"

"github.com/urfave/cli/v2"

"github.com/ethereum-optimism/optimism/op-dispute-mon/config"
opservice "github.com/ethereum-optimism/optimism/op-service"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/ethereum-optimism/optimism/op-service/oppprof"
)

const (
envVarPrefix = "OP_DISPUTE_MON"
)

func prefixEnvVars(name string) []string {
return opservice.PrefixEnvVar(envVarPrefix, name)
}

var (
// Required Flags
L1EthRpcFlag = &cli.StringFlag{
Name: "l1-eth-rpc",
Usage: "HTTP provider URL for L1.",
EnvVars: prefixEnvVars("L1_ETH_RPC"),
}
// Optional Flags
)

// requiredFlags are checked by [CheckRequired]
var requiredFlags = []cli.Flag{
L1EthRpcFlag,
}

// optionalFlags is a list of unchecked cli flags
var optionalFlags = []cli.Flag{}

func init() {
optionalFlags = append(optionalFlags, oplog.CLIFlags(envVarPrefix)...)
optionalFlags = append(optionalFlags, opmetrics.CLIFlags(envVarPrefix)...)
optionalFlags = append(optionalFlags, oppprof.CLIFlags(envVarPrefix)...)

Flags = append(requiredFlags, optionalFlags...)
}

// Flags contains the list of configuration options available to the binary.
var Flags []cli.Flag

func CheckRequired(ctx *cli.Context) error {
for _, f := range requiredFlags {
if !ctx.IsSet(f.Names()[0]) {
return fmt.Errorf("flag %s is required", f.Names()[0])
}
}
return nil
}

// NewConfigFromCLI parses the Config from the provided flags or environment variables.
func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
if err := CheckRequired(ctx); err != nil {
return nil, err
}

metricsConfig := opmetrics.ReadCLIConfig(ctx)
pprofConfig := oppprof.ReadCLIConfig(ctx)

return &config.Config{
L1EthRpc: ctx.String(L1EthRpcFlag.Name),
MetricsConfig: metricsConfig,
PprofConfig: pprofConfig,
}, nil
}
82 changes: 82 additions & 0 deletions op-dispute-mon/flags/flags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package flags

import (
"fmt"
"reflect"
"strings"
"testing"

opservice "github.com/ethereum-optimism/optimism/op-service"

"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
)

// TestUniqueFlags asserts that all flag names are unique, to avoid accidental conflicts between the many flags.
func TestUniqueFlags(t *testing.T) {
seenCLI := make(map[string]struct{})
for _, flag := range Flags {
for _, name := range flag.Names() {
if _, ok := seenCLI[name]; ok {
t.Errorf("duplicate flag %s", name)
continue
}
seenCLI[name] = struct{}{}
}
}
}

// TestUniqueEnvVars asserts that all flag env vars are unique, to avoid accidental conflicts between the many flags.
func TestUniqueEnvVars(t *testing.T) {
seenCLI := make(map[string]struct{})
for _, flag := range Flags {
envVar := envVarForFlag(flag)
if _, ok := seenCLI[envVar]; envVar != "" && ok {
t.Errorf("duplicate flag env var %s", envVar)
continue
}
seenCLI[envVar] = struct{}{}
}
}

func TestCorrectEnvVarPrefix(t *testing.T) {
for _, flag := range Flags {
envVar := envVarForFlag(flag)
if envVar == "" {
t.Errorf("Failed to find EnvVar for flag %v", flag.Names()[0])
}
if !strings.HasPrefix(envVar, fmt.Sprintf("%s_", envVarPrefix)) {
t.Errorf("Flag %v env var (%v) does not start with %s_", flag.Names()[0], envVar, envVarPrefix)
}
if strings.Contains(envVar, "__") {
t.Errorf("Flag %v env var (%v) has duplicate underscores", flag.Names()[0], envVar)
}
}
}

func envVarForFlag(flag cli.Flag) string {
values := reflect.ValueOf(flag)
envVarValue := values.Elem().FieldByName("EnvVars")
if envVarValue == (reflect.Value{}) || envVarValue.Len() == 0 {
return ""
}
return envVarValue.Index(0).String()
}

func TestEnvVarFormat(t *testing.T) {
for _, flag := range Flags {
flag := flag
flagName := flag.Names()[0]

t.Run(flagName, func(t *testing.T) {
envFlagGetter, ok := flag.(interface {
GetEnvVars() []string
})
envFlags := envFlagGetter.GetEnvVars()
require.True(t, ok, "must be able to cast the flag to an EnvVar interface")
require.Equal(t, 1, len(envFlags), "flags should have exactly one env var")
expectedEnvVar := opservice.FlagNameToEnvVarName(flagName, envVarPrefix)
require.Equal(t, expectedEnvVar, envFlags[0])
})
}
}
Loading

0 comments on commit 256b4e4

Please sign in to comment.