diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index f15e487d..c2288848 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -28,9 +28,9 @@ jobs: - name: Lint uses: golangci/golangci-lint-action@v9 with: - version: v2.10.1 + version: v2.11.3 args: --timeout 10m - skip-cache: false + skip-cache: true - name: Vet run: make vet - name: Whitespace check diff --git a/AGENTS.md b/AGENTS.md index 6b281c28..3ce51c5e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ Beekeeper consumes Bee as a library (`github.com/ethersphere/bee/v2` in `go.mod` ```bash make binary # build ./dist/beekeeper (CGO disabled, version stamped via -ldflags) make test # unit tests: go test -v ./pkg/... -make lint # golangci-lint (pinned v2.10.1; auto-installs if missing) +make lint # golangci-lint (pinned v2.11.3; auto-installs if missing) ``` Run a single test: `go test -run TestName ./pkg//...` (add `-race` to mirror CI). diff --git a/Makefile b/Makefile index 86744fd9..4d6ca66e 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ GO ?= go GOLANGCI_LINT ?= golangci-lint -GOLANGCI_LINT_VERSION ?= v2.10.1 +GOLANGCI_LINT_VERSION ?= v2.11.3 COMMIT ?= "$(shell git describe --long --dirty --always --match "" || true)" VERSION ?= "$(shell git describe --tags --abbrev=0 | cut -c2-)" LDFLAGS ?= -s -w \ diff --git a/config/config.yaml b/config/config.yaml index 5a806628..1da28211 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -104,7 +104,7 @@ bee-configs: db-disable-seeks-compaction: false db-open-files-limit: 200 db-write-buffer-size: 33554432 - full-node: true + node-mode: full mainnet: false nat-addr: "" network-id: 12345 @@ -134,6 +134,7 @@ bee-configs: bootnode: _inherit: "default" bootnode-mode: true + storage-incentives-enable: false # no effect on bootnodes; bee warns when it is true # checks defines checks Beekeeper can execute against the cluster # type filed allows defining same check with different names and options diff --git a/config/light-node.yaml b/config/light-node.yaml index 3722e979..746191f5 100644 --- a/config/light-node.yaml +++ b/config/light-node.yaml @@ -2,7 +2,8 @@ bee-configs: light-node: _inherit: default - full-node: false + node-mode: light + storage-incentives-enable: false # inherited true is rejected in light mode # node groups for light nodes node-groups: diff --git a/config/local.yaml b/config/local.yaml index d389ee2b..748b67e7 100644 --- a/config/local.yaml +++ b/config/local.yaml @@ -42,6 +42,11 @@ clusters: config: local-dns count: 3 mode: node + bee-legacy: + bee-config: bee-local-dns-legacy + config: local-dns + count: 1 + mode: node light: bee-config: bee-local-light config: local-light @@ -162,7 +167,7 @@ bee-configs: db-disable-seeks-compaction: false db-open-files-limit: 200 db-write-buffer-size: 33554432 - full-node: true + node-mode: full mainnet: false nat-addr: "" nat-wss-addr: "" @@ -192,6 +197,7 @@ bee-configs: bootnode-local-dns-autotls: _inherit: "bee-local-dns" bootnode-mode: true + storage-incentives-enable: false # no effect on bootnodes; bee warns when it is true p2p-wss-enable: true bee-local-autotls: _inherit: "bee-local-dns" @@ -201,20 +207,30 @@ bee-configs: p2p-wss-enable: true bee-local-ultralight-autotls: _inherit: "bee-local-dns" - full-node: false + node-mode: ultra-light p2p-wss-enable: true - blockchain-rpc-endpoint: # ultralight nodes don't connect to the blockchain + blockchain-rpc-endpoint: "" # ultralight nodes don't connect to the blockchain; empty, not null, so the inherited endpoint is cleared + swap-enable: false + chequebook-enable: false # inherited true is rejected without swap + storage-incentives-enable: false # inherited true is rejected in ultra-light mode bootnode-local: _inherit: "bee-local" bootnode-mode: true + storage-incentives-enable: false # no effect on bootnodes; bee warns when it is true bee-local-dns: _inherit: "bee-local" + bee-local-dns-legacy: + _inherit: "bee-local-dns" + node-mode: "" # overrides the inherited mode so bee infers it from full-node (legacy regime) + full-node: true bootnode-local-dns: _inherit: "bee-local" bootnode-mode: true + storage-incentives-enable: false # no effect on bootnodes; bee warns when it is true bee-local-light: _inherit: "bee-local" - full-node: false + node-mode: light + storage-incentives-enable: false # inherited true is rejected in light mode bee-local-gc: _inherit: "bee-local" cache-capacity: 10 @@ -222,6 +238,7 @@ bee-configs: _inherit: "bee-local" cache-capacity: 10 bootnode-mode: true + storage-incentives-enable: false # no effect on bootnodes; bee warns when it is true # CI checks checks: diff --git a/config/public-testnet.yaml b/config/public-testnet.yaml index 05ca6c4d..d1b3e41f 100644 --- a/config/public-testnet.yaml +++ b/config/public-testnet.yaml @@ -47,7 +47,7 @@ bee-configs: sepolia: _inherit: "" bootnode: ["/dnsaddr/testnet.ethswarm.org"] - full-node: true + node-mode: full checks: pt-pingpong: diff --git a/config/staging.yaml b/config/staging.yaml index e28ebeff..a8d94544 100644 --- a/config/staging.yaml +++ b/config/staging.yaml @@ -35,7 +35,7 @@ bee-configs: api-addr: ":1633" blockchain-rpc-endpoint: http://rpc-sepolia-haproxy.default.svc.swarm1.local bootnode: ["/dnsaddr/testnet.ethswarm.org"] - full-node: true + node-mode: full mainnet: false network-id: 10 p2p-addr: ":1634" diff --git a/config/testnet-bee-playground.yaml b/config/testnet-bee-playground.yaml index f0a3bc80..74fd7205 100644 --- a/config/testnet-bee-playground.yaml +++ b/config/testnet-bee-playground.yaml @@ -52,7 +52,7 @@ bee-configs: bootnode: [] chequebook-enable: true data-dir: "/home/bee/.bee" - full-node: true + node-mode: full mainnet: false network-id: 12345 p2p-addr: :1634 @@ -65,6 +65,7 @@ bee-configs: geth-playground-bootnode: _inherit: "geth-playground" bootnode-mode: true + storage-incentives-enable: false # no effect on bootnodes; bee warns when it is true checks: pg-pingpong: diff --git a/pkg/config/inherit_test.go b/pkg/config/inherit_test.go index 20e37847..de2ed91b 100644 --- a/pkg/config/inherit_test.go +++ b/pkg/config/inherit_test.go @@ -2,10 +2,12 @@ package config_test import ( "io" + "os" "testing" "github.com/ethersphere/beekeeper/pkg/config" "github.com/ethersphere/beekeeper/pkg/logging" + "gopkg.in/yaml.v3" ) // TestBeeConfigInheritance verifies that _inherit still merges per flag (not @@ -47,3 +49,67 @@ bee-configs: t.Errorf("FullNode = %v, want child override false", c.FullNode) } } + +// TestLocalDNSLegacyNodeGroup verifies that the local-dns cluster runs one node +// group in bee's legacy regime next to the node-mode groups: the legacy +// profile overrides the inherited node-mode with an empty value, so bee infers +// the mode from full-node, while its siblings keep the explicit mode. +func TestLocalDNSLegacyNodeGroup(t *testing.T) { + t.Parallel() + + in, err := os.ReadFile("../../config/local.yaml") + if err != nil { + t.Fatalf("read local.yaml: %v", err) + } + cfg, err := config.Read(logging.New(io.Discard, 0), []config.YamlFile{{Name: "local.yaml", Content: in}}) + if err != nil { + t.Fatalf("read config: %v", err) + } + + cluster, ok := cfg.Clusters["local-dns"] + if !ok { + t.Fatal("local-dns cluster not found") + } + ng, ok := cluster.GetNodeGroups()["bee-legacy"] + if !ok { + t.Fatal("bee-legacy node group not found in local-dns") + } + + legacy, ok := cfg.BeeConfigs[ng.BeeConfig] + if !ok { + t.Fatalf("bee-config %q not found", ng.BeeConfig) + } + l := legacy.Export() + if l.NodeMode == nil { + t.Error("NodeMode = nil, want explicit empty override") + } else if *l.NodeMode != "" { + t.Errorf("NodeMode = %q, want explicit empty override", *l.NodeMode) + } + if l.FullNode == nil || !*l.FullNode { + t.Errorf("FullNode = %v, want true", l.FullNode) + } + if !l.IsFullNode() { + t.Error("IsFullNode() = false, want true from the full-node fallback") + } + + out, err := yaml.Marshal(l) + if err != nil { + t.Fatalf("marshal bee config: %v", err) + } + rendered := map[string]any{} + if err := yaml.Unmarshal(out, &rendered); err != nil { + t.Fatalf("unmarshal rendered config: %v", err) + } + if got, ok := rendered["node-mode"]; !ok || got != "" { + t.Errorf("rendered node-mode = %v (present %t), want empty string so bee selects the legacy regime", got, ok) + } + if got := rendered["full-node"]; got != true { + t.Errorf("rendered full-node = %v, want true", got) + } + + dns := cfg.BeeConfigs["bee-local-dns"] + modern := dns.Export() + if !modern.IsFullNode() || modern.NodeMode == nil || *modern.NodeMode != "full" { + t.Error("bee-local-dns should keep the explicit node-mode full") + } +} diff --git a/pkg/orchestration/cluster.go b/pkg/orchestration/cluster.go index 6b52b571..a7081bda 100644 --- a/pkg/orchestration/cluster.go +++ b/pkg/orchestration/cluster.go @@ -23,6 +23,7 @@ type Cluster interface { FullNodeNames() (names []string) GlobalReplicationFactor(ctx context.Context, a swarm.Address) (grf int, err error) LightNodeNames() (names []string) + UltraLightNodeNames() (names []string) Name() string Namespace() string NodeGroup(name string) (ng NodeGroup, err error) diff --git a/pkg/orchestration/config_test.go b/pkg/orchestration/config_test.go index 6da47e2c..a951a035 100644 --- a/pkg/orchestration/config_test.go +++ b/pkg/orchestration/config_test.go @@ -60,3 +60,78 @@ func TestDeref(t *testing.T) { t.Errorf("Deref(new(false)) = %v, want false", got) } } + +func TestNodeModeHelpers(t *testing.T) { + t.Parallel() + + strPtr := func(s string) *string { return &s } + boolPtr := func(b bool) *bool { return &b } + + tests := []struct { + name string + cfg orchestration.Config + wantFull bool + wantLight bool + wantUltraLight bool + }{ + { + name: "explicit node-mode full", + cfg: orchestration.Config{NodeMode: strPtr("full")}, + wantFull: true, + wantLight: false, + wantUltraLight: false, + }, + { + name: "explicit node-mode light", + cfg: orchestration.Config{NodeMode: strPtr("light")}, + wantFull: false, + wantLight: true, + wantUltraLight: false, + }, + { + name: "explicit node-mode ultra-light", + cfg: orchestration.Config{NodeMode: strPtr("ultra-light")}, + wantFull: false, + wantLight: false, + wantUltraLight: true, + }, + { + name: "legacy full-node true", + cfg: orchestration.Config{FullNode: boolPtr(true)}, + wantFull: true, + wantLight: false, + wantUltraLight: false, + }, + { + name: "legacy full-node false with rpc (light)", + cfg: orchestration.Config{ + FullNode: boolPtr(false), + BlockchainRPCEndpoint: strPtr("http://localhost:8545"), + }, + wantFull: false, + wantLight: true, + wantUltraLight: false, + }, + { + name: "legacy full-node false without rpc (ultra-light)", + cfg: orchestration.Config{FullNode: boolPtr(false)}, + wantFull: false, + wantLight: false, + wantUltraLight: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.cfg.IsFullNode(); got != tt.wantFull { + t.Errorf("IsFullNode() = %v, want %v", got, tt.wantFull) + } + if got := tt.cfg.IsLightNode(); got != tt.wantLight { + t.Errorf("IsLightNode() = %v, want %v", got, tt.wantLight) + } + if got := tt.cfg.IsUltraLightNode(); got != tt.wantUltraLight { + t.Errorf("IsUltraLightNode() = %v, want %v", got, tt.wantUltraLight) + } + }) + } +} diff --git a/pkg/orchestration/k8s/cluster.go b/pkg/orchestration/k8s/cluster.go index cc689aba..bbb91de5 100644 --- a/pkg/orchestration/k8s/cluster.go +++ b/pkg/orchestration/k8s/cluster.go @@ -226,7 +226,17 @@ func (c *Cluster) NodeNames() (names []string) { // LightNodeNames returns a list of light node names func (c *Cluster) LightNodeNames() (names []string) { for name, node := range c.Nodes() { - if !orchestration.Deref(node.Config().FullNode) { + if node.Config().IsLightNode() { + names = append(names, name) + } + } + return names +} + +// UltraLightNodeNames returns a list of ultra-light node names +func (c *Cluster) UltraLightNodeNames() (names []string) { + for name, node := range c.Nodes() { + if node.Config().IsUltraLightNode() { names = append(names, name) } } @@ -237,7 +247,7 @@ func (c *Cluster) LightNodeNames() (names []string) { func (c *Cluster) FullNodeNames() (names []string) { for name, node := range c.Nodes() { cfg := node.Config() - if orchestration.Deref(cfg.FullNode) && !orchestration.Deref(cfg.BootnodeMode) { + if cfg.IsFullNode() && !orchestration.Deref(cfg.BootnodeMode) { names = append(names, name) } } @@ -249,7 +259,7 @@ func (c *Cluster) ShuffledFullNodeClients(ctx context.Context, r *rand.Rand) (or var res orchestration.ClientList for _, node := range c.Nodes() { cfg := node.Config() - if orchestration.Deref(cfg.FullNode) && !orchestration.Deref(cfg.BootnodeMode) { + if cfg.IsFullNode() && !orchestration.Deref(cfg.BootnodeMode) { res = append(res, node.Client()) } } @@ -464,7 +474,7 @@ func (c *Cluster) ClosestFullNodeClient(ctx context.Context, s *bee.Client) (*be } cfg := node.Config() // closet peer is not a full node. Check other peers in the same bin - if !orchestration.Deref(cfg.FullNode) || orchestration.Deref(cfg.BootnodeMode) { + if !cfg.IsFullNode() || orchestration.Deref(cfg.BootnodeMode) { skipList = append(skipList, addr) b-- continue diff --git a/pkg/orchestration/node.go b/pkg/orchestration/node.go index f450cd9c..de7ff6a1 100644 --- a/pkg/orchestration/node.go +++ b/pkg/orchestration/node.go @@ -113,7 +113,8 @@ type Config struct { DbDisableSeeksCompaction *bool `yaml:"db-disable-seeks-compaction,omitempty"` // disables db compactions triggered by seeks DbOpenFilesLimit *uint64 `yaml:"db-open-files-limit,omitempty"` // number of open files allowed by database DbWriteBufferSize *uint64 `yaml:"db-write-buffer-size,omitempty"` // size of the database write buffer in bytes - FullNode *bool `yaml:"full-node,omitempty"` // cause the node to start in full mode + FullNode *bool `yaml:"full-node,omitempty"` // cause the node to start in full mode (deprecated: use NodeMode) + NodeMode *string `yaml:"node-mode,omitempty"` // node operational mode: full, light, or ultra-light GasLimitFallback *uint64 `yaml:"gas-limit-fallback,omitempty"` // gas limit fallback when estimation fails for contract transactions Mainnet *bool `yaml:"mainnet,omitempty"` // triggers connect to main net bootnodes MinimumGasTipCap *uint64 `yaml:"minimum-gas-tip-cap,omitempty"` // minimum gas tip cap in wei for transactions, 0 means use suggested gas tip cap @@ -174,3 +175,28 @@ func Deref[T any](p *T) T { } return *p } + +// IsFullNode reports whether the node is configured as a full node. +// It checks NodeMode first; falls back to the deprecated FullNode bool. +func (c Config) IsFullNode() bool { + if c.NodeMode != nil && *c.NodeMode != "" { + return *c.NodeMode == "full" + } + return Deref(c.FullNode) +} + +// IsLightNode reports whether the node is configured as a light node. +func (c Config) IsLightNode() bool { + if c.NodeMode != nil && *c.NodeMode != "" { + return *c.NodeMode == "light" + } + return !Deref(c.FullNode) && Deref(c.BlockchainRPCEndpoint) != "" +} + +// IsUltraLightNode reports whether the node is configured as an ultra-light node. +func (c Config) IsUltraLightNode() bool { + if c.NodeMode != nil && *c.NodeMode != "" { + return *c.NodeMode == "ultra-light" + } + return !Deref(c.FullNode) && Deref(c.BlockchainRPCEndpoint) == "" +}