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 @@ -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 @@ -304,9 +305,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)")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this change will affect old users ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe some tests could help for those changes ?

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 @@ -322,7 +327,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