Skip to content

Commit e8ff2d5

Browse files
gystemdgammazeroGiulio Pivalidel
authored
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: #10782 (comment) --------- Co-authored-by: gystemd <[email protected]> Co-authored-by: gammazero <[email protected]> Co-authored-by: Giulio Piva <[email protected]> Co-authored-by: Marcin Rataj <[email protected]>
1 parent b3973fa commit e8ff2d5

File tree

7 files changed

+241
-11
lines changed

7 files changed

+241
-11
lines changed

config/bitswap.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ type Config struct {
4242
Version Version
4343

4444
Internal Internal // experimental/unstable options
45+
46+
Bitswap Bitswap `json:",omitempty"`
4547
}
4648

4749
const (

core/node/bitswap.go

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package node
22

33
import (
44
"context"
5+
"io"
56
"time"
67

78
"github.com/ipfs/boxo/bitswap"
@@ -12,12 +13,16 @@ import (
1213
"github.com/ipfs/boxo/exchange/providing"
1314
provider "github.com/ipfs/boxo/provider"
1415
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"
1519
"github.com/ipfs/kubo/config"
1620
irouting "github.com/ipfs/kubo/routing"
1721
"github.com/libp2p/go-libp2p/core/host"
1822
"github.com/libp2p/go-libp2p/core/routing"
1923
"go.uber.org/fx"
2024

25+
blocks "github.com/ipfs/go-block-format"
2126
"github.com/ipfs/kubo/core/node/helpers"
2227
)
2328

@@ -72,18 +77,21 @@ type bitswapIn struct {
7277
}
7378

7479
// 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.
7782
func Bitswap(provide bool) interface{} {
7883
return func(in bitswapIn, lc fx.Lifecycle) (*bitswap.Bitswap, error) {
7984
bitswapNetwork := bsnet.NewFromIpfsHost(in.Host)
80-
85+
var blockstoree blockstore.Blockstore = in.Bs
8186
var provider routing.ContentDiscovery
87+
8288
if provide {
89+
8390
var maxProviders int = DefaultMaxProviders
8491
if in.Cfg.Internal.Bitswap != nil {
8592
maxProviders = int(in.Cfg.Internal.Bitswap.ProviderSearchMaxResults.WithDefault(DefaultMaxProviders))
8693
}
94+
8795
pqm, err := rpqm.New(bitswapNetwork,
8896
in.Rt,
8997
rpqm.WithMaxProviders(maxProviders),
@@ -93,10 +101,16 @@ func Bitswap(provide bool) interface{} {
93101
return nil, err
94102
}
95103
in.BitswapOpts = append(in.BitswapOpts, bitswap.WithClientOption(client.WithDefaultProviderQueryManager(false)))
104+
in.BitswapOpts = append(in.BitswapOpts, bitswap.WithServerEnabled(true))
96105
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))
98111
}
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...)
100114

101115
lc.Append(fx.Hook{
102116
OnStop: func(ctx context.Context) error {
@@ -108,8 +122,12 @@ func Bitswap(provide bool) interface{} {
108122
}
109123

110124
// 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{} {
112127
return func(in *bitswap.Bitswap, lc fx.Lifecycle) exchange.Interface {
128+
if !isBitswapActive {
129+
return &noopExchange{closer: in}
130+
}
113131
lc.Append(fx.Hook{
114132
OnStop: func(ctx context.Context) error {
115133
return in.Close()
@@ -144,3 +162,25 @@ func ProvidingExchange(provide bool) interface{} {
144162
return exch
145163
}
146164
}
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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -335,13 +335,15 @@ func Online(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part
335335
recordLifetime = d
336336
}
337337

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
340342

341343
return fx.Options(
342344
fx.Provide(BitswapOptions(cfg)),
343345
fx.Provide(Bitswap(shouldBitswapProvide)),
344-
fx.Provide(OnlineExchange()),
346+
fx.Provide(OnlineExchange(isBitswapEnabled)),
345347
// Replace our Exchange with a Providing exchange!
346348
fx.Decorate(ProvidingExchange(shouldBitswapProvide)),
347349
fx.Provide(DNSResolver),

docs/changelogs/v0.35.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ See [`Reprovider.Strategy`](https://github.com/ipfs/kubo/blob/master/docs/config
4141

4242
#### Additional new configuration options
4343

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.
4546
- [`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.
4647

4748
#### Grid view in WebUI
@@ -144,5 +145,4 @@ See other caveats and configuration options at [`kubo/docs/datastores.md#pebbled
144145
- update `pebble` to [v2.0.3](https://github.com/cockroachdb/pebble/releases/tag/v2.0.3)
145146

146147
### 📝 Changelog
147-
148148
### 👨‍👩‍👧‍👦 Contributors

docs/config.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ config file at runtime.
3636
- [`AutoTLS.RegistrationToken`](#autotlsregistrationtoken)
3737
- [`AutoTLS.RegistrationDelay`](#autotlsregistrationdelay)
3838
- [`AutoTLS.CAEndpoint`](#autotlscaendpoint)
39+
- [`Bitswap`](#bitswap)
40+
- [`Bitswap.Enabled`](#bitswapenabled)
41+
- [`Bitswap.ServerEnabled`](#bitswapserverenabled)
3942
- [`Bootstrap`](#bootstrap)
4043
- [`Datastore`](#datastore)
4144
- [`Datastore.StorageMax`](#datastorestoragemax)
@@ -619,6 +622,33 @@ Default: [certmagic.LetsEncryptProductionCA](https://pkg.go.dev/github.com/caddy
619622

620623
Type: `optionalString`
621624

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+
622652
## `Bootstrap`
623653

624654
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
11511181
### `Internal.Bitswap`
11521182

11531183
`Internal.Bitswap` contains knobs for tuning bitswap resource utilization.
1184+
1185+
> [!TIP]
1186+
> For high level configuration see [`Bitswap`](#bitswap).
1187+
11541188
The knobs (below) document how their value should related to each other.
11551189
Whether their values should be raised or lowered should be determined
11561190
based on the metrics `ipfs_bitswap_active_tasks`, `ipfs_bitswap_pending_tasks`,

test/cli/bitswap_config_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)