Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
14 changes: 14 additions & 0 deletions config/bitswap.go
Original file line number Diff line number Diff line change
@@ -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
)
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 Bitswap `json:",omitempty"`
}

const (
Expand Down
52 changes: 46 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,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"
)

Expand Down Expand Up @@ -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),
Expand All @@ -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 {
Expand All @@ -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()
Expand Down Expand Up @@ -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()
}
8 changes: 5 additions & 3 deletions core/node/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
4 changes: 2 additions & 2 deletions docs/changelogs/v0.35.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,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
Expand Down Expand Up @@ -119,5 +120,4 @@ but for reprovides only.
- update `ipfs-webui` to [v4.7.0](https://github.com/ipfs/ipfs-webui/releases/tag/v4.7.0)

### 📝 Changelog

### 👨‍👩‍👧‍👦 Contributors
34 changes: 34 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -616,6 +619,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.
Expand Down Expand Up @@ -1124,6 +1154,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`,
Expand Down
138 changes: 138 additions & 0 deletions test/cli/bitswap_config_test.go
Original file line number Diff line number Diff line change
@@ -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>")
// 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)
// }
// })
}
Loading