Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/beekeeper.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ env:
SETUP_CONTRACT_IMAGE: "ethersphere/bee-localchain"
SETUP_CONTRACT_IMAGE_TAG: "0.9.4"
BEELOCAL_BRANCH: "main"
BEEKEEPER_BRANCH: "master"
BEEKEEPER_BRANCH: "refactor/node-mode-config"
Comment thread
martinconic marked this conversation as resolved.
BEEKEEPER_METRICS_ENABLED: false
REACHABILITY_OVERRIDE_PUBLIC: true
BATCHFACTOR_OVERRIDE_PUBLIC: 2
Expand Down
13 changes: 9 additions & 4 deletions cmd/bee/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,12 @@ const (
optionNameBzzTokenAddress = "bzz-token-address"
optionNameSwapFactoryAddress = "swap-factory-address"
optionNameSwapInitialDeposit = "swap-initial-deposit"
optionNameNodeMode = "node-mode"
optionNameSwapEnable = "swap-enable"
optionNameChequebookEnable = "chequebook-enable"
optionNameChequebookVerification = "chequebook-verification"
optionNameChequebookMinBalance = "chequebook-min-balance"
optionNameFullNode = "full-node"
optionNameFullNode = "full-node" // Deprecated: use node-mode instead.
optionNamePostageContractAddress = "postage-stamp-address"
optionNamePostageContractStartBlock = "postage-stamp-start-block"
optionNamePriceOracleAddress = "price-oracle-address"
Expand Down Expand Up @@ -356,11 +357,15 @@ func (c *command) setAllFlags(cmd *cobra.Command) {
cmd.Flags().String(optionNameSwapFactoryAddress, "", "swap factory addresses")
cmd.Flags().String(optionNameBzzTokenAddress, "", "bzz token contract address")
cmd.Flags().String(optionNameSwapInitialDeposit, "0", "initial deposit if deploying a new chequebook")
cmd.Flags().String(optionNameNodeMode, string(node.UltraLightMode), "node operational mode: full, light, or ultra-light")
cmd.Flags().Bool(optionNameSwapEnable, false, "enable swap")
cmd.Flags().Bool(optionNameChequebookEnable, true, "enable chequebook")
cmd.Flags().Bool(optionNameChequebookEnable, false, "enable chequebook (requires swap-enable)")
Comment thread
martinconic marked this conversation as resolved.
cmd.Flags().Bool(optionNameChequebookVerification, false, "reject full-node hive/handshake records that carry no chequebook address")
cmd.Flags().String(optionNameChequebookMinBalance, "110000000000000000", "minimum chequebook token balance required for verification, in token small units (default 11 BZZ)")
cmd.Flags().Bool(optionNameFullNode, false, "cause the node to start in full mode")
cmd.Flags().Bool(optionNameFullNode, false, "cause the node to start in full mode (deprecated: use --node-mode=full)")
if err := cmd.Flags().MarkDeprecated(optionNameFullNode, "use --node-mode=full instead"); err != nil {
panic(err)
}
cmd.Flags().String(optionNamePostageContractAddress, "", "postage stamp contract address")
cmd.Flags().Uint64(optionNamePostageContractStartBlock, 0, "postage stamp contract start block number")
cmd.Flags().String(optionNamePriceOracleAddress, "", "price oracle contract address")
Expand All @@ -376,7 +381,7 @@ func (c *command) setAllFlags(cmd *cobra.Command) {
cmd.Flags().Bool(optionNamePProfMutex, false, "enable pprof mutex profile")
cmd.Flags().StringSlice(optionNameStaticNodes, []string{}, "protect nodes from getting kicked out on bootnode")
cmd.Flags().Bool(optionNameAllowPrivateCIDRs, false, "allow to advertise private CIDRs to the public network")
cmd.Flags().Bool(optionNameStorageIncentivesEnable, true, "enable storage incentives feature")
cmd.Flags().Bool(optionNameStorageIncentivesEnable, false, "enable storage incentives feature (full node only)")
cmd.Flags().Uint64(optionNameStateStoreCacheCapacity, 100_000, "lru memory caching capacity in number of statestore entries")
cmd.Flags().String(optionNameTargetNeighborhood, "", "neighborhood to target in binary format (ex: 111111001) for mining the initial overlay")
cmd.Flags().String(optionNameNeighborhoodSuggester, "https://api.swarmscan.io/v1/network/neighborhoods/suggestion", "suggester for target neighborhood")
Expand Down
281 changes: 281 additions & 0 deletions cmd/bee/cmd/resolve_node_mode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
// Copyright 2026 The Swarm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cmd

import (
"strings"
"testing"

"github.com/ethersphere/bee/v2/pkg/log"
"github.com/ethersphere/bee/v2/pkg/node"
"github.com/spf13/viper"
)

func TestResolveNodeMode(t *testing.T) {
tests := []struct {
name string
config map[string]any
wantMode node.NodeMode
wantErr string
}{
// ── Explicit node-mode: strict validation ────────────────────────────────
{
name: "full mode with rpc, swap, chequebook and incentives succeeds",
config: map[string]any{
optionNameNodeMode: "full",
configKeyBlockchainRpcEndpoint: "http://localhost:8545",
optionNameSwapEnable: true,
optionNameChequebookEnable: true,
optionNameStorageIncentivesEnable: true,
},
wantMode: node.FullMode,
},
{
name: "full mode without rpc fails",
config: map[string]any{
optionNameNodeMode: "full",
optionNameSwapEnable: true,
},
wantErr: "full node requires blockchain-rpc-endpoint",
},
{
name: "full mode without swap fails",
config: map[string]any{
optionNameNodeMode: "full",
configKeyBlockchainRpcEndpoint: "http://localhost:8545",
},
wantErr: "full node requires swap-enable",
},
{
name: "full mode without chequebook fails",
config: map[string]any{
optionNameNodeMode: "full",
configKeyBlockchainRpcEndpoint: "http://localhost:8545",
optionNameSwapEnable: true,
optionNameStorageIncentivesEnable: true,
},
wantErr: "full node requires chequebook-enable",
},
{
name: "full mode without storage-incentives fails",
config: map[string]any{
optionNameNodeMode: "full",
configKeyBlockchainRpcEndpoint: "http://localhost:8545",
optionNameSwapEnable: true,
optionNameChequebookEnable: true,
},
wantErr: "storage-incentives-enable",
},
{
name: "chequebook-enable without swap-enable fails (light mode)",
config: map[string]any{
optionNameNodeMode: "light",
configKeyBlockchainRpcEndpoint: "http://localhost:8545",
optionNameChequebookEnable: true,
},
wantErr: "chequebook-enable requires swap-enable",
},
{
name: "light mode with rpc succeeds",
config: map[string]any{
optionNameNodeMode: "light",
configKeyBlockchainRpcEndpoint: "http://localhost:8545",
},
wantMode: node.LightMode,
},
{
name: "light mode without rpc fails",
config: map[string]any{
optionNameNodeMode: "light",
},
wantErr: "light node requires blockchain-rpc-endpoint",
},
{
name: "ultra-light mode succeeds",
config: map[string]any{
optionNameNodeMode: "ultra-light",
},
wantMode: node.UltraLightMode,
},
{
name: "ultra-light mode rejects swap-enable",
config: map[string]any{
optionNameNodeMode: "ultra-light",
optionNameSwapEnable: true,
},
wantErr: "ultra-light node cannot have swap-enable",
},
{
name: "invalid node-mode value fails",
config: map[string]any{
optionNameNodeMode: "superlight",
},
wantErr: "invalid node-mode",
},

// ── Legacy path: no node-mode set ────────────────────────────────────────
{
name: "legacy full-node true with all required flags maps to full mode",
config: map[string]any{
optionNameFullNode: true,
configKeyBlockchainRpcEndpoint: "http://localhost:8545",
optionNameSwapEnable: true,
optionNameChequebookEnable: true,
optionNameStorageIncentivesEnable: true,
},
wantMode: node.FullMode,
},
{
// Upgrade compatibility: chequebook-enable and storage-incentives-enable
// defaulted to true before node-mode existed. A legacy --full-node
// config that did not set them explicitly is auto-enabled so the node
// still starts as a fully-functional full node instead of failing
// validation (no silent degradation).
name: "legacy full-node true auto-enables chequebook and incentives",
config: map[string]any{
optionNameFullNode: true,
configKeyBlockchainRpcEndpoint: "http://localhost:8545",
optionNameSwapEnable: true,
},
wantMode: node.FullMode,
},
{
// The compatibility default only restores the old implicit value; an
// operator who explicitly disables chequebook while requesting a full
// node has a genuine misconfiguration that must still fail loudly.
name: "legacy full-node true with chequebook explicitly false fails",
config: map[string]any{
optionNameFullNode: true,
configKeyBlockchainRpcEndpoint: "http://localhost:8545",
optionNameSwapEnable: true,
optionNameChequebookEnable: false,
},
wantErr: "full node requires chequebook-enable",
},
{
// Same: explicitly disabling storage incentives must not be masked.
name: "legacy full-node true with storage-incentives explicitly false fails",
config: map[string]any{
optionNameFullNode: true,
configKeyBlockchainRpcEndpoint: "http://localhost:8545",
optionNameSwapEnable: true,
optionNameChequebookEnable: true,
optionNameStorageIncentivesEnable: false,
},
wantErr: "storage-incentives-enable",
},
{
// swap-enable was also implied by --full-node before node-mode; a
// legacy config with only the RPC endpoint set is fully restored and
// starts as a full node rather than failing.
name: "legacy full-node true with only rpc auto-enables full stack",
config: map[string]any{
optionNameFullNode: true,
configKeyBlockchainRpcEndpoint: "http://localhost:8545",
},
wantMode: node.FullMode,
},
{
// The RPC endpoint is the one thing the compatibility default cannot
// invent; a legacy full-node config without it must still fail.
name: "legacy full-node true without rpc endpoint fails",
config: map[string]any{
optionNameFullNode: true,
},
wantErr: "full node requires blockchain-rpc-endpoint",
},
{
name: "legacy with rpc endpoint infers light mode",
config: map[string]any{
configKeyBlockchainRpcEndpoint: "http://localhost:8545",
},
wantMode: node.LightMode,
},
{
name: "legacy without rpc endpoint infers ultra-light mode",
config: map[string]any{},
wantMode: node.UltraLightMode,
},
{
// Beekeeper's inherited-config scenario: rpc + swap-enable without node-mode.
// Legacy path must NOT apply strict swap validation; this was the CI regression.
name: "legacy with rpc and swap-enable infers light without error",
config: map[string]any{
configKeyBlockchainRpcEndpoint: "http://localhost:8545",
optionNameSwapEnable: true,
},
wantMode: node.LightMode,
},
{
// Same scenario but for ultra-light: no rpc, swap-enable inherited from base.
name: "legacy without rpc but with swap-enable infers ultra-light without error",
config: map[string]any{
optionNameSwapEnable: true,
},
wantMode: node.UltraLightMode,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &command{
config: viper.New(),
logger: log.Noop,
}
for k, v := range tt.config {
c.config.Set(k, v)
}

gotMode, err := c.resolveNodeMode(c.logger)

if tt.wantErr != "" {
if err == nil {
t.Fatalf("expected error containing %q, got nil (mode=%q)", tt.wantErr, gotMode)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("expected error containing %q, got %q", tt.wantErr, err.Error())
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if gotMode != tt.wantMode {
t.Errorf("got mode %q, want %q", gotMode, tt.wantMode)
}
})
}
}

// TestResolveNodeModeLegacyBackcompatWritesConfig verifies that the legacy
// --full-node compatibility path writes the restored swap-enable,
// chequebook-enable and storage-incentives-enable defaults back into the
// config, so the rest of node startup (which reads them directly from config)
// sees them enabled.
func TestResolveNodeModeLegacyBackcompatWritesConfig(t *testing.T) {
c := &command{
config: viper.New(),
logger: log.Noop,
}
c.config.Set(optionNameFullNode, true)
c.config.Set(configKeyBlockchainRpcEndpoint, "http://localhost:8545")

mode, err := c.resolveNodeMode(c.logger)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if mode != node.FullMode {
t.Fatalf("got mode %q, want %q", mode, node.FullMode)
}
for _, key := range []string{
optionNameSwapEnable,
optionNameChequebookEnable,
optionNameStorageIncentivesEnable,
} {
if !c.config.GetBool(key) {
t.Errorf("expected %q to be written back as true", key)
}
}
}
Loading
Loading