From e386677459c1509017d711031a0606182057c312 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Thu, 9 Apr 2026 12:56:27 +0300 Subject: [PATCH 01/22] refactor: replace full-node flag with explicit node-mode config --- cmd/bee/cmd/cmd.go | 13 +- cmd/bee/cmd/start.go | 53 +++++++- packaging/bee.yaml | 216 +++++++++++++++++------------- packaging/homebrew-amd64/bee.yaml | 216 +++++++++++++++++------------- packaging/homebrew-arm64/bee.yaml | 216 +++++++++++++++++------------- packaging/scoop/bee.yaml | 216 +++++++++++++++++------------- pkg/node/node.go | 70 ++++++---- 7 files changed, 580 insertions(+), 420 deletions(-) diff --git a/cmd/bee/cmd/cmd.go b/cmd/bee/cmd/cmd.go index 37542a7bd38..8eb9a631a52 100644 --- a/cmd/bee/cmd/cmd.go +++ b/cmd/bee/cmd/cmd.go @@ -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" @@ -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)") + 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") @@ -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") diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index 5773c4af3e3..9dc382d8f93 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -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") } @@ -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), @@ -337,6 +341,49 @@ 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; the deprecated --full-node flag is honoured as a fallback. +// It also validates that the required options for each mode are present. +func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) { + rpcEndpoint := c.config.GetString(configKeyBlockchainRpcEndpoint) + swapEnable := c.config.GetBool(optionNameSwapEnable) + + // Resolve the mode: explicit node-mode wins, then legacy full-node, then default ultra-light. + var mode node.NodeMode + 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) + } + } else if c.config.GetBool(optionNameFullNode) { + logger.Warning("--full-node is deprecated, use --node-mode=full instead") + mode = node.FullMode + } else { + mode = node.UltraLightMode + } + + // Validate mode-specific requirements. + switch mode { + case node.FullMode: + 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") + } + 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 +} + type program struct { start func() stop func() diff --git a/packaging/bee.yaml b/packaging/bee.yaml index ef04fb716c6..d90ce63c2a1 100644 --- a/packaging/bee.yaml +++ b/packaging/bee.yaml @@ -1,13 +1,13 @@ ## Bee configuration - https://docs.ethswarm.org/docs/working-with-bee/configuration -## allow to advertise private CIDRs to the public network -# allow-private-cidrs: false -## HTTP API listen address -# api-addr: 127.0.0.1:1633 -## chain block time -# block-time: "5" -## block number cache sync interval in blocks -# block-sync-interval: 10 +## ── Node mode ──────────────────────────────────────────────────────────────── +## Selects the operational mode of this node. +## full - participates in storage and incentives; requires swap-enable and blockchain-rpc +## light - uploads and downloads only; requires blockchain-rpc +## ultra-light - free-tier downloads only; no blockchain connection needed (default) +# node-mode: ultra-light + +## ── Blockchain / RPC (required for full and light nodes) ───────────────────── ## blockchain rpc configuration # blockchain-rpc: # endpoint: "" @@ -15,22 +15,96 @@ # tls-timeout: 10s # idle-timeout: 90s # keepalive: 30s +## chain block time +# block-time: "5" +## block number cache sync interval in blocks +# block-sync-interval: 10 + +## ── Swap / chequebook (full and light nodes only) ──────────────────────────── +## enable swap +# swap-enable: false +## enable chequebook +# chequebook-enable: false +## swap factory addresses +# swap-factory-address: "" +## initial deposit if deploying a new chequebook +# swap-initial-deposit: "0" + +## ── Full node only ──────────────────────────────────────────────────────────── +## enable storage incentives feature +# storage-incentives-enable: false +## reserve capacity doubling +# reserve-capacity-doubling: 0 +## minimum radius storage threshold +# minimum-storage-radius: "0" +## neighborhood to target in binary format (ex: 111111001) for mining the initial overlay +# target-neighborhood: "" +## suggester for target neighborhood +# neighborhood-suggester: https://api.swarmscan.io/v1/network/neighborhoods/suggestion +## redistribution contract address +# redistribution-address: "" +## staking contract address +# staking-address: "" + +## ── Network ─────────────────────────────────────────────────────────────────── +## triggers connect to main net bootnodes +# mainnet: true +## ID of the Swarm network +# network-id: "1" ## initial nodes to connect to # bootnode: ["/dnsaddr/mainnet.ethswarm.org"] ## cause the node to always accept incoming connections # bootnode-mode: false -## cache capacity in chunks, multiply by 4096 to get approximate capacity in bytes -# cache-capacity: "1000000" -## enable forwarded content caching -# cache-retrieval: true -## enable chequebook -# chequebook-enable: true -## config file (default is $HOME/.bee.yaml) -config: "/etc/bee/bee.yaml" +## protect nodes from getting kicked out on bootnode +# static-nodes: [] +## P2P listen address +# p2p-addr: :1634 +## enable P2P WebSocket transport +# p2p-ws-enable: false +## enable wss p2p connections +# p2p-wss-enable: false +## wss address +# p2p-wss-addr: :1635 +## NAT exposed address +# nat-addr: "" +## WSS NAT exposed address +# nat-wss-addr: "" +## autotls domain +# autotls-domain: "" +## autotls registration endpoint +# autotls-registration-endpoint: "" +## autotls ca endpoint +# autotls-ca-endpoint: "" +## allow to advertise private CIDRs to the public network +# allow-private-cidrs: false + +## ── HTTP API ────────────────────────────────────────────────────────────────── +## HTTP API listen address +# api-addr: 127.0.0.1:1633 ## origins with CORS headers enabled # cors-allowed-origins: [] + +## ── Storage ─────────────────────────────────────────────────────────────────── ## data directory data-dir: "/var/lib/bee" +## cache capacity in chunks, multiply by 4096 to get approximate capacity in bytes +# cache-capacity: "1000000" +## enable forwarded content caching +# cache-retrieval: true +## postage stamp contract address +# postage-stamp-address: "" +## postage stamp contract start block number +# postage-stamp-start-block: "0" +## skip postage snapshot +# skip-postage-snapshot: false +## forces the node to resync postage contract data +# resync: false +## ENS compatible API endpoint for a TLD and with contract address, can be repeated, format [tld:][contract-addr@]url +# resolver-options: [] +## price oracle contract address +# price-oracle-address: "" + +## ── Database ────────────────────────────────────────────────────────────────── ## size of block cache of the database in bytes # db-block-cache-capacity: "33554432" ## disables db compactions triggered by seeks @@ -39,72 +113,36 @@ data-dir: "/var/lib/bee" # db-open-files-limit: "200" ## size of the database write buffer in bytes # db-write-buffer-size: "33554432" -## cause the node to start in full mode -# full-node: false -## help for printconfig -# help: false -## triggers connect to main net bootnodes. -# mainnet: true +## lru memory caching capacity in number of statestore entries +# statestore-cache-capacity: "100000" + +## ── Payments ────────────────────────────────────────────────────────────────── +## threshold in BZZ where you expect to get paid from your peers +# payment-threshold: "13500000" +## percentage below the peers payment threshold when we initiate settlement +# payment-early-percent: 50 +## excess debt above payment threshold in percentages where you disconnect from your peer +# payment-tolerance-percent: 25 ## minimum gas tip cap in wei for transactions, 0 means use suggested gas tip cap # minimum-gas-tip-cap: 0 -## minimum radius storage threshold -# minimum-storage-radius: "0" -## NAT exposed address -# nat-addr: "" -## suggester for target neighborhood -# neighborhood-suggester: https://api.swarmscan.io/v1/network/neighborhoods/suggestion -## ID of the Swarm network -# network-id: "1" -## P2P listen address -# p2p-addr: :1634 -## enable P2P WebSocket transport -# p2p-ws-enable: false +## gas limit fallback when estimation fails for contract transactions (default 500000) +# gas-limit-fallback: 500000 +## skips the gas estimate step for contract transactions +# transaction-debug-mode: false +## withdrawal target addresses +# withdrawal-addresses-whitelist: [] + +## ── Keys / identity ─────────────────────────────────────────────────────────── +## config file (default is $HOME/.bee.yaml) +config: "/etc/bee/bee.yaml" ## password for decrypting keys # password: "" ## path to a file that contains password for decrypting keys password-file: "/var/lib/bee/password" -## percentage below the peers payment threshold when we initiate settlement -# payment-early-percent: 50 -## threshold in BZZ where you expect to get paid from your peers -# payment-threshold: "13500000" -## excess debt above payment threshold in percentages where you disconnect from your peer -# payment-tolerance-percent: 25 -## postage stamp contract address -# postage-stamp-address: "" -## postage stamp contract start block number -# postage-stamp-start-block: "0" -## enable pprof mutex profile -# pprof-mutex: false -## enable pprof block profile -# pprof-profile: false -## price oracle contract address -# price-oracle-address: "" -## redistribution contract address -# redistribution-address: "" -## reserve capacity doubling -# reserve-capacity-doubling: 0 -## ENS compatible API endpoint for a TLD and with contract address, can be repeated, format [tld:][contract-addr@]url -# resolver-options: [] -## forces the node to resync postage contract data -# resync: false -## skip postage snapshot -# skip-postage-snapshot: false -## staking contract address -# staking-address: "" -## lru memory caching capacity in number of statestore entries -# statestore-cache-capacity: "100000" -## protect nodes from getting kicked out on bootnode -# static-nodes: [] -## enable storage incentives feature -# storage-incentives-enable: true -## enable swap -# swap-enable: false -## swap factory addresses -# swap-factory-address: "" -## initial deposit if deploying a new chequebook -# swap-initial-deposit: "0" -## neighborhood to target in binary format (ex: 111111001) for mining the initial overlay -# target-neighborhood: "" + +## ── Logging / tracing ───────────────────────────────────────────────────────── +## log verbosity level 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace +# verbosity: info ## enable tracing # tracing-enable: false ## endpoint to send tracing data @@ -115,27 +153,13 @@ password-file: "/var/lib/bee/password" # tracing-port: "" ## service name identifier for tracing # tracing-service-name: bee -## gas limit fallback when estimation fails for contract transactions (default 500000) -# gas-limit-fallback: 500000 -## skips the gas estimate step for contract transactions -# transaction-debug-mode: false -## log verbosity level 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace -# verbosity: info +## enable pprof mutex profile +# pprof-mutex: false +## enable pprof block profile +# pprof-profile: false + +## ── Miscellaneous ───────────────────────────────────────────────────────────── ## maximum node warmup duration; proceeds when stable or after this time # warmup-time: 5m0s ## send a welcome message string during handshakes # welcome-message: "" -## withdrawal target addresses -# withdrawal-addresses-whitelist: [] -## enable wss p2p connections (default: false) -# p2p-wss-enable: false -## wss address (default: :1635) -# p2p-wss-addr: :1635 -## WSS NAT exposed address -# nat-wss-addr: "" -## autotls domain (default: libp2p.direct) -# autotls-domain: "" -## autotls registration endpoint (default: https://registration.libp2p.direct) -# autotls-registration-endpoint: "" -## autotls ca endpoint (default: https://acme-v02.api.letsencrypt.org/directory) -# autotls-ca-endpoint: "" diff --git a/packaging/homebrew-amd64/bee.yaml b/packaging/homebrew-amd64/bee.yaml index 2aac12016fe..1258eee70b8 100644 --- a/packaging/homebrew-amd64/bee.yaml +++ b/packaging/homebrew-amd64/bee.yaml @@ -1,13 +1,13 @@ ## Bee configuration - https://docs.ethswarm.org/docs/working-with-bee/configuration -## allow to advertise private CIDRs to the public network -# allow-private-cidrs: false -## HTTP API listen address -# api-addr: 127.0.0.1:1633 -## chain block time -# block-time: "5" -## block number cache sync interval in blocks -# block-sync-interval: 10 +## ── Node mode ──────────────────────────────────────────────────────────────── +## Selects the operational mode of this node. +## full - participates in storage and incentives; requires swap-enable and blockchain-rpc +## light - uploads and downloads only; requires blockchain-rpc +## ultra-light - free-tier downloads only; no blockchain connection needed (default) +# node-mode: ultra-light + +## ── Blockchain / RPC (required for full and light nodes) ───────────────────── ## blockchain rpc configuration # blockchain-rpc: # endpoint: "" @@ -15,22 +15,96 @@ # tls-timeout: 10s # idle-timeout: 90s # keepalive: 30s +## chain block time +# block-time: "5" +## block number cache sync interval in blocks +# block-sync-interval: 10 + +## ── Swap / chequebook (full and light nodes only) ──────────────────────────── +## enable swap +# swap-enable: false +## enable chequebook +# chequebook-enable: false +## swap factory addresses +# swap-factory-address: "" +## initial deposit if deploying a new chequebook +# swap-initial-deposit: "0" + +## ── Full node only ──────────────────────────────────────────────────────────── +## enable storage incentives feature +# storage-incentives-enable: false +## reserve capacity doubling +# reserve-capacity-doubling: 0 +## minimum radius storage threshold +# minimum-storage-radius: "0" +## neighborhood to target in binary format (ex: 111111001) for mining the initial overlay +# target-neighborhood: "" +## suggester for target neighborhood +# neighborhood-suggester: https://api.swarmscan.io/v1/network/neighborhoods/suggestion +## redistribution contract address +# redistribution-address: "" +## staking contract address +# staking-address: "" + +## ── Network ─────────────────────────────────────────────────────────────────── +## triggers connect to main net bootnodes +# mainnet: true +## ID of the Swarm network +# network-id: "1" ## initial nodes to connect to # bootnode: ["/dnsaddr/mainnet.ethswarm.org"] ## cause the node to always accept incoming connections # bootnode-mode: false -## cache capacity in chunks, multiply by 4096 to get approximate capacity in bytes -# cache-capacity: "1000000" -## enable forwarded content caching -# cache-retrieval: true -## enable chequebook -# chequebook-enable: true -## config file (default is $HOME/.bee.yaml) -config: "/usr/local/etc/swarm-bee/bee.yaml" +## protect nodes from getting kicked out on bootnode +# static-nodes: [] +## P2P listen address +# p2p-addr: :1634 +## enable P2P WebSocket transport +# p2p-ws-enable: false +## enable wss p2p connections +# p2p-wss-enable: false +## wss address +# p2p-wss-addr: :1635 +## NAT exposed address +# nat-addr: "" +## WSS NAT exposed address +# nat-wss-addr: "" +## autotls domain +# autotls-domain: "" +## autotls registration endpoint +# autotls-registration-endpoint: "" +## autotls ca endpoint +# autotls-ca-endpoint: "" +## allow to advertise private CIDRs to the public network +# allow-private-cidrs: false + +## ── HTTP API ────────────────────────────────────────────────────────────────── +## HTTP API listen address +# api-addr: 127.0.0.1:1633 ## origins with CORS headers enabled # cors-allowed-origins: [] + +## ── Storage ─────────────────────────────────────────────────────────────────── ## data directory data-dir: "/usr/local/var/lib/swarm-bee" +## cache capacity in chunks, multiply by 4096 to get approximate capacity in bytes +# cache-capacity: "1000000" +## enable forwarded content caching +# cache-retrieval: true +## postage stamp contract address +# postage-stamp-address: "" +## postage stamp contract start block number +# postage-stamp-start-block: "0" +## skip postage snapshot +# skip-postage-snapshot: false +## forces the node to resync postage contract data +# resync: false +## ENS compatible API endpoint for a TLD and with contract address, can be repeated, format [tld:][contract-addr@]url +# resolver-options: [] +## price oracle contract address +# price-oracle-address: "" + +## ── Database ────────────────────────────────────────────────────────────────── ## size of block cache of the database in bytes # db-block-cache-capacity: "33554432" ## disables db compactions triggered by seeks @@ -39,72 +113,36 @@ data-dir: "/usr/local/var/lib/swarm-bee" # db-open-files-limit: "200" ## size of the database write buffer in bytes # db-write-buffer-size: "33554432" -## cause the node to start in full mode -# full-node: false -## help for printconfig -# help: false -## triggers connect to main net bootnodes. -# mainnet: true +## lru memory caching capacity in number of statestore entries +# statestore-cache-capacity: "100000" + +## ── Payments ────────────────────────────────────────────────────────────────── +## threshold in BZZ where you expect to get paid from your peers +# payment-threshold: "13500000" +## percentage below the peers payment threshold when we initiate settlement +# payment-early-percent: 50 +## excess debt above payment threshold in percentages where you disconnect from your peer +# payment-tolerance-percent: 25 ## minimum gas tip cap in wei for transactions, 0 means use suggested gas tip cap # minimum-gas-tip-cap: 0 -## minimum radius storage threshold -# minimum-storage-radius: "0" -## NAT exposed address -# nat-addr: "" -## suggester for target neighborhood -# neighborhood-suggester: https://api.swarmscan.io/v1/network/neighborhoods/suggestion -## ID of the Swarm network -# network-id: "1" -## P2P listen address -# p2p-addr: :1634 -## enable P2P WebSocket transport -# p2p-ws-enable: false +## gas limit fallback when estimation fails for contract transactions (default 500000) +# gas-limit-fallback: 500000 +## skips the gas estimate step for contract transactions +# transaction-debug-mode: false +## withdrawal target addresses +# withdrawal-addresses-whitelist: [] + +## ── Keys / identity ─────────────────────────────────────────────────────────── +## config file (default is $HOME/.bee.yaml) +config: "/usr/local/etc/swarm-bee/bee.yaml" ## password for decrypting keys # password: "" ## path to a file that contains password for decrypting keys password-file: "/usr/local/var/lib/swarm-bee/password" -## percentage below the peers payment threshold when we initiate settlement -# payment-early-percent: 50 -## threshold in BZZ where you expect to get paid from your peers -# payment-threshold: "13500000" -## excess debt above payment threshold in percentages where you disconnect from your peer -# payment-tolerance-percent: 25 -## postage stamp contract address -# postage-stamp-address: "" -## postage stamp contract start block number -# postage-stamp-start-block: "0" -## enable pprof mutex profile -# pprof-mutex: false -## enable pprof block profile -# pprof-profile: false -## price oracle contract address -# price-oracle-address: "" -## redistribution contract address -# redistribution-address: "" -## reserve capacity doubling -# reserve-capacity-doubling: 0 -## ENS compatible API endpoint for a TLD and with contract address, can be repeated, format [tld:][contract-addr@]url -# resolver-options: [] -## forces the node to resync postage contract data -# resync: false -## skip postage snapshot -# skip-postage-snapshot: false -## staking contract address -# staking-address: "" -## lru memory caching capacity in number of statestore entries -# statestore-cache-capacity: "100000" -## protect nodes from getting kicked out on bootnode -# static-nodes: [] -## enable storage incentives feature -# storage-incentives-enable: true -## enable swap -# swap-enable: false -## swap factory addresses -# swap-factory-address: "" -## initial deposit if deploying a new chequebook -# swap-initial-deposit: "0" -## neighborhood to target in binary format (ex: 111111001) for mining the initial overlay -# target-neighborhood: "" + +## ── Logging / tracing ───────────────────────────────────────────────────────── +## log verbosity level 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace +# verbosity: info ## enable tracing # tracing-enable: false ## endpoint to send tracing data @@ -115,27 +153,13 @@ password-file: "/usr/local/var/lib/swarm-bee/password" # tracing-port: "" ## service name identifier for tracing # tracing-service-name: bee -## gas limit fallback when estimation fails for contract transactions (default 500000) -# gas-limit-fallback: 500000 -## skips the gas estimate step for contract transactions -# transaction-debug-mode: false -## log verbosity level 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace -# verbosity: info +## enable pprof mutex profile +# pprof-mutex: false +## enable pprof block profile +# pprof-profile: false + +## ── Miscellaneous ───────────────────────────────────────────────────────────── ## maximum node warmup duration; proceeds when stable or after this time # warmup-time: 5m0s ## send a welcome message string during handshakes # welcome-message: "" -## withdrawal target addresses -# withdrawal-addresses-whitelist: [] -## enable wss p2p connections (default: false) -# p2p-wss-enable: false -## wss address (default: :1635) -# p2p-wss-addr: :1635 -## WSS NAT exposed address -# nat-wss-addr: "" -## autotls domain (default: libp2p.direct) -# autotls-domain: "" -## autotls registration endpoint (default: https://registration.libp2p.direct) -# autotls-registration-endpoint: "" -## autotls ca endpoint (default: https://acme-v02.api.letsencrypt.org/directory) -# autotls-ca-endpoint: "" diff --git a/packaging/homebrew-arm64/bee.yaml b/packaging/homebrew-arm64/bee.yaml index f812f395f07..5734d6eb924 100644 --- a/packaging/homebrew-arm64/bee.yaml +++ b/packaging/homebrew-arm64/bee.yaml @@ -1,13 +1,13 @@ ## Bee configuration - https://docs.ethswarm.org/docs/working-with-bee/configuration -## allow to advertise private CIDRs to the public network -# allow-private-cidrs: false -## HTTP API listen address -# api-addr: 127.0.0.1:1633 -## chain block time -# block-time: "5" -## block number cache sync interval in blocks -# block-sync-interval: 10 +## ── Node mode ──────────────────────────────────────────────────────────────── +## Selects the operational mode of this node. +## full - participates in storage and incentives; requires swap-enable and blockchain-rpc +## light - uploads and downloads only; requires blockchain-rpc +## ultra-light - free-tier downloads only; no blockchain connection needed (default) +# node-mode: ultra-light + +## ── Blockchain / RPC (required for full and light nodes) ───────────────────── ## blockchain rpc configuration # blockchain-rpc: # endpoint: "" @@ -15,22 +15,96 @@ # tls-timeout: 10s # idle-timeout: 90s # keepalive: 30s +## chain block time +# block-time: "5" +## block number cache sync interval in blocks +# block-sync-interval: 10 + +## ── Swap / chequebook (full and light nodes only) ──────────────────────────── +## enable swap +# swap-enable: false +## enable chequebook +# chequebook-enable: false +## swap factory addresses +# swap-factory-address: "" +## initial deposit if deploying a new chequebook +# swap-initial-deposit: "0" + +## ── Full node only ──────────────────────────────────────────────────────────── +## enable storage incentives feature +# storage-incentives-enable: false +## reserve capacity doubling +# reserve-capacity-doubling: 0 +## minimum radius storage threshold +# minimum-storage-radius: "0" +## neighborhood to target in binary format (ex: 111111001) for mining the initial overlay +# target-neighborhood: "" +## suggester for target neighborhood +# neighborhood-suggester: https://api.swarmscan.io/v1/network/neighborhoods/suggestion +## redistribution contract address +# redistribution-address: "" +## staking contract address +# staking-address: "" + +## ── Network ─────────────────────────────────────────────────────────────────── +## triggers connect to main net bootnodes +# mainnet: true +## ID of the Swarm network +# network-id: "1" ## initial nodes to connect to # bootnode: ["/dnsaddr/mainnet.ethswarm.org"] ## cause the node to always accept incoming connections # bootnode-mode: false -## cache capacity in chunks, multiply by 4096 to get approximate capacity in bytes -# cache-capacity: "1000000" -## enable forwarded content caching -# cache-retrieval: true -## enable chequebook -# chequebook-enable: true -## config file (default is $HOME/.bee.yaml) -config: "/opt/homebrew/etc/swarm-bee/bee.yaml" +## protect nodes from getting kicked out on bootnode +# static-nodes: [] +## P2P listen address +# p2p-addr: :1634 +## enable P2P WebSocket transport +# p2p-ws-enable: false +## enable wss p2p connections +# p2p-wss-enable: false +## wss address +# p2p-wss-addr: :1635 +## NAT exposed address +# nat-addr: "" +## WSS NAT exposed address +# nat-wss-addr: "" +## autotls domain +# autotls-domain: "" +## autotls registration endpoint +# autotls-registration-endpoint: "" +## autotls ca endpoint +# autotls-ca-endpoint: "" +## allow to advertise private CIDRs to the public network +# allow-private-cidrs: false + +## ── HTTP API ────────────────────────────────────────────────────────────────── +## HTTP API listen address +# api-addr: 127.0.0.1:1633 ## origins with CORS headers enabled # cors-allowed-origins: [] + +## ── Storage ─────────────────────────────────────────────────────────────────── ## data directory data-dir: "/opt/homebrew/var/lib/swarm-bee" +## cache capacity in chunks, multiply by 4096 to get approximate capacity in bytes +# cache-capacity: "1000000" +## enable forwarded content caching +# cache-retrieval: true +## postage stamp contract address +# postage-stamp-address: "" +## postage stamp contract start block number +# postage-stamp-start-block: "0" +## skip postage snapshot +# skip-postage-snapshot: false +## forces the node to resync postage contract data +# resync: false +## ENS compatible API endpoint for a TLD and with contract address, can be repeated, format [tld:][contract-addr@]url +# resolver-options: [] +## price oracle contract address +# price-oracle-address: "" + +## ── Database ────────────────────────────────────────────────────────────────── ## size of block cache of the database in bytes # db-block-cache-capacity: "33554432" ## disables db compactions triggered by seeks @@ -39,72 +113,36 @@ data-dir: "/opt/homebrew/var/lib/swarm-bee" # db-open-files-limit: "200" ## size of the database write buffer in bytes # db-write-buffer-size: "33554432" -## cause the node to start in full mode -# full-node: false -## help for printconfig -# help: false -## triggers connect to main net bootnodes. -# mainnet: true +## lru memory caching capacity in number of statestore entries +# statestore-cache-capacity: "100000" + +## ── Payments ────────────────────────────────────────────────────────────────── +## threshold in BZZ where you expect to get paid from your peers +# payment-threshold: "13500000" +## percentage below the peers payment threshold when we initiate settlement +# payment-early-percent: 50 +## excess debt above payment threshold in percentages where you disconnect from your peer +# payment-tolerance-percent: 25 ## minimum gas tip cap in wei for transactions, 0 means use suggested gas tip cap # minimum-gas-tip-cap: 0 -## minimum radius storage threshold -# minimum-storage-radius: "0" -## NAT exposed address -# nat-addr: "" -## suggester for target neighborhood -# neighborhood-suggester: https://api.swarmscan.io/v1/network/neighborhoods/suggestion -## ID of the Swarm network -# network-id: "1" -## P2P listen address -# p2p-addr: :1634 -## enable P2P WebSocket transport -# p2p-ws-enable: false +## gas limit fallback when estimation fails for contract transactions (default 500000) +# gas-limit-fallback: 500000 +## skips the gas estimate step for contract transactions +# transaction-debug-mode: false +## withdrawal target addresses +# withdrawal-addresses-whitelist: [] + +## ── Keys / identity ─────────────────────────────────────────────────────────── +## config file (default is $HOME/.bee.yaml) +config: "/opt/homebrew/etc/swarm-bee/bee.yaml" ## password for decrypting keys # password: "" ## path to a file that contains password for decrypting keys password-file: "/opt/homebrew/var/lib/swarm-bee/password" -## percentage below the peers payment threshold when we initiate settlement -# payment-early-percent: 50 -## threshold in BZZ where you expect to get paid from your peers -# payment-threshold: "13500000" -## excess debt above payment threshold in percentages where you disconnect from your peer -# payment-tolerance-percent: 25 -## postage stamp contract address -# postage-stamp-address: "" -## postage stamp contract start block number -# postage-stamp-start-block: "0" -## enable pprof mutex profile -# pprof-mutex: false -## enable pprof block profile -# pprof-profile: false -## price oracle contract address -# price-oracle-address: "" -## redistribution contract address -# redistribution-address: "" -## reserve capacity doubling -# reserve-capacity-doubling: 0 -## ENS compatible API endpoint for a TLD and with contract address, can be repeated, format [tld:][contract-addr@]url -# resolver-options: [] -## forces the node to resync postage contract data -# resync: false -## skip postage snapshot -# skip-postage-snapshot: false -## staking contract address -# staking-address: "" -## lru memory caching capacity in number of statestore entries -# statestore-cache-capacity: "100000" -## protect nodes from getting kicked out on bootnode -# static-nodes: [] -## enable storage incentives feature -# storage-incentives-enable: true -## enable swap -# swap-enable: false -## swap factory addresses -# swap-factory-address: "" -## initial deposit if deploying a new chequebook -# swap-initial-deposit: "0" -## neighborhood to target in binary format (ex: 111111001) for mining the initial overlay -# target-neighborhood: "" + +## ── Logging / tracing ───────────────────────────────────────────────────────── +## log verbosity level 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace +# verbosity: info ## enable tracing # tracing-enable: false ## endpoint to send tracing data @@ -115,27 +153,13 @@ password-file: "/opt/homebrew/var/lib/swarm-bee/password" # tracing-port: "" ## service name identifier for tracing # tracing-service-name: bee -## gas limit fallback when estimation fails for contract transactions (default 500000) -# gas-limit-fallback: 500000 -## skips the gas estimate step for contract transactions -# transaction-debug-mode: false -## log verbosity level 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace -# verbosity: info +## enable pprof mutex profile +# pprof-mutex: false +## enable pprof block profile +# pprof-profile: false + +## ── Miscellaneous ───────────────────────────────────────────────────────────── ## maximum node warmup duration; proceeds when stable or after this time # warmup-time: 5m0s ## send a welcome message string during handshakes # welcome-message: "" -## withdrawal target addresses -# withdrawal-addresses-whitelist: [] -## enable wss p2p connections (default: false) -# p2p-wss-enable: false -## wss address (default: :1635) -# p2p-wss-addr: :1635 -## WSS NAT exposed address -# nat-wss-addr: "" -## autotls domain (default: libp2p.direct) -# autotls-domain: "" -## autotls registration endpoint (default: https://registration.libp2p.direct) -# autotls-registration-endpoint: "" -## autotls ca endpoint (default: https://acme-v02.api.letsencrypt.org/directory) -# autotls-ca-endpoint: "" diff --git a/packaging/scoop/bee.yaml b/packaging/scoop/bee.yaml index f44aaf0e0d0..620245831b8 100644 --- a/packaging/scoop/bee.yaml +++ b/packaging/scoop/bee.yaml @@ -1,13 +1,13 @@ ## Bee configuration - https://docs.ethswarm.org/docs/working-with-bee/configuration -## allow to advertise private CIDRs to the public network -# allow-private-cidrs: false -## HTTP API listen address -# api-addr: 127.0.0.1:1633 -## chain block time -# block-time: "5" -## block number cache sync interval in blocks -# block-sync-interval: 10 +## ── Node mode ──────────────────────────────────────────────────────────────── +## Selects the operational mode of this node. +## full - participates in storage and incentives; requires swap-enable and blockchain-rpc +## light - uploads and downloads only; requires blockchain-rpc +## ultra-light - free-tier downloads only; no blockchain connection needed (default) +# node-mode: ultra-light + +## ── Blockchain / RPC (required for full and light nodes) ───────────────────── ## blockchain rpc configuration # blockchain-rpc: # endpoint: "" @@ -15,22 +15,96 @@ # tls-timeout: 10s # idle-timeout: 90s # keepalive: 30s +## chain block time +# block-time: "5" +## block number cache sync interval in blocks +# block-sync-interval: 10 + +## ── Swap / chequebook (full and light nodes only) ──────────────────────────── +## enable swap +# swap-enable: false +## enable chequebook +# chequebook-enable: false +## swap factory addresses +# swap-factory-address: "" +## initial deposit if deploying a new chequebook +# swap-initial-deposit: "0" + +## ── Full node only ──────────────────────────────────────────────────────────── +## enable storage incentives feature +# storage-incentives-enable: false +## reserve capacity doubling +# reserve-capacity-doubling: 0 +## minimum radius storage threshold +# minimum-storage-radius: "0" +## neighborhood to target in binary format (ex: 111111001) for mining the initial overlay +# target-neighborhood: "" +## suggester for target neighborhood +# neighborhood-suggester: https://api.swarmscan.io/v1/network/neighborhoods/suggestion +## redistribution contract address +# redistribution-address: "" +## staking contract address +# staking-address: "" + +## ── Network ─────────────────────────────────────────────────────────────────── +## triggers connect to main net bootnodes +# mainnet: true +## ID of the Swarm network +# network-id: "1" ## initial nodes to connect to # bootnode: ["/dnsaddr/mainnet.ethswarm.org"] ## cause the node to always accept incoming connections # bootnode-mode: false -## cache capacity in chunks, multiply by 4096 to get approximate capacity in bytes -# cache-capacity: "1000000" -## enable forwarded content caching -# cache-retrieval: true -## enable chequebook -# chequebook-enable: true -## config file (default is $HOME/.bee.yaml) -config: "./bee.yaml" +## protect nodes from getting kicked out on bootnode +# static-nodes: [] +## P2P listen address +# p2p-addr: :1634 +## enable P2P WebSocket transport +# p2p-ws-enable: false +## enable wss p2p connections +# p2p-wss-enable: false +## wss address +# p2p-wss-addr: :1635 +## NAT exposed address +# nat-addr: "" +## WSS NAT exposed address +# nat-wss-addr: "" +## autotls domain +# autotls-domain: "" +## autotls registration endpoint +# autotls-registration-endpoint: "" +## autotls ca endpoint +# autotls-ca-endpoint: "" +## allow to advertise private CIDRs to the public network +# allow-private-cidrs: false + +## ── HTTP API ────────────────────────────────────────────────────────────────── +## HTTP API listen address +# api-addr: 127.0.0.1:1633 ## origins with CORS headers enabled # cors-allowed-origins: [] + +## ── Storage ─────────────────────────────────────────────────────────────────── ## data directory data-dir: "./data" +## cache capacity in chunks, multiply by 4096 to get approximate capacity in bytes +# cache-capacity: "1000000" +## enable forwarded content caching +# cache-retrieval: true +## postage stamp contract address +# postage-stamp-address: "" +## postage stamp contract start block number +# postage-stamp-start-block: "0" +## skip postage snapshot +# skip-postage-snapshot: false +## forces the node to resync postage contract data +# resync: false +## ENS compatible API endpoint for a TLD and with contract address, can be repeated, format [tld:][contract-addr@]url +# resolver-options: [] +## price oracle contract address +# price-oracle-address: "" + +## ── Database ────────────────────────────────────────────────────────────────── ## size of block cache of the database in bytes # db-block-cache-capacity: "33554432" ## disables db compactions triggered by seeks @@ -39,72 +113,36 @@ data-dir: "./data" # db-open-files-limit: "200" ## size of the database write buffer in bytes # db-write-buffer-size: "33554432" -## cause the node to start in full mode -# full-node: false -## help for printconfig -# help: false -## triggers connect to main net bootnodes. -# mainnet: true +## lru memory caching capacity in number of statestore entries +# statestore-cache-capacity: "100000" + +## ── Payments ────────────────────────────────────────────────────────────────── +## threshold in BZZ where you expect to get paid from your peers +# payment-threshold: "13500000" +## percentage below the peers payment threshold when we initiate settlement +# payment-early-percent: 50 +## excess debt above payment threshold in percentages where you disconnect from your peer +# payment-tolerance-percent: 25 ## minimum gas tip cap in wei for transactions, 0 means use suggested gas tip cap # minimum-gas-tip-cap: 0 -## minimum radius storage threshold -# minimum-storage-radius: "0" -## NAT exposed address -# nat-addr: "" -## suggester for target neighborhood -# neighborhood-suggester: https://api.swarmscan.io/v1/network/neighborhoods/suggestion -## ID of the Swarm network -# network-id: "1" -## P2P listen address -# p2p-addr: :1634 -## enable P2P WebSocket transport -# p2p-ws-enable: false +## gas limit fallback when estimation fails for contract transactions (default 500000) +# gas-limit-fallback: 500000 +## skips the gas estimate step for contract transactions +# transaction-debug-mode: false +## withdrawal target addresses +# withdrawal-addresses-whitelist: [] + +## ── Keys / identity ─────────────────────────────────────────────────────────── +## config file (default is $HOME/.bee.yaml) +config: "./bee.yaml" ## password for decrypting keys # password: "" ## path to a file that contains password for decrypting keys password-file: "./password" -## percentage below the peers payment threshold when we initiate settlement -# payment-early-percent: 50 -## threshold in BZZ where you expect to get paid from your peers -# payment-threshold: "13500000" -## excess debt above payment threshold in percentages where you disconnect from your peer -# payment-tolerance-percent: 25 -## postage stamp contract address -# postage-stamp-address: "" -## postage stamp contract start block number -# postage-stamp-start-block: "0" -## enable pprof mutex profile -# pprof-mutex: false -## enable pprof block profile -# pprof-profile: false -## price oracle contract address -# price-oracle-address: "" -## redistribution contract address -# redistribution-address: "" -## reserve capacity doubling -# reserve-capacity-doubling: 0 -## ENS compatible API endpoint for a TLD and with contract address, can be repeated, format [tld:][contract-addr@]url -# resolver-options: [] -## forces the node to resync postage contract data -# resync: false -## skip postage snapshot -# skip-postage-snapshot: false -## staking contract address -# staking-address: "" -## lru memory caching capacity in number of statestore entries -# statestore-cache-capacity: "100000" -## protect nodes from getting kicked out on bootnode -# static-nodes: [] -## enable storage incentives feature -# storage-incentives-enable: true -## enable swap -# swap-enable: false -## swap factory addresses -# swap-factory-address: "" -## initial deposit if deploying a new chequebook -# swap-initial-deposit: "0" -## neighborhood to target in binary format (ex: 111111001) for mining the initial overlay -# target-neighborhood: "" + +## ── Logging / tracing ───────────────────────────────────────────────────────── +## log verbosity level 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace +# verbosity: info ## enable tracing # tracing-enable: false ## endpoint to send tracing data @@ -115,27 +153,13 @@ password-file: "./password" # tracing-port: "" ## service name identifier for tracing # tracing-service-name: bee -## gas limit fallback when estimation fails for contract transactions (default 500000) -# gas-limit-fallback: 500000 -## skips the gas estimate step for contract transactions -# transaction-debug-mode: false -## log verbosity level 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace -# verbosity: info +## enable pprof mutex profile +# pprof-mutex: false +## enable pprof block profile +# pprof-profile: false + +## ── Miscellaneous ───────────────────────────────────────────────────────────── ## maximum node warmup duration; proceeds when stable or after this time # warmup-time: 5m0s ## send a welcome message string during handshakes # welcome-message: "" -## withdrawal target addresses -# withdrawal-addresses-whitelist: [] -## enable wss p2p connections (default: false) -# p2p-wss-enable: false -## wss address (default: :1635) -# p2p-wss-addr: :1635 -## WSS NAT exposed address -# nat-wss-addr: "" -## autotls domain (default: libp2p.direct) -# autotls-domain: "" -## autotls registration endpoint (default: https://registration.libp2p.direct) -# autotls-registration-endpoint: "" -## autotls ca endpoint (default: https://acme-v02.api.letsencrypt.org/directory) -# autotls-ca-endpoint: "" diff --git a/pkg/node/node.go b/pkg/node/node.go index 0b3be605b53..bd2ff2309a0 100644 --- a/pkg/node/node.go +++ b/pkg/node/node.go @@ -127,6 +127,23 @@ type Bee struct { ethClientCloser func() } +// NodeMode represents the operational mode of a Bee node as configured by the operator. +type NodeMode string + +const ( + FullMode NodeMode = "full" + LightMode NodeMode = "light" + UltraLightMode NodeMode = "ultra-light" +) + +func (m NodeMode) IsValid() bool { + switch m { + case FullMode, LightMode, UltraLightMode: + return true + } + return false +} + type Options struct { Addr string AllowPrivateCIDRs bool @@ -158,7 +175,7 @@ type Options struct { EnableWS bool AutoTLSDomain string AutoTLSRegistrationEndpoint string - FullNodeMode bool + NodeMode NodeMode GasLimitFallback uint64 Logger log.Logger MinimumGasTipCap uint64 @@ -258,7 +275,7 @@ func NewBee( // light nodes have zero warmup time for pull/pushsync protocols warmupTime := o.WarmupTime - if !o.FullNodeMode { + if o.NodeMode != FullMode { warmupTime = 0 } @@ -284,7 +301,7 @@ func NewBee( } }(b) - if !o.FullNodeMode && o.ReserveCapacityDoubling != 0 { + if o.NodeMode != FullMode && o.ReserveCapacityDoubling != 0 { return nil, fmt.Errorf("reserve capacity doubling is only allowed for full nodes") } @@ -389,7 +406,7 @@ func NewBee( erc20Service erc20.Service ) - chainEnabled := isChainEnabled(o, o.BlockchainRpcEndpoint, logger) + chainEnabled := isChainEnabled(o, logger) var batchStore postage.Storer = new(postage.NoOpBatchStore) var evictFn func([]byte) error @@ -437,10 +454,13 @@ func NewBee( b.transactionCloser = tracerCloser b.transactionMonitorCloser = transactionMonitor - beeNodeMode := api.LightMode - if o.FullNodeMode { + var beeNodeMode api.BeeNodeMode + switch o.NodeMode { + case FullMode: beeNodeMode = api.FullMode - } else if !chainEnabled { + case LightMode: + beeNodeMode = api.LightMode + default: beeNodeMode = api.UltraLightMode } @@ -663,7 +683,7 @@ func NewBee( AutoTLSRegistrationEndpoint: o.AutoTLSRegistrationEndpoint, AutoTLSCAEndpoint: o.AutoTLSCAEndpoint, WelcomeMessage: o.WelcomeMessage, - FullNode: o.FullNodeMode, + FullNode: o.NodeMode == FullMode, Nonce: nonce, ValidateOverlay: chainEnabled, Registry: registry, @@ -789,7 +809,7 @@ func NewBee( MinimumStorageRadius: o.MinimumStorageRadius, } - if o.FullNodeMode && !o.BootnodeMode { + if o.NodeMode == FullMode && !o.BootnodeMode { // configure reserve only for full node lo.ReserveCapacity = reserveCapacity lo.ReserveWakeUpDuration = reserveWakeUpDuration @@ -867,7 +887,7 @@ func NewBee( return nil, errors.New("postage contract is paused") } - if o.FullNodeMode { + if o.NodeMode == FullMode { err = batchSvc.Start(ctx, postageSyncStart) syncStatus.Store(true) if err != nil { @@ -892,7 +912,7 @@ func NewBee( minThreshold := big.NewInt(2 * refreshRate) maxThreshold := big.NewInt(24 * refreshRate) - if !o.FullNodeMode { + if o.NodeMode != FullMode { minThreshold = big.NewInt(2 * lightRefreshRate) } @@ -925,7 +945,7 @@ func NewBee( var enforcedRefreshRate *big.Int - if o.FullNodeMode { + if o.NodeMode == FullMode { enforcedRefreshRate = big.NewInt(refreshRate) } else { enforcedRefreshRate = big.NewInt(lightRefreshRate) @@ -1020,7 +1040,7 @@ func NewBee( if prev == uint32(swarm.MaxBins) { close(initialRadiusC) } - if !o.FullNodeMode { // light and ultra-light nodes do not have a reserve worker to set the radius. + if o.NodeMode != FullMode { // light and ultra-light nodes do not have a reserve worker to set the radius. kad.SetStorageRadius(r) } case <-ctx.Done(): @@ -1047,7 +1067,7 @@ func NewBee( } } - pushSyncProtocol := pushsync.New(swarmAddress, networkID, nonce, p2ps, localStore, waitNetworkRFunc, kad, o.FullNodeMode && !o.BootnodeMode, pssService.TryUnwrap, gsocService.Handle, validStamp, logger, acc, pricer, signer, tracer, detector, uint8(shallowReceiptTolerance)) + pushSyncProtocol := pushsync.New(swarmAddress, networkID, nonce, p2ps, localStore, waitNetworkRFunc, kad, o.NodeMode == FullMode && !o.BootnodeMode, pssService.TryUnwrap, gsocService.Handle, validStamp, logger, acc, pricer, signer, tracer, detector, uint8(shallowReceiptTolerance)) b.pushSyncCloser = pushSyncProtocol // set the pushSyncer in the PSS @@ -1070,7 +1090,7 @@ func NewBee( pushSyncProtocolSpec := pushSyncProtocol.Protocol() pullSyncProtocolSpec := pullSyncProtocol.Protocol() - if o.FullNodeMode && !o.BootnodeMode { + if o.NodeMode == FullMode && !o.BootnodeMode { logger.Info("starting in full mode") } else { if chainEnabled { @@ -1153,7 +1173,7 @@ func NewBee( agent *storageincentives.Agent ) - if o.FullNodeMode && !o.BootnodeMode { + if o.NodeMode == FullMode && !o.BootnodeMode { pullerService = puller.New(swarmAddress, stateStore, kad, localStore, pullSyncProtocol, p2ps, logger, puller.Options{}) b.pullerCloser = pullerService @@ -1473,21 +1493,13 @@ func (b *Bee) Shutdown() error { var ErrShutdownInProgress = errors.New("shutdown in progress") -func isChainEnabled(o *Options, swapEndpoint string, logger log.Logger) bool { - chainDisabled := swapEndpoint == "" - lightMode := !o.FullNodeMode - - if lightMode && chainDisabled { - logger.Info("chain backend disabled - starting in ultra-light mode", - "full_node_mode", o.FullNodeMode, - "blockchain-rpc-endpoint", swapEndpoint) +func isChainEnabled(o *Options, logger log.Logger) bool { + if o.NodeMode == UltraLightMode { + logger.Info("chain backend disabled - starting in ultra-light mode") return false } - - logger.Info("chain backend enabled - blockchain functionality available", - "full_node_mode", o.FullNodeMode, - "blockchain-rpc-endpoint", swapEndpoint) - return true // all other modes operate require chain enabled + logger.Info("chain backend enabled - blockchain functionality available", "node_mode", o.NodeMode) + return true } func validatePublicAddress(addr string) error { From b58c43a0135b63c35fadcb6f70189182556992a3 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Tue, 14 Apr 2026 12:17:15 +0300 Subject: [PATCH 02/22] fix: restore legacy mode detection in resolveNodeMode --- .github/workflows/beekeeper.yml | 2 +- cmd/bee/cmd/start.go | 61 ++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/.github/workflows/beekeeper.yml b/.github/workflows/beekeeper.yml index 939d82b4f97..de1f6c1bf22 100644 --- a/.github/workflows/beekeeper.yml +++ b/.github/workflows/beekeeper.yml @@ -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" BEEKEEPER_METRICS_ENABLED: false REACHABILITY_OVERRIDE_PUBLIC: true BATCHFACTOR_OVERRIDE_PUBLIC: 2 diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index 9dc382d8f93..a1be35d0dac 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -342,46 +342,51 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo } // resolveNodeMode determines the effective node mode from config. -// --node-mode takes precedence; the deprecated --full-node flag is honoured as a fallback. -// It also validates that the required options for each mode are present. +// --node-mode takes precedence and triggers strict per-mode validation. +// The deprecated --full-node flag is honoured as a fallback. +// 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) - // Resolve the mode: explicit node-mode wins, then legacy full-node, then default ultra-light. - var mode node.NodeMode if c.config.IsSet(optionNameNodeMode) { - mode = node.NodeMode(c.config.GetString(optionNameNodeMode)) + // Explicit node-mode: validate strictly. + 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) } - } else if c.config.GetBool(optionNameFullNode) { - logger.Warning("--full-node is deprecated, use --node-mode=full instead") - mode = node.FullMode - } else { - mode = node.UltraLightMode + switch mode { + case node.FullMode: + 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") + } + 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 } - // Validate mode-specific requirements. - switch mode { - case node.FullMode: - 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") - } - 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") - } + // Legacy path: node-mode not set, fall back to deprecated flags / old detection. + if c.config.GetBool(optionNameFullNode) { + logger.Warning("--full-node is deprecated, use --node-mode=full instead") + return node.FullMode, nil } - return mode, 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 { From 9d736a05bf7354d94f30f24c41924f423a0d3db0 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Wed, 22 Apr 2026 17:26:35 +0300 Subject: [PATCH 03/22] test: add table-driven tests for resolveNodeMode --- cmd/bee/cmd/resolve_node_mode_test.go | 156 ++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 cmd/bee/cmd/resolve_node_mode_test.go diff --git a/cmd/bee/cmd/resolve_node_mode_test.go b/cmd/bee/cmd/resolve_node_mode_test.go new file mode 100644 index 00000000000..5880a3f57e7 --- /dev/null +++ b/cmd/bee/cmd/resolve_node_mode_test.go @@ -0,0 +1,156 @@ +// 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 and swap succeeds", + config: map[string]any{ + optionNameNodeMode: "full", + configKeyBlockchainRpcEndpoint: "http://localhost:8545", + optionNameSwapEnable: 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: "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 maps to full mode", + config: map[string]any{ + optionNameFullNode: true, + }, + wantMode: node.FullMode, + }, + { + 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) + } + }) + } +} From 7e687994dc80500c35387d0bd7c2c87cdde15a99 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Thu, 30 Apr 2026 13:31:58 +0300 Subject: [PATCH 04/22] fix: tighten node-mode validation to prevent silent upgrade regressions --- cmd/bee/cmd/resolve_node_mode_test.go | 59 ++++++++++++++++++++++++--- cmd/bee/cmd/start.go | 47 +++++++++++++++++---- pkg/node/node.go | 2 +- 3 files changed, 93 insertions(+), 15 deletions(-) diff --git a/cmd/bee/cmd/resolve_node_mode_test.go b/cmd/bee/cmd/resolve_node_mode_test.go index 5880a3f57e7..10ccb9017ab 100644 --- a/cmd/bee/cmd/resolve_node_mode_test.go +++ b/cmd/bee/cmd/resolve_node_mode_test.go @@ -22,11 +22,13 @@ func TestResolveNodeMode(t *testing.T) { }{ // ── Explicit node-mode: strict validation ──────────────────────────────── { - name: "full mode with rpc and swap succeeds", + name: "full mode with rpc, swap, chequebook and incentives succeeds", config: map[string]any{ - optionNameNodeMode: "full", - configKeyBlockchainRpcEndpoint: "http://localhost:8545", - optionNameSwapEnable: true, + optionNameNodeMode: "full", + configKeyBlockchainRpcEndpoint: "http://localhost:8545", + optionNameSwapEnable: true, + optionNameChequebookEnable: true, + optionNameStorageIncentivesEnable: true, }, wantMode: node.FullMode, }, @@ -46,6 +48,35 @@ func TestResolveNodeMode(t *testing.T) { }, 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{ @@ -86,12 +117,28 @@ func TestResolveNodeMode(t *testing.T) { // ── Legacy path: no node-mode set ──────────────────────────────────────── { - name: "legacy full-node true maps to full mode", + name: "legacy full-node true with all required flags maps to full mode", config: map[string]any{ - optionNameFullNode: true, + 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{ diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index a1be35d0dac..68713bf42a1 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -343,26 +343,52 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo // 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. +// 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) { - // Explicit node-mode: validate strictly. 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 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 err := validateFullMode(); err != nil { + return "", err } case node.LightMode: if rpcEndpoint == "" { @@ -376,9 +402,14 @@ func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) { return mode, nil } - // Legacy path: node-mode not set, fall back to deprecated flags / old detection. + // 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 } diff --git a/pkg/node/node.go b/pkg/node/node.go index bd2ff2309a0..2bb92749d6b 100644 --- a/pkg/node/node.go +++ b/pkg/node/node.go @@ -553,7 +553,7 @@ func NewBee( } } - if o.SwapEnable { + if o.SwapEnable && chainEnabled { chequebookFactory, err := InitChequebookFactory(logger, chainBackend, chainID, transactionService, o.SwapFactoryAddress) if err != nil { return nil, fmt.Errorf("init chequebook factory: %w", err) From 479c7599affc05e63614cdff0ea2ea5d220f3cfd Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Wed, 3 Jun 2026 18:28:58 +0300 Subject: [PATCH 05/22] fix(node-mode): keep legacy --full-node configs starting after upgrade --- cmd/bee/cmd/resolve_node_mode_test.go | 88 +++++++++++++++++++++++++-- cmd/bee/cmd/start.go | 32 ++++++++-- 2 files changed, 111 insertions(+), 9 deletions(-) diff --git a/cmd/bee/cmd/resolve_node_mode_test.go b/cmd/bee/cmd/resolve_node_mode_test.go index 10ccb9017ab..43c051f7cb7 100644 --- a/cmd/bee/cmd/resolve_node_mode_test.go +++ b/cmd/bee/cmd/resolve_node_mode_test.go @@ -128,16 +128,63 @@ func TestResolveNodeMode(t *testing.T) { 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", + // 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, - optionNameStorageIncentivesEnable: true, + optionNameChequebookEnable: true, + optionNameStorageIncentivesEnable: false, }, - wantErr: "full node requires chequebook-enable", + 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", @@ -201,3 +248,34 @@ func TestResolveNodeMode(t *testing.T) { }) } } + +// 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) + } + } +} diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index 7dba2948e86..34b697ba9f3 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -371,6 +371,31 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo // 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) { + // Backward compatibility: before node-mode existed, --full-node implied a + // full node with swap, chequebook and storage incentives (chequebook and + // incentives even defaulted to true). With the new explicit defaults a + // legacy --full-node config that relied on them would fail strict full-mode + // validation and the node would not start. When --full-node is used as the + // legacy selector (node-mode unset), restore those implied values for any + // the operator did not set explicitly, so the upgraded node keeps running as + // a fully-functional full node (no silent degradation) instead of failing to + // start. Explicitly disabled options are left untouched and still fail + // validation below. The values are written back to config so the rest of + // node startup picks them up. + if c.config.GetBool(optionNameFullNode) && !c.config.IsSet(optionNameNodeMode) { + logger.Warning("--full-node is deprecated, use --node-mode=full instead") + for _, key := range []string{ + optionNameSwapEnable, + optionNameChequebookEnable, + optionNameStorageIncentivesEnable, + } { + if !c.config.IsSet(key) { + logger.Warning("enabling option implied by legacy --full-node; set it explicitly or use --node-mode=full", "option", key) + c.config.Set(key, true) + } + } + } + rpcEndpoint := c.config.GetString(configKeyBlockchainRpcEndpoint) swapEnable := c.config.GetBool(optionNameSwapEnable) chequebookEnable := c.config.GetBool(optionNameChequebookEnable) @@ -423,11 +448,10 @@ func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) { 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. + // Legacy path: node-mode not set. --full-node requests full mode; implied + // options were restored above, so any remaining failure is a genuine + // misconfiguration (missing rpc endpoint, or an explicitly disabled option). if c.config.GetBool(optionNameFullNode) { - logger.Warning("--full-node is deprecated, use --node-mode=full instead") if err := validateFullMode(); err != nil { return "", err } From ebc5f4aa64b07c397f071e3da87821b40607a0c6 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Thu, 27 Aug 2026 22:28:02 +0300 Subject: [PATCH 06/22] test(cmd): add edge-case and precedence tests for resolveNodeMode --- .golangci.yml | 4 +++ cmd/bee/cmd/resolve_node_mode_test.go | 48 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index ad34693a925..ae7330aaace 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -73,6 +73,10 @@ linters: - linters: - staticcheck text: "SA5008: malformed `json` tag: invalid trailing ',' character" + - linters: + - staticcheck + path: cmd/bee/cmd + text: "(this comparison is always true|never returns a nil interface value|the lhs of the comparison)" paths: - third_party$ - builtin$ diff --git a/cmd/bee/cmd/resolve_node_mode_test.go b/cmd/bee/cmd/resolve_node_mode_test.go index 43c051f7cb7..c9e4ad9fc4d 100644 --- a/cmd/bee/cmd/resolve_node_mode_test.go +++ b/cmd/bee/cmd/resolve_node_mode_test.go @@ -216,6 +216,54 @@ func TestResolveNodeMode(t *testing.T) { }, wantMode: node.UltraLightMode, }, + { + name: "node-mode takes precedence over legacy full-node with warning", + config: map[string]any{ + optionNameNodeMode: "light", + optionNameFullNode: true, + configKeyBlockchainRpcEndpoint: "http://localhost:8545", + }, + wantMode: node.LightMode, + }, + { + name: "ultra-light with rpc endpoint ignores rpc and succeeds", + config: map[string]any{ + optionNameNodeMode: "ultra-light", + configKeyBlockchainRpcEndpoint: "http://localhost:8545", + }, + wantMode: node.UltraLightMode, + }, + { + name: "light mode with rpc, swap and chequebook succeeds", + config: map[string]any{ + optionNameNodeMode: "light", + configKeyBlockchainRpcEndpoint: "http://localhost:8545", + optionNameSwapEnable: true, + optionNameChequebookEnable: true, + }, + wantMode: node.LightMode, + }, + { + name: "empty node-mode string fails validation", + config: map[string]any{ + optionNameNodeMode: "", + }, + wantErr: "invalid node-mode", + }, + { + name: "uppercase node-mode string fails validation", + config: map[string]any{ + optionNameNodeMode: "FULL", + }, + wantErr: "invalid node-mode", + }, + { + name: "whitespace node-mode string fails validation", + config: map[string]any{ + optionNameNodeMode: " full ", + }, + wantErr: "invalid node-mode", + }, } for _, tt := range tests { From 8b061de5b3eca62bb76935d7b09cc97561d5299d Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Wed, 2 Sep 2026 13:07:12 +0300 Subject: [PATCH 07/22] fix(cmd): reject storage incentives in light and ultra-light modes --- cmd/bee/cmd/resolve_node_mode_test.go | 17 +++++++++++++++++ cmd/bee/cmd/start.go | 6 ++++++ 2 files changed, 23 insertions(+) diff --git a/cmd/bee/cmd/resolve_node_mode_test.go b/cmd/bee/cmd/resolve_node_mode_test.go index c9e4ad9fc4d..085fb1d8417 100644 --- a/cmd/bee/cmd/resolve_node_mode_test.go +++ b/cmd/bee/cmd/resolve_node_mode_test.go @@ -92,6 +92,15 @@ func TestResolveNodeMode(t *testing.T) { }, wantErr: "light node requires blockchain-rpc-endpoint", }, + { + name: "light mode rejects storage-incentives-enable", + config: map[string]any{ + optionNameNodeMode: "light", + configKeyBlockchainRpcEndpoint: "http://localhost:8545", + optionNameStorageIncentivesEnable: true, + }, + wantErr: "light node cannot have storage-incentives-enable", + }, { name: "ultra-light mode succeeds", config: map[string]any{ @@ -107,6 +116,14 @@ func TestResolveNodeMode(t *testing.T) { }, wantErr: "ultra-light node cannot have swap-enable", }, + { + name: "ultra-light mode rejects storage-incentives-enable", + config: map[string]any{ + optionNameNodeMode: "ultra-light", + optionNameStorageIncentivesEnable: true, + }, + wantErr: "ultra-light node cannot have storage-incentives-enable", + }, { name: "invalid node-mode value fails", config: map[string]any{ diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index 392e33804cb..0b1a833b3b4 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -451,10 +451,16 @@ func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) { if rpcEndpoint == "" { return "", errors.New("light node requires blockchain-rpc-endpoint to be set") } + if incentivesEnable { + return "", errors.New("light node cannot have storage-incentives-enable set to true") + } case node.UltraLightMode: if swapEnable { return "", errors.New("ultra-light node cannot have swap-enable set to true") } + if incentivesEnable { + return "", errors.New("ultra-light node cannot have storage-incentives-enable set to true") + } } return mode, nil } From 5ef43da3165f931887c1039615f83a4024cb0399 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Thu, 3 Sep 2026 13:39:34 +0300 Subject: [PATCH 08/22] refactor(cmd): resolve node-mode in two regimes Split resolveNodeMode into a legacy regime (node-mode unset) that reproduces the behaviour of releases before node-mode existed, and a mode-owned regime (node-mode set) where the mode supplies defaults for swap-enable, chequebook-enable and storage-incentives-enable. Legacy regime: restore the former true defaults of chequebook-enable and storage-incentives-enable when unset, infer the mode from full-node and blockchain-rpc-endpoint as before, apply no new validation, and log a deprecation warning naming the inferred mode and the removal release. This fixes upgraded configs that were broken in both directions: a light node with swap-enable only silently lost cheque settlement, and a config carrying the old chequebook-enable default without swap, or a bootnode with storage-incentives-enable false, refused to start. Mode-owned regime: full implies swap, chequebook and incentives unless explicitly disabled (bootnodes exempt, NewBee never starts them there), light and full require blockchain-rpc-endpoint, and options a mode cannot support are rejected only when explicitly enabled. node-mode=full is now sufficient on its own; validateFullMode and the full-node restore loop are removed. The node-mode flag default becomes empty so an unset value selects the legacy regime without relying on viper's IsSet semantics for pflag defaults. Tests cover both regimes, including a run against the real bound start flags. --- cmd/bee/cmd/cmd.go | 2 +- cmd/bee/cmd/resolve_node_mode_test.go | 471 ++++++++++++++++++-------- cmd/bee/cmd/start.go | 195 ++++++----- 3 files changed, 428 insertions(+), 240 deletions(-) diff --git a/cmd/bee/cmd/cmd.go b/cmd/bee/cmd/cmd.go index adf8a33cb39..23056913c0d 100644 --- a/cmd/bee/cmd/cmd.go +++ b/cmd/bee/cmd/cmd.go @@ -358,7 +358,7 @@ 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().String(optionNameNodeMode, "", "node operational mode: full, light, or ultra-light (unset: inferred from deprecated full-node and blockchain-rpc-endpoint)") cmd.Flags().Bool(optionNameSwapEnable, false, "enable swap") cmd.Flags().Bool(optionNameChequebookEnable, false, "enable chequebook (requires swap-enable)") cmd.Flags().Bool(optionNameChequebookVerification, false, "reject full-node hive/handshake records that carry no chequebook address") diff --git a/cmd/bee/cmd/resolve_node_mode_test.go b/cmd/bee/cmd/resolve_node_mode_test.go index 085fb1d8417..c9c9e5c6fdb 100644 --- a/cmd/bee/cmd/resolve_node_mode_test.go +++ b/cmd/bee/cmd/resolve_node_mode_test.go @@ -13,19 +13,39 @@ import ( "github.com/spf13/viper" ) +const testRPCEndpoint = "http://localhost:8545" + func TestResolveNodeMode(t *testing.T) { + t.Parallel() + tests := []struct { name string config map[string]any wantMode node.NodeMode wantErr string + // wantOptions holds the values the resolver must leave in config for the + // rest of startup to read. Keys not listed are not checked. + wantOptions map[string]bool }{ - // ── Explicit node-mode: strict validation ──────────────────────────────── + // ── node-mode set: the mode owns the config ────────────────────────────── + { + name: "full with rpc only implies swap, chequebook and incentives", + config: map[string]any{ + optionNameNodeMode: "full", + configKeyBlockchainRpcEndpoint: testRPCEndpoint, + }, + wantMode: node.FullMode, + wantOptions: map[string]bool{ + optionNameSwapEnable: true, + optionNameChequebookEnable: true, + optionNameStorageIncentivesEnable: true, + }, + }, { - name: "full mode with rpc, swap, chequebook and incentives succeeds", + name: "full with all options explicitly enabled succeeds", config: map[string]any{ optionNameNodeMode: "full", - configKeyBlockchainRpcEndpoint: "http://localhost:8545", + configKeyBlockchainRpcEndpoint: testRPCEndpoint, optionNameSwapEnable: true, optionNameChequebookEnable: true, optionNameStorageIncentivesEnable: true, @@ -33,83 +53,158 @@ func TestResolveNodeMode(t *testing.T) { wantMode: node.FullMode, }, { - name: "full mode without rpc fails", + name: "full without rpc fails", config: map[string]any{ - optionNameNodeMode: "full", - optionNameSwapEnable: true, + optionNameNodeMode: "full", }, wantErr: "full node requires blockchain-rpc-endpoint", }, { - name: "full mode without swap fails", + // A non-staking full node is a legitimate opt-out. + name: "full with storage-incentives explicitly false honours the opt-out", config: map[string]any{ - optionNameNodeMode: "full", - configKeyBlockchainRpcEndpoint: "http://localhost:8545", + optionNameNodeMode: "full", + configKeyBlockchainRpcEndpoint: testRPCEndpoint, + optionNameStorageIncentivesEnable: false, + }, + wantMode: node.FullMode, + wantOptions: map[string]bool{ + optionNameSwapEnable: true, + optionNameChequebookEnable: true, + optionNameStorageIncentivesEnable: false, }, - wantErr: "full node requires swap-enable", }, { - name: "full mode without chequebook fails", + // Disabling swap must not drag an implied chequebook into a + // contradiction the operator never wrote. + name: "full with swap explicitly false leaves chequebook off", config: map[string]any{ - optionNameNodeMode: "full", - configKeyBlockchainRpcEndpoint: "http://localhost:8545", - optionNameSwapEnable: true, + optionNameNodeMode: "full", + configKeyBlockchainRpcEndpoint: testRPCEndpoint, + optionNameSwapEnable: false, + }, + wantMode: node.FullMode, + wantOptions: map[string]bool{ + optionNameSwapEnable: false, + optionNameChequebookEnable: false, optionNameStorageIncentivesEnable: true, }, - wantErr: "full node requires chequebook-enable", }, { - name: "full mode without storage-incentives fails", + // Receive-only swap: cash out cheques without issuing them. + name: "full with chequebook explicitly false keeps swap on", config: map[string]any{ optionNameNodeMode: "full", - configKeyBlockchainRpcEndpoint: "http://localhost:8545", - optionNameSwapEnable: true, - optionNameChequebookEnable: true, + configKeyBlockchainRpcEndpoint: testRPCEndpoint, + optionNameChequebookEnable: false, + }, + wantMode: node.FullMode, + wantOptions: map[string]bool{ + optionNameSwapEnable: true, + optionNameChequebookEnable: false, }, - wantErr: "storage-incentives-enable", }, { - name: "chequebook-enable without swap-enable fails (light mode)", + name: "full with chequebook explicitly true and swap explicitly false fails", config: map[string]any{ - optionNameNodeMode: "light", - configKeyBlockchainRpcEndpoint: "http://localhost:8545", + optionNameNodeMode: "full", + configKeyBlockchainRpcEndpoint: testRPCEndpoint, + optionNameSwapEnable: false, optionNameChequebookEnable: true, }, wantErr: "chequebook-enable requires swap-enable", }, { - name: "light mode with rpc succeeds", + // NewBee never starts swap, push-sync or the incentives agent for a + // bootnode, so full mode must not imply them there. + name: "full bootnode does not imply swap, chequebook or incentives", + config: map[string]any{ + optionNameNodeMode: "full", + optionNameBootnodeMode: true, + configKeyBlockchainRpcEndpoint: testRPCEndpoint, + }, + wantMode: node.FullMode, + wantOptions: map[string]bool{ + optionNameSwapEnable: false, + optionNameChequebookEnable: false, + optionNameStorageIncentivesEnable: false, + }, + }, + { + name: "full bootnode with storage-incentives explicitly false succeeds", + config: map[string]any{ + optionNameNodeMode: "full", + optionNameBootnodeMode: true, + configKeyBlockchainRpcEndpoint: testRPCEndpoint, + optionNameStorageIncentivesEnable: false, + }, + wantMode: node.FullMode, + }, + { + name: "light with rpc succeeds without swap", config: map[string]any{ optionNameNodeMode: "light", - configKeyBlockchainRpcEndpoint: "http://localhost:8545", + configKeyBlockchainRpcEndpoint: testRPCEndpoint, }, wantMode: node.LightMode, + wantOptions: map[string]bool{ + optionNameSwapEnable: false, + optionNameChequebookEnable: false, + optionNameStorageIncentivesEnable: false, + }, }, { - name: "light mode without rpc fails", + name: "light with rpc, swap and chequebook succeeds", + config: map[string]any{ + optionNameNodeMode: "light", + configKeyBlockchainRpcEndpoint: testRPCEndpoint, + optionNameSwapEnable: true, + optionNameChequebookEnable: true, + }, + wantMode: node.LightMode, + }, + { + name: "light without rpc fails", config: map[string]any{ optionNameNodeMode: "light", }, wantErr: "light node requires blockchain-rpc-endpoint", }, { - name: "light mode rejects storage-incentives-enable", + name: "light with chequebook but no swap fails", + config: map[string]any{ + optionNameNodeMode: "light", + configKeyBlockchainRpcEndpoint: testRPCEndpoint, + optionNameChequebookEnable: true, + }, + wantErr: "chequebook-enable requires swap-enable", + }, + { + name: "light rejects storage-incentives-enable", config: map[string]any{ optionNameNodeMode: "light", - configKeyBlockchainRpcEndpoint: "http://localhost:8545", + configKeyBlockchainRpcEndpoint: testRPCEndpoint, optionNameStorageIncentivesEnable: true, }, wantErr: "light node cannot have storage-incentives-enable", }, { - name: "ultra-light mode succeeds", + name: "ultra-light succeeds", config: map[string]any{ optionNameNodeMode: "ultra-light", }, wantMode: node.UltraLightMode, }, { - name: "ultra-light mode rejects swap-enable", + name: "ultra-light with rpc ignores rpc and succeeds", + config: map[string]any{ + optionNameNodeMode: "ultra-light", + configKeyBlockchainRpcEndpoint: testRPCEndpoint, + }, + wantMode: node.UltraLightMode, + }, + { + name: "ultra-light rejects swap-enable", config: map[string]any{ optionNameNodeMode: "ultra-light", optionNameSwapEnable: true, @@ -117,13 +212,22 @@ func TestResolveNodeMode(t *testing.T) { wantErr: "ultra-light node cannot have swap-enable", }, { - name: "ultra-light mode rejects storage-incentives-enable", + name: "ultra-light rejects storage-incentives-enable", config: map[string]any{ optionNameNodeMode: "ultra-light", optionNameStorageIncentivesEnable: true, }, wantErr: "ultra-light node cannot have storage-incentives-enable", }, + { + name: "node-mode takes precedence over legacy full-node", + config: map[string]any{ + optionNameNodeMode: "light", + optionNameFullNode: true, + configKeyBlockchainRpcEndpoint: testRPCEndpoint, + }, + wantMode: node.LightMode, + }, { name: "invalid node-mode value fails", config: map[string]any{ @@ -131,160 +235,146 @@ func TestResolveNodeMode(t *testing.T) { }, wantErr: "invalid node-mode", }, + { + name: "uppercase node-mode fails", + config: map[string]any{ + optionNameNodeMode: "FULL", + }, + wantErr: "invalid node-mode", + }, + { + name: "whitespace node-mode fails", + config: map[string]any{ + optionNameNodeMode: " full ", + }, + wantErr: "invalid node-mode", + }, - // ── Legacy path: no node-mode set ──────────────────────────────────────── + // ── node-mode unset: legacy behaviour, verbatim ───────────────────────── { - name: "legacy full-node true with all required flags maps to full mode", + // The most common pre-node-mode light config. chequebook-enable used + // to default to true, so this node issued cheques; it must keep doing so. + name: "legacy light with swap only restores chequebook default", config: map[string]any{ - optionNameFullNode: true, - configKeyBlockchainRpcEndpoint: "http://localhost:8545", + configKeyBlockchainRpcEndpoint: testRPCEndpoint, + optionNameSwapEnable: true, + }, + wantMode: node.LightMode, + wantOptions: map[string]bool{ 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", + // The old shipped default; chequebook stays gated on swap in NewBee. + name: "legacy chequebook without swap starts", config: map[string]any{ - optionNameFullNode: true, - configKeyBlockchainRpcEndpoint: "http://localhost:8545", - optionNameSwapEnable: true, + optionNameChequebookEnable: true, }, - wantMode: node.FullMode, + wantMode: node.UltraLightMode, }, { - // 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", + name: "legacy full-node with rpc only restores old defaults and leaves swap off", config: map[string]any{ optionNameFullNode: true, - configKeyBlockchainRpcEndpoint: "http://localhost:8545", - optionNameSwapEnable: true, - optionNameChequebookEnable: false, + configKeyBlockchainRpcEndpoint: testRPCEndpoint, + }, + wantMode: node.FullMode, + wantOptions: map[string]bool{ + optionNameSwapEnable: false, + optionNameChequebookEnable: true, + optionNameStorageIncentivesEnable: true, }, - 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", + name: "legacy full-node with all options set maps to full", config: map[string]any{ optionNameFullNode: true, - configKeyBlockchainRpcEndpoint: "http://localhost:8545", + configKeyBlockchainRpcEndpoint: testRPCEndpoint, optionNameSwapEnable: true, optionNameChequebookEnable: true, + optionNameStorageIncentivesEnable: true, + }, + wantMode: node.FullMode, + }, + { + name: "legacy full-node with explicit opt-outs is not validated", + config: map[string]any{ + optionNameFullNode: true, + configKeyBlockchainRpcEndpoint: testRPCEndpoint, + optionNameSwapEnable: false, + optionNameChequebookEnable: false, + optionNameStorageIncentivesEnable: false, + }, + wantMode: node.FullMode, + wantOptions: map[string]bool{ + optionNameSwapEnable: false, + optionNameChequebookEnable: false, 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", + name: "legacy bootnode with storage-incentives false starts", config: map[string]any{ - optionNameFullNode: true, - configKeyBlockchainRpcEndpoint: "http://localhost:8545", + optionNameFullNode: true, + optionNameBootnodeMode: true, + configKeyBlockchainRpcEndpoint: testRPCEndpoint, + optionNameStorageIncentivesEnable: false, }, 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", + // Previous releases enabled the chain backend for every full node and + // failed at chain init without an endpoint; keep failing, earlier. + name: "legacy full-node without rpc fails", config: map[string]any{ optionNameFullNode: true, }, wantErr: "full node requires blockchain-rpc-endpoint", }, { - name: "legacy with rpc endpoint infers light mode", + name: "legacy with rpc infers light", config: map[string]any{ - configKeyBlockchainRpcEndpoint: "http://localhost:8545", + configKeyBlockchainRpcEndpoint: testRPCEndpoint, }, wantMode: node.LightMode, }, { - name: "legacy without rpc endpoint infers ultra-light mode", + name: "legacy without rpc infers ultra-light", 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", + // Beekeeper's inherited-config scenario: swap-enable inherited from the + // base profile on a node without rpc. Legacy must not validate it. + name: "legacy without rpc but with swap infers ultra-light without error", config: map[string]any{ optionNameSwapEnable: true, }, wantMode: node.UltraLightMode, }, { - name: "node-mode takes precedence over legacy full-node with warning", - config: map[string]any{ - optionNameNodeMode: "light", - optionNameFullNode: true, - configKeyBlockchainRpcEndpoint: "http://localhost:8545", - }, - wantMode: node.LightMode, - }, - { - name: "ultra-light with rpc endpoint ignores rpc and succeeds", - config: map[string]any{ - optionNameNodeMode: "ultra-light", - configKeyBlockchainRpcEndpoint: "http://localhost:8545", - }, - wantMode: node.UltraLightMode, - }, - { - name: "light mode with rpc, swap and chequebook succeeds", + // Legacy must not restore a default the operator overrode. + name: "legacy explicit false is preserved", config: map[string]any{ - optionNameNodeMode: "light", - configKeyBlockchainRpcEndpoint: "http://localhost:8545", - optionNameSwapEnable: true, - optionNameChequebookEnable: true, + configKeyBlockchainRpcEndpoint: testRPCEndpoint, + optionNameSwapEnable: true, + optionNameChequebookEnable: false, + optionNameStorageIncentivesEnable: false, }, wantMode: node.LightMode, - }, - { - name: "empty node-mode string fails validation", - config: map[string]any{ - optionNameNodeMode: "", - }, - wantErr: "invalid node-mode", - }, - { - name: "uppercase node-mode string fails validation", - config: map[string]any{ - optionNameNodeMode: "FULL", - }, - wantErr: "invalid node-mode", - }, - { - name: "whitespace node-mode string fails validation", - config: map[string]any{ - optionNameNodeMode: " full ", + wantOptions: map[string]bool{ + optionNameChequebookEnable: false, + optionNameStorageIncentivesEnable: false, }, - wantErr: "invalid node-mode", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + c := &command{ config: viper.New(), logger: log.Noop, @@ -310,37 +400,118 @@ func TestResolveNodeMode(t *testing.T) { if gotMode != tt.wantMode { t.Errorf("got mode %q, want %q", gotMode, tt.wantMode) } + for key, want := range tt.wantOptions { + if got := c.config.GetBool(key); got != want { + t.Errorf("option %q: got %t, want %t", key, got, want) + } + } }) } } -// 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") +// TestResolveNodeModeWithBoundFlags runs the resolver against a viper bound to +// the real start command flags, as production does, so the flag defaults are +// exercised: an unset node-mode must select the legacy regime, and the flag +// defaults of the sub-options must not count as explicitly set. +func TestResolveNodeModeWithBoundFlags(t *testing.T) { + t.Parallel() - 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) + tests := []struct { + name string + args []string + wantMode node.NodeMode + wantOptions map[string]bool + }{ + { + name: "no flags selects legacy regime and restores old defaults", + args: nil, + wantMode: node.UltraLightMode, + wantOptions: map[string]bool{ + optionNameChequebookEnable: true, + optionNameStorageIncentivesEnable: true, + }, + }, + { + name: "legacy full-node flag with rpc", + args: []string{"--full-node", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + wantMode: node.FullMode, + wantOptions: map[string]bool{ + optionNameSwapEnable: false, + optionNameChequebookEnable: true, + optionNameStorageIncentivesEnable: true, + }, + }, + { + name: "node-mode full with rpc implies the full stack", + args: []string{"--node-mode=full", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + wantMode: node.FullMode, + wantOptions: map[string]bool{ + optionNameSwapEnable: true, + optionNameChequebookEnable: true, + optionNameStorageIncentivesEnable: true, + }, + }, + { + name: "node-mode light with rpc keeps sub-option defaults", + args: []string{"--node-mode=light", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + wantMode: node.LightMode, + wantOptions: map[string]bool{ + optionNameSwapEnable: false, + optionNameChequebookEnable: false, + optionNameStorageIncentivesEnable: false, + }, + }, + { + name: "node-mode full with incentives explicitly disabled", + args: []string{"--node-mode=full", "--blockchain-rpc-endpoint=" + testRPCEndpoint, "--storage-incentives-enable=false"}, + wantMode: node.FullMode, + wantOptions: map[string]bool{ + optionNameSwapEnable: true, + optionNameChequebookEnable: true, + optionNameStorageIncentivesEnable: false, + }, + }, } - for _, key := range []string{ - optionNameSwapEnable, - optionNameChequebookEnable, - optionNameStorageIncentivesEnable, - } { - if !c.config.GetBool(key) { - t.Errorf("expected %q to be written back as true", key) - } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + root, err := newCommand(func(c *command) { c.homeDir = t.TempDir() }) + if err != nil { + t.Fatal(err) + } + startCmd := root.SubCommandForTest("start") + if startCmd == nil { + t.Fatal("start subcommand not found") + } + if err := startCmd.ParseFlags(tt.args); err != nil { + t.Fatal(err) + } + + // Mirror the start command's PreRunE: bind flags, then map the flat + // blockchain-rpc-* flags onto their nested config keys. + c := &command{ + config: viper.New(), + logger: log.Noop, + } + if err := c.config.BindPFlags(startCmd.Flags()); err != nil { + t.Fatal(err) + } + c.bindBlockchainRpcConfig(startCmd) + + gotMode, err := c.resolveNodeMode(c.logger) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gotMode != tt.wantMode { + t.Errorf("got mode %q, want %q", gotMode, tt.wantMode) + } + for key, want := range tt.wantOptions { + if got := c.config.GetBool(key); got != want { + t.Errorf("option %q: got %t, want %t", key, got, want) + } + } + }) } } diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index 0b1a833b3b4..2cff7db0fc4 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -373,113 +373,130 @@ 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. +// legacyNodeModeRemovalVersion is the release in which node-mode inference from +// the deprecated full-node option and blockchain-rpc-endpoint presence is +// removed. From that release on, node-mode is required. +const legacyNodeModeRemovalVersion = "v2.11.0" + +// resolveNodeMode determines the effective node mode from config. There are +// two regimes, selected by whether node-mode is set. +// +// node-mode unset (legacy): the node behaves exactly as releases before +// node-mode existed. chequebook-enable and storage-incentives-enable fall back +// to their former default of true when not set, the mode is inferred from the +// deprecated full-node option and the presence of blockchain-rpc-endpoint, and +// no further validation is applied. A deprecation warning names the equivalent +// node-mode value and the release in which the inference is removed. +// +// node-mode set: the mode owns the config. Options the mode implies are +// enabled unless the operator explicitly disabled them, options the mode +// cannot support are rejected when explicitly enabled, and +// blockchain-rpc-endpoint is required for light and full nodes. Implied values +// are written back to config so the rest of node startup picks them up. func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) { - // Backward compatibility: before node-mode existed, --full-node implied a - // full node with swap, chequebook and storage incentives (chequebook and - // incentives even defaulted to true). With the new explicit defaults a - // legacy --full-node config that relied on them would fail strict full-mode - // validation and the node would not start. When --full-node is used as the - // legacy selector (node-mode unset), restore those implied values for any - // the operator did not set explicitly, so the upgraded node keeps running as - // a fully-functional full node (no silent degradation) instead of failing to - // start. Explicitly disabled options are left untouched and still fail - // validation below. The values are written back to config so the rest of - // node startup picks them up. - if c.config.GetBool(optionNameFullNode) && !c.config.IsSet(optionNameNodeMode) { - logger.Warning("--full-node is deprecated, use --node-mode=full instead") - for _, key := range []string{ - optionNameSwapEnable, - optionNameChequebookEnable, - optionNameStorageIncentivesEnable, - } { - if !c.config.IsSet(key) { - logger.Warning("enabling option implied by legacy --full-node; set it explicitly or use --node-mode=full", "option", key) - c.config.Set(key, true) - } - } + modeStr := c.config.GetString(optionNameNodeMode) + if modeStr == "" { + return c.resolveLegacyNodeMode(logger) } - 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") + mode := node.NodeMode(modeStr) + 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") } - validateFullMode := func() error { + rpcEndpoint := c.config.GetString(configKeyBlockchainRpcEndpoint) + + switch mode { + case node.FullMode: if rpcEndpoint == "" { - return errors.New("full node requires blockchain-rpc-endpoint to be set") + 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") + // A full node implies swap, chequebook and storage incentives. Bootnodes + // are exempt: NewBee never starts swap, push-sync or the incentives agent + // for them, so implying the options would only cost a chequebook deploy. + if !c.config.GetBool(optionNameBootnodeMode) { + c.enableImpliedOption(logger, mode, optionNameSwapEnable) + if c.config.GetBool(optionNameSwapEnable) { + c.enableImpliedOption(logger, mode, optionNameChequebookEnable) + } + c.enableImpliedOption(logger, mode, optionNameStorageIncentivesEnable) } - if !chequebookEnable { - return errors.New("full node requires chequebook-enable to be true (cheque issuance)") + case node.LightMode: + if rpcEndpoint == "" { + return "", errors.New("light node requires blockchain-rpc-endpoint to be set") } - if !incentivesEnable { - return errors.New("full node requires storage-incentives-enable to be true") + if c.config.GetBool(optionNameStorageIncentivesEnable) { + return "", errors.New("light node cannot have storage-incentives-enable set to 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) + case node.UltraLightMode: + if c.config.GetBool(optionNameSwapEnable) { + return "", errors.New("ultra-light node cannot have swap-enable set to true") } - if c.config.GetBool(optionNameFullNode) { - logger.Warning("--full-node is set alongside --node-mode; --full-node is ignored") + if c.config.GetBool(optionNameStorageIncentivesEnable) { + return "", errors.New("ultra-light node cannot have storage-incentives-enable set to true") } - 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") - } - if incentivesEnable { - return "", errors.New("light node cannot have storage-incentives-enable set to true") - } - case node.UltraLightMode: - if swapEnable { - return "", errors.New("ultra-light node cannot have swap-enable set to true") - } - if incentivesEnable { - return "", errors.New("ultra-light node cannot have storage-incentives-enable set to true") - } - } - return mode, nil } - // Legacy path: node-mode not set. --full-node requests full mode; implied - // options were restored above, so any remaining failure is a genuine - // misconfiguration (missing rpc endpoint, or an explicitly disabled option). - if c.config.GetBool(optionNameFullNode) { - if err := validateFullMode(); err != nil { - return "", err + // chequebook init is gated on swap-enable in NewBee. With node-mode set the + // implied values above never produce this combination, so it is always an + // explicit contradiction rather than a silent no-op. + if c.config.GetBool(optionNameChequebookEnable) && !c.config.GetBool(optionNameSwapEnable) { + return "", errors.New("chequebook-enable requires swap-enable to be true") + } + + return mode, nil +} + +// enableImpliedOption turns on an option implied by the node mode unless the +// operator set it explicitly. An explicit false is honoured and reported, since +// it degrades the node relative to what the mode normally provides. +func (c *command) enableImpliedOption(logger log.Logger, mode node.NodeMode, key string) { + if !c.config.IsSet(key) { + logger.Debug("enabling option implied by node-mode", "node_mode", mode, "option", key) + c.config.Set(key, true) + return + } + if !c.config.GetBool(key) { + logger.Warning("option implied by node-mode is explicitly disabled", "node_mode", mode, "option", key) + } +} + +// resolveLegacyNodeMode reproduces the behaviour of releases before node-mode +// existed, for configs that do not set node-mode. The only check kept is that +// a full node has a blockchain-rpc-endpoint: those releases enabled the chain +// backend for every full node and failed at chain init without one, so the +// early error changes the message, not the outcome. +func (c *command) resolveLegacyNodeMode(logger log.Logger) (node.NodeMode, error) { + // chequebook-enable and storage-incentives-enable used to default to true. + // Restore that for configs that leave them unset so an upgraded node keeps + // its settlement and incentives behaviour. Both stay gated in NewBee + // (chequebook on swap-enable, incentives on full mode), so this cannot start + // anything the previous release would not have started. + for _, key := range []string{optionNameChequebookEnable, optionNameStorageIncentivesEnable} { + if !c.config.IsSet(key) { + c.config.Set(key, true) } - return node.FullMode, nil } - // Infer light vs ultra-light from RPC endpoint presence (original behaviour). - if rpcEndpoint != "" { - return node.LightMode, nil + mode := node.UltraLightMode + switch { + case c.config.GetBool(optionNameFullNode): + if c.config.GetString(configKeyBlockchainRpcEndpoint) == "" { + return "", errors.New("full node requires blockchain-rpc-endpoint to be set") + } + mode = node.FullMode + case c.config.GetString(configKeyBlockchainRpcEndpoint) != "": + mode = node.LightMode } - return node.UltraLightMode, nil + + logger.Warning("node-mode is not set and was inferred from legacy options; add it to your config, inference will be removed", + "node_mode", mode, + "removed_in", legacyNodeModeRemovalVersion, + ) + return mode, nil } type program struct { From 1bf0f0c92268cb28adb32e4655500a9e20764c92 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Thu, 3 Sep 2026 13:39:34 +0300 Subject: [PATCH 09/22] fix(packaging): pass BEE_NODE_MODE through docker compose and refresh defaults Add BEE_NODE_MODE to the docker-compose environment passthrough and the env template, mark BEE_FULL_NODE deprecated, and correct the stale true defaults documented for BEE_CHEQUEBOOK_ENABLE and BEE_STORAGE_INCENTIVES_ENABLE. Document in the bee.yaml variants that full mode implies the swap, chequebook and incentives options and that an unset node-mode falls back to deprecated inference. --- packaging/bee.yaml | 11 +++++++---- packaging/docker/README.md | 2 +- packaging/docker/docker-compose.yml | 1 + packaging/docker/env | 12 +++++++----- packaging/homebrew-amd64/bee.yaml | 11 +++++++---- packaging/homebrew-arm64/bee.yaml | 11 +++++++---- packaging/scoop/bee.yaml | 11 +++++++---- 7 files changed, 37 insertions(+), 22 deletions(-) diff --git a/packaging/bee.yaml b/packaging/bee.yaml index 786989dec0f..0c94069ee94 100644 --- a/packaging/bee.yaml +++ b/packaging/bee.yaml @@ -2,9 +2,12 @@ ## ── Node mode ──────────────────────────────────────────────────────────────── ## Selects the operational mode of this node. -## full - participates in storage and incentives; requires swap-enable and blockchain-rpc +## full - participates in storage and incentives; requires blockchain-rpc; +## implies swap-enable, chequebook-enable and storage-incentives-enable ## light - uploads and downloads only; requires blockchain-rpc -## ultra-light - free-tier downloads only; no blockchain connection needed (default) +## ultra-light - free-tier downloads only; no blockchain connection needed +## When unset, the mode is inferred from the deprecated full-node option and +## blockchain-rpc presence (deprecated, removed in v2.11.0). # node-mode: ultra-light ## ── Blockchain / RPC (required for full and light nodes) ───────────────────── @@ -23,7 +26,7 @@ ## ── Swap / chequebook (full and light nodes only) ──────────────────────────── ## enable swap # swap-enable: false -## enable chequebook +## enable chequebook (requires swap-enable; implied by node-mode: full) # chequebook-enable: false ## reject full-node hive/handshake records that carry no chequebook address # chequebook-verification: false @@ -33,7 +36,7 @@ # swap-initial-deposit: "0" ## ── Full node only ──────────────────────────────────────────────────────────── -## enable storage incentives feature +## enable storage incentives feature (implied by node-mode: full) # storage-incentives-enable: false ## reserve capacity doubling # reserve-capacity-doubling: 0 diff --git a/packaging/docker/README.md b/packaging/docker/README.md index db118ff1ef0..ee73934432c 100644 --- a/packaging/docker/README.md +++ b/packaging/docker/README.md @@ -11,7 +11,7 @@ wget -q https://raw.githubusercontent.com/ethersphere/bee/master/packaging/docke Set all configuration variables inside `.env` -If you want to run node in full mode, set `BEE_FULL_NODE=true` +Select the node mode with `BEE_NODE_MODE` (`full`, `light`, or `ultra-light`). A full node needs `BEE_BLOCKCHAIN_RPC_ENDPOINT` and implies swap, chequebook and storage incentives. Bee requires an Ethereum endpoint to function. Obtain a free Infura account and set: diff --git a/packaging/docker/docker-compose.yml b/packaging/docker/docker-compose.yml index 16818215c7c..4f8ba110f12 100644 --- a/packaging/docker/docker-compose.yml +++ b/packaging/docker/docker-compose.yml @@ -39,6 +39,7 @@ services: - BEE_NAT_WSS_ADDR - BEE_NEIGHBORHOOD_SUGGESTER - BEE_NETWORK_ID + - BEE_NODE_MODE - BEE_P2P_ADDR - BEE_P2P_WS_ENABLE - BEE_P2P_WSS_ADDR diff --git a/packaging/docker/env b/packaging/docker/env index f33a62c0f15..cfb51cbbafb 100644 --- a/packaging/docker/env +++ b/packaging/docker/env @@ -37,8 +37,8 @@ # BEE_CACHE_CAPACITY=1000000 ## enable forwarded content caching (default true) # BEE_CACHE_RETRIEVAL=true -## enable chequebook (default true) -# BEE_CHEQUEBOOK_ENABLE=true +## enable chequebook; requires swap; implied by BEE_NODE_MODE=full (default false) +# BEE_CHEQUEBOOK_ENABLE=false ## reject full-node hive/handshake records that carry no chequebook address # BEE_CHEQUEBOOK_VERIFICATION=false ## origins with CORS headers enabled (default []) @@ -53,7 +53,7 @@ # BEE_DB_OPEN_FILES_LIMIT=200 ## size of the database write buffer in bytes (default 33554432) # BEE_DB_WRITE_BUFFER_SIZE=33554432 -## cause the node to start in full mode (default false) +## cause the node to start in full mode (deprecated: use BEE_NODE_MODE=full) (default false) # BEE_FULL_NODE=false ## gas limit fallback when estimation fails for contract transactions (default 500000) # BEE_GAS_LIMIT_FALLBACK=500000 @@ -71,6 +71,8 @@ # BEE_NEIGHBORHOOD_SUGGESTER=https://api.swarmscan.io/v1/network/neighborhoods/suggestion ## ID of the Swarm network (default mainnet id from bee) # BEE_NETWORK_ID=1 +## node operational mode: full, light, or ultra-light; unset infers the mode from BEE_FULL_NODE and BEE_BLOCKCHAIN_RPC_ENDPOINT (deprecated) +# BEE_NODE_MODE=ultra-light ## P2P listen address (default :1634) # BEE_P2P_ADDR=:1634 ## enable P2P WebSocket transport (default false) @@ -115,8 +117,8 @@ # BEE_STATIC_NODES=[] ## lru memory caching capacity in number of statestore entries (default 100000) # BEE_STATESTORE_CACHE_CAPACITY=100000 -## enable storage incentives feature (default true) -# BEE_STORAGE_INCENTIVES_ENABLE=true +## enable storage incentives feature; full node only; implied by BEE_NODE_MODE=full (default false) +# BEE_STORAGE_INCENTIVES_ENABLE=false ## enable swap (default false) # BEE_SWAP_ENABLE=false ## swap factory addresses (default empty) diff --git a/packaging/homebrew-amd64/bee.yaml b/packaging/homebrew-amd64/bee.yaml index 5967d6e56fa..315b58689bd 100644 --- a/packaging/homebrew-amd64/bee.yaml +++ b/packaging/homebrew-amd64/bee.yaml @@ -2,9 +2,12 @@ ## ── Node mode ──────────────────────────────────────────────────────────────── ## Selects the operational mode of this node. -## full - participates in storage and incentives; requires swap-enable and blockchain-rpc +## full - participates in storage and incentives; requires blockchain-rpc; +## implies swap-enable, chequebook-enable and storage-incentives-enable ## light - uploads and downloads only; requires blockchain-rpc -## ultra-light - free-tier downloads only; no blockchain connection needed (default) +## ultra-light - free-tier downloads only; no blockchain connection needed +## When unset, the mode is inferred from the deprecated full-node option and +## blockchain-rpc presence (deprecated, removed in v2.11.0). # node-mode: ultra-light ## ── Blockchain / RPC (required for full and light nodes) ───────────────────── @@ -23,7 +26,7 @@ ## ── Swap / chequebook (full and light nodes only) ──────────────────────────── ## enable swap # swap-enable: false -## enable chequebook +## enable chequebook (requires swap-enable; implied by node-mode: full) # chequebook-enable: false ## reject full-node hive/handshake records that carry no chequebook address # chequebook-verification: false @@ -33,7 +36,7 @@ # swap-initial-deposit: "0" ## ── Full node only ──────────────────────────────────────────────────────────── -## enable storage incentives feature +## enable storage incentives feature (implied by node-mode: full) # storage-incentives-enable: false ## reserve capacity doubling # reserve-capacity-doubling: 0 diff --git a/packaging/homebrew-arm64/bee.yaml b/packaging/homebrew-arm64/bee.yaml index 19bc0e3b21a..6da2d4e6ef4 100644 --- a/packaging/homebrew-arm64/bee.yaml +++ b/packaging/homebrew-arm64/bee.yaml @@ -2,9 +2,12 @@ ## ── Node mode ──────────────────────────────────────────────────────────────── ## Selects the operational mode of this node. -## full - participates in storage and incentives; requires swap-enable and blockchain-rpc +## full - participates in storage and incentives; requires blockchain-rpc; +## implies swap-enable, chequebook-enable and storage-incentives-enable ## light - uploads and downloads only; requires blockchain-rpc -## ultra-light - free-tier downloads only; no blockchain connection needed (default) +## ultra-light - free-tier downloads only; no blockchain connection needed +## When unset, the mode is inferred from the deprecated full-node option and +## blockchain-rpc presence (deprecated, removed in v2.11.0). # node-mode: ultra-light ## ── Blockchain / RPC (required for full and light nodes) ───────────────────── @@ -23,7 +26,7 @@ ## ── Swap / chequebook (full and light nodes only) ──────────────────────────── ## enable swap # swap-enable: false -## enable chequebook +## enable chequebook (requires swap-enable; implied by node-mode: full) # chequebook-enable: false ## reject full-node hive/handshake records that carry no chequebook address # chequebook-verification: false @@ -33,7 +36,7 @@ # swap-initial-deposit: "0" ## ── Full node only ──────────────────────────────────────────────────────────── -## enable storage incentives feature +## enable storage incentives feature (implied by node-mode: full) # storage-incentives-enable: false ## reserve capacity doubling # reserve-capacity-doubling: 0 diff --git a/packaging/scoop/bee.yaml b/packaging/scoop/bee.yaml index ee2b45a18ed..52eca12ada6 100644 --- a/packaging/scoop/bee.yaml +++ b/packaging/scoop/bee.yaml @@ -2,9 +2,12 @@ ## ── Node mode ──────────────────────────────────────────────────────────────── ## Selects the operational mode of this node. -## full - participates in storage and incentives; requires swap-enable and blockchain-rpc +## full - participates in storage and incentives; requires blockchain-rpc; +## implies swap-enable, chequebook-enable and storage-incentives-enable ## light - uploads and downloads only; requires blockchain-rpc -## ultra-light - free-tier downloads only; no blockchain connection needed (default) +## ultra-light - free-tier downloads only; no blockchain connection needed +## When unset, the mode is inferred from the deprecated full-node option and +## blockchain-rpc presence (deprecated, removed in v2.11.0). # node-mode: ultra-light ## ── Blockchain / RPC (required for full and light nodes) ───────────────────── @@ -23,7 +26,7 @@ ## ── Swap / chequebook (full and light nodes only) ──────────────────────────── ## enable swap # swap-enable: false -## enable chequebook +## enable chequebook (requires swap-enable; implied by node-mode: full) # chequebook-enable: false ## reject full-node hive/handshake records that carry no chequebook address # chequebook-verification: false @@ -33,7 +36,7 @@ # swap-initial-deposit: "0" ## ── Full node only ──────────────────────────────────────────────────────────── -## enable storage incentives feature +## enable storage incentives feature (implied by node-mode: full) # storage-incentives-enable: false ## reserve capacity doubling # reserve-capacity-doubling: 0 From d8086ba54ea11d8c821beafc10f1bf6b09832923 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Thu, 3 Sep 2026 14:57:43 +0300 Subject: [PATCH 10/22] ci: point Beekeeper job at the refactor/node-mode-two-regimes companion branch --- .github/workflows/beekeeper.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beekeeper.yml b/.github/workflows/beekeeper.yml index 63c9792c02e..f8e5cf97fea 100644 --- a/.github/workflows/beekeeper.yml +++ b/.github/workflows/beekeeper.yml @@ -20,7 +20,7 @@ env: SETUP_CONTRACT_IMAGE: "ethersphere/bee-localchain" SETUP_CONTRACT_IMAGE_TAG: "0.9.4" BEELOCAL_BRANCH: "main" - BEEKEEPER_BRANCH: "refactor/node-mode-config" + BEEKEEPER_BRANCH: "refactor/node-mode-two-regimes" BEEKEEPER_METRICS_ENABLED: false REACHABILITY_OVERRIDE_PUBLIC: true BATCHFACTOR_OVERRIDE_PUBLIC: 2 From e00782d2cb8935f67927db752f8bb9c16f318b03 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Fri, 11 Sep 2026 11:12:59 +0300 Subject: [PATCH 11/22] fix(cmd): preserve flag defaults and apply mode overrides when node-mode is set --- cmd/bee/cmd/cmd.go | 4 +-- cmd/bee/cmd/start.go | 43 ++++++++++++++++++++++--------- packaging/bee.yaml | 4 +-- packaging/docker/env | 8 +++--- packaging/homebrew-amd64/bee.yaml | 4 +-- packaging/homebrew-arm64/bee.yaml | 4 +-- packaging/scoop/bee.yaml | 4 +-- 7 files changed, 45 insertions(+), 26 deletions(-) diff --git a/cmd/bee/cmd/cmd.go b/cmd/bee/cmd/cmd.go index 23056913c0d..0b2e5aa0d2f 100644 --- a/cmd/bee/cmd/cmd.go +++ b/cmd/bee/cmd/cmd.go @@ -360,7 +360,7 @@ func (c *command) setAllFlags(cmd *cobra.Command) { cmd.Flags().String(optionNameSwapInitialDeposit, "0", "initial deposit if deploying a new chequebook") cmd.Flags().String(optionNameNodeMode, "", "node operational mode: full, light, or ultra-light (unset: inferred from deprecated full-node and blockchain-rpc-endpoint)") cmd.Flags().Bool(optionNameSwapEnable, false, "enable swap") - cmd.Flags().Bool(optionNameChequebookEnable, false, "enable chequebook (requires swap-enable)") + cmd.Flags().Bool(optionNameChequebookEnable, true, "enable chequebook") 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 (deprecated: use --node-mode=full)") @@ -383,7 +383,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, false, "enable storage incentives feature (full node only)") + cmd.Flags().Bool(optionNameStorageIncentivesEnable, true, "enable storage incentives feature") 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") diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index 2cff7db0fc4..bced39750f2 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -382,17 +382,18 @@ const legacyNodeModeRemovalVersion = "v2.11.0" // two regimes, selected by whether node-mode is set. // // node-mode unset (legacy): the node behaves exactly as releases before -// node-mode existed. chequebook-enable and storage-incentives-enable fall back -// to their former default of true when not set, the mode is inferred from the +// node-mode existed. Flag defaults of true for chequebook-enable and +// storage-incentives-enable remain in effect, the mode is inferred from the // deprecated full-node option and the presence of blockchain-rpc-endpoint, and // no further validation is applied. A deprecation warning names the equivalent // node-mode value and the release in which the inference is removed. // // node-mode set: the mode owns the config. Options the mode implies are // enabled unless the operator explicitly disabled them, options the mode -// cannot support are rejected when explicitly enabled, and -// blockchain-rpc-endpoint is required for light and full nodes. Implied values -// are written back to config so the rest of node startup picks them up. +// cannot support are rejected when explicitly enabled (overriding the pflag +// defaults of true when unset), and blockchain-rpc-endpoint is required for +// light and full nodes. Implied values are written back to config so the rest +// of node startup picks them up. func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) { modeStr := c.config.GetString(optionNameNodeMode) if modeStr == "" { @@ -419,6 +420,12 @@ func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) { // for them, so implying the options would only cost a chequebook deploy. if !c.config.GetBool(optionNameBootnodeMode) { c.enableImpliedOption(logger, mode, optionNameSwapEnable) + // If swap was explicitly turned off by the operator and chequebook + // was not explicitly configured, turn off chequebook to prevent an + // accidental contradiction with disabled swap. + if !c.config.GetBool(optionNameSwapEnable) && !c.config.IsSet(optionNameChequebookEnable) { + c.config.Set(optionNameChequebookEnable, false) + } if c.config.GetBool(optionNameSwapEnable) { c.enableImpliedOption(logger, mode, optionNameChequebookEnable) } @@ -428,16 +435,30 @@ func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) { if rpcEndpoint == "" { return "", errors.New("light node requires blockchain-rpc-endpoint to be set") } - if c.config.GetBool(optionNameStorageIncentivesEnable) { + // Storage incentives are full-node only; override the pflag default of true when unset. + if !c.config.IsSet(optionNameStorageIncentivesEnable) { + c.config.Set(optionNameStorageIncentivesEnable, false) + } else if c.config.GetBool(optionNameStorageIncentivesEnable) { return "", errors.New("light node cannot have storage-incentives-enable set to true") } + // Chequebook requires swap; if swap is off and chequebook was not explicitly configured, override to false. + if !c.config.GetBool(optionNameSwapEnable) && !c.config.IsSet(optionNameChequebookEnable) { + c.config.Set(optionNameChequebookEnable, false) + } case node.UltraLightMode: if c.config.GetBool(optionNameSwapEnable) { return "", errors.New("ultra-light node cannot have swap-enable set to true") } - if c.config.GetBool(optionNameStorageIncentivesEnable) { + // Storage incentives are not supported on ultra-light; override the pflag default of true when unset. + if !c.config.IsSet(optionNameStorageIncentivesEnable) { + c.config.Set(optionNameStorageIncentivesEnable, false) + } else if c.config.GetBool(optionNameStorageIncentivesEnable) { return "", errors.New("ultra-light node cannot have storage-incentives-enable set to true") } + // Chequebook is not supported on ultra-light; override the pflag default of true when unset. + if !c.config.IsSet(optionNameChequebookEnable) { + c.config.Set(optionNameChequebookEnable, false) + } } // chequebook init is gated on swap-enable in NewBee. With node-mode set the @@ -470,11 +491,9 @@ func (c *command) enableImpliedOption(logger log.Logger, mode node.NodeMode, key // backend for every full node and failed at chain init without one, so the // early error changes the message, not the outcome. func (c *command) resolveLegacyNodeMode(logger log.Logger) (node.NodeMode, error) { - // chequebook-enable and storage-incentives-enable used to default to true. - // Restore that for configs that leave them unset so an upgraded node keeps - // its settlement and incentives behaviour. Both stay gated in NewBee - // (chequebook on swap-enable, incentives on full mode), so this cannot start - // anything the previous release would not have started. + // chequebook-enable and storage-incentives-enable default to true. + // Ensure they are populated in config when unset (e.g. bare viper in tests) + // so an upgraded node keeps its settlement and incentives behaviour. for _, key := range []string{optionNameChequebookEnable, optionNameStorageIncentivesEnable} { if !c.config.IsSet(key) { c.config.Set(key, true) diff --git a/packaging/bee.yaml b/packaging/bee.yaml index 0c94069ee94..bb8b599a931 100644 --- a/packaging/bee.yaml +++ b/packaging/bee.yaml @@ -27,7 +27,7 @@ ## enable swap # swap-enable: false ## enable chequebook (requires swap-enable; implied by node-mode: full) -# chequebook-enable: false +# chequebook-enable: true ## reject full-node hive/handshake records that carry no chequebook address # chequebook-verification: false ## swap factory addresses @@ -37,7 +37,7 @@ ## ── Full node only ──────────────────────────────────────────────────────────── ## enable storage incentives feature (implied by node-mode: full) -# storage-incentives-enable: false +# storage-incentives-enable: true ## reserve capacity doubling # reserve-capacity-doubling: 0 ## minimum radius storage threshold diff --git a/packaging/docker/env b/packaging/docker/env index cfb51cbbafb..5a5eabcc1bd 100644 --- a/packaging/docker/env +++ b/packaging/docker/env @@ -37,8 +37,8 @@ # BEE_CACHE_CAPACITY=1000000 ## enable forwarded content caching (default true) # BEE_CACHE_RETRIEVAL=true -## enable chequebook; requires swap; implied by BEE_NODE_MODE=full (default false) -# BEE_CHEQUEBOOK_ENABLE=false +## enable chequebook (default true) +# BEE_CHEQUEBOOK_ENABLE=true ## reject full-node hive/handshake records that carry no chequebook address # BEE_CHEQUEBOOK_VERIFICATION=false ## origins with CORS headers enabled (default []) @@ -117,8 +117,8 @@ # BEE_STATIC_NODES=[] ## lru memory caching capacity in number of statestore entries (default 100000) # BEE_STATESTORE_CACHE_CAPACITY=100000 -## enable storage incentives feature; full node only; implied by BEE_NODE_MODE=full (default false) -# BEE_STORAGE_INCENTIVES_ENABLE=false +## enable storage incentives feature (default true) +# BEE_STORAGE_INCENTIVES_ENABLE=true ## enable swap (default false) # BEE_SWAP_ENABLE=false ## swap factory addresses (default empty) diff --git a/packaging/homebrew-amd64/bee.yaml b/packaging/homebrew-amd64/bee.yaml index 315b58689bd..6700a9b9767 100644 --- a/packaging/homebrew-amd64/bee.yaml +++ b/packaging/homebrew-amd64/bee.yaml @@ -27,7 +27,7 @@ ## enable swap # swap-enable: false ## enable chequebook (requires swap-enable; implied by node-mode: full) -# chequebook-enable: false +# chequebook-enable: true ## reject full-node hive/handshake records that carry no chequebook address # chequebook-verification: false ## swap factory addresses @@ -37,7 +37,7 @@ ## ── Full node only ──────────────────────────────────────────────────────────── ## enable storage incentives feature (implied by node-mode: full) -# storage-incentives-enable: false +# storage-incentives-enable: true ## reserve capacity doubling # reserve-capacity-doubling: 0 ## minimum radius storage threshold diff --git a/packaging/homebrew-arm64/bee.yaml b/packaging/homebrew-arm64/bee.yaml index 6da2d4e6ef4..ddc431faa14 100644 --- a/packaging/homebrew-arm64/bee.yaml +++ b/packaging/homebrew-arm64/bee.yaml @@ -27,7 +27,7 @@ ## enable swap # swap-enable: false ## enable chequebook (requires swap-enable; implied by node-mode: full) -# chequebook-enable: false +# chequebook-enable: true ## reject full-node hive/handshake records that carry no chequebook address # chequebook-verification: false ## swap factory addresses @@ -37,7 +37,7 @@ ## ── Full node only ──────────────────────────────────────────────────────────── ## enable storage incentives feature (implied by node-mode: full) -# storage-incentives-enable: false +# storage-incentives-enable: true ## reserve capacity doubling # reserve-capacity-doubling: 0 ## minimum radius storage threshold diff --git a/packaging/scoop/bee.yaml b/packaging/scoop/bee.yaml index 52eca12ada6..90e4a2ac989 100644 --- a/packaging/scoop/bee.yaml +++ b/packaging/scoop/bee.yaml @@ -27,7 +27,7 @@ ## enable swap # swap-enable: false ## enable chequebook (requires swap-enable; implied by node-mode: full) -# chequebook-enable: false +# chequebook-enable: true ## reject full-node hive/handshake records that carry no chequebook address # chequebook-verification: false ## swap factory addresses @@ -37,7 +37,7 @@ ## ── Full node only ──────────────────────────────────────────────────────────── ## enable storage incentives feature (implied by node-mode: full) -# storage-incentives-enable: false +# storage-incentives-enable: true ## reserve capacity doubling # reserve-capacity-doubling: 0 ## minimum radius storage threshold From 909ac03f97b12fceda34c3e20f4b0b1f4d48ef6f Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Thu, 15 Oct 2026 20:02:53 +0300 Subject: [PATCH 12/22] test(cmd): move node-mode resolution tests to cmd_test Use the external test package, as the other tests in cmd/bee/cmd do, and expose the option names and resolver through export_test.go. --- cmd/bee/cmd/export_test.go | 33 ++++ cmd/bee/cmd/resolve_node_mode_test.go | 275 +++++++++++++------------- 2 files changed, 166 insertions(+), 142 deletions(-) diff --git a/cmd/bee/cmd/export_test.go b/cmd/bee/cmd/export_test.go index 917851c925b..3ec3029459c 100644 --- a/cmd/bee/cmd/export_test.go +++ b/cmd/bee/cmd/export_test.go @@ -7,7 +7,10 @@ package cmd import ( "io" + "github.com/ethersphere/bee/v2/pkg/log" + "github.com/ethersphere/bee/v2/pkg/node" "github.com/spf13/cobra" + "github.com/spf13/viper" ) type ( @@ -89,3 +92,33 @@ func WithPasswordReader(r PasswordReader) func(c *Command) { func (c *Command) SetCfgFileForTest(f string) { c.cfgFile = f } + +// Config keys read by node-mode resolution. +const ( + OptionNameNodeMode = optionNameNodeMode + OptionNameFullNode = optionNameFullNode + OptionNameBootnodeMode = optionNameBootnodeMode + OptionNameSwapEnable = optionNameSwapEnable + OptionNameChequebookEnable = optionNameChequebookEnable + OptionNameStorageIncentivesEnable = optionNameStorageIncentivesEnable + ConfigKeyBlockchainRpcEndpoint = configKeyBlockchainRpcEndpoint +) + +// ResolveNodeModeForTest runs node-mode resolution against cfg, leaving the +// resolved option values in cfg. +func ResolveNodeModeForTest(cfg *viper.Viper) (node.NodeMode, error) { + c := &command{config: cfg, logger: log.Noop} + return c.resolveNodeMode(c.logger) +} + +// BindStartFlagsForTest mirrors the start command's PreRunE: it binds cfg to +// the command flags, then maps the flat blockchain-rpc-* flags onto their +// nested config keys. +func BindStartFlagsForTest(cfg *viper.Viper, startCmd *cobra.Command) error { + if err := cfg.BindPFlags(startCmd.Flags()); err != nil { + return err + } + c := &command{config: cfg} + c.bindBlockchainRpcConfig(startCmd) + return nil +} diff --git a/cmd/bee/cmd/resolve_node_mode_test.go b/cmd/bee/cmd/resolve_node_mode_test.go index c9c9e5c6fdb..ee1002539bf 100644 --- a/cmd/bee/cmd/resolve_node_mode_test.go +++ b/cmd/bee/cmd/resolve_node_mode_test.go @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package cmd +package cmd_test import ( "strings" "testing" - "github.com/ethersphere/bee/v2/pkg/log" + "github.com/ethersphere/bee/v2/cmd/bee/cmd" "github.com/ethersphere/bee/v2/pkg/node" "github.com/spf13/viper" ) @@ -31,31 +31,31 @@ func TestResolveNodeMode(t *testing.T) { { name: "full with rpc only implies swap, chequebook and incentives", config: map[string]any{ - optionNameNodeMode: "full", - configKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameNodeMode: "full", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, }, wantMode: node.FullMode, wantOptions: map[string]bool{ - optionNameSwapEnable: true, - optionNameChequebookEnable: true, - optionNameStorageIncentivesEnable: true, + cmd.OptionNameSwapEnable: true, + cmd.OptionNameChequebookEnable: true, + cmd.OptionNameStorageIncentivesEnable: true, }, }, { name: "full with all options explicitly enabled succeeds", config: map[string]any{ - optionNameNodeMode: "full", - configKeyBlockchainRpcEndpoint: testRPCEndpoint, - optionNameSwapEnable: true, - optionNameChequebookEnable: true, - optionNameStorageIncentivesEnable: true, + cmd.OptionNameNodeMode: "full", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameSwapEnable: true, + cmd.OptionNameChequebookEnable: true, + cmd.OptionNameStorageIncentivesEnable: true, }, wantMode: node.FullMode, }, { name: "full without rpc fails", config: map[string]any{ - optionNameNodeMode: "full", + cmd.OptionNameNodeMode: "full", }, wantErr: "full node requires blockchain-rpc-endpoint", }, @@ -63,15 +63,15 @@ func TestResolveNodeMode(t *testing.T) { // A non-staking full node is a legitimate opt-out. name: "full with storage-incentives explicitly false honours the opt-out", config: map[string]any{ - optionNameNodeMode: "full", - configKeyBlockchainRpcEndpoint: testRPCEndpoint, - optionNameStorageIncentivesEnable: false, + cmd.OptionNameNodeMode: "full", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameStorageIncentivesEnable: false, }, wantMode: node.FullMode, wantOptions: map[string]bool{ - optionNameSwapEnable: true, - optionNameChequebookEnable: true, - optionNameStorageIncentivesEnable: false, + cmd.OptionNameSwapEnable: true, + cmd.OptionNameChequebookEnable: true, + cmd.OptionNameStorageIncentivesEnable: false, }, }, { @@ -79,38 +79,38 @@ func TestResolveNodeMode(t *testing.T) { // contradiction the operator never wrote. name: "full with swap explicitly false leaves chequebook off", config: map[string]any{ - optionNameNodeMode: "full", - configKeyBlockchainRpcEndpoint: testRPCEndpoint, - optionNameSwapEnable: false, + cmd.OptionNameNodeMode: "full", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameSwapEnable: false, }, wantMode: node.FullMode, wantOptions: map[string]bool{ - optionNameSwapEnable: false, - optionNameChequebookEnable: false, - optionNameStorageIncentivesEnable: true, + cmd.OptionNameSwapEnable: false, + cmd.OptionNameChequebookEnable: false, + cmd.OptionNameStorageIncentivesEnable: true, }, }, { // Receive-only swap: cash out cheques without issuing them. name: "full with chequebook explicitly false keeps swap on", config: map[string]any{ - optionNameNodeMode: "full", - configKeyBlockchainRpcEndpoint: testRPCEndpoint, - optionNameChequebookEnable: false, + cmd.OptionNameNodeMode: "full", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameChequebookEnable: false, }, wantMode: node.FullMode, wantOptions: map[string]bool{ - optionNameSwapEnable: true, - optionNameChequebookEnable: false, + cmd.OptionNameSwapEnable: true, + cmd.OptionNameChequebookEnable: false, }, }, { name: "full with chequebook explicitly true and swap explicitly false fails", config: map[string]any{ - optionNameNodeMode: "full", - configKeyBlockchainRpcEndpoint: testRPCEndpoint, - optionNameSwapEnable: false, - optionNameChequebookEnable: true, + cmd.OptionNameNodeMode: "full", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameSwapEnable: false, + cmd.OptionNameChequebookEnable: true, }, wantErr: "chequebook-enable requires swap-enable", }, @@ -119,133 +119,133 @@ func TestResolveNodeMode(t *testing.T) { // bootnode, so full mode must not imply them there. name: "full bootnode does not imply swap, chequebook or incentives", config: map[string]any{ - optionNameNodeMode: "full", - optionNameBootnodeMode: true, - configKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameNodeMode: "full", + cmd.OptionNameBootnodeMode: true, + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, }, wantMode: node.FullMode, wantOptions: map[string]bool{ - optionNameSwapEnable: false, - optionNameChequebookEnable: false, - optionNameStorageIncentivesEnable: false, + cmd.OptionNameSwapEnable: false, + cmd.OptionNameChequebookEnable: false, + cmd.OptionNameStorageIncentivesEnable: false, }, }, { name: "full bootnode with storage-incentives explicitly false succeeds", config: map[string]any{ - optionNameNodeMode: "full", - optionNameBootnodeMode: true, - configKeyBlockchainRpcEndpoint: testRPCEndpoint, - optionNameStorageIncentivesEnable: false, + cmd.OptionNameNodeMode: "full", + cmd.OptionNameBootnodeMode: true, + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameStorageIncentivesEnable: false, }, wantMode: node.FullMode, }, { name: "light with rpc succeeds without swap", config: map[string]any{ - optionNameNodeMode: "light", - configKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameNodeMode: "light", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, }, wantMode: node.LightMode, wantOptions: map[string]bool{ - optionNameSwapEnable: false, - optionNameChequebookEnable: false, - optionNameStorageIncentivesEnable: false, + cmd.OptionNameSwapEnable: false, + cmd.OptionNameChequebookEnable: false, + cmd.OptionNameStorageIncentivesEnable: false, }, }, { name: "light with rpc, swap and chequebook succeeds", config: map[string]any{ - optionNameNodeMode: "light", - configKeyBlockchainRpcEndpoint: testRPCEndpoint, - optionNameSwapEnable: true, - optionNameChequebookEnable: true, + cmd.OptionNameNodeMode: "light", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameSwapEnable: true, + cmd.OptionNameChequebookEnable: true, }, wantMode: node.LightMode, }, { name: "light without rpc fails", config: map[string]any{ - optionNameNodeMode: "light", + cmd.OptionNameNodeMode: "light", }, wantErr: "light node requires blockchain-rpc-endpoint", }, { name: "light with chequebook but no swap fails", config: map[string]any{ - optionNameNodeMode: "light", - configKeyBlockchainRpcEndpoint: testRPCEndpoint, - optionNameChequebookEnable: true, + cmd.OptionNameNodeMode: "light", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameChequebookEnable: true, }, wantErr: "chequebook-enable requires swap-enable", }, { name: "light rejects storage-incentives-enable", config: map[string]any{ - optionNameNodeMode: "light", - configKeyBlockchainRpcEndpoint: testRPCEndpoint, - optionNameStorageIncentivesEnable: true, + cmd.OptionNameNodeMode: "light", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameStorageIncentivesEnable: true, }, wantErr: "light node cannot have storage-incentives-enable", }, { name: "ultra-light succeeds", config: map[string]any{ - optionNameNodeMode: "ultra-light", + cmd.OptionNameNodeMode: "ultra-light", }, wantMode: node.UltraLightMode, }, { name: "ultra-light with rpc ignores rpc and succeeds", config: map[string]any{ - optionNameNodeMode: "ultra-light", - configKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameNodeMode: "ultra-light", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, }, wantMode: node.UltraLightMode, }, { name: "ultra-light rejects swap-enable", config: map[string]any{ - optionNameNodeMode: "ultra-light", - optionNameSwapEnable: true, + cmd.OptionNameNodeMode: "ultra-light", + cmd.OptionNameSwapEnable: true, }, wantErr: "ultra-light node cannot have swap-enable", }, { name: "ultra-light rejects storage-incentives-enable", config: map[string]any{ - optionNameNodeMode: "ultra-light", - optionNameStorageIncentivesEnable: true, + cmd.OptionNameNodeMode: "ultra-light", + cmd.OptionNameStorageIncentivesEnable: true, }, wantErr: "ultra-light node cannot have storage-incentives-enable", }, { name: "node-mode takes precedence over legacy full-node", config: map[string]any{ - optionNameNodeMode: "light", - optionNameFullNode: true, - configKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameNodeMode: "light", + cmd.OptionNameFullNode: true, + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, }, wantMode: node.LightMode, }, { name: "invalid node-mode value fails", config: map[string]any{ - optionNameNodeMode: "superlight", + cmd.OptionNameNodeMode: "superlight", }, wantErr: "invalid node-mode", }, { name: "uppercase node-mode fails", config: map[string]any{ - optionNameNodeMode: "FULL", + cmd.OptionNameNodeMode: "FULL", }, wantErr: "invalid node-mode", }, { name: "whitespace node-mode fails", config: map[string]any{ - optionNameNodeMode: " full ", + cmd.OptionNameNodeMode: " full ", }, wantErr: "invalid node-mode", }, @@ -256,71 +256,71 @@ func TestResolveNodeMode(t *testing.T) { // to default to true, so this node issued cheques; it must keep doing so. name: "legacy light with swap only restores chequebook default", config: map[string]any{ - configKeyBlockchainRpcEndpoint: testRPCEndpoint, - optionNameSwapEnable: true, + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameSwapEnable: true, }, wantMode: node.LightMode, wantOptions: map[string]bool{ - optionNameSwapEnable: true, - optionNameChequebookEnable: true, - optionNameStorageIncentivesEnable: true, + cmd.OptionNameSwapEnable: true, + cmd.OptionNameChequebookEnable: true, + cmd.OptionNameStorageIncentivesEnable: true, }, }, { // The old shipped default; chequebook stays gated on swap in NewBee. name: "legacy chequebook without swap starts", config: map[string]any{ - optionNameChequebookEnable: true, + cmd.OptionNameChequebookEnable: true, }, wantMode: node.UltraLightMode, }, { name: "legacy full-node with rpc only restores old defaults and leaves swap off", config: map[string]any{ - optionNameFullNode: true, - configKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameFullNode: true, + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, }, wantMode: node.FullMode, wantOptions: map[string]bool{ - optionNameSwapEnable: false, - optionNameChequebookEnable: true, - optionNameStorageIncentivesEnable: true, + cmd.OptionNameSwapEnable: false, + cmd.OptionNameChequebookEnable: true, + cmd.OptionNameStorageIncentivesEnable: true, }, }, { name: "legacy full-node with all options set maps to full", config: map[string]any{ - optionNameFullNode: true, - configKeyBlockchainRpcEndpoint: testRPCEndpoint, - optionNameSwapEnable: true, - optionNameChequebookEnable: true, - optionNameStorageIncentivesEnable: true, + cmd.OptionNameFullNode: true, + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameSwapEnable: true, + cmd.OptionNameChequebookEnable: true, + cmd.OptionNameStorageIncentivesEnable: true, }, wantMode: node.FullMode, }, { name: "legacy full-node with explicit opt-outs is not validated", config: map[string]any{ - optionNameFullNode: true, - configKeyBlockchainRpcEndpoint: testRPCEndpoint, - optionNameSwapEnable: false, - optionNameChequebookEnable: false, - optionNameStorageIncentivesEnable: false, + cmd.OptionNameFullNode: true, + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameSwapEnable: false, + cmd.OptionNameChequebookEnable: false, + cmd.OptionNameStorageIncentivesEnable: false, }, wantMode: node.FullMode, wantOptions: map[string]bool{ - optionNameSwapEnable: false, - optionNameChequebookEnable: false, - optionNameStorageIncentivesEnable: false, + cmd.OptionNameSwapEnable: false, + cmd.OptionNameChequebookEnable: false, + cmd.OptionNameStorageIncentivesEnable: false, }, }, { name: "legacy bootnode with storage-incentives false starts", config: map[string]any{ - optionNameFullNode: true, - optionNameBootnodeMode: true, - configKeyBlockchainRpcEndpoint: testRPCEndpoint, - optionNameStorageIncentivesEnable: false, + cmd.OptionNameFullNode: true, + cmd.OptionNameBootnodeMode: true, + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameStorageIncentivesEnable: false, }, wantMode: node.FullMode, }, @@ -329,14 +329,14 @@ func TestResolveNodeMode(t *testing.T) { // failed at chain init without an endpoint; keep failing, earlier. name: "legacy full-node without rpc fails", config: map[string]any{ - optionNameFullNode: true, + cmd.OptionNameFullNode: true, }, wantErr: "full node requires blockchain-rpc-endpoint", }, { name: "legacy with rpc infers light", config: map[string]any{ - configKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, }, wantMode: node.LightMode, }, @@ -350,7 +350,7 @@ func TestResolveNodeMode(t *testing.T) { // base profile on a node without rpc. Legacy must not validate it. name: "legacy without rpc but with swap infers ultra-light without error", config: map[string]any{ - optionNameSwapEnable: true, + cmd.OptionNameSwapEnable: true, }, wantMode: node.UltraLightMode, }, @@ -358,15 +358,15 @@ func TestResolveNodeMode(t *testing.T) { // Legacy must not restore a default the operator overrode. name: "legacy explicit false is preserved", config: map[string]any{ - configKeyBlockchainRpcEndpoint: testRPCEndpoint, - optionNameSwapEnable: true, - optionNameChequebookEnable: false, - optionNameStorageIncentivesEnable: false, + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameSwapEnable: true, + cmd.OptionNameChequebookEnable: false, + cmd.OptionNameStorageIncentivesEnable: false, }, wantMode: node.LightMode, wantOptions: map[string]bool{ - optionNameChequebookEnable: false, - optionNameStorageIncentivesEnable: false, + cmd.OptionNameChequebookEnable: false, + cmd.OptionNameStorageIncentivesEnable: false, }, }, } @@ -375,15 +375,12 @@ func TestResolveNodeMode(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - c := &command{ - config: viper.New(), - logger: log.Noop, - } + cfg := viper.New() for k, v := range tt.config { - c.config.Set(k, v) + cfg.Set(k, v) } - gotMode, err := c.resolveNodeMode(c.logger) + gotMode, err := cmd.ResolveNodeModeForTest(cfg) if tt.wantErr != "" { if err == nil { @@ -401,7 +398,7 @@ func TestResolveNodeMode(t *testing.T) { t.Errorf("got mode %q, want %q", gotMode, tt.wantMode) } for key, want := range tt.wantOptions { - if got := c.config.GetBool(key); got != want { + if got := cfg.GetBool(key); got != want { t.Errorf("option %q: got %t, want %t", key, got, want) } } @@ -427,8 +424,8 @@ func TestResolveNodeModeWithBoundFlags(t *testing.T) { args: nil, wantMode: node.UltraLightMode, wantOptions: map[string]bool{ - optionNameChequebookEnable: true, - optionNameStorageIncentivesEnable: true, + cmd.OptionNameChequebookEnable: true, + cmd.OptionNameStorageIncentivesEnable: true, }, }, { @@ -436,9 +433,9 @@ func TestResolveNodeModeWithBoundFlags(t *testing.T) { args: []string{"--full-node", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, wantMode: node.FullMode, wantOptions: map[string]bool{ - optionNameSwapEnable: false, - optionNameChequebookEnable: true, - optionNameStorageIncentivesEnable: true, + cmd.OptionNameSwapEnable: false, + cmd.OptionNameChequebookEnable: true, + cmd.OptionNameStorageIncentivesEnable: true, }, }, { @@ -446,9 +443,9 @@ func TestResolveNodeModeWithBoundFlags(t *testing.T) { args: []string{"--node-mode=full", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, wantMode: node.FullMode, wantOptions: map[string]bool{ - optionNameSwapEnable: true, - optionNameChequebookEnable: true, - optionNameStorageIncentivesEnable: true, + cmd.OptionNameSwapEnable: true, + cmd.OptionNameChequebookEnable: true, + cmd.OptionNameStorageIncentivesEnable: true, }, }, { @@ -456,9 +453,9 @@ func TestResolveNodeModeWithBoundFlags(t *testing.T) { args: []string{"--node-mode=light", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, wantMode: node.LightMode, wantOptions: map[string]bool{ - optionNameSwapEnable: false, - optionNameChequebookEnable: false, - optionNameStorageIncentivesEnable: false, + cmd.OptionNameSwapEnable: false, + cmd.OptionNameChequebookEnable: false, + cmd.OptionNameStorageIncentivesEnable: false, }, }, { @@ -466,9 +463,9 @@ func TestResolveNodeModeWithBoundFlags(t *testing.T) { args: []string{"--node-mode=full", "--blockchain-rpc-endpoint=" + testRPCEndpoint, "--storage-incentives-enable=false"}, wantMode: node.FullMode, wantOptions: map[string]bool{ - optionNameSwapEnable: true, - optionNameChequebookEnable: true, - optionNameStorageIncentivesEnable: false, + cmd.OptionNameSwapEnable: true, + cmd.OptionNameChequebookEnable: true, + cmd.OptionNameStorageIncentivesEnable: false, }, }, } @@ -477,7 +474,7 @@ func TestResolveNodeModeWithBoundFlags(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - root, err := newCommand(func(c *command) { c.homeDir = t.TempDir() }) + root, err := cmd.NewCommand(cmd.WithHomeDir(t.TempDir())) if err != nil { t.Fatal(err) } @@ -489,18 +486,12 @@ func TestResolveNodeModeWithBoundFlags(t *testing.T) { t.Fatal(err) } - // Mirror the start command's PreRunE: bind flags, then map the flat - // blockchain-rpc-* flags onto their nested config keys. - c := &command{ - config: viper.New(), - logger: log.Noop, - } - if err := c.config.BindPFlags(startCmd.Flags()); err != nil { + cfg := viper.New() + if err := cmd.BindStartFlagsForTest(cfg, startCmd); err != nil { t.Fatal(err) } - c.bindBlockchainRpcConfig(startCmd) - gotMode, err := c.resolveNodeMode(c.logger) + gotMode, err := cmd.ResolveNodeModeForTest(cfg) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -508,7 +499,7 @@ func TestResolveNodeModeWithBoundFlags(t *testing.T) { t.Errorf("got mode %q, want %q", gotMode, tt.wantMode) } for key, want := range tt.wantOptions { - if got := c.config.GetBool(key); got != want { + if got := cfg.GetBool(key); got != want { t.Errorf("option %q: got %t, want %t", key, got, want) } } From 2b8338d1d9195f822bf709ad5b9b42a808111d7c Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Thu, 15 Oct 2026 20:02:56 +0300 Subject: [PATCH 13/22] fix(cmd): disable unset chequebook and incentives on full bootnodes A full bootnode does not get swap implied, but chequebook-enable kept its flag default of true, so every full bootnode in the node-mode regime was rejected with "chequebook-enable requires swap-enable to be true". Set chequebook-enable and storage-incentives-enable to false when unset, as the light and ultra-light modes do, and cover the case with bound flags. --- cmd/bee/cmd/resolve_node_mode_test.go | 10 ++++++++++ cmd/bee/cmd/start.go | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/cmd/bee/cmd/resolve_node_mode_test.go b/cmd/bee/cmd/resolve_node_mode_test.go index ee1002539bf..c00a875aeca 100644 --- a/cmd/bee/cmd/resolve_node_mode_test.go +++ b/cmd/bee/cmd/resolve_node_mode_test.go @@ -458,6 +458,16 @@ func TestResolveNodeModeWithBoundFlags(t *testing.T) { cmd.OptionNameStorageIncentivesEnable: false, }, }, + { + name: "node-mode full bootnode does not inherit chequebook or incentives defaults", + args: []string{"--node-mode=full", "--bootnode-mode", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + wantMode: node.FullMode, + wantOptions: map[string]bool{ + cmd.OptionNameSwapEnable: false, + cmd.OptionNameChequebookEnable: false, + cmd.OptionNameStorageIncentivesEnable: false, + }, + }, { name: "node-mode full with incentives explicitly disabled", args: []string{"--node-mode=full", "--blockchain-rpc-endpoint=" + testRPCEndpoint, "--storage-incentives-enable=false"}, diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index c3be994328b..68262c5d8d9 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -431,6 +431,15 @@ func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) { c.enableImpliedOption(logger, mode, optionNameChequebookEnable) } c.enableImpliedOption(logger, mode, optionNameStorageIncentivesEnable) + } else { + // Override the pflag defaults of true when unset, since swap is not + // implied for bootnodes and chequebook requires it. + if !c.config.IsSet(optionNameChequebookEnable) { + c.config.Set(optionNameChequebookEnable, false) + } + if !c.config.IsSet(optionNameStorageIncentivesEnable) { + c.config.Set(optionNameStorageIncentivesEnable, false) + } } case node.LightMode: if rpcEndpoint == "" { From 3ef5392dacc89081efc024fb59ac27278a01027f Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Thu, 15 Oct 2026 20:03:01 +0300 Subject: [PATCH 14/22] fix(cmd): exit with an error on invalid node-mode configuration Resolve the node mode in the start command before the node is built in the background. A rejected configuration is now returned from RunE, so bee prints the error and exits with a non-zero status instead of logging it and exiting 0, and it is rejected before the keystore is unlocked. --- cmd/bee/cmd/start.go | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index 68262c5d8d9..d7a38309c62 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -62,6 +62,17 @@ func (c *command) initStartCmd() (err error) { fmt.Print(beeWelcomeMessage) logger.Info("bee version", "version", bee.Version) + // Resolve the node mode before the node is built, so that an invalid + // configuration exits with a non-zero status, and before the keystore + // is unlocked. + nodeMode, err := c.resolveNodeMode(logger) + if err != nil { + return err + } + if c.config.GetBool(optionNameBootnodeMode) && nodeMode != node.FullMode { + return errors.New("boot node must be started as a full node") + } + // ctx is global context of bee node; which is canceled after interrupt signal is received. ctx, cancel := context.WithCancel(context.Background()) sysInterruptChannel := make(chan os.Signal, 1) @@ -79,7 +90,7 @@ func (c *command) initStartCmd() (err error) { // Building bee node can take up some time (because node.NewBee(...) is compute have function ) // Because of this we need to do it in background so that program could be terminated when interrupt signal is received // while bee node is being constructed. - respC := buildBeeNodeAsync(ctx, c, cmd, logger) + respC := buildBeeNodeAsync(ctx, c, cmd, logger, nodeMode) var beeNode atomic.Value p := &program{ @@ -180,18 +191,18 @@ type buildBeeNodeResp struct { err error } -func buildBeeNodeAsync(ctx context.Context, c *command, cmd *cobra.Command, logger log.Logger) <-chan buildBeeNodeResp { +func buildBeeNodeAsync(ctx context.Context, c *command, cmd *cobra.Command, logger log.Logger, nodeMode node.NodeMode) <-chan buildBeeNodeResp { respC := make(chan buildBeeNodeResp, 1) go func() { - bee, err := buildBeeNode(ctx, c, cmd, logger) + bee, err := buildBeeNode(ctx, c, cmd, logger, nodeMode) respC <- buildBeeNodeResp{bee, err} }() return respC } -func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger log.Logger) (*node.Bee, error) { +func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger log.Logger, nodeMode node.NodeMode) (*node.Bee, error) { var err error // If the resolver is specified, resolve all connection strings @@ -212,15 +223,6 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo bootNode := c.config.GetBool(optionNameBootnodeMode) - 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") - } - mainnet := c.config.GetBool(optionNameMainNet) userHasSetNetworkID := c.config.IsSet(optionNameNetworkID) From bacbce67b120f117043e409341f77b928bb6d8d4 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Thu, 15 Oct 2026 20:03:03 +0300 Subject: [PATCH 15/22] fix(node): enable the chain backend only for full and light modes An unset or unknown NodeMode enabled the chain backend while the API reported the node as ultra-light. --- pkg/node/export_test.go | 1 + pkg/node/node.go | 2 +- pkg/node/node_test.go | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pkg/node/export_test.go b/pkg/node/export_test.go index b67e259a588..8b459b3bd1e 100644 --- a/pkg/node/export_test.go +++ b/pkg/node/export_test.go @@ -9,6 +9,7 @@ import "io" var ( ValidatePublicAddress = validatePublicAddress UseEmbeddedSnapshot = useEmbeddedSnapshot + IsChainEnabled = isChainEnabled ) // NewTestBeeWithClosers builds a Bee with only the push-sync and retrieval diff --git a/pkg/node/node.go b/pkg/node/node.go index 0a1d92adb17..0c02fef774d 100644 --- a/pkg/node/node.go +++ b/pkg/node/node.go @@ -1624,7 +1624,7 @@ func (b *Bee) Shutdown() error { var ErrShutdownInProgress = errors.New("shutdown in progress") func isChainEnabled(o *Options, logger log.Logger) bool { - if o.NodeMode == UltraLightMode { + if o.NodeMode != FullMode && o.NodeMode != LightMode { logger.Info("chain backend disabled - starting in ultra-light mode") return false } diff --git a/pkg/node/node_test.go b/pkg/node/node_test.go index 5b2d46bd75f..92e11c83b74 100644 --- a/pkg/node/node_test.go +++ b/pkg/node/node_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/ethersphere/bee/v2/pkg/api" + "github.com/ethersphere/bee/v2/pkg/log" "github.com/ethersphere/bee/v2/pkg/node" ) @@ -183,3 +184,22 @@ func TestShutdownRegistersPushSyncAndRetrieval(t *testing.T) { } } } + +func TestIsChainEnabled(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + mode node.NodeMode + want bool + }{ + {mode: node.FullMode, want: true}, + {mode: node.LightMode, want: true}, + {mode: node.UltraLightMode, want: false}, + {mode: "", want: false}, + {mode: "unknown", want: false}, + } { + if got := node.IsChainEnabled(&node.Options{NodeMode: tc.mode}, log.Noop); got != tc.want { + t.Errorf("node mode %q: got %t, want %t", tc.mode, got, tc.want) + } + } +} From 32f97e44c5bac84d06b2cbb016762606cb7ca2eb Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Thu, 15 Oct 2026 20:03:03 +0300 Subject: [PATCH 16/22] chore(lint): remove unused staticcheck exclusion for cmd/bee/cmd The exclusion matches nothing; golangci-lint reports no issues without it. --- .golangci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index ae7330aaace..ad34693a925 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -73,10 +73,6 @@ linters: - linters: - staticcheck text: "SA5008: malformed `json` tag: invalid trailing ',' character" - - linters: - - staticcheck - path: cmd/bee/cmd - text: "(this comparison is always true|never returns a nil interface value|the lhs of the comparison)" paths: - third_party$ - builtin$ From e289cf5b1ca3d92166f9c356a8c930c925360db0 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Thu, 15 Oct 2026 20:03:03 +0300 Subject: [PATCH 17/22] docs(packaging): show node-mode as unset by default The examples showed ultra-light, but the default is unset, which infers the mode from the legacy options. --- packaging/bee.yaml | 6 +++--- packaging/docker/env | 4 ++-- packaging/homebrew-amd64/bee.yaml | 6 +++--- packaging/homebrew-arm64/bee.yaml | 6 +++--- packaging/scoop/bee.yaml | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packaging/bee.yaml b/packaging/bee.yaml index 6bd678eb9bd..672a2742475 100644 --- a/packaging/bee.yaml +++ b/packaging/bee.yaml @@ -6,9 +6,9 @@ ## implies swap-enable, chequebook-enable and storage-incentives-enable ## light - uploads and downloads only; requires blockchain-rpc ## ultra-light - free-tier downloads only; no blockchain connection needed -## When unset, the mode is inferred from the deprecated full-node option and -## blockchain-rpc presence (deprecated, removed in v2.11.0). -# node-mode: ultra-light +## When unset (the default), the mode is inferred from the deprecated full-node +## option and blockchain-rpc presence (deprecated, removed in v2.11.0). +# node-mode: "" ## ── Blockchain / RPC (required for full and light nodes) ───────────────────── ## blockchain rpc configuration diff --git a/packaging/docker/env b/packaging/docker/env index 5a5eabcc1bd..3f229912138 100644 --- a/packaging/docker/env +++ b/packaging/docker/env @@ -71,8 +71,8 @@ # BEE_NEIGHBORHOOD_SUGGESTER=https://api.swarmscan.io/v1/network/neighborhoods/suggestion ## ID of the Swarm network (default mainnet id from bee) # BEE_NETWORK_ID=1 -## node operational mode: full, light, or ultra-light; unset infers the mode from BEE_FULL_NODE and BEE_BLOCKCHAIN_RPC_ENDPOINT (deprecated) -# BEE_NODE_MODE=ultra-light +## node operational mode: full, light, or ultra-light; unset infers the mode from BEE_FULL_NODE and BEE_BLOCKCHAIN_RPC_ENDPOINT (deprecated) (default unset) +# BEE_NODE_MODE= ## P2P listen address (default :1634) # BEE_P2P_ADDR=:1634 ## enable P2P WebSocket transport (default false) diff --git a/packaging/homebrew-amd64/bee.yaml b/packaging/homebrew-amd64/bee.yaml index 955098528fa..3152cbea001 100644 --- a/packaging/homebrew-amd64/bee.yaml +++ b/packaging/homebrew-amd64/bee.yaml @@ -6,9 +6,9 @@ ## implies swap-enable, chequebook-enable and storage-incentives-enable ## light - uploads and downloads only; requires blockchain-rpc ## ultra-light - free-tier downloads only; no blockchain connection needed -## When unset, the mode is inferred from the deprecated full-node option and -## blockchain-rpc presence (deprecated, removed in v2.11.0). -# node-mode: ultra-light +## When unset (the default), the mode is inferred from the deprecated full-node +## option and blockchain-rpc presence (deprecated, removed in v2.11.0). +# node-mode: "" ## ── Blockchain / RPC (required for full and light nodes) ───────────────────── ## blockchain rpc configuration diff --git a/packaging/homebrew-arm64/bee.yaml b/packaging/homebrew-arm64/bee.yaml index 479e9f54a78..ada9b5e3bd1 100644 --- a/packaging/homebrew-arm64/bee.yaml +++ b/packaging/homebrew-arm64/bee.yaml @@ -6,9 +6,9 @@ ## implies swap-enable, chequebook-enable and storage-incentives-enable ## light - uploads and downloads only; requires blockchain-rpc ## ultra-light - free-tier downloads only; no blockchain connection needed -## When unset, the mode is inferred from the deprecated full-node option and -## blockchain-rpc presence (deprecated, removed in v2.11.0). -# node-mode: ultra-light +## When unset (the default), the mode is inferred from the deprecated full-node +## option and blockchain-rpc presence (deprecated, removed in v2.11.0). +# node-mode: "" ## ── Blockchain / RPC (required for full and light nodes) ───────────────────── ## blockchain rpc configuration diff --git a/packaging/scoop/bee.yaml b/packaging/scoop/bee.yaml index 556ec1b7aa7..cbcb6588e8e 100644 --- a/packaging/scoop/bee.yaml +++ b/packaging/scoop/bee.yaml @@ -6,9 +6,9 @@ ## implies swap-enable, chequebook-enable and storage-incentives-enable ## light - uploads and downloads only; requires blockchain-rpc ## ultra-light - free-tier downloads only; no blockchain connection needed -## When unset, the mode is inferred from the deprecated full-node option and -## blockchain-rpc presence (deprecated, removed in v2.11.0). -# node-mode: ultra-light +## When unset (the default), the mode is inferred from the deprecated full-node +## option and blockchain-rpc presence (deprecated, removed in v2.11.0). +# node-mode: "" ## ── Blockchain / RPC (required for full and light nodes) ───────────────────── ## blockchain rpc configuration From 610f34b36d05c774e82bcf69115d44ef0f1a460d Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Thu, 15 Oct 2026 20:27:01 +0300 Subject: [PATCH 18/22] fix(cmd): let unset chequebook-enable follow swap-enable with node-mode 2b8338d1 forced chequebook-enable to false on every full bootnode where it was unset, so a bootnode with swap explicitly enabled ran swap without a chequebook. Replace the per-mode overrides with one rule after the mode switch: when chequebook-enable is not set explicitly it follows swap-enable. Outcomes through the bound start flags are otherwise unchanged, and bare-config resolution now agrees with them. Ultra-light now rejects an explicit chequebook-enable with a mode-specific error instead of pointing at swap-enable, and warns when a blockchain-rpc-endpoint is configured but ignored. Correct the bootnode comments, which claimed NewBee never starts swap there, and document the exemption in the packaged configs. --- cmd/bee/cmd/export_test.go | 8 +- cmd/bee/cmd/resolve_node_mode_test.go | 147 ++++++++++++++++++++++---- cmd/bee/cmd/start.go | 51 ++++----- packaging/bee.yaml | 3 +- packaging/homebrew-amd64/bee.yaml | 3 +- packaging/homebrew-arm64/bee.yaml | 3 +- packaging/scoop/bee.yaml | 3 +- 7 files changed, 165 insertions(+), 53 deletions(-) diff --git a/cmd/bee/cmd/export_test.go b/cmd/bee/cmd/export_test.go index 3ec3029459c..eee92478037 100644 --- a/cmd/bee/cmd/export_test.go +++ b/cmd/bee/cmd/export_test.go @@ -107,7 +107,13 @@ const ( // ResolveNodeModeForTest runs node-mode resolution against cfg, leaving the // resolved option values in cfg. func ResolveNodeModeForTest(cfg *viper.Viper) (node.NodeMode, error) { - c := &command{config: cfg, logger: log.Noop} + return ResolveNodeModeWithLoggerForTest(cfg, log.Noop) +} + +// ResolveNodeModeWithLoggerForTest is ResolveNodeModeForTest with a logger, for +// tests that assert on the warnings resolution emits. +func ResolveNodeModeWithLoggerForTest(cfg *viper.Viper, logger log.Logger) (node.NodeMode, error) { + c := &command{config: cfg, logger: logger} return c.resolveNodeMode(c.logger) } diff --git a/cmd/bee/cmd/resolve_node_mode_test.go b/cmd/bee/cmd/resolve_node_mode_test.go index c00a875aeca..09e16f0fc1f 100644 --- a/cmd/bee/cmd/resolve_node_mode_test.go +++ b/cmd/bee/cmd/resolve_node_mode_test.go @@ -5,10 +5,12 @@ package cmd_test import ( + "bytes" "strings" "testing" "github.com/ethersphere/bee/v2/cmd/bee/cmd" + "github.com/ethersphere/bee/v2/pkg/log" "github.com/ethersphere/bee/v2/pkg/node" "github.com/spf13/viper" ) @@ -115,8 +117,9 @@ func TestResolveNodeMode(t *testing.T) { wantErr: "chequebook-enable requires swap-enable", }, { - // NewBee never starts swap, push-sync or the incentives agent for a - // bootnode, so full mode must not imply them there. + // NewBee never runs the reserve or the incentives agent for a + // bootnode, so full mode must not imply swap, chequebook or + // incentives there. name: "full bootnode does not imply swap, chequebook or incentives", config: map[string]any{ cmd.OptionNameNodeMode: "full", @@ -130,6 +133,20 @@ func TestResolveNodeMode(t *testing.T) { cmd.OptionNameStorageIncentivesEnable: false, }, }, + { + name: "full bootnode with swap explicitly enabled enables chequebook", + config: map[string]any{ + cmd.OptionNameNodeMode: "full", + cmd.OptionNameBootnodeMode: true, + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameSwapEnable: true, + }, + wantMode: node.FullMode, + wantOptions: map[string]bool{ + cmd.OptionNameSwapEnable: true, + cmd.OptionNameChequebookEnable: true, + }, + }, { name: "full bootnode with storage-incentives explicitly false succeeds", config: map[string]any{ @@ -179,6 +196,19 @@ func TestResolveNodeMode(t *testing.T) { }, wantErr: "chequebook-enable requires swap-enable", }, + { + name: "light with swap enables chequebook", + config: map[string]any{ + cmd.OptionNameNodeMode: "light", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameSwapEnable: true, + }, + wantMode: node.LightMode, + wantOptions: map[string]bool{ + cmd.OptionNameSwapEnable: true, + cmd.OptionNameChequebookEnable: true, + }, + }, { name: "light rejects storage-incentives-enable", config: map[string]any{ @@ -219,6 +249,14 @@ func TestResolveNodeMode(t *testing.T) { }, wantErr: "ultra-light node cannot have storage-incentives-enable", }, + { + name: "ultra-light rejects chequebook-enable", + config: map[string]any{ + cmd.OptionNameNodeMode: "ultra-light", + cmd.OptionNameChequebookEnable: true, + }, + wantErr: "ultra-light node cannot have chequebook-enable", + }, { name: "node-mode takes precedence over legacy full-node", config: map[string]any{ @@ -417,6 +455,7 @@ func TestResolveNodeModeWithBoundFlags(t *testing.T) { name string args []string wantMode node.NodeMode + wantErr string wantOptions map[string]bool }{ { @@ -449,7 +488,7 @@ func TestResolveNodeModeWithBoundFlags(t *testing.T) { }, }, { - name: "node-mode light with rpc keeps sub-option defaults", + name: "node-mode light with rpc leaves swap and chequebook off", args: []string{"--node-mode=light", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, wantMode: node.LightMode, wantOptions: map[string]bool{ @@ -468,6 +507,31 @@ func TestResolveNodeModeWithBoundFlags(t *testing.T) { cmd.OptionNameStorageIncentivesEnable: false, }, }, + { + name: "node-mode full bootnode with swap enables chequebook", + args: []string{"--node-mode=full", "--bootnode-mode", "--swap-enable", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + wantMode: node.FullMode, + wantOptions: map[string]bool{ + cmd.OptionNameSwapEnable: true, + cmd.OptionNameChequebookEnable: true, + cmd.OptionNameStorageIncentivesEnable: false, + }, + }, + { + name: "node-mode ultra-light leaves chequebook and incentives off", + args: []string{"--node-mode=ultra-light"}, + wantMode: node.UltraLightMode, + wantOptions: map[string]bool{ + cmd.OptionNameSwapEnable: false, + cmd.OptionNameChequebookEnable: false, + cmd.OptionNameStorageIncentivesEnable: false, + }, + }, + { + name: "node-mode ultra-light rejects chequebook-enable", + args: []string{"--node-mode=ultra-light", "--chequebook-enable"}, + wantErr: "ultra-light node cannot have chequebook-enable", + }, { name: "node-mode full with incentives explicitly disabled", args: []string{"--node-mode=full", "--blockchain-rpc-endpoint=" + testRPCEndpoint, "--storage-incentives-enable=false"}, @@ -484,24 +548,15 @@ func TestResolveNodeModeWithBoundFlags(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - root, err := cmd.NewCommand(cmd.WithHomeDir(t.TempDir())) - if err != nil { - t.Fatal(err) - } - startCmd := root.SubCommandForTest("start") - if startCmd == nil { - t.Fatal("start subcommand not found") - } - if err := startCmd.ParseFlags(tt.args); err != nil { - t.Fatal(err) - } - - cfg := viper.New() - if err := cmd.BindStartFlagsForTest(cfg, startCmd); err != nil { - t.Fatal(err) - } + cfg := boundStartConfig(t, tt.args) gotMode, err := cmd.ResolveNodeModeForTest(cfg) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("expected error containing %q, got %v", tt.wantErr, err) + } + return + } if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -516,3 +571,57 @@ func TestResolveNodeModeWithBoundFlags(t *testing.T) { }) } } + +func TestResolveNodeModeUltraLightRPCWarning(t *testing.T) { + t.Parallel() + + const warning = "blockchain-rpc-endpoint is ignored in ultra-light mode" + + for _, tc := range []struct { + name string + args []string + wantWarn bool + }{ + {name: "ultra-light with rpc", args: []string{"--node-mode=ultra-light", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, wantWarn: true}, + {name: "ultra-light without rpc", args: []string{"--node-mode=ultra-light"}, wantWarn: false}, + {name: "light with rpc", args: []string{"--node-mode=light", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, wantWarn: false}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + cfg := boundStartConfig(t, tc.args) + + var buf bytes.Buffer + logger := log.NewLogger("resolve-node-mode-"+t.Name(), log.WithSink(&buf)) + if _, err := cmd.ResolveNodeModeWithLoggerForTest(cfg, logger); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := strings.Contains(buf.String(), warning); got != tc.wantWarn { + t.Errorf("warning logged: got %t, want %t; log: %s", got, tc.wantWarn, buf.String()) + } + }) + } +} + +// boundStartConfig returns a viper bound to the real start command flags +// parsed from args, as production does. +func boundStartConfig(t *testing.T, args []string) *viper.Viper { + t.Helper() + + root, err := cmd.NewCommand(cmd.WithHomeDir(t.TempDir())) + if err != nil { + t.Fatal(err) + } + startCmd := root.SubCommandForTest("start") + if startCmd == nil { + t.Fatal("start subcommand not found") + } + if err := startCmd.ParseFlags(args); err != nil { + t.Fatal(err) + } + cfg := viper.New() + if err := cmd.BindStartFlagsForTest(cfg, startCmd); err != nil { + t.Fatal(err) + } + return cfg +} diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index d7a38309c62..c1d75eb506e 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -392,11 +392,12 @@ const legacyNodeModeRemovalVersion = "v2.11.0" // node-mode value and the release in which the inference is removed. // // node-mode set: the mode owns the config. Options the mode implies are -// enabled unless the operator explicitly disabled them, options the mode -// cannot support are rejected when explicitly enabled (overriding the pflag -// defaults of true when unset), and blockchain-rpc-endpoint is required for -// light and full nodes. Implied values are written back to config so the rest -// of node startup picks them up. +// enabled unless the operator explicitly disabled them (full bootnodes are +// exempt), options the mode cannot support are rejected when explicitly enabled +// (overriding the pflag defaults of true when unset), and +// blockchain-rpc-endpoint is required for light and full nodes. An unset +// chequebook-enable follows swap-enable. Implied values are written back to +// config so the rest of node startup picks them up. func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) { modeStr := c.config.GetString(optionNameNodeMode) if modeStr == "" { @@ -419,29 +420,17 @@ func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) { return "", errors.New("full node requires blockchain-rpc-endpoint to be set") } // A full node implies swap, chequebook and storage incentives. Bootnodes - // are exempt: NewBee never starts swap, push-sync or the incentives agent - // for them, so implying the options would only cost a chequebook deploy. + // are exempt: NewBee never runs the reserve or the incentives agent for + // them, so they have no use for the implied options. Swap still starts on + // a bootnode when explicitly enabled, and chequebook follows it. if !c.config.GetBool(optionNameBootnodeMode) { c.enableImpliedOption(logger, mode, optionNameSwapEnable) - // If swap was explicitly turned off by the operator and chequebook - // was not explicitly configured, turn off chequebook to prevent an - // accidental contradiction with disabled swap. - if !c.config.GetBool(optionNameSwapEnable) && !c.config.IsSet(optionNameChequebookEnable) { - c.config.Set(optionNameChequebookEnable, false) - } if c.config.GetBool(optionNameSwapEnable) { c.enableImpliedOption(logger, mode, optionNameChequebookEnable) } c.enableImpliedOption(logger, mode, optionNameStorageIncentivesEnable) - } else { - // Override the pflag defaults of true when unset, since swap is not - // implied for bootnodes and chequebook requires it. - if !c.config.IsSet(optionNameChequebookEnable) { - c.config.Set(optionNameChequebookEnable, false) - } - if !c.config.IsSet(optionNameStorageIncentivesEnable) { - c.config.Set(optionNameStorageIncentivesEnable, false) - } + } else if !c.config.IsSet(optionNameStorageIncentivesEnable) { + c.config.Set(optionNameStorageIncentivesEnable, false) } case node.LightMode: if rpcEndpoint == "" { @@ -453,11 +442,10 @@ func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) { } else if c.config.GetBool(optionNameStorageIncentivesEnable) { return "", errors.New("light node cannot have storage-incentives-enable set to true") } - // Chequebook requires swap; if swap is off and chequebook was not explicitly configured, override to false. - if !c.config.GetBool(optionNameSwapEnable) && !c.config.IsSet(optionNameChequebookEnable) { - c.config.Set(optionNameChequebookEnable, false) - } case node.UltraLightMode: + if rpcEndpoint != "" { + logger.Warning("blockchain-rpc-endpoint is ignored in ultra-light mode", "node_mode", mode) + } if c.config.GetBool(optionNameSwapEnable) { return "", errors.New("ultra-light node cannot have swap-enable set to true") } @@ -467,12 +455,17 @@ func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) { } else if c.config.GetBool(optionNameStorageIncentivesEnable) { return "", errors.New("ultra-light node cannot have storage-incentives-enable set to true") } - // Chequebook is not supported on ultra-light; override the pflag default of true when unset. - if !c.config.IsSet(optionNameChequebookEnable) { - c.config.Set(optionNameChequebookEnable, false) + if c.config.IsSet(optionNameChequebookEnable) && c.config.GetBool(optionNameChequebookEnable) { + return "", errors.New("ultra-light node cannot have chequebook-enable set to true") } } + // Chequebook requires swap. When chequebook is not explicitly configured, + // follow swap rather than the pflag default of true. + if !c.config.IsSet(optionNameChequebookEnable) { + c.config.Set(optionNameChequebookEnable, c.config.GetBool(optionNameSwapEnable)) + } + // chequebook init is gated on swap-enable in NewBee. With node-mode set the // implied values above never produce this combination, so it is always an // explicit contradiction rather than a silent no-op. diff --git a/packaging/bee.yaml b/packaging/bee.yaml index 672a2742475..1301a4c4b6a 100644 --- a/packaging/bee.yaml +++ b/packaging/bee.yaml @@ -4,6 +4,7 @@ ## Selects the operational mode of this node. ## full - participates in storage and incentives; requires blockchain-rpc; ## implies swap-enable, chequebook-enable and storage-incentives-enable +## (bootnodes are exempt: swap only when set, chequebook follows swap) ## light - uploads and downloads only; requires blockchain-rpc ## ultra-light - free-tier downloads only; no blockchain connection needed ## When unset (the default), the mode is inferred from the deprecated full-node @@ -26,7 +27,7 @@ ## ── Swap / chequebook (full and light nodes only) ──────────────────────────── ## enable swap # swap-enable: false -## enable chequebook (requires swap-enable; implied by node-mode: full) +## enable chequebook (requires swap-enable; when node-mode is set and this is unset, follows swap-enable) # chequebook-enable: true ## reject full-node hive/handshake records that carry no chequebook address # chequebook-verification: false diff --git a/packaging/homebrew-amd64/bee.yaml b/packaging/homebrew-amd64/bee.yaml index 3152cbea001..68174a5094a 100644 --- a/packaging/homebrew-amd64/bee.yaml +++ b/packaging/homebrew-amd64/bee.yaml @@ -4,6 +4,7 @@ ## Selects the operational mode of this node. ## full - participates in storage and incentives; requires blockchain-rpc; ## implies swap-enable, chequebook-enable and storage-incentives-enable +## (bootnodes are exempt: swap only when set, chequebook follows swap) ## light - uploads and downloads only; requires blockchain-rpc ## ultra-light - free-tier downloads only; no blockchain connection needed ## When unset (the default), the mode is inferred from the deprecated full-node @@ -26,7 +27,7 @@ ## ── Swap / chequebook (full and light nodes only) ──────────────────────────── ## enable swap # swap-enable: false -## enable chequebook (requires swap-enable; implied by node-mode: full) +## enable chequebook (requires swap-enable; when node-mode is set and this is unset, follows swap-enable) # chequebook-enable: true ## reject full-node hive/handshake records that carry no chequebook address # chequebook-verification: false diff --git a/packaging/homebrew-arm64/bee.yaml b/packaging/homebrew-arm64/bee.yaml index ada9b5e3bd1..a2f51271258 100644 --- a/packaging/homebrew-arm64/bee.yaml +++ b/packaging/homebrew-arm64/bee.yaml @@ -4,6 +4,7 @@ ## Selects the operational mode of this node. ## full - participates in storage and incentives; requires blockchain-rpc; ## implies swap-enable, chequebook-enable and storage-incentives-enable +## (bootnodes are exempt: swap only when set, chequebook follows swap) ## light - uploads and downloads only; requires blockchain-rpc ## ultra-light - free-tier downloads only; no blockchain connection needed ## When unset (the default), the mode is inferred from the deprecated full-node @@ -26,7 +27,7 @@ ## ── Swap / chequebook (full and light nodes only) ──────────────────────────── ## enable swap # swap-enable: false -## enable chequebook (requires swap-enable; implied by node-mode: full) +## enable chequebook (requires swap-enable; when node-mode is set and this is unset, follows swap-enable) # chequebook-enable: true ## reject full-node hive/handshake records that carry no chequebook address # chequebook-verification: false diff --git a/packaging/scoop/bee.yaml b/packaging/scoop/bee.yaml index cbcb6588e8e..b99b94288f4 100644 --- a/packaging/scoop/bee.yaml +++ b/packaging/scoop/bee.yaml @@ -4,6 +4,7 @@ ## Selects the operational mode of this node. ## full - participates in storage and incentives; requires blockchain-rpc; ## implies swap-enable, chequebook-enable and storage-incentives-enable +## (bootnodes are exempt: swap only when set, chequebook follows swap) ## light - uploads and downloads only; requires blockchain-rpc ## ultra-light - free-tier downloads only; no blockchain connection needed ## When unset (the default), the mode is inferred from the deprecated full-node @@ -26,7 +27,7 @@ ## ── Swap / chequebook (full and light nodes only) ──────────────────────────── ## enable swap # swap-enable: false -## enable chequebook (requires swap-enable; implied by node-mode: full) +## enable chequebook (requires swap-enable; when node-mode is set and this is unset, follows swap-enable) # chequebook-enable: true ## reject full-node hive/handshake records that carry no chequebook address # chequebook-verification: false From 7e91e18e66edc7d3de2962ccd01a468306d21f4b Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Thu, 15 Oct 2026 22:04:22 +0300 Subject: [PATCH 19/22] fix(cmd): list the exact node-mode migration and validate modes early The deprecation warning told legacy operators to add node-mode, but doing only that changed what half of the legacy configs run: a full node without swap gained swap and a chequebook, and configs carrying the old defaults explicitly were rejected. The warning now lists the options to set and to remove so the node keeps its behavior, and notes where that is not possible (chequebook-verification without swap). A test applies the listed migration to every legacy combination of the mode options. Mode-dependent checks (bootnode must be full, reserve-capacity-doubling only on full nodes, swap needs a chain, chequebook-verification needs a full node with chequebook and chain) run in both regimes before the node is built, so they exit non-zero before the keystore is unlocked. Legacy configs are rejected exactly as before node-mode existed. With node-mode set, the value is matched case-insensitively and the blockchain-rpc-endpoint is trimmed, and a bootnode warns that storage-incentives-enable has no effect. The resolver tests run on the real start flags, so the legacy default-restoring loop is gone. --- cmd/bee/cmd/cmd.go | 4 +- cmd/bee/cmd/export_test.go | 15 + cmd/bee/cmd/resolve_node_mode_test.go | 390 +++++++++++++++++++++++--- cmd/bee/cmd/start.go | 184 +++++++++--- 4 files changed, 519 insertions(+), 74 deletions(-) diff --git a/cmd/bee/cmd/cmd.go b/cmd/bee/cmd/cmd.go index 8011d998edc..4891b1eabe7 100644 --- a/cmd/bee/cmd/cmd.go +++ b/cmd/bee/cmd/cmd.go @@ -366,9 +366,7 @@ func (c *command) setAllFlags(cmd *cobra.Command) { 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 (deprecated: use --node-mode=full)") - if err := cmd.Flags().MarkDeprecated(optionNameFullNode, "use --node-mode=full instead"); err != nil { - panic(err) - } + _ = cmd.Flags().MarkDeprecated(optionNameFullNode, "use --node-mode=full instead") cmd.Flags().Int(optionNameLightNodeLimit, 100, "light node limit") cmd.Flags().String(optionNamePostageContractAddress, "", "postage stamp contract address") cmd.Flags().Uint64(optionNamePostageContractStartBlock, 0, "postage stamp contract start block number") diff --git a/cmd/bee/cmd/export_test.go b/cmd/bee/cmd/export_test.go index eee92478037..98e0add9e43 100644 --- a/cmd/bee/cmd/export_test.go +++ b/cmd/bee/cmd/export_test.go @@ -101,6 +101,8 @@ const ( OptionNameSwapEnable = optionNameSwapEnable OptionNameChequebookEnable = optionNameChequebookEnable OptionNameStorageIncentivesEnable = optionNameStorageIncentivesEnable + OptionNameChequebookVerification = optionNameChequebookVerification + OptionNameReserveCapacityDoubling = optionReserveCapacityDoubling ConfigKeyBlockchainRpcEndpoint = configKeyBlockchainRpcEndpoint ) @@ -128,3 +130,16 @@ func BindStartFlagsForTest(cfg *viper.Viper, startCmd *cobra.Command) error { c.bindBlockchainRpcConfig(startCmd) return nil } + +// LegacyMigrationForTest resolves a config without node-mode through the +// production path and returns the inferred mode with the migration the +// deprecation warning lists. +func LegacyMigrationForTest(cfg *viper.Viper) (mode node.NodeMode, set, remove, notes []string, err error) { + c := &command{config: cfg, logger: log.Noop} + mode, err = c.resolveNodeMode(c.logger) + if err != nil { + return "", nil, nil, nil, err + } + m := c.legacyMigration(mode) + return mode, m.set, m.remove, m.notes, nil +} diff --git a/cmd/bee/cmd/resolve_node_mode_test.go b/cmd/bee/cmd/resolve_node_mode_test.go index 09e16f0fc1f..ab3a0f74dc3 100644 --- a/cmd/bee/cmd/resolve_node_mode_test.go +++ b/cmd/bee/cmd/resolve_node_mode_test.go @@ -55,7 +55,7 @@ func TestResolveNodeMode(t *testing.T) { wantMode: node.FullMode, }, { - name: "full without rpc fails", + name: "full without rpc is rejected", config: map[string]any{ cmd.OptionNameNodeMode: "full", }, @@ -63,7 +63,7 @@ func TestResolveNodeMode(t *testing.T) { }, { // A non-staking full node is a legitimate opt-out. - name: "full with storage-incentives explicitly false honours the opt-out", + name: "full with storage-incentives explicitly false honors the opt-out", config: map[string]any{ cmd.OptionNameNodeMode: "full", cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, @@ -107,7 +107,7 @@ func TestResolveNodeMode(t *testing.T) { }, }, { - name: "full with chequebook explicitly true and swap explicitly false fails", + name: "full with chequebook explicitly true and swap explicitly false is rejected", config: map[string]any{ cmd.OptionNameNodeMode: "full", cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, @@ -181,14 +181,14 @@ func TestResolveNodeMode(t *testing.T) { wantMode: node.LightMode, }, { - name: "light without rpc fails", + name: "light without rpc is rejected", config: map[string]any{ cmd.OptionNameNodeMode: "light", }, wantErr: "light node requires blockchain-rpc-endpoint", }, { - name: "light with chequebook but no swap fails", + name: "light with chequebook but no swap is rejected", config: map[string]any{ cmd.OptionNameNodeMode: "light", cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, @@ -267,32 +267,113 @@ func TestResolveNodeMode(t *testing.T) { wantMode: node.LightMode, }, { - name: "invalid node-mode value fails", + name: "invalid node-mode value is rejected", config: map[string]any{ cmd.OptionNameNodeMode: "superlight", }, wantErr: "invalid node-mode", }, { - name: "uppercase node-mode fails", + name: "uppercase node-mode is accepted", config: map[string]any{ - cmd.OptionNameNodeMode: "FULL", + cmd.OptionNameNodeMode: "FULL", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, }, - wantErr: "invalid node-mode", + wantMode: node.FullMode, + }, + { + name: "node-mode surrounded by whitespace is accepted", + config: map[string]any{ + cmd.OptionNameNodeMode: " light ", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + }, + wantMode: node.LightMode, }, { - name: "whitespace node-mode fails", + name: "whitespace-only node-mode is rejected", config: map[string]any{ - cmd.OptionNameNodeMode: " full ", + cmd.OptionNameNodeMode: " ", }, wantErr: "invalid node-mode", }, + { + name: "full with whitespace-only rpc is rejected", + config: map[string]any{ + cmd.OptionNameNodeMode: "full", + cmd.ConfigKeyBlockchainRpcEndpoint: " ", + }, + wantErr: "full node requires blockchain-rpc-endpoint", + }, + { + name: "light with whitespace-only rpc is rejected", + config: map[string]any{ + cmd.OptionNameNodeMode: "light", + cmd.ConfigKeyBlockchainRpcEndpoint: " ", + }, + wantErr: "light node requires blockchain-rpc-endpoint", + }, + { + name: "light rejects reserve-capacity-doubling", + config: map[string]any{ + cmd.OptionNameNodeMode: "light", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameReserveCapacityDoubling: 1, + }, + wantErr: "reserve capacity doubling is only allowed for full nodes", + }, + { + name: "full accepts reserve-capacity-doubling", + config: map[string]any{ + cmd.OptionNameNodeMode: "full", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameReserveCapacityDoubling: 1, + }, + wantMode: node.FullMode, + }, + { + name: "light rejects chequebook-verification", + config: map[string]any{ + cmd.OptionNameNodeMode: "light", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameSwapEnable: true, + cmd.OptionNameChequebookVerification: true, + }, + wantErr: "chequebook-verification requires", + }, + { + name: "full with swap off rejects chequebook-verification", + config: map[string]any{ + cmd.OptionNameNodeMode: "full", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameSwapEnable: false, + cmd.OptionNameChequebookVerification: true, + }, + wantErr: "chequebook-verification requires", + }, + { + name: "full accepts chequebook-verification", + config: map[string]any{ + cmd.OptionNameNodeMode: "full", + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameChequebookVerification: true, + }, + wantMode: node.FullMode, + }, + { + name: "light bootnode is rejected", + config: map[string]any{ + cmd.OptionNameNodeMode: "light", + cmd.OptionNameBootnodeMode: true, + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + }, + wantErr: "boot node must be started as a full node", + }, - // ── node-mode unset: legacy behaviour, verbatim ───────────────────────── + // ── node-mode unset: legacy behavior, as before node-mode ─────────────── { // The most common pre-node-mode light config. chequebook-enable used // to default to true, so this node issued cheques; it must keep doing so. - name: "legacy light with swap only restores chequebook default", + name: "legacy light with swap only keeps chequebook default", config: map[string]any{ cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, cmd.OptionNameSwapEnable: true, @@ -313,7 +394,7 @@ func TestResolveNodeMode(t *testing.T) { wantMode: node.UltraLightMode, }, { - name: "legacy full-node with rpc only restores old defaults and leaves swap off", + name: "legacy full-node with rpc only keeps old defaults and leaves swap off", config: map[string]any{ cmd.OptionNameFullNode: true, cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, @@ -337,7 +418,7 @@ func TestResolveNodeMode(t *testing.T) { wantMode: node.FullMode, }, { - name: "legacy full-node with explicit opt-outs is not validated", + name: "legacy full-node keeps explicit opt-outs", config: map[string]any{ cmd.OptionNameFullNode: true, cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, @@ -365,7 +446,7 @@ func TestResolveNodeMode(t *testing.T) { { // Previous releases enabled the chain backend for every full node and // failed at chain init without an endpoint; keep failing, earlier. - name: "legacy full-node without rpc fails", + name: "legacy full-node without rpc is rejected", config: map[string]any{ cmd.OptionNameFullNode: true, }, @@ -385,12 +466,46 @@ func TestResolveNodeMode(t *testing.T) { }, { // Beekeeper's inherited-config scenario: swap-enable inherited from the - // base profile on a node without rpc. Legacy must not validate it. - name: "legacy without rpc but with swap infers ultra-light without error", + // base profile on a node without rpc. Releases before node-mode failed + // it in NewBee; it is now rejected before the node is built. + name: "legacy without rpc but with swap is rejected", config: map[string]any{ cmd.OptionNameSwapEnable: true, }, - wantMode: node.UltraLightMode, + wantErr: "swap is enabled but the chain backend is not", + }, + { + name: "legacy light bootnode is rejected", + config: map[string]any{ + cmd.OptionNameBootnodeMode: true, + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + }, + wantErr: "boot node must be started as a full node", + }, + { + name: "legacy light rejects reserve-capacity-doubling", + config: map[string]any{ + cmd.ConfigKeyBlockchainRpcEndpoint: testRPCEndpoint, + cmd.OptionNameReserveCapacityDoubling: 1, + }, + wantErr: "reserve capacity doubling is only allowed for full nodes", + }, + { + // Before node-mode, a whitespace-only endpoint selected light mode + // and failed at chain init; inference keeps that. + name: "legacy whitespace-only rpc infers light", + config: map[string]any{ + cmd.ConfigKeyBlockchainRpcEndpoint: " ", + }, + wantMode: node.LightMode, + }, + { + name: "legacy full-node with whitespace-only rpc is rejected", + config: map[string]any{ + cmd.OptionNameFullNode: true, + cmd.ConfigKeyBlockchainRpcEndpoint: " ", + }, + wantErr: "full node requires blockchain-rpc-endpoint", }, { // Legacy must not restore a default the operator overrode. @@ -413,7 +528,9 @@ func TestResolveNodeMode(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - cfg := viper.New() + // Start from the real start flags, so options a case leaves out + // keep their production defaults. + cfg := boundStartConfig(t, nil) for k, v := range tt.config { cfg.Set(k, v) } @@ -459,7 +576,7 @@ func TestResolveNodeModeWithBoundFlags(t *testing.T) { wantOptions map[string]bool }{ { - name: "no flags selects legacy regime and restores old defaults", + name: "no flags selects legacy regime and keeps old defaults", args: nil, wantMode: node.UltraLightMode, wantOptions: map[string]bool{ @@ -572,19 +689,69 @@ func TestResolveNodeModeWithBoundFlags(t *testing.T) { } } -func TestResolveNodeModeUltraLightRPCWarning(t *testing.T) { +func TestResolveNodeModeWarnings(t *testing.T) { t.Parallel() - const warning = "blockchain-rpc-endpoint is ignored in ultra-light mode" - for _, tc := range []struct { - name string - args []string - wantWarn bool + name string + args []string + // wantErr is the expected resolution error, empty for none. want lists + // substrings the log must contain; absent lists substrings it must not. + wantErr string + want, absent []string }{ - {name: "ultra-light with rpc", args: []string{"--node-mode=ultra-light", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, wantWarn: true}, - {name: "ultra-light without rpc", args: []string{"--node-mode=ultra-light"}, wantWarn: false}, - {name: "light with rpc", args: []string{"--node-mode=light", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, wantWarn: false}, + { + name: "ultra-light with rpc", + args: []string{"--node-mode=ultra-light", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + want: []string{"blockchain-rpc-endpoint is ignored in ultra-light mode"}, + }, + { + name: "ultra-light without rpc", + args: []string{"--node-mode=ultra-light"}, + absent: []string{"blockchain-rpc-endpoint is ignored"}, + }, + { + name: "light with rpc", + args: []string{"--node-mode=light", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + absent: []string{"blockchain-rpc-endpoint is ignored"}, + }, + { + name: "full bootnode with storage incentives enabled", + args: []string{"--node-mode=full", "--bootnode-mode", "--storage-incentives-enable", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + want: []string{"storage-incentives-enable has no effect on a bootnode"}, + }, + { + name: "full bootnode without storage incentives", + args: []string{"--node-mode=full", "--bootnode-mode", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + absent: []string{"has no effect on a bootnode"}, + }, + { + name: "legacy full node lists the migration", + args: []string{"--full-node", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + want: []string{"node-mode is not set", `"set"=["node-mode=full","swap-enable=false"]`, `"remove"=["full-node"]`}, + }, + { + name: "legacy light node lists options to remove", + args: []string{"--blockchain-rpc-endpoint=" + testRPCEndpoint, "--storage-incentives-enable"}, + want: []string{`"set"=["node-mode=light"]`, `"remove"=["storage-incentives-enable"]`}, + absent: []string{"behavior_change"}, + }, + { + name: "legacy full node with verification notes the swap change", + args: []string{"--full-node", "--chequebook-verification", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + want: []string{`"set"=["node-mode=full"]`, "set swap-enable=false and remove chequebook-verification"}, + }, + { + name: "legacy whitespace-only rpc notes that light needs an endpoint", + args: []string{"--blockchain-rpc-endpoint= "}, + want: []string{`"set"=["node-mode=light"]`, "blockchain-rpc-endpoint contains only whitespace"}, + }, + { + name: "legacy bootnode error is not preceded by the migration warning", + args: []string{"--bootnode-mode", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + wantErr: "boot node must be started as a full node", + absent: []string{"node-mode is not set"}, + }, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() @@ -593,11 +760,168 @@ func TestResolveNodeModeUltraLightRPCWarning(t *testing.T) { var buf bytes.Buffer logger := log.NewLogger("resolve-node-mode-"+t.Name(), log.WithSink(&buf)) - if _, err := cmd.ResolveNodeModeWithLoggerForTest(cfg, logger); err != nil { + _, err := cmd.ResolveNodeModeWithLoggerForTest(cfg, logger) + switch { + case tc.wantErr == "" && err != nil: + t.Fatalf("unexpected error: %v", err) + case tc.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tc.wantErr)): + t.Fatalf("expected error containing %q, got %v", tc.wantErr, err) + } + for _, w := range tc.want { + if !strings.Contains(buf.String(), w) { + t.Errorf("log does not contain %q; log: %s", w, buf.String()) + } + } + for _, a := range tc.absent { + if strings.Contains(buf.String(), a) { + t.Errorf("log contains %q; log: %s", a, buf.String()) + } + } + }) + } +} + +// TestLegacyMigrationPreservesBehavior applies the migration listed by the +// deprecation warning to every legacy combination of the mode-related options +// and checks that the node then runs the same services with node-mode set. +// Where behavior cannot be preserved, the warning must say so. Combinations +// rejected before migration must be exactly those releases before node-mode +// rejected. +// +// The chequebook is modeled as running, which needs swap. The API's +// chequebookEnabled flag reports the option itself and does change for nodes +// without swap: chequebook-enable follows swap-enable once node-mode is set. +func TestLegacyMigrationPreservesBehavior(t *testing.T) { + t.Parallel() + + tri := []string{"", "true", "false"} + type effect struct { + mode node.NodeMode + swap, chequebook, incentives, verification bool + } + effectOf := func(cfg *viper.Viper, mode node.NodeMode, bootnode bool) effect { + swap := cfg.GetBool(cmd.OptionNameSwapEnable) + return effect{ + mode: mode, + swap: swap, + chequebook: swap && cfg.GetBool(cmd.OptionNameChequebookEnable), + incentives: mode == node.FullMode && !bootnode && cfg.GetBool(cmd.OptionNameStorageIncentivesEnable), + verification: cfg.GetBool(cmd.OptionNameChequebookVerification), + } + } + + var checked, rejected, changed int + for _, bootnode := range []bool{false, true} { + for _, rpc := range []bool{false, true} { + for _, fullNode := range tri { + for _, swap := range tri { + for _, chequebook := range tri { + for _, incentives := range tri { + for _, verification := range []string{"", "true"} { + var args []string + add := func(name, value string) { + if value != "" { + args = append(args, "--"+name+"="+value) + } + } + if bootnode { + add(cmd.OptionNameBootnodeMode, "true") + } + if rpc { + add("blockchain-rpc-endpoint", testRPCEndpoint) + } + add(cmd.OptionNameFullNode, fullNode) + add(cmd.OptionNameSwapEnable, swap) + add(cmd.OptionNameChequebookEnable, chequebook) + add(cmd.OptionNameStorageIncentivesEnable, incentives) + add(cmd.OptionNameChequebookVerification, verification) + + // Rules of releases before node-mode: a bootnode must be + // full, a full node needs a chain, swap needs a chain, + // and verification needs a full node with chequebook and + // chain. chequebook-enable defaults to true. + full := fullNode == "true" + chain := full || rpc + wantRejected := (bootnode && !full) || (full && !rpc) || (swap == "true" && !chain) || + (verification == "true" && (!full || chequebook == "false" || !chain)) + + legacyCfg := boundStartConfig(t, args) + mode, set, remove, notes, err := cmd.LegacyMigrationForTest(legacyCfg) + if (err != nil) != wantRejected { + t.Errorf("%v: rejected=%t, want %t (err: %v)", args, err != nil, wantRejected, err) + continue + } + if err != nil { + rejected++ + continue + } + checked++ + before := effectOf(legacyCfg, mode, bootnode) + + var migrated []string + next: + for _, a := range args { + for _, r := range remove { + if strings.HasPrefix(a, "--"+r+"=") { + continue next + } + } + migrated = append(migrated, a) + } + for _, kv := range set { + k, v, _ := strings.Cut(kv, "=") + migrated = append(migrated, "--"+k+"="+v) + } + + migratedCfg := boundStartConfig(t, migrated) + gotMode, err := cmd.ResolveNodeModeForTest(migratedCfg) + if len(notes) > 0 { + changed++ + if err == nil && effectOf(migratedCfg, gotMode, bootnode) == before { + t.Errorf("%v: warning notes a behavior change %v, but the migrated config %v behaves the same", args, notes, migrated) + } + continue + } + if err != nil { + t.Errorf("%v: migrated config %v fails: %v", args, migrated, err) + continue + } + if after := effectOf(migratedCfg, gotMode, bootnode); after != before { + t.Errorf("%v: migrated config %v changes behavior: before %+v, after %+v", args, migrated, before, after) + } + } + } + } + } + } + } + } + if checked == 0 { + t.Fatal("no legacy configuration was checked") + } + t.Logf("checked %d legacy configurations (%d with a noted behavior change), %d rejected as before node-mode", checked, changed, rejected) +} + +func TestResolveNodeModeTrimsRPCEndpoint(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + args []string + want string + }{ + {name: "node-mode set trims the endpoint", args: []string{"--node-mode=full", "--blockchain-rpc-endpoint= " + testRPCEndpoint + " "}, want: testRPCEndpoint}, + {name: "legacy keeps the endpoint as configured", args: []string{"--full-node", "--blockchain-rpc-endpoint= " + testRPCEndpoint + " "}, want: " " + testRPCEndpoint + " "}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + cfg := boundStartConfig(t, tc.args) + if _, err := cmd.ResolveNodeModeForTest(cfg); err != nil { t.Fatalf("unexpected error: %v", err) } - if got := strings.Contains(buf.String(), warning); got != tc.wantWarn { - t.Errorf("warning logged: got %t, want %t; log: %s", got, tc.wantWarn, buf.String()) + if got := cfg.GetString(cmd.ConfigKeyBlockchainRpcEndpoint); got != tc.want { + t.Errorf("endpoint: got %q, want %q", got, tc.want) } }) } diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index c1d75eb506e..2d304759380 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -16,6 +16,7 @@ import ( "os/signal" "path/filepath" "runtime" + "strings" "sync/atomic" "syscall" "time" @@ -69,9 +70,6 @@ func (c *command) initStartCmd() (err error) { if err != nil { return err } - if c.config.GetBool(optionNameBootnodeMode) && nodeMode != node.FullMode { - return errors.New("boot node must be started as a full node") - } // ctx is global context of bee node; which is canceled after interrupt signal is received. ctx, cancel := context.WithCancel(context.Background()) @@ -384,12 +382,12 @@ const legacyNodeModeRemovalVersion = "v2.11.0" // resolveNodeMode determines the effective node mode from config. There are // two regimes, selected by whether node-mode is set. // -// node-mode unset (legacy): the node behaves exactly as releases before -// node-mode existed. Flag defaults of true for chequebook-enable and -// storage-incentives-enable remain in effect, the mode is inferred from the -// deprecated full-node option and the presence of blockchain-rpc-endpoint, and -// no further validation is applied. A deprecation warning names the equivalent -// node-mode value and the release in which the inference is removed. +// node-mode unset (legacy): the node behaves as releases before node-mode +// existed. The mode is inferred from the deprecated full-node option and the +// presence of blockchain-rpc-endpoint, and the flag defaults of true for +// chequebook-enable and storage-incentives-enable stay in effect. A deprecation +// warning lists the config changes that keep the node's behavior once +// node-mode is set, and the release in which the inference is removed. // // node-mode set: the mode owns the config. Options the mode implies are // enabled unless the operator explicitly disabled them (full bootnodes are @@ -398,21 +396,51 @@ const legacyNodeModeRemovalVersion = "v2.11.0" // blockchain-rpc-endpoint is required for light and full nodes. An unset // chequebook-enable follows swap-enable. Implied values are written back to // config so the rest of node startup picks them up. +// +// In both regimes, options the resolved mode cannot run with are rejected +// before the node is built. func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) { - modeStr := c.config.GetString(optionNameNodeMode) - if modeStr == "" { - return c.resolveLegacyNodeMode(logger) + raw := c.config.GetString(optionNameNodeMode) + legacy := raw == "" + + var ( + mode node.NodeMode + err error + ) + if legacy { + mode, err = c.resolveLegacyNodeMode() + } else { + mode, err = c.resolveExplicitNodeMode(logger, raw) } + if err != nil { + return "", err + } + if err := c.validateNodeModeOptions(mode); err != nil { + return "", err + } + if legacy { + c.logLegacyNodeModeMigration(logger, mode) + } + return mode, nil +} - mode := node.NodeMode(modeStr) +// resolveExplicitNodeMode applies the regime in which node-mode is set. The +// value is matched case-insensitively, ignoring surrounding whitespace. +func (c *command) resolveExplicitNodeMode(logger log.Logger, raw string) (node.NodeMode, error) { + mode := node.NodeMode(strings.ToLower(strings.TrimSpace(raw))) if !mode.IsValid() { - return "", fmt.Errorf("invalid node-mode %q: must be one of full, light, ultra-light", mode) + return "", fmt.Errorf("invalid node-mode %q: must be one of full, light, ultra-light", raw) } if c.config.GetBool(optionNameFullNode) { logger.Warning("--full-node is set alongside --node-mode; --full-node is ignored") } - rpcEndpoint := c.config.GetString(configKeyBlockchainRpcEndpoint) + // Surrounding whitespace is not significant: a whitespace-only endpoint + // counts as unset, and the trimmed value is what the node dials. + rpcEndpoint := strings.TrimSpace(c.config.GetString(configKeyBlockchainRpcEndpoint)) + if rpcEndpoint != c.config.GetString(configKeyBlockchainRpcEndpoint) { + c.config.Set(configKeyBlockchainRpcEndpoint, rpcEndpoint) + } switch mode { case node.FullMode: @@ -431,6 +459,8 @@ func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) { c.enableImpliedOption(logger, mode, optionNameStorageIncentivesEnable) } else if !c.config.IsSet(optionNameStorageIncentivesEnable) { c.config.Set(optionNameStorageIncentivesEnable, false) + } else if c.config.GetBool(optionNameStorageIncentivesEnable) { + logger.Warning("storage-incentives-enable has no effect on a bootnode", "node_mode", mode) } case node.LightMode: if rpcEndpoint == "" { @@ -477,7 +507,7 @@ func (c *command) resolveNodeMode(logger log.Logger) (node.NodeMode, error) { } // enableImpliedOption turns on an option implied by the node mode unless the -// operator set it explicitly. An explicit false is honoured and reported, since +// operator set it explicitly. An explicit false is honored and reported, since // it degrades the node relative to what the mode normally provides. func (c *command) enableImpliedOption(logger log.Logger, mode node.NodeMode, key string) { if !c.config.IsSet(key) { @@ -490,37 +520,115 @@ func (c *command) enableImpliedOption(logger log.Logger, mode node.NodeMode, key } } -// resolveLegacyNodeMode reproduces the behaviour of releases before node-mode -// existed, for configs that do not set node-mode. The only check kept is that -// a full node has a blockchain-rpc-endpoint: those releases enabled the chain -// backend for every full node and failed at chain init without one, so the -// early error changes the message, not the outcome. -func (c *command) resolveLegacyNodeMode(logger log.Logger) (node.NodeMode, error) { - // chequebook-enable and storage-incentives-enable default to true. - // Ensure they are populated in config when unset (e.g. bare viper in tests) - // so an upgraded node keeps its settlement and incentives behaviour. - for _, key := range []string{optionNameChequebookEnable, optionNameStorageIncentivesEnable} { - if !c.config.IsSet(key) { - c.config.Set(key, true) - } - } - - mode := node.UltraLightMode +// resolveLegacyNodeMode infers the mode as releases before node-mode did, for +// configs that do not set node-mode. A full node must have a +// blockchain-rpc-endpoint: those releases enabled the chain backend for every +// full node and failed at chain init without one, so the early error changes +// the message, not the outcome. +func (c *command) resolveLegacyNodeMode() (node.NodeMode, error) { switch { case c.config.GetBool(optionNameFullNode): - if c.config.GetString(configKeyBlockchainRpcEndpoint) == "" { + if strings.TrimSpace(c.config.GetString(configKeyBlockchainRpcEndpoint)) == "" { return "", errors.New("full node requires blockchain-rpc-endpoint to be set") } - mode = node.FullMode + return node.FullMode, nil case c.config.GetString(configKeyBlockchainRpcEndpoint) != "": - mode = node.LightMode + // Untrimmed, as before node-mode existed: a whitespace-only endpoint + // still selects light mode and fails at chain init instead of silently + // starting an ultra-light node. + return node.LightMode, nil + default: + return node.UltraLightMode, nil + } +} + +// validateNodeModeOptions rejects options the resolved mode cannot run with, in +// both regimes. Releases before node-mode rejected the same configurations, +// the bootnode one in the start command and the others in NewBee, which keeps +// its checks for other callers. Checking here rejects the configuration before +// the node is built, with a non-zero exit status. +func (c *command) validateNodeModeOptions(mode node.NodeMode) error { + chainEnabled := mode == node.FullMode || mode == node.LightMode + switch { + case c.config.GetBool(optionNameBootnodeMode) && mode != node.FullMode: + return errors.New("boot node must be started as a full node") + case mode != node.FullMode && c.config.GetInt(optionReserveCapacityDoubling) != 0: + return errors.New("reserve capacity doubling is only allowed for full nodes") + case c.config.GetBool(optionNameSwapEnable) && !chainEnabled: + return errors.New("swap is enabled but the chain backend is not; provide --blockchain-rpc-endpoint or disable swap") + case c.config.GetBool(optionNameChequebookVerification) && (mode != node.FullMode || !c.config.GetBool(optionNameChequebookEnable) || !chainEnabled): + return fmt.Errorf("chequebook-verification requires full-node mode, chequebook-enable, and an enabled chain backend (full_node=%t, chequebook_enable=%t, chain_enabled=%t)", + mode == node.FullMode, c.config.GetBool(optionNameChequebookEnable), chainEnabled) } + return nil +} + +// legacyNodeModeMigration lists the config changes that make a legacy config +// behave the same once node-mode is set to the inferred mode, as option=value +// pairs to set and options to remove. Notes describe behavior that cannot be +// preserved. +type legacyNodeModeMigration struct { + set []string + remove []string + notes []string +} - logger.Warning("node-mode is not set and was inferred from legacy options; add it to your config, inference will be removed", +func (c *command) legacyMigration(mode node.NodeMode) legacyNodeModeMigration { + var m legacyNodeModeMigration + bootnode := c.config.GetBool(optionNameBootnodeMode) + swap := c.config.GetBool(optionNameSwapEnable) + verification := c.config.GetBool(optionNameChequebookVerification) + + m.set = append(m.set, optionNameNodeMode+"="+string(mode)) + if c.config.IsSet(optionNameFullNode) { + m.remove = append(m.remove, optionNameFullNode) + } + // With node-mode set, chequebook-enable follows swap-enable and + // chequebook-verification requires the chequebook, so verification cannot + // keep running without swap. + switch { + case verification && !swap && mode == node.FullMode && !bootnode && !c.config.IsSet(optionNameSwapEnable): + m.notes = append(m.notes, "node-mode: full enables swap and chequebook, which chequebook-verification requires; to keep swap off, set swap-enable=false and remove chequebook-verification") + case verification && !swap: + m.notes = append(m.notes, "chequebook-verification requires swap-enable once node-mode is set; set swap-enable=true or remove chequebook-verification") + case mode == node.FullMode && !bootnode && !c.config.IsSet(optionNameSwapEnable): + // node-mode: full implies swap, which legacy full nodes did not. + m.set = append(m.set, optionNameSwapEnable+"=false") + } + // With node-mode set, an explicit chequebook-enable=true is rejected + // without swap, and on ultra-light nodes. + if c.config.IsSet(optionNameChequebookEnable) && c.config.GetBool(optionNameChequebookEnable) && (mode == node.UltraLightMode || !swap) { + m.remove = append(m.remove, optionNameChequebookEnable) + } + // Legacy inference treats a whitespace-only endpoint as set; with node-mode + // set it counts as unset, which light mode rejects. + if mode == node.LightMode && strings.TrimSpace(c.config.GetString(configKeyBlockchainRpcEndpoint)) == "" { + m.notes = append(m.notes, "blockchain-rpc-endpoint contains only whitespace; node-mode=light requires an endpoint") + } + // Storage incentives only run on full nodes that are not bootnodes; with + // node-mode set an explicit true is rejected on light and ultra-light nodes. + if c.config.IsSet(optionNameStorageIncentivesEnable) && c.config.GetBool(optionNameStorageIncentivesEnable) && (mode != node.FullMode || bootnode) { + m.remove = append(m.remove, optionNameStorageIncentivesEnable) + } + return m +} + +// logLegacyNodeModeMigration warns that the mode was inferred and lists the +// config changes that keep the node's behavior once node-mode is set. +func (c *command) logLegacyNodeModeMigration(logger log.Logger, mode node.NodeMode) { + m := c.legacyMigration(mode) + kv := []any{ "node_mode", mode, "removed_in", legacyNodeModeRemovalVersion, - ) - return mode, nil + "set", m.set, + } + if len(m.remove) > 0 { + kv = append(kv, "remove", m.remove) + } + if len(m.notes) > 0 { + kv = append(kv, "behavior_change", m.notes) + } + logger.Warning("node-mode is not set and was inferred from legacy options, which will be removed; to keep the current behavior, apply the listed options in the config file, flags or BEE_* environment variables", kv...) } type program struct { From bf279f9f5029c1e73208136fc246264e654cd82d Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Fri, 16 Oct 2026 13:39:33 +0300 Subject: [PATCH 20/22] feat(cmd): explain the pseudosettle limit of light nodes without swap node-mode: light keeps swap opt-in, as light nodes had before node-mode. Without swap the node pays peers only through the free pseudosettle allowance, so it now logs that uploads and downloads are limited to it and how to exceed it. The packaged configs say the same. --- cmd/bee/cmd/resolve_node_mode_test.go | 20 ++++++++++++++++++++ cmd/bee/cmd/start.go | 6 ++++++ packaging/bee.yaml | 3 ++- packaging/homebrew-amd64/bee.yaml | 3 ++- packaging/homebrew-arm64/bee.yaml | 3 ++- packaging/scoop/bee.yaml | 3 ++- 6 files changed, 34 insertions(+), 4 deletions(-) diff --git a/cmd/bee/cmd/resolve_node_mode_test.go b/cmd/bee/cmd/resolve_node_mode_test.go index ab3a0f74dc3..011d8090401 100644 --- a/cmd/bee/cmd/resolve_node_mode_test.go +++ b/cmd/bee/cmd/resolve_node_mode_test.go @@ -715,6 +715,26 @@ func TestResolveNodeModeWarnings(t *testing.T) { args: []string{"--node-mode=light", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, absent: []string{"blockchain-rpc-endpoint is ignored"}, }, + { + name: "light without swap explains the upload limit", + args: []string{"--node-mode=light", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + want: []string{"uploads and downloads are limited to the free pseudosettle allowance"}, + }, + { + name: "light with swap does not explain the upload limit", + args: []string{"--node-mode=light", "--swap-enable", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + absent: []string{"pseudosettle allowance"}, + }, + { + name: "full without swap does not explain the upload limit", + args: []string{"--node-mode=full", "--swap-enable=false", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + absent: []string{"pseudosettle allowance"}, + }, + { + name: "legacy light does not explain the upload limit", + args: []string{"--blockchain-rpc-endpoint=" + testRPCEndpoint}, + absent: []string{"pseudosettle allowance"}, + }, { name: "full bootnode with storage incentives enabled", args: []string{"--node-mode=full", "--bootnode-mode", "--storage-incentives-enable", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index 2d304759380..e7adec605b1 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -472,6 +472,12 @@ func (c *command) resolveExplicitNodeMode(logger log.Logger, raw string) (node.N } else if c.config.GetBool(optionNameStorageIncentivesEnable) { return "", errors.New("light node cannot have storage-incentives-enable set to true") } + // Light mode does not imply swap, as before node-mode. Without it the + // node pays peers only through the free pseudosettle allowance, which + // limits uploads and downloads. + if !c.config.GetBool(optionNameSwapEnable) { + logger.Info("swap is disabled, so uploads and downloads are limited to the free pseudosettle allowance; to exceed it set swap-enable=true and fund the chequebook", "node_mode", mode) + } case node.UltraLightMode: if rpcEndpoint != "" { logger.Warning("blockchain-rpc-endpoint is ignored in ultra-light mode", "node_mode", mode) diff --git a/packaging/bee.yaml b/packaging/bee.yaml index 1301a4c4b6a..ab5d0a63436 100644 --- a/packaging/bee.yaml +++ b/packaging/bee.yaml @@ -5,7 +5,8 @@ ## full - participates in storage and incentives; requires blockchain-rpc; ## implies swap-enable, chequebook-enable and storage-incentives-enable ## (bootnodes are exempt: swap only when set, chequebook follows swap) -## light - uploads and downloads only; requires blockchain-rpc +## light - uploads and downloads only; requires blockchain-rpc; beyond the +## free allowance also needs swap-enable and a funded chequebook ## ultra-light - free-tier downloads only; no blockchain connection needed ## When unset (the default), the mode is inferred from the deprecated full-node ## option and blockchain-rpc presence (deprecated, removed in v2.11.0). diff --git a/packaging/homebrew-amd64/bee.yaml b/packaging/homebrew-amd64/bee.yaml index 68174a5094a..fa34524080a 100644 --- a/packaging/homebrew-amd64/bee.yaml +++ b/packaging/homebrew-amd64/bee.yaml @@ -5,7 +5,8 @@ ## full - participates in storage and incentives; requires blockchain-rpc; ## implies swap-enable, chequebook-enable and storage-incentives-enable ## (bootnodes are exempt: swap only when set, chequebook follows swap) -## light - uploads and downloads only; requires blockchain-rpc +## light - uploads and downloads only; requires blockchain-rpc; beyond the +## free allowance also needs swap-enable and a funded chequebook ## ultra-light - free-tier downloads only; no blockchain connection needed ## When unset (the default), the mode is inferred from the deprecated full-node ## option and blockchain-rpc presence (deprecated, removed in v2.11.0). diff --git a/packaging/homebrew-arm64/bee.yaml b/packaging/homebrew-arm64/bee.yaml index a2f51271258..fc77c491d3d 100644 --- a/packaging/homebrew-arm64/bee.yaml +++ b/packaging/homebrew-arm64/bee.yaml @@ -5,7 +5,8 @@ ## full - participates in storage and incentives; requires blockchain-rpc; ## implies swap-enable, chequebook-enable and storage-incentives-enable ## (bootnodes are exempt: swap only when set, chequebook follows swap) -## light - uploads and downloads only; requires blockchain-rpc +## light - uploads and downloads only; requires blockchain-rpc; beyond the +## free allowance also needs swap-enable and a funded chequebook ## ultra-light - free-tier downloads only; no blockchain connection needed ## When unset (the default), the mode is inferred from the deprecated full-node ## option and blockchain-rpc presence (deprecated, removed in v2.11.0). diff --git a/packaging/scoop/bee.yaml b/packaging/scoop/bee.yaml index b99b94288f4..f47ef8d7b8e 100644 --- a/packaging/scoop/bee.yaml +++ b/packaging/scoop/bee.yaml @@ -5,7 +5,8 @@ ## full - participates in storage and incentives; requires blockchain-rpc; ## implies swap-enable, chequebook-enable and storage-incentives-enable ## (bootnodes are exempt: swap only when set, chequebook follows swap) -## light - uploads and downloads only; requires blockchain-rpc +## light - uploads and downloads only; requires blockchain-rpc; beyond the +## free allowance also needs swap-enable and a funded chequebook ## ultra-light - free-tier downloads only; no blockchain connection needed ## When unset (the default), the mode is inferred from the deprecated full-node ## option and blockchain-rpc presence (deprecated, removed in v2.11.0). From e31a6ce96c15bc8c2a774e2c24068bde19611b51 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Fri, 16 Oct 2026 13:45:18 +0300 Subject: [PATCH 21/22] fix(cmd): report explicitly disabled implied options at info level An explicit swap-enable, chequebook-enable or storage-incentives-enable false on a full node is a deliberate choice, and the legacy migration suggests swap-enable=false itself, so logging it as a warning on every start gave operators a warning they could not resolve. Report it at Info, which stays visible at the default verbosity. --- cmd/bee/cmd/resolve_node_mode_test.go | 11 +++++++++++ cmd/bee/cmd/start.go | 7 ++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/cmd/bee/cmd/resolve_node_mode_test.go b/cmd/bee/cmd/resolve_node_mode_test.go index 011d8090401..fc8c4b34517 100644 --- a/cmd/bee/cmd/resolve_node_mode_test.go +++ b/cmd/bee/cmd/resolve_node_mode_test.go @@ -735,6 +735,17 @@ func TestResolveNodeModeWarnings(t *testing.T) { args: []string{"--blockchain-rpc-endpoint=" + testRPCEndpoint}, absent: []string{"pseudosettle allowance"}, }, + { + name: "full with swap explicitly disabled reports it at info", + args: []string{"--node-mode=full", "--swap-enable=false", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + want: []string{`"level"="info"`, `"msg"="option implied by node-mode is explicitly disabled" "node_mode"="full" "option"="swap-enable"`}, + absent: []string{`"level"="warning"`}, + }, + { + name: "full with implied options unset does not report them", + args: []string{"--node-mode=full", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, + absent: []string{"explicitly disabled"}, + }, { name: "full bootnode with storage incentives enabled", args: []string{"--node-mode=full", "--bootnode-mode", "--storage-incentives-enable", "--blockchain-rpc-endpoint=" + testRPCEndpoint}, diff --git a/cmd/bee/cmd/start.go b/cmd/bee/cmd/start.go index e7adec605b1..e42ba3791a4 100644 --- a/cmd/bee/cmd/start.go +++ b/cmd/bee/cmd/start.go @@ -513,8 +513,9 @@ func (c *command) resolveExplicitNodeMode(logger log.Logger, raw string) (node.N } // enableImpliedOption turns on an option implied by the node mode unless the -// operator set it explicitly. An explicit false is honored and reported, since -// it degrades the node relative to what the mode normally provides. +// operator set it explicitly. An explicit false is honored and reported at Info: +// it is a deliberate choice (the legacy migration may suggest it), not a +// problem for the operator to act on. func (c *command) enableImpliedOption(logger log.Logger, mode node.NodeMode, key string) { if !c.config.IsSet(key) { logger.Debug("enabling option implied by node-mode", "node_mode", mode, "option", key) @@ -522,7 +523,7 @@ func (c *command) enableImpliedOption(logger log.Logger, mode node.NodeMode, key return } if !c.config.GetBool(key) { - logger.Warning("option implied by node-mode is explicitly disabled", "node_mode", mode, "option", key) + logger.Info("option implied by node-mode is explicitly disabled", "node_mode", mode, "option", key) } } From e289384c0c9a3dffbafeeb0a20a9eeff8ccf7bb1 Mon Sep 17 00:00:00 2001 From: Calin Martinconi Date: Fri, 16 Oct 2026 18:00:40 +0300 Subject: [PATCH 22/22] ci: run beekeeper from master Beekeeper master configures nodes with full-node, which bee resolves through the legacy node mode. The beekeeper node-mode profiles merge after a bee release that knows node-mode. --- .github/workflows/beekeeper.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/beekeeper.yml b/.github/workflows/beekeeper.yml index feddb475aa4..f8c62a2add9 100644 --- a/.github/workflows/beekeeper.yml +++ b/.github/workflows/beekeeper.yml @@ -17,7 +17,7 @@ env: SETUP_CONTRACT_IMAGE: "ethersphere/bee-localchain" SETUP_CONTRACT_IMAGE_TAG: "0.9.4" BEELOCAL_BRANCH: "main" - BEEKEEPER_BRANCH: "refactor/node-mode-two-regimes" + BEEKEEPER_BRANCH: "master" BEEKEEPER_METRICS_ENABLED: false REACHABILITY_OVERRIDE_PUBLIC: true BATCHFACTOR_OVERRIDE_PUBLIC: 2