Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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 @@ -52,9 +52,10 @@ const (
optionNameBootnodeMode = "bootnode-mode"
optionNameSwapFactoryAddress = "swap-factory-address"
optionNameSwapInitialDeposit = "swap-initial-deposit"
optionNameNodeMode = "node-mode"
optionNameSwapEnable = "swap-enable"
optionNameChequebookEnable = "chequebook-enable"
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 @@ -299,9 +300,13 @@ func (c *command) setAllFlags(cmd *cobra.Command) {
cmd.Flags().Duration(optionNameBlockchainRpcKeepalive, 30*time.Second, "blockchain rpc TCP keepalive interval")
cmd.Flags().String(optionNameSwapFactoryAddress, "", "swap factory addresses")
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(optionNameFullNode, false, "cause the node to start in full mode")
cmd.Flags().Bool(optionNameChequebookEnable, false, "enable chequebook (requires swap-enable)")
Comment thread
martinconic marked this conversation as resolved.
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 @@ -317,7 +322,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
203 changes: 203 additions & 0 deletions cmd/bee/cmd/resolve_node_mode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// 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,
},
{
// Upgraders relying on the old chequebook-enable=true default must
// now fail loudly instead of silently degrading to pseudo-settle.
name: "legacy full-node true without chequebook fails",
config: map[string]any{
optionNameFullNode: true,
configKeyBlockchainRpcEndpoint: "http://localhost:8545",
optionNameSwapEnable: true,
optionNameStorageIncentivesEnable: true,
},
wantErr: "full node requires chequebook-enable",
},
{
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)
}
})
}
}
89 changes: 86 additions & 3 deletions cmd/bee/cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,13 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo
}

bootNode := c.config.GetBool(optionNameBootnodeMode)
fullNode := c.config.GetBool(optionNameFullNode)

if bootNode && !fullNode {
nodeMode, err := c.resolveNodeMode(logger)
if err != nil {
return nil, err
}

if bootNode && nodeMode != node.FullMode {
return nil, errors.New("boot node must be started as a full node")
}

Expand Down Expand Up @@ -297,7 +301,7 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo
EnableWS: c.config.GetBool(optionNameP2PWSEnable),
AutoTLSDomain: c.config.GetString(optionAutoTLSDomain),
AutoTLSRegistrationEndpoint: c.config.GetString(optionAutoTLSRegistrationEndpoint),
FullNodeMode: fullNode,
NodeMode: nodeMode,
Logger: logger,
MinimumGasTipCap: c.config.GetUint64(optionNameMinimumGasTipCap),
GasLimitFallback: c.config.GetUint64(optionNameGasLimitFallback),
Expand Down Expand Up @@ -337,6 +341,85 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo
return b, err
}

// resolveNodeMode determines the effective node mode from config.
// --node-mode takes precedence and triggers strict per-mode validation.
// The deprecated --full-node flag is honoured as a fallback and validated
// the same way when it requests full mode, so upgraders relying on the old
// chequebook-enable / storage-incentives-enable defaults fail loudly instead
// of silently degrading to pseudo-settle / no incentives.
// When neither is set, mode is inferred from blockchain-rpc-endpoint presence
// (legacy behaviour) without strict validation, for backward compatibility.
func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) {
rpcEndpoint := c.config.GetString(configKeyBlockchainRpcEndpoint)
swapEnable := c.config.GetBool(optionNameSwapEnable)
chequebookEnable := c.config.GetBool(optionNameChequebookEnable)
incentivesEnable := c.config.GetBool(optionNameStorageIncentivesEnable)

// chequebook init is gated on swap-enable in NewBee, so this combo is a
// silent no-op. Catch it eagerly regardless of mode.
if chequebookEnable && !swapEnable {
return "", errors.New("chequebook-enable requires swap-enable to be true")
}

validateFullMode := func() error {
if rpcEndpoint == "" {
return errors.New("full node requires blockchain-rpc-endpoint to be set")
}
if !swapEnable {
return errors.New("full node requires swap-enable to be true")
}
if !chequebookEnable {
return errors.New("full node requires chequebook-enable to be true (cheque issuance)")
}
if !incentivesEnable {
return errors.New("full node requires storage-incentives-enable to be true")
}
return nil
}

if c.config.IsSet(optionNameNodeMode) {
mode := node.NodeMode(c.config.GetString(optionNameNodeMode))
if !mode.IsValid() {
return "", fmt.Errorf("invalid node-mode %q: must be one of full, light, ultra-light", mode)
}
if c.config.GetBool(optionNameFullNode) {
logger.Warning("--full-node is set alongside --node-mode; --full-node is ignored")
}
switch mode {
case node.FullMode:
if err := validateFullMode(); err != nil {
return "", err
}
case node.LightMode:
if rpcEndpoint == "" {
return "", errors.New("light node requires blockchain-rpc-endpoint to be set")
}
case node.UltraLightMode:
if swapEnable {
return "", errors.New("ultra-light node cannot have swap-enable set to true")
}
}
return mode, nil
}

// Legacy path: node-mode not set. Apply strict validation when --full-node
// requests full mode so upgraders don't silently lose chequebook +
// incentives because of the new defaults.
if c.config.GetBool(optionNameFullNode) {
logger.Warning("--full-node is deprecated, use --node-mode=full instead")
if err := validateFullMode(); err != nil {
return "", err
}
return node.FullMode, nil
}

// Infer light vs ultra-light from RPC endpoint presence (original behaviour).
if rpcEndpoint != "" {
return node.LightMode, nil
}
return node.UltraLightMode, nil
}

type program struct {
start func()
stop func()
Expand Down
Loading
Loading