From e8ff2d59de68b014ffee2c0d741f33612e5af5a3 Mon Sep 17 00:00:00 2001 From: IGP <84940636+gystemd@users.noreply.github.com> Date: Wed, 30 Apr 2025 22:23:51 +0200 Subject: [PATCH] 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 Co-authored-by: gammazero <11790789+gammazero@users.noreply.github.com> Co-authored-by: Giulio Piva Co-authored-by: Marcin Rataj --- config/bitswap.go | 14 ++++ config/config.go | 2 + core/node/bitswap.go | 52 ++++++++++-- core/node/groups.go | 8 +- docs/changelogs/v0.35.md | 4 +- docs/config.md | 34 ++++++++ test/cli/bitswap_config_test.go | 138 ++++++++++++++++++++++++++++++++ 7 files changed, 241 insertions(+), 11 deletions(-) create mode 100644 config/bitswap.go create mode 100644 test/cli/bitswap_config_test.go diff --git a/config/bitswap.go b/config/bitswap.go new file mode 100644 index 000000000..38c817577 --- /dev/null +++ b/config/bitswap.go @@ -0,0 +1,14 @@ +package config + +// Bitswap holds Bitswap configuration options +type Bitswap struct { + // Enabled controls both client and server (enabled by default) + Enabled Flag `json:",omitempty"` + // ServerEnabled controls if the node responds to WANTs (depends on Enabled, enabled by default) + ServerEnabled Flag `json:",omitempty"` +} + +const ( + DefaultBitswapEnabled = true + DefaultBitswapServerEnabled = true +) diff --git a/config/config.go b/config/config.go index 3db7573d0..68e57e1c3 100644 --- a/config/config.go +++ b/config/config.go @@ -42,6 +42,8 @@ type Config struct { Version Version Internal Internal // experimental/unstable options + + Bitswap Bitswap `json:",omitempty"` } const ( diff --git a/core/node/bitswap.go b/core/node/bitswap.go index 2408fe371..b69e7f510 100644 --- a/core/node/bitswap.go +++ b/core/node/bitswap.go @@ -2,6 +2,7 @@ package node import ( "context" + "io" "time" "github.com/ipfs/boxo/bitswap" @@ -12,12 +13,16 @@ import ( "github.com/ipfs/boxo/exchange/providing" provider "github.com/ipfs/boxo/provider" rpqm "github.com/ipfs/boxo/routing/providerquerymanager" + "github.com/ipfs/go-cid" + "github.com/ipfs/go-datastore" + ipld "github.com/ipfs/go-ipld-format" "github.com/ipfs/kubo/config" irouting "github.com/ipfs/kubo/routing" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/routing" "go.uber.org/fx" + blocks "github.com/ipfs/go-block-format" "github.com/ipfs/kubo/core/node/helpers" ) @@ -72,18 +77,21 @@ type bitswapIn struct { } // Bitswap creates the BitSwap server/client instance. -// Additional options to bitswap.New can be provided via the "bitswap-options" -// group. +// If Bitswap.ServerEnabled is false, the node will act only as a client +// using an empty blockstore to prevent serving blocks to other peers. func Bitswap(provide bool) interface{} { return func(in bitswapIn, lc fx.Lifecycle) (*bitswap.Bitswap, error) { bitswapNetwork := bsnet.NewFromIpfsHost(in.Host) - + var blockstoree blockstore.Blockstore = in.Bs var provider routing.ContentDiscovery + if provide { + var maxProviders int = DefaultMaxProviders if in.Cfg.Internal.Bitswap != nil { maxProviders = int(in.Cfg.Internal.Bitswap.ProviderSearchMaxResults.WithDefault(DefaultMaxProviders)) } + pqm, err := rpqm.New(bitswapNetwork, in.Rt, rpqm.WithMaxProviders(maxProviders), @@ -93,10 +101,16 @@ func Bitswap(provide bool) interface{} { return nil, err } in.BitswapOpts = append(in.BitswapOpts, bitswap.WithClientOption(client.WithDefaultProviderQueryManager(false))) + in.BitswapOpts = append(in.BitswapOpts, bitswap.WithServerEnabled(true)) provider = pqm - + } else { + provider = nil + // When server is disabled, use an empty blockstore to prevent serving blocks + blockstoree = blockstore.NewBlockstore(datastore.NewMapDatastore()) + in.BitswapOpts = append(in.BitswapOpts, bitswap.WithServerEnabled(false)) } - bs := bitswap.New(helpers.LifecycleCtx(in.Mctx, lc), bitswapNetwork, provider, in.Bs, in.BitswapOpts...) + + bs := bitswap.New(helpers.LifecycleCtx(in.Mctx, lc), bitswapNetwork, provider, blockstoree, in.BitswapOpts...) lc.Append(fx.Hook{ OnStop: func(ctx context.Context) error { @@ -108,8 +122,12 @@ func Bitswap(provide bool) interface{} { } // OnlineExchange creates new LibP2P backed block exchange. -func OnlineExchange() interface{} { +// Returns a no-op exchange if Bitswap is disabled. +func OnlineExchange(isBitswapActive bool) interface{} { return func(in *bitswap.Bitswap, lc fx.Lifecycle) exchange.Interface { + if !isBitswapActive { + return &noopExchange{closer: in} + } lc.Append(fx.Hook{ OnStop: func(ctx context.Context) error { return in.Close() @@ -144,3 +162,25 @@ func ProvidingExchange(provide bool) interface{} { return exch } } + +type noopExchange struct { + closer io.Closer +} + +func (e *noopExchange) GetBlock(ctx context.Context, c cid.Cid) (blocks.Block, error) { + return nil, ipld.ErrNotFound{Cid: c} +} + +func (e *noopExchange) GetBlocks(ctx context.Context, cids []cid.Cid) (<-chan blocks.Block, error) { + ch := make(chan blocks.Block) + close(ch) + return ch, nil +} + +func (e *noopExchange) NotifyNewBlocks(ctx context.Context, blocks ...blocks.Block) error { + return nil +} + +func (e *noopExchange) Close() error { + return e.closer.Close() +} diff --git a/core/node/groups.go b/core/node/groups.go index 0e28444be..f82da293f 100644 --- a/core/node/groups.go +++ b/core/node/groups.go @@ -335,13 +335,15 @@ func Online(bcfg *BuildCfg, cfg *config.Config, userResourceOverrides rcmgr.Part recordLifetime = d } - /* don't provide from bitswap when the strategic provider service is active */ - shouldBitswapProvide := !cfg.Experimental.StrategicProviding + isBitswapEnabled := cfg.Bitswap.Enabled.WithDefault(config.DefaultBitswapEnabled) + isBitswapServerEnabled := cfg.Bitswap.ServerEnabled.WithDefault(config.DefaultBitswapServerEnabled) + // Don't provide from bitswap when the strategic provider service is active + shouldBitswapProvide := isBitswapEnabled && isBitswapServerEnabled && !cfg.Experimental.StrategicProviding return fx.Options( fx.Provide(BitswapOptions(cfg)), fx.Provide(Bitswap(shouldBitswapProvide)), - fx.Provide(OnlineExchange()), + fx.Provide(OnlineExchange(isBitswapEnabled)), // Replace our Exchange with a Providing exchange! fx.Decorate(ProvidingExchange(shouldBitswapProvide)), fx.Provide(DNSResolver), diff --git a/docs/changelogs/v0.35.md b/docs/changelogs/v0.35.md index 825d536fa..07543b24d 100644 --- a/docs/changelogs/v0.35.md +++ b/docs/changelogs/v0.35.md @@ -41,7 +41,8 @@ See [`Reprovider.Strategy`](https://github.com/ipfs/kubo/blob/master/docs/config #### Additional new configuration options -- [`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. +- [`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. +- [`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. - [`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. #### Grid view in WebUI @@ -144,5 +145,4 @@ See other caveats and configuration options at [`kubo/docs/datastores.md#pebbled - update `pebble` to [v2.0.3](https://github.com/cockroachdb/pebble/releases/tag/v2.0.3) ### ๐Ÿ“ Changelog - ### ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributors diff --git a/docs/config.md b/docs/config.md index 504597aa2..770155783 100644 --- a/docs/config.md +++ b/docs/config.md @@ -36,6 +36,9 @@ config file at runtime. - [`AutoTLS.RegistrationToken`](#autotlsregistrationtoken) - [`AutoTLS.RegistrationDelay`](#autotlsregistrationdelay) - [`AutoTLS.CAEndpoint`](#autotlscaendpoint) + - [`Bitswap`](#bitswap) + - [`Bitswap.Enabled`](#bitswapenabled) + - [`Bitswap.ServerEnabled`](#bitswapserverenabled) - [`Bootstrap`](#bootstrap) - [`Datastore`](#datastore) - [`Datastore.StorageMax`](#datastorestoragemax) @@ -619,6 +622,33 @@ Default: [certmagic.LetsEncryptProductionCA](https://pkg.go.dev/github.com/caddy Type: `optionalString` +## `Bitswap` + +High level client and server configuration of the [Bitswap Protocol](https://specs.ipfs.tech/bitswap-protocol/). + +For internal configuration see [`Internal.Bitswap`](#internalbitswap). + +### `Bitswap.Enabled` + +Manages both Bitswap client and server functionality. For testing or operating a node without Bitswap requirements. + +> [!WARNING] +> 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. + +Default: `true` + +Type: `flag` + +### `Bitswap.ServerEnabled` + +Determines whether Kubo functions as a Bitswap server to host and respond to block requests. + +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. + +Default: `true` + +Type: `flag` + ## `Bootstrap` 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 ### `Internal.Bitswap` `Internal.Bitswap` contains knobs for tuning bitswap resource utilization. + +> [!TIP] +> For high level configuration see [`Bitswap`](#bitswap). + The knobs (below) document how their value should related to each other. Whether their values should be raised or lowered should be determined based on the metrics `ipfs_bitswap_active_tasks`, `ipfs_bitswap_pending_tasks`, diff --git a/test/cli/bitswap_config_test.go b/test/cli/bitswap_config_test.go new file mode 100644 index 000000000..19ae3e34d --- /dev/null +++ b/test/cli/bitswap_config_test.go @@ -0,0 +1,138 @@ +package cli + +import ( + "testing" + "time" + + "github.com/ipfs/kubo/config" + "github.com/ipfs/kubo/test/cli/harness" + "github.com/ipfs/kubo/test/cli/testutils" + "github.com/stretchr/testify/assert" +) + +func TestBitswapConfig(t *testing.T) { + t.Parallel() + + // Create test data that will be shared between nodes + testData := testutils.RandomBytes(100) + + t.Run("server enabled (default)", func(t *testing.T) { + t.Parallel() + h := harness.NewT(t) + provider := h.NewNode().Init().StartDaemon() + requester := h.NewNode().Init().StartDaemon() + + hash := provider.IPFSAddStr(string(testData)) + requester.Connect(provider) + + res := requester.IPFS("cat", hash) + assert.Equal(t, testData, res.Stdout.Bytes(), "retrieved data should match original") + }) + + t.Run("server disabled", func(t *testing.T) { + t.Parallel() + h := harness.NewT(t) + + provider := h.NewNode().Init() + provider.SetIPFSConfig("Bitswap.ServerEnabled", false) + provider = provider.StartDaemon() + + requester := h.NewNode().Init().StartDaemon() + + hash := provider.IPFSAddStr(string(testData)) + requester.Connect(provider) + + // If the data was available, it would be retrieved immediately. + // Therefore, after the timeout, we can assume the data is not available + // i.e. the server is disabled + timeout := time.After(3 * time.Second) + dataChan := make(chan []byte) + + go func() { + res := requester.RunIPFS("cat", hash) + dataChan <- res.Stdout.Bytes() + }() + + select { + case data := <-dataChan: + assert.NotEqual(t, testData, data, "retrieved data should not match original") + case <-timeout: + t.Log("Test passed: operation timed out after 3 seconds as expected") + } + }) + + t.Run("server disabled and client enabled", func(t *testing.T) { + t.Parallel() + h := harness.NewT(t) + + provider := h.NewNode().Init() + provider.SetIPFSConfig("Bitswap.ServerEnabled", false) + provider.StartDaemon() + + requester := h.NewNode().Init().StartDaemon() + hash := requester.IPFSAddStr(string(testData)) + + provider.Connect(requester) + + // Even when the server is disabled, the client should be able to retrieve data + res := provider.RunIPFS("cat", hash) + assert.Equal(t, testData, res.Stdout.Bytes(), "retrieved data should match original") + }) + + t.Run("bitswap completely disabled", func(t *testing.T) { + t.Parallel() + h := harness.NewT(t) + + requester := h.NewNode().Init() + requester.UpdateConfig(func(cfg *config.Config) { + cfg.Bitswap.Enabled = config.False + cfg.Bitswap.ServerEnabled = config.False + }) + requester.StartDaemon() + + provider := h.NewNode().Init().StartDaemon() + hash := provider.IPFSAddStr(string(testData)) + + requester.Connect(provider) + res := requester.RunIPFS("cat", hash) + assert.Equal(t, []byte{}, res.Stdout.Bytes(), "cat should not return any data") + assert.Contains(t, res.Stderr.String(), "Error: ipld: could not find") + + // Verify that basic operations still work with bitswap disabled + res = requester.IPFS("id") + assert.Equal(t, 0, res.ExitCode(), "basic IPFS operations should work") + res = requester.IPFS("bitswap", "stat") + assert.Equal(t, 0, res.ExitCode(), "bitswap stat should work even with bitswap disabled") + res = requester.IPFS("bitswap", "wantlist") + assert.Equal(t, 0, res.ExitCode(), "bitswap wantlist should work even with bitswap disabled") + + // Verify local operations still work + hashNew := requester.IPFSAddStr("random") + res = requester.IPFS("cat", hashNew) + assert.Equal(t, []byte("random"), res.Stdout.Bytes(), "cat should return the added data") + }) + + // TODO: Disabling Bitswap.Enabled should remove /ifps/bitswap* protocols from `ipfs id` + // t.Run("bitswap protocols disabled", func(t *testing.T) { + // t.Parallel() + // harness.EnableDebugLogging() + // h := harness.NewT(t) + + // provider := h.NewNode().Init() + // provider.SetIPFSConfig("Bitswap.ServerEnabled", false) + // provider = provider.StartDaemon() + // requester := h.NewNode().Init().StartDaemon() + // requester.Connect(provider) + // // Parse and check ID output + // res := provider.IPFS("id", "-f", "") + // protocols := strings.Split(strings.TrimSpace(res.Stdout.String()), "\n") + + // // No bitswap protocols should be present + // for _, proto := range protocols { + // assert.NotContains(t, proto, bsnet.ProtocolBitswap, "bitswap protocol %s should not be advertised when server is disabled", proto) + // assert.NotContains(t, proto, bsnet.ProtocolBitswapNoVers, "bitswap protocol %s should not be advertised when server is disabled", proto) + // assert.NotContains(t, proto, bsnet.ProtocolBitswapOneOne, "bitswap protocol %s should not be advertised when server is disabled", proto) + // assert.NotContains(t, proto, bsnet.ProtocolBitswapOneZero, "bitswap protocol %s should not be advertised when server is disabled", proto) + // } + // }) +}