feat(config): ability to disable Bitswap fully or just server (#10782)
* feat: add Bitswap configuration and related tests * fix: update Bitswap function to use 'provide' parameter for server enablement * docs: update changelog for Bitswap functionality changes * fix: update Bitswap server enablement logic and improve related tests * fix: rename BitswapConfig to Bitswap and update references * docs: config and changelog * fix: `ipfs cat` panic when `Bitswap.Enabled=false` Fixes panic described in: https://github.com/ipfs/kubo/pull/10782#discussion_r2069116219 --------- Co-authored-by: gystemd <gystemd@gmail.com> Co-authored-by: gammazero <11790789+gammazero@users.noreply.github.com> Co-authored-by: Giulio Piva <giulio.piva@dedicated.world> Co-authored-by: Marcin Rataj <lidel@lidel.org>
IGP committed
Apr 30, 2025 at 22:23 UTC
e8ff2d59de68b014ffee2c0d741f33612e5af5a3
7 files changed
+241
-11
config/bitswap.go
new
+14
@@ -0,0 +1,14 @@
1
+package config
2
+
3
+// Bitswap holds Bitswap configuration options
4
+type Bitswap struct {
5
+ // Enabled controls both client and server (enabled by default)
6
+ Enabled Flag `json:",omitempty"`
7
+ // ServerEnabled controls if the node responds to WANTs (depends on Enabled, enabled by default)
8
+ ServerEnabled Flag `json:",omitempty"`
9
+}
10
+
11
+const (
12
+ DefaultBitswapEnabled = true
13
+ DefaultBitswapServerEnabled = true
14
+)
config/config.go
+2
@@ -42,6 +42,8 @@ type Config struct {
42
Version Version
43
44
Internal Internal // experimental/unstable options
45
+
46
+ Bitswap Bitswap `json:",omitempty"`
47
}
48
49
const (
core/node/bitswap.go
+46
-6
@@ -2,6 +2,7 @@ package node
2
3
import (
4
"context"
5
+ "io"
6
"time"
7
8
"github.com/ipfs/boxo/bitswap"
@@ -12,12 +13,16 @@ import (
13
"github.com/ipfs/boxo/exchange/providing"
14
provider "github.com/ipfs/boxo/provider"
15
rpqm "github.com/ipfs/boxo/routing/providerquerymanager"
16
+ "github.com/ipfs/go-cid"
17
+ "github.com/ipfs/go-datastore"
18
+ ipld "github.com/ipfs/go-ipld-format"
19
"github.com/ipfs/kubo/config"
20
irouting "github.com/ipfs/kubo/routing"
21
"github.com/libp2p/go-libp2p/core/host"
22
"github.com/libp2p/go-libp2p/core/routing"
23
"go.uber.org/fx"
24
25
+ blocks "github.com/ipfs/go-block-format"
26
"github.com/ipfs/kubo/core/node/helpers"
27
)
28
@@ -72,18 +77,21 @@ type bitswapIn struct {
77
}
78
79
// Bitswap creates the BitSwap server/client instance.
75
-// Additional options to bitswap.New can be provided via the "bitswap-options"
76
-// group.
80
+// If Bitswap.ServerEnabled is false, the node will act only as a client
81
+// using an empty blockstore to prevent serving blocks to other peers.
82
func Bitswap(provide bool) interface{} {
83
return func(in bitswapIn, lc fx.Lifecycle) (*bitswap.Bitswap, error) {
84
bitswapNetwork := bsnet.NewFromIpfsHost(in.Host)
80
-
85
+ var blockstoree blockstore.Blockstore = in.Bs
86
var provider routing.ContentDiscovery
87
+
88
if provide {
89
+
90
var maxProviders int = DefaultMaxProviders
91
if in.Cfg.Internal.Bitswap != nil {
92
maxProviders = int(in.Cfg.Internal.Bitswap.ProviderSearchMaxResults.WithDefault(DefaultMaxProviders))
93
}
94
+
95
pqm, err := rpqm.New(bitswapNetwork,
96
in.Rt,
97
rpqm.WithMaxProviders(maxProviders),
@@ -93,10 +101,16 @@ func Bitswap(provide bool) interface{} {
101
return nil, err
102
}
103
in.BitswapOpts = append(in.BitswapOpts, bitswap.WithClientOption(client.WithDefaultProviderQueryManager(false)))
104
+ in.BitswapOpts = append(in.BitswapOpts, bitswap.WithServerEnabled(true))
105
provider = pqm
97
-
106
+ } else {
107
+ provider = nil
108
+ // When server is disabled, use an empty blockstore to prevent serving blocks
109
+ blockstoree = blockstore.NewBlockstore(datastore.NewMapDatastore())
110
+ in.BitswapOpts = append(in.BitswapOpts, bitswap.WithServerEnabled(false))
111
}
99
- bs := bitswap.New(helpers.LifecycleCtx(in.Mctx, lc), bitswapNetwork, provider, in.Bs, in.BitswapOpts...)
112
+
113
+ bs := bitswap.New(helpers.LifecycleCtx(in.Mctx, lc), bitswapNetwork, provider, blockstoree, in.BitswapOpts...)
114
115
lc.Append(fx.Hook{
116
OnStop: func(ctx context.Context) error {
@@ -108,8 +122,12 @@ func Bitswap(provide bool) interface{} {
122
}
123
124
// OnlineExchange creates new LibP2P backed block exchange.
111
-func OnlineExchange() interface{} {
125
+// Returns a no-op exchange if Bitswap is disabled.
126
+func OnlineExchange(isBitswapActive bool) interface{} {
127
return func(in *bitswap.Bitswap, lc fx.Lifecycle) exchange.Interface {
128
+ if !isBitswapActive {
129
+ return &noopExchange{closer: in}
130
+ }
131
lc.Append(fx.Hook{
132
OnStop: func(ctx context.Context) error {
133
return in.Close()
@@ -144,3 +162,25 @@ func ProvidingExchange(provide bool) interface{} {
162
return exch
163
}
164
}
165
+
166
+type noopExchange struct {
167
+ closer io.Closer
168
+}
169
+
170
+func (e *noopExchange) GetBlock(ctx context.Context, c cid.Cid) (blocks.Block, error) {
171
+ return nil, ipld.ErrNotFound{Cid: c}
172
+}
173
+
174
+func (e *noopExchange) GetBlocks(ctx context.Context, cids []cid.Cid) (<-chan blocks.Block, error) {
175
+ ch := make(chan blocks.Block)
176
+ close(ch)
177
+ return ch, nil
178
+}
179
+
180
+func (e *noopExchange) NotifyNewBlocks(ctx context.Context, blocks ...blocks.Block) error {
181
+ return nil
182
+}
183
+
184
+func (e *noopExchange) Close() error {
185
+ return e.closer.Close()
186
+}
core/node/groups.go
+5
-3
@@ -335,13 +335,15 @@ func Online(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part
335
recordLifetime = d
336
}
337
338
- /* don't provide from bitswap when the strategic provider service is active */
339
- shouldBitswapProvide := !cfg.Experimental.StrategicProviding
338
+ isBitswapEnabled := cfg.Bitswap.Enabled.WithDefault(config.DefaultBitswapEnabled)
339
+ isBitswapServerEnabled := cfg.Bitswap.ServerEnabled.WithDefault(config.DefaultBitswapServerEnabled)
340
+ // Don't provide from bitswap when the strategic provider service is active
341
+ shouldBitswapProvide := isBitswapEnabled && isBitswapServerEnabled && !cfg.Experimental.StrategicProviding
342
343
return fx.Options(
344
fx.Provide(BitswapOptions(cfg)),
345
fx.Provide(Bitswap(shouldBitswapProvide)),
344
- fx.Provide(OnlineExchange()),
346
+ fx.Provide(OnlineExchange(isBitswapEnabled)),
347
// Replace our Exchange with a Providing exchange!
348
fx.Decorate(ProvidingExchange(shouldBitswapProvide)),
349
fx.Provide(DNSResolver),
docs/changelogs/v0.35.md
+2
-2
@@ -41,7 +41,8 @@ See [`Reprovider.Strategy`](https://github.com/ipfs/kubo/blob/master/docs/config
41
42
#### Additional new configuration options
43
44
-- [`Internal.Bitswap.ProviderSearchMaxResults`](https://github.com/ipfs/kubo/blob/master/docs/config.md##internalbitswapprovidersearchmaxresults) for adjusting the maximum number of providers bitswap client should aim at before it stops searching for new ones.
44
+- [`Bitswap`](https://github.com/ipfs/kubo/blob/master/docs/config.md#bitswap) section with `Enabled` and `ServerEnabled` flags determine whether Kubo initializes Bitswap, enabling just the client or both the client and server.
45
+- [`Internal.Bitswap.ProviderSearchMaxResults`](https://github.com/ipfs/kubo/blob/master/docs/config.md#internalbitswapprovidersearchmaxresults) for adjusting the maximum number of providers bitswap client should aim at before it stops searching for new ones.
46
- [`Routing.IgnoreProviders`](https://github.com/ipfs/kubo/blob/master/docs/config.md#routingignoreproviders) allows ignoring specific peer IDs when returned by the content routing system as providers of content.
47
48
#### Grid view in WebUI
@@ -144,5 +145,4 @@ See other caveats and configuration options at [`kubo/docs/datastores.md#pebbled
145
- update `pebble` to [v2.0.3](https://github.com/cockroachdb/pebble/releases/tag/v2.0.3)
146
147
### 📝 Changelog
147
-
148
### 👨👩👧👦 Contributors
docs/config.md
+34
@@ -36,6 +36,9 @@ config file at runtime.
36
- [`AutoTLS.RegistrationToken`](#autotlsregistrationtoken)
37
- [`AutoTLS.RegistrationDelay`](#autotlsregistrationdelay)
38
- [`AutoTLS.CAEndpoint`](#autotlscaendpoint)
39
+ - [`Bitswap`](#bitswap)
40
+ - [`Bitswap.Enabled`](#bitswapenabled)
41
+ - [`Bitswap.ServerEnabled`](#bitswapserverenabled)
42
- [`Bootstrap`](#bootstrap)
43
- [`Datastore`](#datastore)
44
- [`Datastore.StorageMax`](#datastorestoragemax)
@@ -619,6 +622,33 @@ Default: [certmagic.LetsEncryptProductionCA](https://pkg.go.dev/github.com/caddy
622
623
Type: `optionalString`
624
625
+## `Bitswap`
626
+
627
+High level client and server configuration of the [Bitswap Protocol](https://specs.ipfs.tech/bitswap-protocol/).
628
+
629
+For internal configuration see [`Internal.Bitswap`](#internalbitswap).
630
+
631
+### `Bitswap.Enabled`
632
+
633
+Manages both Bitswap client and server functionality. For testing or operating a node without Bitswap requirements.
634
+
635
+> [!WARNING]
636
+> Bitswap is a core component of Kubo, and disabling it completely may cause unpredictable outcomes. Treat this as experimental and use it solely for testing purposes.
637
+
638
+Default: `true`
639
+
640
+Type: `flag`
641
+
642
+### `Bitswap.ServerEnabled`
643
+
644
+Determines whether Kubo functions as a Bitswap server to host and respond to block requests.
645
+
646
+Disabling the server retains client and protocol support in libp2p identify responses but causes Kubo to reply with "don't have" to all block requests.
647
+
648
+Default: `true`
649
+
650
+Type: `flag`
651
+
652
## `Bootstrap`
653
654
Bootstrap is an array of [multiaddrs][multiaddr] of trusted nodes that your node connects to, to fetch other nodes of the network on startup.
@@ -1151,6 +1181,10 @@ This section includes internal knobs for various subsystems to allow advanced us
1181
### `Internal.Bitswap`
1182
1183
`Internal.Bitswap` contains knobs for tuning bitswap resource utilization.
1184
+
1185
+> [!TIP]
1186
+> For high level configuration see [`Bitswap`](#bitswap).
1187
+
1188
The knobs (below) document how their value should related to each other.
1189
Whether their values should be raised or lowered should be determined
1190
based on the metrics `ipfs_bitswap_active_tasks`, `ipfs_bitswap_pending_tasks`,
test/cli/bitswap_config_test.go
new
+138
@@ -0,0 +1,138 @@
1
+package cli
2
+
3
+import (
4
+ "testing"
5
+ "time"
6
+
7
+ "github.com/ipfs/kubo/config"
8
+ "github.com/ipfs/kubo/test/cli/harness"
9
+ "github.com/ipfs/kubo/test/cli/testutils"
10
+ "github.com/stretchr/testify/assert"
11
+)
12
+
13
+func TestBitswapConfig(t *testing.T) {
14
+ t.Parallel()
15
+
16
+ // Create test data that will be shared between nodes
17
+ testData := testutils.RandomBytes(100)
18
+
19
+ t.Run("server enabled (default)", func(t *testing.T) {
20
+ t.Parallel()
21
+ h := harness.NewT(t)
22
+ provider := h.NewNode().Init().StartDaemon()
23
+ requester := h.NewNode().Init().StartDaemon()
24
+
25
+ hash := provider.IPFSAddStr(string(testData))
26
+ requester.Connect(provider)
27
+
28
+ res := requester.IPFS("cat", hash)
29
+ assert.Equal(t, testData, res.Stdout.Bytes(), "retrieved data should match original")
30
+ })
31
+
32
+ t.Run("server disabled", func(t *testing.T) {
33
+ t.Parallel()
34
+ h := harness.NewT(t)
35
+
36
+ provider := h.NewNode().Init()
37
+ provider.SetIPFSConfig("Bitswap.ServerEnabled", false)
38
+ provider = provider.StartDaemon()
39
+
40
+ requester := h.NewNode().Init().StartDaemon()
41
+
42
+ hash := provider.IPFSAddStr(string(testData))
43
+ requester.Connect(provider)
44
+
45
+ // If the data was available, it would be retrieved immediately.
46
+ // Therefore, after the timeout, we can assume the data is not available
47
+ // i.e. the server is disabled
48
+ timeout := time.After(3 * time.Second)
49
+ dataChan := make(chan []byte)
50
+
51
+ go func() {
52
+ res := requester.RunIPFS("cat", hash)
53
+ dataChan <- res.Stdout.Bytes()
54
+ }()
55
+
56
+ select {
57
+ case data := <-dataChan:
58
+ assert.NotEqual(t, testData, data, "retrieved data should not match original")
59
+ case <-timeout:
60
+ t.Log("Test passed: operation timed out after 3 seconds as expected")
61
+ }
62
+ })
63
+
64
+ t.Run("server disabled and client enabled", func(t *testing.T) {
65
+ t.Parallel()
66
+ h := harness.NewT(t)
67
+
68
+ provider := h.NewNode().Init()
69
+ provider.SetIPFSConfig("Bitswap.ServerEnabled", false)
70
+ provider.StartDaemon()
71
+
72
+ requester := h.NewNode().Init().StartDaemon()
73
+ hash := requester.IPFSAddStr(string(testData))
74
+
75
+ provider.Connect(requester)
76
+
77
+ // Even when the server is disabled, the client should be able to retrieve data
78
+ res := provider.RunIPFS("cat", hash)
79
+ assert.Equal(t, testData, res.Stdout.Bytes(), "retrieved data should match original")
80
+ })
81
+
82
+ t.Run("bitswap completely disabled", func(t *testing.T) {
83
+ t.Parallel()
84
+ h := harness.NewT(t)
85
+
86
+ requester := h.NewNode().Init()
87
+ requester.UpdateConfig(func(cfg *config.Config) {
88
+ cfg.Bitswap.Enabled = config.False
89
+ cfg.Bitswap.ServerEnabled = config.False
90
+ })
91
+ requester.StartDaemon()
92
+
93
+ provider := h.NewNode().Init().StartDaemon()
94
+ hash := provider.IPFSAddStr(string(testData))
95
+
96
+ requester.Connect(provider)
97
+ res := requester.RunIPFS("cat", hash)
98
+ assert.Equal(t, []byte{}, res.Stdout.Bytes(), "cat should not return any data")
99
+ assert.Contains(t, res.Stderr.String(), "Error: ipld: could not find")
100
+
101
+ // Verify that basic operations still work with bitswap disabled
102
+ res = requester.IPFS("id")
103
+ assert.Equal(t, 0, res.ExitCode(), "basic IPFS operations should work")
104
+ res = requester.IPFS("bitswap", "stat")
105
+ assert.Equal(t, 0, res.ExitCode(), "bitswap stat should work even with bitswap disabled")
106
+ res = requester.IPFS("bitswap", "wantlist")
107
+ assert.Equal(t, 0, res.ExitCode(), "bitswap wantlist should work even with bitswap disabled")
108
+
109
+ // Verify local operations still work
110
+ hashNew := requester.IPFSAddStr("random")
111
+ res = requester.IPFS("cat", hashNew)
112
+ assert.Equal(t, []byte("random"), res.Stdout.Bytes(), "cat should return the added data")
113
+ })
114
+
115
+ // TODO: Disabling Bitswap.Enabled should remove /ifps/bitswap* protocols from `ipfs id`
116
+ // t.Run("bitswap protocols disabled", func(t *testing.T) {
117
+ // t.Parallel()
118
+ // harness.EnableDebugLogging()
119
+ // h := harness.NewT(t)
120
+
121
+ // provider := h.NewNode().Init()
122
+ // provider.SetIPFSConfig("Bitswap.ServerEnabled", false)
123
+ // provider = provider.StartDaemon()
124
+ // requester := h.NewNode().Init().StartDaemon()
125
+ // requester.Connect(provider)
126
+ // // Parse and check ID output
127
+ // res := provider.IPFS("id", "-f", "<protocols>")
128
+ // protocols := strings.Split(strings.TrimSpace(res.Stdout.String()), "\n")
129
+
130
+ // // No bitswap protocols should be present
131
+ // for _, proto := range protocols {
132
+ // assert.NotContains(t, proto, bsnet.ProtocolBitswap, "bitswap protocol %s should not be advertised when server is disabled", proto)
133
+ // assert.NotContains(t, proto, bsnet.ProtocolBitswapNoVers, "bitswap protocol %s should not be advertised when server is disabled", proto)
134
+ // assert.NotContains(t, proto, bsnet.ProtocolBitswapOneOne, "bitswap protocol %s should not be advertised when server is disabled", proto)
135
+ // assert.NotContains(t, proto, bsnet.ProtocolBitswapOneZero, "bitswap protocol %s should not be advertised when server is disabled", proto)
136
+ // }
137
+ // })
138
+}