diff --git a/app/app.go b/app/app.go
index 3fff0e58e..1a30bc0b4 100644
--- a/app/app.go
+++ b/app/app.go
@@ -37,6 +37,7 @@ import (
"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/eth2wrap"
+ "github.com/obolnetwork/charon/app/featureset"
"github.com/obolnetwork/charon/app/lifecycle"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/app/retry"
@@ -64,6 +65,7 @@ import (
type Config struct {
P2P p2p.Config
Log log.Config
+ Feature featureset.Config
ManifestFile string
DataDir string
MonitoringAddr string
@@ -116,6 +118,10 @@ func Run(ctx context.Context, conf Config) (err error) {
return err
}
+ if err := featureset.Init(ctx, conf.Feature); err != nil {
+ return err
+ }
+
hash, timestamp := GitCommit()
log.Info(ctx, "Charon starting",
z.Str("version", version.Version),
diff --git a/app/app_test.go b/app/app_test.go
index c4e7ab723..a14b5f14e 100644
--- a/app/app_test.go
+++ b/app/app_test.go
@@ -34,6 +34,7 @@ import (
"github.com/obolnetwork/charon/app"
"github.com/obolnetwork/charon/app/errors"
+ "github.com/obolnetwork/charon/app/featureset"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/cmd"
"github.com/obolnetwork/charon/p2p"
@@ -168,6 +169,7 @@ func pingCluster(t *testing.T, test pingTest) {
for i := 0; i < n; i++ {
conf := app.Config{
Log: log.DefaultConfig(),
+ Feature: featureset.DefaultConfig(),
SimnetBMock: true,
MonitoringAddr: testutil.AvailableAddr(t).String(), // Random monitoring address
ValidatorAPIAddr: testutil.AvailableAddr(t).String(), // Random validatorapi address
diff --git a/app/featureset/config.go b/app/featureset/config.go
new file mode 100644
index 000000000..55099b40b
--- /dev/null
+++ b/app/featureset/config.go
@@ -0,0 +1,117 @@
+// Copyright © 2022 Obol Labs Inc.
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program. If not, see .
+
+package featureset
+
+import (
+ "context"
+ "math"
+ "strings"
+ "testing"
+
+ "github.com/obolnetwork/charon/app/errors"
+ "github.com/obolnetwork/charon/app/log"
+ "github.com/obolnetwork/charon/app/z"
+)
+
+const (
+ enable status = math.MaxInt
+ disable status = 0
+)
+
+// Config configures the feature set package.
+type Config struct {
+ // MinStatus defines the minimum enabled status.
+ MinStatus string
+ // Enabled overrides min status and enables a list of features.
+ Enabled []string
+ // Disabled overrides min status and disables a list of features.
+ Disabled []string
+}
+
+// DefaultConfig returns the default config enabling only stable features.
+func DefaultConfig() Config {
+ return Config{
+ MinStatus: statusStable.String(),
+ }
+}
+
+// Init initialises the global feature set state.
+func Init(ctx context.Context, config Config) error {
+ var ok bool
+ for s := statusAlpha; s < statusSentinel; s++ {
+ if strings.ToLower(config.MinStatus) == strings.ToLower(s.String()) {
+ minStatus = s
+ ok = true
+
+ break
+ }
+ }
+ if !ok {
+ return errors.New("unknown min status", z.Str("min_status", config.MinStatus))
+ }
+
+ for _, f := range config.Enabled {
+ var ok bool
+ for feature := range state {
+ if strings.ToLower(string(feature)) == strings.ToLower(f) {
+ state[feature] = enable
+ ok = true
+ }
+ }
+ if !ok {
+ log.Warn(ctx, "Ignoring unknown enabled feature", z.Str("feature", f))
+ }
+ }
+
+ for _, f := range config.Disabled {
+ var ok bool
+ for feature := range state {
+ if strings.ToLower(string(feature)) == strings.ToLower(f) {
+ state[feature] = disable
+ ok = true
+ }
+ }
+ if !ok {
+ log.Warn(ctx, "Ignoring unknown disabled feature", z.Str("feature", f))
+ }
+ }
+
+ return nil
+}
+
+// EnableForT enables a feature for testing.
+func EnableForT(t *testing.T, feature Feature) {
+ t.Helper()
+
+ cache := state[feature]
+ t.Cleanup(func() {
+ state[feature] = cache
+ })
+
+ state[feature] = enable
+}
+
+// DisableForT disables a feature for testing.
+func DisableForT(t *testing.T, feature Feature) {
+ t.Helper()
+
+ cache := state[feature]
+ t.Cleanup(func() {
+ state[feature] = cache
+ })
+
+ state[feature] = disable
+}
diff --git a/app/featureset/config_test.go b/app/featureset/config_test.go
new file mode 100644
index 000000000..92193601f
--- /dev/null
+++ b/app/featureset/config_test.go
@@ -0,0 +1,70 @@
+// Copyright © 2022 Obol Labs Inc.
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program. If not, see .
+
+package featureset_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/obolnetwork/charon/app/featureset"
+)
+
+// setup initialises global variable per test.
+func setup(t *testing.T) {
+ t.Helper()
+
+ err := featureset.Init(context.Background(), featureset.DefaultConfig())
+ require.NoError(t, err)
+}
+
+func TestConfig(t *testing.T) {
+ setup(t)
+
+ err := featureset.Init(context.Background(), featureset.DefaultConfig())
+ require.NoError(t, err)
+
+ err = featureset.Init(context.Background(), featureset.Config{
+ MinStatus: "alpha",
+ Enabled: []string{"ignored"},
+ })
+ require.NoError(t, err)
+
+ require.True(t, featureset.Enabled(featureset.QBFTConsensus))
+}
+
+func TestEnableForT(t *testing.T) {
+ setup(t)
+
+ testFeature := featureset.Feature("test")
+ require.False(t, featureset.Enabled(testFeature))
+
+ featureset.EnableForT(t, testFeature)
+ require.True(t, featureset.Enabled(testFeature))
+
+ featureset.DisableForT(t, testFeature)
+ require.False(t, featureset.Enabled(testFeature))
+}
+
+func TestQBFT(t *testing.T) {
+ setup(t)
+
+ require.False(t, featureset.Enabled(featureset.QBFTConsensus))
+
+ featureset.EnableForT(t, featureset.QBFTConsensus)
+ require.True(t, featureset.Enabled(featureset.QBFTConsensus))
+}
diff --git a/app/featureset/featureset.go b/app/featureset/featureset.go
new file mode 100644
index 000000000..f54a19c51
--- /dev/null
+++ b/app/featureset/featureset.go
@@ -0,0 +1,57 @@
+// Copyright © 2022 Obol Labs Inc.
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program. If not, see .
+
+// Package featureset defines a set of global features and their rollout status.
+package featureset
+
+//go:generate stringer -type=status -trimprefix=status
+
+// status enumerates the rollout status of a feature.
+type status int
+
+const (
+ // statusAlpha is for internal devnet testing.
+ statusAlpha status = iota + 1
+ // statusBeta is for internal and external testnet testing.
+ statusBeta
+ // statusStable is for stable feature ready for production.
+ statusStable
+ // statusSentinel is an internal tail-end placeholder.
+ statusSentinel // Must always be last
+)
+
+// Feature is a feature being rolled out.
+type Feature string
+
+const (
+ // QBFTConsensus introduces qbft consensus, see https://github.com/ObolNetwork/charon/issues/445.
+ QBFTConsensus Feature = "qbft_consensus"
+)
+
+var (
+ // state defines the current rollout status of each feature.
+ state = map[Feature]status{
+ QBFTConsensus: statusAlpha,
+ // Add all features and there status here.
+ }
+
+ // minStatus defines the minimum enabled status.
+ minStatus = statusStable
+)
+
+// Enabled returns true if the feature is enabled.
+func Enabled(feature Feature) bool {
+ return state[feature] >= minStatus
+}
diff --git a/app/featureset/featureset_internal_test.go b/app/featureset/featureset_internal_test.go
new file mode 100644
index 000000000..65d19b173
--- /dev/null
+++ b/app/featureset/featureset_internal_test.go
@@ -0,0 +1,35 @@
+// Copyright © 2022 Obol Labs Inc.
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program. If not, see .
+
+package featureset
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestAllFeatureStatus(t *testing.T) {
+ // Add all features to this test
+ features := []Feature{
+ QBFTConsensus,
+ }
+
+ for _, feature := range features {
+ status, ok := state[feature]
+ require.True(t, ok)
+ require.Greater(t, status, 0)
+ }
+}
diff --git a/app/featureset/status_string.go b/app/featureset/status_string.go
new file mode 100644
index 000000000..42683360e
--- /dev/null
+++ b/app/featureset/status_string.go
@@ -0,0 +1,41 @@
+// Copyright © 2022 Obol Labs Inc.
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program. If not, see .
+
+// Code generated by "stringer -type=status -trimprefix=status"; DO NOT EDIT.
+
+package featureset
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[statusAlpha-1]
+ _ = x[statusBeta-2]
+ _ = x[statusStable-3]
+}
+
+const _status_name = "AlphaBetaStable"
+
+var _status_index = [...]uint8{0, 5, 9, 15}
+
+func (i status) String() string {
+ i -= 1
+ if i < 0 || i >= status(len(_status_index)-1) {
+ return "status(" + strconv.FormatInt(int64(i+1), 10) + ")"
+ }
+ return _status_name[_status_index[i]:_status_index[i+1]]
+}
diff --git a/app/simnet_test.go b/app/simnet_test.go
index d163b6bcc..65af8ec22 100644
--- a/app/simnet_test.go
+++ b/app/simnet_test.go
@@ -34,6 +34,7 @@ import (
"github.com/obolnetwork/charon/app"
"github.com/obolnetwork/charon/app/errors"
+ "github.com/obolnetwork/charon/app/featureset"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/core"
"github.com/obolnetwork/charon/core/leadercast"
@@ -132,6 +133,7 @@ func testSimnet(t *testing.T, args simnetArgs, propose bool) {
for i := 0; i < args.N; i++ {
conf := app.Config{
Log: log.DefaultConfig(),
+ Feature: featureset.DefaultConfig(),
SimnetBMock: true,
SimnetVMock: args.VMocks[i],
MonitoringAddr: testutil.AvailableAddr(t).String(), // Random monitoring address
diff --git a/cmd/cmd_internal_test.go b/cmd/cmd_internal_test.go
index 4d2c1bc2d..d3aefae31 100644
--- a/cmd/cmd_internal_test.go
+++ b/cmd/cmd_internal_test.go
@@ -27,6 +27,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/obolnetwork/charon/app"
+ "github.com/obolnetwork/charon/app/featureset"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/p2p"
)
@@ -73,6 +74,11 @@ func TestCmdFlags(t *testing.T) {
Denylist: "",
DBPath: "",
},
+ Feature: featureset.Config{
+ MinStatus: "stable",
+ Enabled: nil,
+ Disabled: nil,
+ },
ManifestFile: "./charon/manifest.json",
DataDir: "from_env",
MonitoringAddr: "127.0.0.1:16001",
diff --git a/cmd/run.go b/cmd/run.go
index cf9050a9c..ce9e80aa9 100644
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -24,6 +24,7 @@ import (
"github.com/spf13/pflag"
"github.com/obolnetwork/charon/app"
+ "github.com/obolnetwork/charon/app/featureset"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/p2p"
)
@@ -47,6 +48,7 @@ func newRunCmd(runFunc func(context.Context, app.Config) error) *cobra.Command {
bindDataDirFlag(cmd.Flags(), &conf.DataDir)
bindP2PFlags(cmd.Flags(), &conf.P2P)
bindLogFlags(cmd.Flags(), &conf.Log)
+ bindFeatureFlags(cmd.Flags(), &conf.Feature)
return cmd
}
@@ -83,3 +85,9 @@ func bindP2PFlags(flags *pflag.FlagSet, config *p2p.Config) {
flags.StringVar(&config.Allowlist, "p2p-allowlist", "", "Comma-separated list of CIDR subnets for allowing only certain peer connections. Example: 192.168.0.0/16 would permit connections to peers on your local network only. The default is to accept all connections.")
flags.StringVar(&config.Denylist, "p2p-denylist", "", "Comma-separated list of CIDR subnets for disallowing certain peer connections. Example: 192.168.0.0/16 would disallow connections to peers on your local network. The default is to accept all connections.")
}
+
+func bindFeatureFlags(flags *pflag.FlagSet, config *featureset.Config) {
+ flags.StringSliceVar(&config.Enabled, "feature-set-enable", nil, "Comma-separated list of features to enable, overriding the default minimum feature set.")
+ flags.StringSliceVar(&config.Disabled, "feature-set-disable", nil, "Comma-separated list of features to disable, overriding the default minimum feature set.")
+ flags.StringVar(&config.MinStatus, "feature-set", "stable", "Minimum feature set to enable by default: alpha, beta, or stable. Warning: modify at own risk.")
+}
diff --git a/docs/configuration.md b/docs/configuration.md
index 57a8187ad..5afb51986 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -85,6 +85,9 @@ Usage:
Flags:
--beacon-node-endpoint string Beacon node endpoint URL (default "http://localhost/")
--data-dir string The directory where charon will store all its internal data (default "./charon/data")
+ --feature-set string Minimum feature set to enable by default: alpha, beta, or stable. Warning: modify at own risk. (default "stable")
+ --feature-set-disable strings Comma-separated list of features to disable, overriding the default minimum feature set.
+ --feature-set-enable strings Comma-separated list of features to enable, overriding the default minimum feature set.
-h, --help Help for run
--jaeger-address string Listening address for jaeger tracing
--jaeger-service string Service name used for jaeger tracing (default "charon")