Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions config/bitswap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package config

// BitswapConfig holds Bitswap configuration options
type BitswapConfig struct {
Comment thread
lidel marked this conversation as resolved.
Outdated
// 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"`
}
2 changes: 2 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type Config struct {
Version Version

Internal Internal // experimental/unstable options

Bitswap BitswapConfig `json:",omitempty"`
}

const (
Expand Down
49 changes: 43 additions & 6 deletions core/node/bitswap.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package node

import (
"context"
"io"
"time"

"github.com/ipfs/boxo/bitswap"
Expand All @@ -12,12 +13,15 @@ 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"
"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"
)

Expand Down Expand Up @@ -72,18 +76,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),
Expand All @@ -94,9 +101,13 @@ func Bitswap(provide bool) interface{} {
}
in.BitswapOpts = append(in.BitswapOpts, bitswap.WithClientOption(client.WithDefaultProviderQueryManager(false)))
provider = pqm

} else {
provider = nil
// When server is disabled, use an empty blockstore to prevent serving blocks
blockstoree = blockstore.NewBlockstore(datastore.NewMapDatastore())
}
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...)
Comment thread
lidel marked this conversation as resolved.

lc.Append(fx.Hook{
OnStop: func(ctx context.Context) error {
Expand All @@ -108,8 +119,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()
Expand Down Expand Up @@ -144,3 +159,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, nil
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tested latest commit 258fb25 and not returning error here seems to be why we have a panic when executing ipfs cat when Bitswap.Enabled is set to false:

$ ipfs daemon
...
Daemon is ready
... ipfs cat for lon-local cid is executed from other terminal
2025-04-30T17:00:40.931+0200	ERROR	cmds/http	http/handler.go:96	a panic has occurred in the commands handler!
2025-04-30T17:00:40.931+0200	ERROR	cmds/http	http/handler.go:97	runtime error: invalid memory address or nil pointer dereference
2025-04-30T17:00:40.933+0200	ERROR	cmds/http	http/handler.go:98	stack trace:
goroutine 50865 [running]:
runtime/debug.Stack()
	runtime/debug/stack.go:26 +0x5e
github.com/ipfs/go-ipfs-cmds/http.(*handler).ServeHTTP.func1()
	github.com/ipfs/go-ipfs-cmds@v0.14.1/http/handler.go:98 +0xef
panic({0x2e5e7c0?, 0x4b6e100?})
	runtime/panic.go:792 +0x132
github.com/ipfs/boxo/blockstore.(*idstore).Put(0xc0011d9400, {0x385dbf0, 0xc002d40060}, {0x0, 0x0})
	github.com/ipfs/boxo@v0.30.0/blockstore/idstore.go:93 +0x27
github.com/ipfs/boxo/blockservice.getBlock({0x385dbf0, 0xc002d40060}, {{0xc0022d8fc0?, 0x1d?}}, {0x3871b68, 0xc00130a270}, 0xc001462740)
	github.com/ipfs/boxo@v0.30.0/blockservice/blockservice.go:278 +0x242
github.com/ipfs/boxo/blockservice.(*Session).GetBlock(0xc002e559c0, {0x385dbf0, 0xc0004afe00}, {{0xc0022d8fc0?, 0x248f580?}})
	github.com/ipfs/boxo@v0.30.0/blockservice/blockservice.go:462 +0x33c
github.com/ipfs/boxo/ipld/merkledag.(*sesGetter).Get(0xc002ed7940, {0x385dbf0, 0xc0004afe00}, {{0xc0022d8fc0?, 0xc001462788?}})
	github.com/ipfs/boxo@v0.30.0/ipld/merkledag/merkledag.go:143 +0x2e
github.com/ipfs/boxo/ipld/merkledag.(*ComboService).Get(0xc002356300?, {0x385dbf0?, 0xc0004afe00?}, {{0xc0022d8fc0?, 0xc00074cdc0?}})
	github.com/ipfs/boxo@v0.30.0/ipld/merkledag/rwservice.go:31 +0x2a
github.com/ipfs/kubo/core/coreapi.(*CoreAPI).ResolveNode(0xc002356300, {0x385dbf0, 0xc0004afdd0}, {0x385db48, 0xc00074cdc0})
	github.com/ipfs/kubo/core/coreapi/path.go:31 +0x338
github.com/ipfs/kubo/core/coreapi.(*UnixfsAPI).Get(0xc000b48480, {0x385dc28, 0xc0042f09b0}, {0x385db48, 0xc00074cdc0})
	github.com/ipfs/kubo/core/coreapi/unixfs.go:210 +0x318
github.com/ipfs/kubo/core/commands.cat({0x385dc28, 0xc0042f09b0}, {0x387bba0, 0xc000b48480}, {0xc002ed7840, 0x1, 0x0?}, 0x0, 0xffffffffffffffff)
	github.com/ipfs/kubo/core/commands/cat.go:136 +0x17b

Seems to be an easy fix, I'll push one soon.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fix: b11a65a

}

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()
}
7 changes: 4 additions & 3 deletions core/node/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,13 +335,14 @@ 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(true) && cfg.Bitswap.ServerEnabled.WithDefault(true)
// Don't provide from bitswap when the strategic provider service is active
shouldBitswapProvide := isBitswapEnabled && !cfg.Experimental.StrategicProviding

return fx.Options(
fx.Provide(BitswapOptions(cfg)),
fx.Provide(Bitswap(shouldBitswapProvide)),
fx.Provide(OnlineExchange()),
fx.Provide(OnlineExchange(cfg.Bitswap.Enabled.WithDefault(true))),
// Replace our Exchange with a Providing exchange!
fx.Decorate(ProvidingExchange(shouldBitswapProvide)),
fx.Provide(DNSResolver),
Expand Down
3 changes: 2 additions & 1 deletion docs/changelogs/v0.35.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@ The WebUI, accessible at http://127.0.0.1:5001/webui/, now includes support for
- update `ipfs-webui` to [v4.7.0](https://github.com/ipfs/ipfs-webui/releases/tag/v4.7.0)

### 📝 Changelog

- Completely disable Bitswap functionality through the `Bitswap.Enabled` flag
- Control server behavior separately with `Bitswap.ServerEnabled`, allowing nodes to operate in client-only mode
### 👨‍👩‍👧‍👦 Contributors
113 changes: 113 additions & 0 deletions test/cli/bitswap_config_test.go
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We need a test that confirms ipfs id peerid no longer includes /ipfs/bitswap protocols when Bitswap.ServerEnabled is set to false (ensuring we no longer announce ourselves as server)

Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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 = 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, []uint8([]byte{}), res.Stdout.Bytes(), "cat should not return any data")

// 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
hash_new := requester.IPFSAddStr(string("random"))
res = requester.IPFS("cat", hash_new)
assert.Equal(t, []uint8([]byte("random")), res.Stdout.Bytes(), "cat should return the added data")
})
}
Loading