diff --git a/cmd/flags/common.go b/cmd/flags/common.go index 1aeeda8ee..26d08292c 100644 --- a/cmd/flags/common.go +++ b/cmd/flags/common.go @@ -128,6 +128,12 @@ var ( Category: commonCategory, Value: 12 * time.Second, } + // ConfigFile Path to a config file + ConfigFile = &cli.StringFlag{ + Name: "config.file", + Usage: "Path to a config file to use for setting up application", + Category: commonCategory, + } ) // CommonFlags All common flags. @@ -145,6 +151,7 @@ var CommonFlags = []cli.Flag{ BackOffMaxRetrys, BackOffRetryInterval, RPCTimeout, + ConfigFile, } // MergeFlags merges the given flag slices. diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go new file mode 100644 index 000000000..c8db27b2f --- /dev/null +++ b/cmd/utils/flags.go @@ -0,0 +1,44 @@ +package utils + +import ( + "bufio" + "errors" + "fmt" + "os" + "reflect" + "unicode" + + "github.com/naoina/toml" +) + +var tomlSettings = toml.Config{ + NormFieldName: func(_ reflect.Type, key string) string { + return key + }, + FieldToKey: func(_ reflect.Type, field string) string { + return field + }, + MissingField: func(rt reflect.Type, field string) error { + var link string + if unicode.IsUpper(rune(rt.Name()[0])) && rt.PkgPath() != "main" { + link = fmt.Sprintf(", see https://godoc.org/%s#%s for available fields", rt.PkgPath(), rt.Name()) + } + return fmt.Errorf("field '%s' is not defined in %s%s", field, rt.String(), link) + }, +} + +func LoadConfigFile(file string, cfg interface{}) error { + f, err := os.Open(file) + if err != nil { + return err + } + defer f.Close() + + err = tomlSettings.NewDecoder(bufio.NewReader(f)).Decode(cfg) + // Add file name to errors that have a line number. + var lineError *toml.LineError + if errors.As(err, &lineError) { + err = errors.New(file + ", " + err.Error()) + } + return err +} diff --git a/driver/config.go b/driver/config.go index 852578f3a..bfef4d7c4 100644 --- a/driver/config.go +++ b/driver/config.go @@ -6,12 +6,13 @@ import ( "net/url" "time" + "github.com/cenkalti/backoff/v4" "github.com/ethereum/go-ethereum/common" - "github.com/urfave/cli/v2" - "github.com/taikoxyz/taiko-client/cmd/flags" + "github.com/taikoxyz/taiko-client/cmd/utils" "github.com/taikoxyz/taiko-client/pkg/jwt" "github.com/taikoxyz/taiko-client/pkg/rpc" + "github.com/urfave/cli/v2" ) // Config contains the configurations to initialize a Taiko driver. @@ -28,51 +29,102 @@ type Config struct { // NewConfigFromCliContext creates a new config instance from // the command line inputs. func NewConfigFromCliContext(c *cli.Context) (*Config, error) { - jwtSecret, err := jwt.ParseSecretFromFile(c.String(flags.JWTSecret.Name)) - if err != nil { - return nil, fmt.Errorf("invalid JWT secret file: %w", err) + // Defaults config + cfg := Config{ + ClientConfig: &rpc.ClientConfig{ + // TODO: To be confirmed whether taiko addresses in L1 or L2 are constant + TaikoL1Address: common.HexToAddress(c.String(flags.TaikoL1Address.Name)), + TaikoL2Address: common.HexToAddress(c.String(flags.TaikoL2Address.Name)), + Timeout: 12 * time.Second, + }, + RetryInterval: backoff.DefaultMaxInterval, + P2PSyncVerifiedBlocks: false, + P2PSyncTimeout: 1 * time.Hour, + RPCTimeout: 12 * time.Second, + MaxExponent: 0, } - var ( - p2pSyncVerifiedBlocks = c.Bool(flags.P2PSyncVerifiedBlocks.Name) - l2CheckPoint = c.String(flags.CheckPointSyncURL.Name) - ) + // Load config file + if file := c.String(flags.ConfigFile.Name); file != "" { + if err := utils.LoadConfigFile(file, &cfg); err != nil { + return nil, fmt.Errorf("%w", err) + } + } + // Apply flag value + err := ApplyFlagValue(c, &cfg) + if err != nil { + return nil, err + } + return &cfg, nil +} - if p2pSyncVerifiedBlocks && len(l2CheckPoint) == 0 { - return nil, errors.New("empty L2 check point URL") +func ApplyFlagValue(c *cli.Context, cfg *Config) error { + if c.IsSet(flags.L1WSEndpoint.Name) { + cfg.ClientConfig.L1Endpoint = c.String(flags.L1WSEndpoint.Name) } if !c.IsSet(flags.L1BeaconEndpoint.Name) { - return nil, errors.New("empty L1 beacon endpoint") + return errors.New("empty L1 beacon endpoint") + } + cfg.ClientConfig.L1BeaconEndpoint = c.String(flags.L1BeaconEndpoint.Name) + + if c.IsSet(flags.L2WSEndpoint.Name) { + cfg.ClientConfig.L2Endpoint = c.String(flags.L2WSEndpoint.Name) + } + + if c.IsSet(flags.TaikoL1Address.Name) { + cfg.ClientConfig.TaikoL1Address = common.HexToAddress(c.String(flags.TaikoL1Address.Name)) + } + + if c.IsSet(flags.TaikoL2Address.Name) { + cfg.ClientConfig.TaikoL2Address = common.HexToAddress(c.String(flags.TaikoL2Address.Name)) + } + + if c.IsSet(flags.L2AuthEndpoint.Name) { + cfg.ClientConfig.L2EngineEndpoint = c.String(flags.L2AuthEndpoint.Name) + } + + if c.IsSet(flags.RPCTimeout.Name) { + cfg.ClientConfig.Timeout = c.Duration(flags.RPCTimeout.Name) + cfg.RPCTimeout = c.Duration(flags.RPCTimeout.Name) + } + + if c.IsSet(flags.BackOffRetryInterval.Name) { + cfg.RetryInterval = c.Duration(flags.BackOffRetryInterval.Name) + } + + if c.IsSet(flags.P2PSyncTimeout.Name) { + cfg.P2PSyncTimeout = c.Duration(flags.P2PSyncTimeout.Name) + } + + if c.IsSet(flags.MaxExponent.Name) { + cfg.MaxExponent = c.Uint64(flags.MaxExponent.Name) } - var blobServerEndpoint *url.URL if c.IsSet(flags.BlobServerEndpoint.Name) { - if blobServerEndpoint, err = url.Parse( + blobServerEndpoint, err := url.Parse( c.String(flags.BlobServerEndpoint.Name), - ); err != nil { - return nil, err + ) + if err != nil { + return err } + cfg.BlobServerEndpoint = blobServerEndpoint } - var timeout = c.Duration(flags.RPCTimeout.Name) - return &Config{ - ClientConfig: &rpc.ClientConfig{ - L1Endpoint: c.String(flags.L1WSEndpoint.Name), - L1BeaconEndpoint: c.String(flags.L1BeaconEndpoint.Name), - L2Endpoint: c.String(flags.L2WSEndpoint.Name), - L2CheckPoint: l2CheckPoint, - TaikoL1Address: common.HexToAddress(c.String(flags.TaikoL1Address.Name)), - TaikoL2Address: common.HexToAddress(c.String(flags.TaikoL2Address.Name)), - L2EngineEndpoint: c.String(flags.L2AuthEndpoint.Name), - JwtSecret: string(jwtSecret), - Timeout: timeout, - }, - RetryInterval: c.Duration(flags.BackOffRetryInterval.Name), - P2PSyncVerifiedBlocks: p2pSyncVerifiedBlocks, - P2PSyncTimeout: c.Duration(flags.P2PSyncTimeout.Name), - RPCTimeout: timeout, - MaxExponent: c.Uint64(flags.MaxExponent.Name), - BlobServerEndpoint: blobServerEndpoint, - }, nil + jwtSecret, err := jwt.ParseSecretFromFile(c.String(flags.JWTSecret.Name)) + if err != nil { + return fmt.Errorf("invalid JWT secret file: %w", err) + } + cfg.ClientConfig.JwtSecret = string(jwtSecret) + + // Must be defined via flags + p2pSyncVerifiedBlocks := c.Bool(flags.P2PSyncVerifiedBlocks.Name) + l2CheckPoint := c.String(flags.CheckPointSyncURL.Name) + if p2pSyncVerifiedBlocks && len(l2CheckPoint) == 0 { + return errors.New("empty L2 check point URL") + } + cfg.ClientConfig.L2CheckPoint = l2CheckPoint + cfg.P2PSyncVerifiedBlocks = p2pSyncVerifiedBlocks + + return nil } diff --git a/driver/config_test.go b/driver/config_test.go index 2d528cb72..fb243ffd6 100644 --- a/driver/config_test.go +++ b/driver/config_test.go @@ -21,6 +21,42 @@ var ( rpcTimeout = 5 * time.Second ) +func (s *DriverTestSuite) TestNewConfigFromFile() { + app := s.SetupApp() + app.Action = func(ctx *cli.Context) error { + c, err := NewConfigFromCliContext(ctx) + s.Nil(err) + s.Equal("ws://localhost:10000", c.L1Endpoint) + return err + } + + s.Nil(app.Run([]string{ + "TestNewConfigFromFile", + "--" + flags.L1BeaconEndpoint.Name, l1BeaconEndpoint, + "--" + flags.JWTSecret.Name, os.Getenv("JWT_SECRET"), + "--" + flags.ConfigFile.Name, "config_test.toml", + })) +} + +func (s *DriverTestSuite) TestDefaultOverwrite() { + app := s.SetupApp() + app.Action = func(ctx *cli.Context) error { + c, err := NewConfigFromCliContext(ctx) + s.Nil(err) + s.Equal(true, c.P2PSyncVerifiedBlocks) + return err + } + + s.Nil(app.Run([]string{ + "TestDefaultOverwrite", + "--" + flags.L1BeaconEndpoint.Name, l1BeaconEndpoint, + "--" + flags.JWTSecret.Name, os.Getenv("JWT_SECRET"), + "--" + flags.P2PSyncVerifiedBlocks.Name, + "--" + flags.CheckPointSyncURL.Name, os.Getenv("L2_EXECUTION_ENGINE_HTTP_ENDPOINT"), + "--" + flags.ConfigFile.Name, "config_test.toml", + })) +} + func (s *DriverTestSuite) TestNewConfigFromCliContext() { app := s.SetupApp() @@ -63,6 +99,7 @@ func (s *DriverTestSuite) TestNewConfigFromCliContextJWTError() { app := s.SetupApp() s.ErrorContains(app.Run([]string{ "TestNewConfigFromCliContext", + "--" + flags.L1BeaconEndpoint.Name, l1BeaconEndpoint, "--" + flags.JWTSecret.Name, "wrongsecretfile.txt", }), "invalid JWT secret file") } @@ -71,6 +108,7 @@ func (s *DriverTestSuite) TestNewConfigFromCliContextEmptyL2CheckPoint() { app := s.SetupApp() s.ErrorContains(app.Run([]string{ "TestNewConfigFromCliContext", + "--" + flags.L1BeaconEndpoint.Name, l1BeaconEndpoint, "--" + flags.JWTSecret.Name, os.Getenv("JWT_SECRET"), "--" + flags.P2PSyncVerifiedBlocks.Name, "--" + flags.L2WSEndpoint.Name, "", @@ -91,6 +129,7 @@ func (s *DriverTestSuite) SetupApp() *cli.App { &cli.DurationFlag{Name: flags.P2PSyncTimeout.Name}, &cli.DurationFlag{Name: flags.RPCTimeout.Name}, &cli.StringFlag{Name: flags.CheckPointSyncURL.Name}, + &cli.StringFlag{Name: flags.ConfigFile.Name}, } app.Action = func(ctx *cli.Context) error { _, err := NewConfigFromCliContext(ctx) diff --git a/driver/config_test.toml b/driver/config_test.toml new file mode 100644 index 000000000..32ae07f0e --- /dev/null +++ b/driver/config_test.toml @@ -0,0 +1,3 @@ +P2PSyncVerifiedBlocks = false +[ClientConfig] +L1Endpoint = "ws://localhost:10000" diff --git a/go.mod b/go.mod index 619b0ca9d..cf2b7b1d2 100644 --- a/go.mod +++ b/go.mod @@ -148,6 +148,8 @@ require ( github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-multistream v0.5.0 // indirect github.com/multiformats/go-varint v0.0.7 // indirect + github.com/naoina/go-stringutil v0.1.0 // indirect + github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/onsi/ginkgo/v2 v2.15.0 // indirect github.com/opencontainers/runtime-spec v1.1.0 // indirect diff --git a/go.sum b/go.sum index df427cb99..cd155a1fa 100644 --- a/go.sum +++ b/go.sum @@ -712,6 +712,10 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 h1:shk/vn9oCoOTmwcouEdwIeOtOGA/ELRUw/GwvxwfT+0= +github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=