fix(example): use bidirectional Swarm().Connect() for reliable bitswap

- connect nodes bidirectionally (A→B and B→A) to simulate mutual peering
- mutual peering protects connection from resource manager culling
- use port 0 for random available ports (avoids CI conflicts)
- enable LoopbackAddressesOnLanDHT for local testing
- move retry logic to test file using require.Eventually
This commit is contained in:
Marcin Rataj 2025-12-20 00:24:38 +01:00
parent d45a0073a5
commit cae9af027f
2 changed files with 34 additions and 36 deletions

View File

@ -10,7 +10,7 @@ require (
github.com/ipfs/boxo v0.35.3-0.20251202220026-0842ad274a0c
github.com/ipfs/kubo v0.0.0-00010101000000-000000000000
github.com/libp2p/go-libp2p v0.46.0
github.com/multiformats/go-multiaddr v0.16.1
github.com/stretchr/testify v1.11.1
)
require (
@ -40,6 +40,7 @@ require (
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf // indirect
github.com/cskr/pubsub v1.0.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
@ -137,6 +138,7 @@ require (
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/multiformats/go-multiaddr v0.16.1 // indirect
github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
@ -169,6 +171,7 @@ require (
github.com/pion/turn/v4 v4.0.2 // indirect
github.com/pion/webrtc/v4 v4.1.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/polydawn/refmt v0.89.0 // indirect
github.com/probe-lab/go-libdht v0.4.0 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect

View File

@ -11,7 +11,6 @@ import (
"sync"
"time"
"github.com/ipfs/boxo/bootstrap"
"github.com/ipfs/boxo/files"
"github.com/ipfs/boxo/path"
icore "github.com/ipfs/kubo/core/coreiface"
@ -46,7 +45,7 @@ func setupPlugins(externalPluginsPath string) error {
return nil
}
func createTempRepo(swarmPort int) (string, error) {
func createTempRepo() (string, error) {
repoPath, err := os.MkdirTemp("", "ipfs-shell")
if err != nil {
return "", fmt.Errorf("failed to get temp dir: %s", err)
@ -58,15 +57,16 @@ func createTempRepo(swarmPort int) (string, error) {
return "", err
}
// Configure custom ports to avoid conflicts with other IPFS instances.
// This demonstrates how to customize the node's network addresses.
// Use port 0 to let the OS assign random available ports.
// This avoids conflicts with other services or parallel test runs.
cfg.Addresses.Swarm = []string{
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", swarmPort),
fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic-v1", swarmPort),
fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic-v1/webtransport", swarmPort),
fmt.Sprintf("/ip4/0.0.0.0/udp/%d/webrtc-direct", swarmPort),
"/ip4/127.0.0.1/tcp/0",
"/ip4/127.0.0.1/udp/0/quic-v1",
}
// Enable loopback addresses on LAN DHT for local testing.
cfg.Routing.LoopbackAddressesOnLanDHT = config.True
// Disable auto-bootstrap. For this example, we manually connect only the peers we need.
// In production, you'd typically keep the default bootstrap peers to join the network.
cfg.Bootstrap = []string{}
@ -121,8 +121,7 @@ func createNode(ctx context.Context, repoPath string) (*core.IpfsNode, error) {
var loadPluginsOnce sync.Once
// Spawns a node to be used just for this run (i.e. creates a tmp repo).
// The swarmPort parameter specifies the port for libp2p swarm listeners.
func spawnEphemeral(ctx context.Context, swarmPort int) (icore.CoreAPI, *core.IpfsNode, error) {
func spawnEphemeral(ctx context.Context) (icore.CoreAPI, *core.IpfsNode, error) {
var onceErr error
loadPluginsOnce.Do(func() {
onceErr = setupPlugins("")
@ -132,7 +131,7 @@ func spawnEphemeral(ctx context.Context, swarmPort int) (icore.CoreAPI, *core.Ip
}
// Create a Temporary Repo
repoPath, err := createTempRepo(swarmPort)
repoPath, err := createTempRepo()
if err != nil {
return nil, nil, fmt.Errorf("failed to create temp repo: %s", err)
}
@ -176,8 +175,7 @@ func main() {
defer cancel()
// Spawn a local peer using a temporary path, for testing purposes
// Using port 4010 to avoid conflict with default IPFS port 4001
ipfsA, nodeA, err := spawnEphemeral(ctx, 4010)
ipfsA, nodeA, err := spawnEphemeral(ctx)
if err != nil {
panic(fmt.Errorf("failed to spawn peer node: %s", err))
}
@ -190,10 +188,9 @@ func main() {
fmt.Printf("Added file to peer with CID %s\n", peerCidFile.String())
// Spawn a node using a temporary path, creating a temporary repo for the run
// Using port 4011 (different from nodeA's port 4010)
// Spawn a second node using a temporary path
fmt.Println("Spawning Kubo node on a temporary repo")
ipfsB, nodeB, err := spawnEphemeral(ctx, 4011)
ipfsB, nodeB, err := spawnEphemeral(ctx)
if err != nil {
panic(fmt.Errorf("failed to spawn ephemeral node: %s", err))
}
@ -270,29 +267,27 @@ func main() {
fmt.Println("\n-- Connecting to nodeA and fetching content via bitswap --")
// Connect nodes bidirectionally for bitswap transfer.
// Mutual peering protects the connection from being culled by the resource manager on either side.
// In production, you'd typically connect to bootstrap peers to join the IPFS network.
// autoconf.FallbackBootstrapPeers from boxo/autoconf provides well-known bootstrap peers:
// "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
// "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
// "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
//
// For this example, we only connect nodeB to nodeA to demonstrate direct bitswap.
// We use Bootstrap() instead of raw Swarm().Connect() because Bootstrap() properly
// integrates peers into the routing system and ensures bitswap learns about them.
fmt.Println("Bootstrapping nodeB to nodeA...")
nodeAPeerInfo := nodeA.Peerstore.PeerInfo(nodeA.Identity)
if err := nodeB.Bootstrap(bootstrap.BootstrapConfigWithPeers([]peer.AddrInfo{nodeAPeerInfo})); err != nil {
panic(fmt.Errorf("failed to bootstrap nodeB to nodeA: %s", err))
nodeAPeerInfo := peer.AddrInfo{
ID: nodeA.Identity,
Addrs: nodeA.Peerstore.Addrs(nodeA.Identity),
}
fmt.Println("nodeB is now connected to nodeA")
// Since nodeB is directly connected to nodeA, bitswap can fetch the content
// without needing DHT-based provider discovery. In a production scenario where
// nodes aren't directly connected, you'd use Routing().Provide() to announce
// content to the DHT (see docs/config.md#providedhtsweepenabled for background providing).
nodeBPeerInfo := peer.AddrInfo{
ID: nodeB.Identity,
Addrs: nodeB.Peerstore.Addrs(nodeB.Identity),
}
fmt.Println("Connecting nodes bidirectionally...")
if err := ipfsB.Swarm().Connect(ctx, nodeAPeerInfo); err != nil {
panic(fmt.Errorf("failed to connect nodeB to nodeA: %s", err))
}
if err := ipfsA.Swarm().Connect(ctx, nodeBPeerInfo); err != nil {
panic(fmt.Errorf("failed to connect nodeA to nodeB: %s", err))
}
fmt.Println("Nodes connected")
exampleCIDStr := peerCidFile.RootCid().String()
fmt.Printf("Fetching a file from the network with CID %s\n", exampleCIDStr)
outputPath := outputBasePath + exampleCIDStr
testCID := path.FromCid(peerCidFile.RootCid())