Commit aae7885c authored by Adrian Sutton's avatar Adrian Sutton

op-challenger: Add tests for CLI args

Refactors config to make it testable and provide programmatic access to the default config.
Unused RPCConfig is removed.
Log config is no longer part of application config - a configured logger is supplied to the application.
Unused Metrics and PProf config is removed - these are not application concepts but rather a service to be passed in like logging.
parent 6e414ce0
package op_challenger
import (
"github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum/go-ethereum/log"
)
// Main is the programmatic entry-point for running op-challenger
func Main(logger log.Logger, cfg *config.Config) error {
logger.Info("Fault game started")
return nil
}
package main
import (
"fmt"
"os"
op_challenger "github.com/ethereum-optimism/optimism/op-challenger"
"github.com/ethereum/go-ethereum/log"
"github.com/urfave/cli/v2"
......@@ -33,6 +35,15 @@ var VersionWithMeta = func() string {
}()
func main() {
args := os.Args
if err := run(args, op_challenger.Main); err != nil {
log.Crit("Application failed", "err", err)
}
}
type ConfigAction func(log log.Logger, config *config.Config) error
func run(args []string, action ConfigAction) error {
oplog.SetupDefaults()
app := cli.NewApp()
......@@ -42,24 +53,26 @@ func main() {
app.Usage = "Challenge outputs"
app.Description = "Ensures that on chain outputs are correct."
app.Action = func(ctx *cli.Context) error {
return FaultGame(VersionWithMeta, ctx)
}
if err := app.Run(os.Args); err != nil {
log.Crit("Application failed", "message", err)
logger, err := setupLogging(ctx)
if err != nil {
return err
}
logger.Info("Starting op-challenger", "version", VersionWithMeta)
cfg, err := config.NewConfigFromCLI(ctx)
if err != nil {
return err
}
return action(logger, cfg)
}
return app.Run(args)
}
type ConfigAction func(log log.Logger, version string, config *config.Config) error
func FaultGame(version string, cliCtx *cli.Context) error {
cfg, err := config.NewConfigFromCLI(cliCtx)
if err != nil {
return err
}
if err := cfg.Check(); err != nil {
return err
func setupLogging(ctx *cli.Context) (log.Logger, error) {
logCfg := oplog.ReadCLIConfig(ctx)
if err := logCfg.Check(); err != nil {
return nil, fmt.Errorf("log config error: %w", err)
}
log := oplog.NewLogger(cfg.LogConfig)
log.Info("Fault game started")
return nil
logger := oplog.NewLogger(logCfg)
return logger, nil
}
package main
import (
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
var (
l1EthRpc = "http://example.com:8545"
gameAddressValue = "0xaa00000000000000000000000000000000000000"
alphabetTrace = "abcdefghijz"
)
func TestLogLevel(t *testing.T) {
t.Run("RejectInvalid", func(t *testing.T) {
verifyArgsInvalid(t, "unknown level: foo", addRequiredArgs("--log.level=foo"))
})
for _, lvl := range []string{"trace", "debug", "info", "error", "crit"} {
lvl := lvl
t.Run("AcceptValid_"+lvl, func(t *testing.T) {
logger, _, err := runWithArgs(addRequiredArgs("--log.level", lvl))
require.NoError(t, err)
require.NotNil(t, logger)
})
}
}
func TestDefaultCLIOptionsMatchDefaultConfig(t *testing.T) {
cfg := configForArgs(t, addRequiredArgs())
defaultCfg := config.NewConfig(l1EthRpc, common.HexToAddress(gameAddressValue), alphabetTrace)
require.Equal(t, defaultCfg, cfg)
}
func TestDefaultConfigIsValid(t *testing.T) {
cfg := config.NewConfig(l1EthRpc, common.HexToAddress(gameAddressValue), alphabetTrace)
require.NoError(t, cfg.Check())
}
func TestL1ETHRPCAddress(t *testing.T) {
t.Run("Required", func(t *testing.T) {
verifyArgsInvalid(t, "flag l1-eth-rpc is required", addRequiredArgsExcept("--l1-eth-rpc"))
})
t.Run("Valid", func(t *testing.T) {
url := "http://example.com:8888"
cfg := configForArgs(t, addRequiredArgsExcept("--l1-eth-rpc", "--l1-eth-rpc="+url))
require.Equal(t, url, cfg.L1EthRpc)
require.Equal(t, url, cfg.TxMgrConfig.L1RPCURL)
})
}
func TestAlphabetTrace(t *testing.T) {
t.Run("Required", func(t *testing.T) {
verifyArgsInvalid(t, "flag alphabet is required", addRequiredArgsExcept("--alphabet"))
})
t.Run("Valid", func(t *testing.T) {
value := "abcde"
cfg := configForArgs(t, addRequiredArgsExcept("--alphabet", "--alphabet="+value))
require.Equal(t, value, cfg.AlphabetTrace)
})
}
func TestGameAddress(t *testing.T) {
t.Run("Required", func(t *testing.T) {
verifyArgsInvalid(t, "flag game-address is required", addRequiredArgsExcept("--game-address"))
})
t.Run("Valid", func(t *testing.T) {
addr := common.Address{0xbb, 0xcc, 0xdd}
cfg := configForArgs(t, addRequiredArgsExcept("--game-address", "--game-address="+addr.Hex()))
require.Equal(t, addr, cfg.GameAddress)
})
t.Run("Invalid", func(t *testing.T) {
verifyArgsInvalid(t, "invalid address: foo", addRequiredArgsExcept("--game-address", "--game-address=foo"))
})
}
func TestTxManagerFlagsSupported(t *testing.T) {
// Not a comprehensive list of flags, just enough to sanity check the txmgr.CLIFlags were defined
cfg := configForArgs(t, addRequiredArgs("--"+txmgr.NumConfirmationsFlagName, "7"))
require.Equal(t, uint64(7), cfg.TxMgrConfig.NumConfirmations)
}
func verifyArgsInvalid(t *testing.T, messageContains string, cliArgs []string) {
_, _, err := runWithArgs(cliArgs)
require.ErrorContains(t, err, messageContains)
}
func configForArgs(t *testing.T, cliArgs []string) config.Config {
_, cfg, err := runWithArgs(cliArgs)
require.NoError(t, err)
return cfg
}
func runWithArgs(cliArgs []string) (log.Logger, config.Config, error) {
cfg := new(config.Config)
var logger log.Logger
fullArgs := append([]string{"op-program"}, cliArgs...)
err := run(fullArgs, func(log log.Logger, config *config.Config) error {
logger = log
cfg = config
return nil
})
return logger, *cfg, err
}
func addRequiredArgs(args ...string) []string {
req := requiredArgs()
combined := toArgList(req)
return append(combined, args...)
}
func addRequiredArgsExcept(name string, optionalArgs ...string) []string {
req := requiredArgs()
delete(req, name)
return append(toArgList(req), optionalArgs...)
}
func requiredArgs() map[string]string {
return map[string]string{
"--l1-eth-rpc": l1EthRpc,
"--game-address": gameAddressValue,
"--alphabet": alphabetTrace,
}
}
func toArgList(req map[string]string) []string {
var combined []string
for name, value := range req {
combined = append(combined, name)
combined = append(combined, value)
}
return combined
}
......@@ -8,10 +8,6 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/flags"
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"
oppprof "github.com/ethereum-optimism/optimism/op-service/pprof"
oprpc "github.com/ethereum-optimism/optimism/op-service/rpc"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
)
......@@ -29,31 +25,18 @@ type Config struct {
GameAddress common.Address // Address of the fault game
AlphabetTrace string // String for the AlphabetTraceProvider
TxMgrConfig txmgr.CLIConfig
RPCConfig oprpc.CLIConfig
LogConfig oplog.CLIConfig
MetricsConfig opmetrics.CLIConfig
PprofConfig oppprof.CLIConfig
TxMgrConfig txmgr.CLIConfig
}
func NewConfig(L1EthRpc string,
func NewConfig(l1EthRpc string,
GameAddress common.Address,
AlphabetTrace string,
TxMgrConfig txmgr.CLIConfig,
RPCConfig oprpc.CLIConfig,
LogConfig oplog.CLIConfig,
MetricsConfig opmetrics.CLIConfig,
PprofConfig oppprof.CLIConfig,
) Config {
return Config{
L1EthRpc,
GameAddress,
AlphabetTrace,
TxMgrConfig,
RPCConfig,
LogConfig,
MetricsConfig,
PprofConfig,
L1EthRpc: l1EthRpc,
GameAddress: GameAddress,
AlphabetTrace: AlphabetTrace,
TxMgrConfig: txmgr.NewCLIConfig(l1EthRpc),
}
}
......@@ -67,18 +50,6 @@ func (c Config) Check() error {
if c.AlphabetTrace == "" {
return ErrMissingAlphabetTrace
}
if err := c.RPCConfig.Check(); err != nil {
return err
}
if err := c.LogConfig.Check(); err != nil {
return err
}
if err := c.MetricsConfig.Check(); err != nil {
return err
}
if err := c.PprofConfig.Check(); err != nil {
return err
}
if err := c.TxMgrConfig.Check(); err != nil {
return err
}
......@@ -96,10 +67,6 @@ func NewConfigFromCLI(ctx *cli.Context) (*Config, error) {
}
txMgrConfig := txmgr.ReadCLIConfig(ctx)
rpcConfig := oprpc.ReadCLIConfig(ctx)
logConfig := oplog.ReadCLIConfig(ctx)
metricsConfig := opmetrics.ReadCLIConfig(ctx)
pprofConfig := oppprof.ReadCLIConfig(ctx)
return &Config{
// Required Flags
......@@ -107,10 +74,5 @@ func NewConfigFromCLI(ctx *cli.Context) (*Config, error) {
GameAddress: dgfAddress,
AlphabetTrace: ctx.String(flags.AlphabetFlag.Name),
TxMgrConfig: txMgrConfig,
// Optional Flags
RPCConfig: rpcConfig,
LogConfig: logConfig,
MetricsConfig: metricsConfig,
PprofConfig: pprofConfig,
}, nil
}
......@@ -2,65 +2,20 @@ package config
import (
"testing"
"time"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
oppprof "github.com/ethereum-optimism/optimism/op-service/pprof"
oprpc "github.com/ethereum-optimism/optimism/op-service/rpc"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum-optimism/optimism/op-signer/client"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
var (
validL1EthRpc = "http://localhost:8545"
validGameAddress = common.HexToAddress("0x7bdd3b028C4796eF0EAf07d11394d0d9d8c24139")
validNetworkTimeout = time.Duration(5) * time.Second
validAlphabetTrace = "abcdefgh"
validL1EthRpc = "http://localhost:8545"
validGameAddress = common.HexToAddress("0x7bdd3b028C4796eF0EAf07d11394d0d9d8c24139")
validAlphabetTrace = "abcdefgh"
)
var validTxMgrConfig = txmgr.CLIConfig{
L1RPCURL: validL1EthRpc,
NumConfirmations: 10,
NetworkTimeout: validNetworkTimeout,
ResubmissionTimeout: time.Duration(5) * time.Second,
ReceiptQueryInterval: time.Duration(5) * time.Second,
TxNotInMempoolTimeout: time.Duration(5) * time.Second,
SafeAbortNonceTooLowCount: 10,
SignerCLIConfig: client.CLIConfig{
Endpoint: "http://localhost:8547",
// First address for the default hardhat mnemonic
Address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
},
}
var validRPCConfig = oprpc.CLIConfig{
ListenAddr: "localhost:8547",
ListenPort: 8547,
}
var validLogConfig = oplog.DefaultCLIConfig()
var validMetricsConfig = opmetrics.CLIConfig{
Enabled: false,
}
var validPprofConfig = oppprof.CLIConfig{
Enabled: false,
}
func validConfig() Config {
cfg := NewConfig(validL1EthRpc,
validGameAddress,
validAlphabetTrace,
validTxMgrConfig,
validRPCConfig,
validLogConfig,
validMetricsConfig,
validPprofConfig)
cfg := NewConfig(validL1EthRpc, validGameAddress, validAlphabetTrace)
return cfg
}
......
......@@ -7,9 +7,6 @@ import (
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"
oppprof "github.com/ethereum-optimism/optimism/op-service/pprof"
oprpc "github.com/ethereum-optimism/optimism/op-service/rpc"
txmgr "github.com/ethereum-optimism/optimism/op-service/txmgr"
)
......@@ -50,10 +47,7 @@ var requiredFlags = []cli.Flag{
var optionalFlags = []cli.Flag{}
func init() {
optionalFlags = append(optionalFlags, oprpc.CLIFlags(envVarPrefix)...)
optionalFlags = append(optionalFlags, oplog.CLIFlags(envVarPrefix)...)
optionalFlags = append(optionalFlags, opmetrics.CLIFlags(envVarPrefix)...)
optionalFlags = append(optionalFlags, oppprof.CLIFlags(envVarPrefix)...)
optionalFlags = append(optionalFlags, txmgr.CLIFlags(envVarPrefix)...)
Flags = append(requiredFlags, optionalFlags...)
......
......@@ -23,6 +23,12 @@ func CLIFlags(envPrefix string) []cli.Flag {
return CLIFlagsWithFlagPrefix(envPrefix, "")
}
var (
defaultTLSCaCert = "tls/ca.crt"
defaultTLSCert = "tls/tls.crt"
defaultTLSKey = "tls/tls.key"
)
// CLIFlagsWithFlagPrefix returns flags with env var and cli flag prefixes
// Should be used for client TLS configs when different from server on the same process
func CLIFlagsWithFlagPrefix(envPrefix string, flagPrefix string) []cli.Flag {
......@@ -36,19 +42,19 @@ func CLIFlagsWithFlagPrefix(envPrefix string, flagPrefix string) []cli.Flag {
&cli.StringFlag{
Name: prefixFunc(TLSCaCertFlagName),
Usage: "tls ca cert path",
Value: "tls/ca.crt",
Value: defaultTLSCaCert,
EnvVars: prefixEnvVars("TLS_CA"),
},
&cli.StringFlag{
Name: prefixFunc(TLSCertFlagName),
Usage: "tls cert path",
Value: "tls/tls.crt",
Value: defaultTLSCert,
EnvVars: prefixEnvVars("TLS_CERT"),
},
&cli.StringFlag{
Name: prefixFunc(TLSKeyFlagName),
Usage: "tls key",
Value: "tls/tls.key",
Value: defaultTLSKey,
EnvVars: prefixEnvVars("TLS_KEY"),
},
}
......@@ -60,6 +66,14 @@ type CLIConfig struct {
TLSKey string
}
func NewCLIConfig() CLIConfig {
return CLIConfig{
TLSCaCert: defaultTLSCaCert,
TLSCert: defaultTLSCert,
TLSKey: defaultTLSKey,
}
}
func (c CLIConfig) Check() error {
if c.TLSEnabled() && (c.TLSCaCert == "" || c.TLSCert == "" || c.TLSKey == "") {
return errors.New("all tls flags must be set if at least one is set")
......
package tls
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
)
func TestDefaultCLIOptionsMatchDefaultConfig(t *testing.T) {
cfg := configForArgs()
defaultCfg := NewCLIConfig()
require.Equal(t, defaultCfg, cfg)
}
func TestDefaultConfigIsValid(t *testing.T) {
err := NewCLIConfig().Check()
require.NoError(t, err)
}
func TestInvalidConfig(t *testing.T) {
tests := []struct {
name string
configChange func(config *CLIConfig)
}{
{"MissingCaCert", func(config *CLIConfig) {
config.TLSCaCert = ""
}},
{"MissingCert", func(config *CLIConfig) {
config.TLSCert = ""
}},
{"MissingKey", func(config *CLIConfig) {
config.TLSKey = ""
}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cfg := NewCLIConfig()
test.configChange(&cfg)
err := cfg.Check()
require.ErrorContains(t, err, "all tls flags must be set if at least one is set")
})
}
}
func configForArgs(args ...string) CLIConfig {
app := cli.NewApp()
app.Flags = CLIFlagsWithFlagPrefix("TEST_", "test")
app.Name = "test"
var config CLIConfig
app.Action = func(ctx *cli.Context) error {
config = ReadCLIConfigWithPrefix(ctx, "test")
return nil
}
_ = app.Run(args)
return config
}
......@@ -48,6 +48,16 @@ var (
}
)
var (
defaultNumConfirmations = uint64(10)
defaultSafeAbortNonceTooLowCount = uint64(3)
defaultResubmissionTimeout = 48 * time.Second
defaultNetworkTimeout = 2 * time.Second
defaultTxSendTimeout = 0 * time.Second
defaultTxNotInMempoolTimeout = 2 * time.Minute
defaultReceiptQueryInterval = 12 * time.Second
)
func CLIFlags(envPrefix string) []cli.Flag {
prefixEnvVars := func(name string) []string {
return opservice.PrefixEnvVar(envPrefix, name)
......@@ -71,43 +81,43 @@ func CLIFlags(envPrefix string) []cli.Flag {
&cli.Uint64Flag{
Name: NumConfirmationsFlagName,
Usage: "Number of confirmations which we will wait after sending a transaction",
Value: 10,
Value: defaultNumConfirmations,
EnvVars: prefixEnvVars("NUM_CONFIRMATIONS"),
},
&cli.Uint64Flag{
Name: SafeAbortNonceTooLowCountFlagName,
Usage: "Number of ErrNonceTooLow observations required to give up on a tx at a particular nonce without receiving confirmation",
Value: 3,
Value: defaultSafeAbortNonceTooLowCount,
EnvVars: prefixEnvVars("SAFE_ABORT_NONCE_TOO_LOW_COUNT"),
},
&cli.DurationFlag{
Name: ResubmissionTimeoutFlagName,
Usage: "Duration we will wait before resubmitting a transaction to L1",
Value: 48 * time.Second,
Value: defaultResubmissionTimeout,
EnvVars: prefixEnvVars("RESUBMISSION_TIMEOUT"),
},
&cli.DurationFlag{
Name: NetworkTimeoutFlagName,
Usage: "Timeout for all network operations",
Value: 2 * time.Second,
Value: defaultNetworkTimeout,
EnvVars: prefixEnvVars("NETWORK_TIMEOUT"),
},
&cli.DurationFlag{
Name: TxSendTimeoutFlagName,
Usage: "Timeout for sending transactions. If 0 it is disabled.",
Value: 0,
Value: defaultTxSendTimeout,
EnvVars: prefixEnvVars("TXMGR_TX_SEND_TIMEOUT"),
},
&cli.DurationFlag{
Name: TxNotInMempoolTimeoutFlagName,
Usage: "Timeout for aborting a tx send if the tx does not make it to the mempool.",
Value: 2 * time.Minute,
Value: defaultTxNotInMempoolTimeout,
EnvVars: prefixEnvVars("TXMGR_TX_NOT_IN_MEMPOOL_TIMEOUT"),
},
&cli.DurationFlag{
Name: ReceiptQueryIntervalFlagName,
Usage: "Frequency to poll for receipts",
Value: 12 * time.Second,
Value: defaultReceiptQueryInterval,
EnvVars: prefixEnvVars("TXMGR_RECEIPT_QUERY_INTERVAL"),
},
}, client.CLIFlags(envPrefix)...)
......@@ -130,6 +140,20 @@ type CLIConfig struct {
TxNotInMempoolTimeout time.Duration
}
func NewCLIConfig(l1RPCURL string) CLIConfig {
return CLIConfig{
L1RPCURL: l1RPCURL,
NumConfirmations: defaultNumConfirmations,
SafeAbortNonceTooLowCount: defaultSafeAbortNonceTooLowCount,
ResubmissionTimeout: defaultResubmissionTimeout,
NetworkTimeout: defaultNetworkTimeout,
TxSendTimeout: defaultTxSendTimeout,
TxNotInMempoolTimeout: defaultTxNotInMempoolTimeout,
ReceiptQueryInterval: defaultReceiptQueryInterval,
SignerCLIConfig: client.NewCLIConfig(),
}
}
func (m CLIConfig) Check() error {
if m.L1RPCURL == "" {
return errors.New("must provide a L1 RPC url")
......
package txmgr
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
)
var (
l1EthRpcValue = "http://localhost:9546"
)
func TestDefaultCLIOptionsMatchDefaultConfig(t *testing.T) {
cfg := configForArgs()
defaultCfg := NewCLIConfig(l1EthRpcValue)
require.Equal(t, defaultCfg, cfg)
}
func TestDefaultConfigIsValid(t *testing.T) {
cfg := NewCLIConfig(l1EthRpcValue)
require.NoError(t, cfg.Check())
}
func configForArgs(args ...string) CLIConfig {
app := cli.NewApp()
// txmgr expects the --l1-eth-rpc option to be declared externally
flags := append(CLIFlags("TEST_"), &cli.StringFlag{
Name: L1RPCFlagName,
Value: l1EthRpcValue,
})
app.Flags = flags
app.Name = "test"
var config CLIConfig
app.Action = func(ctx *cli.Context) error {
config = ReadCLIConfig(ctx)
return nil
}
_ = app.Run(args)
return config
}
......@@ -38,6 +38,12 @@ type CLIConfig struct {
TLSConfig optls.CLIConfig
}
func NewCLIConfig() CLIConfig {
return CLIConfig{
TLSConfig: optls.NewCLIConfig(),
}
}
func (c CLIConfig) Check() error {
if err := c.TLSConfig.Check(); err != nil {
return err
......
package client
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
)
func TestDefaultCLIOptionsMatchDefaultConfig(t *testing.T) {
cfg := configForArgs()
defaultCfg := NewCLIConfig()
require.Equal(t, defaultCfg, cfg)
}
func TestDefaultConfigIsValid(t *testing.T) {
err := NewCLIConfig().Check()
require.NoError(t, err)
}
func TestInvalidConfig(t *testing.T) {
tests := []struct {
name string
expected string
configChange func(config *CLIConfig)
}{
{
name: "MissingEndpoint",
expected: "signer endpoint and address must both be set or not set",
configChange: func(config *CLIConfig) {
config.Address = "0x1234"
},
},
{
name: "MissingAddress",
expected: "signer endpoint and address must both be set or not set",
configChange: func(config *CLIConfig) {
config.Endpoint = "http://localhost"
},
},
{
name: "InvalidTLSConfig",
expected: "all tls flags must be set if at least one is set",
configChange: func(config *CLIConfig) {
config.TLSConfig.TLSKey = ""
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cfg := NewCLIConfig()
test.configChange(&cfg)
err := cfg.Check()
require.ErrorContains(t, err, test.expected)
})
}
}
func configForArgs(args ...string) CLIConfig {
app := cli.NewApp()
app.Flags = CLIFlags("TEST_")
app.Name = "test"
var config CLIConfig
app.Action = func(ctx *cli.Context) error {
config = ReadCLIConfig(ctx)
return nil
}
_ = app.Run(args)
return config
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment