@cryptotaxi247 / kubo / commits / c54428ee7

fix(examples): avoid bitswap race, use ed25519 (#11282)

Reduce example-test flakiness and align with current kubo conventions. - move peer connection before ipfsA.Unixfs().Add() so bitswap's peer-connected event settles before Part IV fetches; on a slow CI runner the 1s ProvSearchDelay can expire and fail the test - use Ed25519 keys via CreateIdentity, matching `ipfs init` default - comments are concise and avoid jargon (cherry picked from commit f449e8b4d8753a601d30c81577eef4b8aefb3d6a)

Marcin Rataj committed Apr 14, 2026 at 19:48 UTC c54428ee7989a83c4eb9d7fa38a467dd28350a90
1 file changed +42 -62
docs/examples/kubo-as-a-library/main.go
+42 -62
@@ -15,13 +15,14 @@ import (
15 "github.com/ipfs/boxo/files"
16 "github.com/ipfs/boxo/path"
17 icore "github.com/ipfs/kubo/core/coreiface"
18 + options "github.com/ipfs/kubo/core/coreiface/options"
19 ma "github.com/multiformats/go-multiaddr"
20
21 "github.com/ipfs/kubo/config"
22 "github.com/ipfs/kubo/core"
23 "github.com/ipfs/kubo/core/coreapi"
24 "github.com/ipfs/kubo/core/node/libp2p"
24 - "github.com/ipfs/kubo/plugin/loader" // This package is needed so that all the preloaded plugins are loaded automatically
25 + "github.com/ipfs/kubo/plugin/loader" // registers built-in plugins
26 "github.com/ipfs/kubo/repo/fsrepo"
27 "github.com/libp2p/go-libp2p/core/peer"
28 )
@@ -29,13 +30,11 @@ import (
30 /// ------ Setting up the IPFS Repo
31
32 func setupPlugins(externalPluginsPath string) error {
32 - // Load any external plugins if available on externalPluginsPath
33 plugins, err := loader.NewPluginLoader(filepath.Join(externalPluginsPath, "plugins"))
34 if err != nil {
35 return fmt.Errorf("error loading plugins: %s", err)
36 }
37
38 - // Load preloaded and external plugins
38 if err := plugins.Initialize(); err != nil {
39 return fmt.Errorf("error initializing plugins: %s", err)
40 }
@@ -53,20 +52,22 @@ func createTempRepo() (string, error) {
52 return "", fmt.Errorf("failed to get temp dir: %s", err)
53 }
54
56 - // Create a config with default options and a 2048 bit key
57 - cfg, err := config.Init(io.Discard, 2048)
55 + identity, err := config.CreateIdentity(io.Discard, []options.KeyGenerateOption{
56 + options.Key.Type(options.Ed25519Key),
57 + })
58 + if err != nil {
59 + return "", err
60 + }
61 + cfg, err := config.InitWithIdentity(identity)
62 if err != nil {
63 return "", err
64 }
65
62 - // Use TCP-only on loopback with random port for reliable local testing.
63 - // This matches what kubo's test harness uses (test/cli/transports_test.go).
64 - // QUIC/UDP transports are avoided because they may be throttled on CI.
66 + // TCP on loopback with a random port. QUIC/UDP is disabled because it can
67 + // be throttled on some networks; TCP is more reliable for local testing.
68 cfg.Addresses.Swarm = []string{
69 "/ip4/127.0.0.1/tcp/0",
70 }
68 -
69 - // Explicitly disable non-TCP transports for reliability.
71 cfg.Swarm.Transports.Network.QUIC = config.False
72 cfg.Swarm.Transports.Network.Relay = config.False
73 cfg.Swarm.Transports.Network.WebTransport = config.False
@@ -74,16 +75,13 @@ func createTempRepo() (string, error) {
75 cfg.Swarm.Transports.Network.Websocket = config.False
76 cfg.AutoTLS.Enabled = config.False
77
77 - // Disable routing - we don't need DHT for direct peer connections.
78 - // Bitswap works with directly connected peers without needing DHT lookups.
78 + // No DHT: we connect peers by address, so content routing is not needed.
79 cfg.Routing.Type = config.NewOptionalString("none")
80
81 - // Disable bootstrap for this example - we manually connect only the peers we need.
81 + // No automatic bootstrap: we connect only the peers we need.
82 cfg.Bootstrap = []string{}
83
84 - // When creating the repository, you can define custom settings on the repository, such as enabling experimental
85 - // features (See experimental-features.md) or customizing the gateway endpoint.
86 - // To do such things, you should modify the variable `cfg`. For example:
84 + // Optional: enable experimental features by modifying cfg before Init, e.g.:
85 if *flagExp {
86 // https://github.com/ipfs/kubo/blob/master/docs/experimental-features.md#ipfs-filestore
87 cfg.Experimental.FilestoreEnabled = true
@@ -94,10 +92,8 @@ func createTempRepo() (string, error) {
92 // https://github.com/ipfs/kubo/blob/master/docs/experimental-features.md#p2p-http-proxy
93 cfg.Experimental.P2pHttpProxy = true
94 // See also: https://github.com/ipfs/kubo/blob/master/docs/config.md
97 - // And: https://github.com/ipfs/kubo/blob/master/docs/experimental-features.md
95 }
96
100 - // Create the repo with the config
97 err = fsrepo.Init(repoPath, cfg)
98 if err != nil {
99 return "", fmt.Errorf("failed to init ephemeral node: %s", err)
@@ -108,23 +104,18 @@ func createTempRepo() (string, error) {
104
105 /// ------ Spawning the node
106
111 -// Creates an IPFS node and returns its coreAPI.
107 +// createNode opens the repo at repoPath and starts an IPFS node.
108 func createNode(ctx context.Context, repoPath string) (*core.IpfsNode, error) {
113 - // Open the repo
109 repo, err := fsrepo.Open(repoPath)
110 if err != nil {
111 return nil, err
112 }
113
119 - // Construct the node
120 -
114 nodeOptions := &core.BuildCfg{
115 Online: true,
123 - // For this example, we use NilRouterOption (no routing) since we connect peers directly.
124 - // Bitswap works with directly connected peers without needing DHT lookups.
125 - // In production, you would typically use:
126 - // Routing: libp2p.DHTOption, // Full DHT node (stores and fetches records)
127 - // Routing: libp2p.DHTClientOption, // DHT client (only fetches records)
116 + // No routing: peers are connected directly by address.
117 + // In production use libp2p.DHTClientOption or libp2p.DHTOption
118 + // so the node can find content and peers on the wider network.
119 Routing: libp2p.NilRouterOption,
120 Repo: repo,
121 }
@@ -134,7 +125,7 @@ func createNode(ctx context.Context, repoPath string) (*core.IpfsNode, error) {
125
126 var loadPluginsOnce sync.Once
127
137 -// Spawns a node to be used just for this run (i.e. creates a tmp repo).
128 +// spawnEphemeral creates a temporary repo, starts a node, and returns its API.
129 func spawnEphemeral(ctx context.Context) (icore.CoreAPI, *core.IpfsNode, error) {
130 var onceErr error
131 loadPluginsOnce.Do(func() {
@@ -144,7 +135,6 @@ func spawnEphemeral(ctx context.Context) (icore.CoreAPI, *core.IpfsNode, error)
135 return nil, nil, onceErr
136 }
137
147 - // Create a Temporary Repo
138 repoPath, err := createTempRepo()
139 if err != nil {
140 return nil, nil, fmt.Errorf("failed to create temp repo: %s", err)
@@ -222,21 +212,12 @@ func main() {
212 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
213 defer cancel()
214
225 - // Spawn a local peer using a temporary path, for testing purposes
215 + // Spawn a local peer using a temporary path, for testing purposes.
216 ipfsA, nodeA, err := spawnEphemeral(ctx)
217 if err != nil {
218 panic(fmt.Errorf("failed to spawn peer node: %s", err))
219 }
220
231 - peerCidFile, err := ipfsA.Unixfs().Add(ctx,
232 - files.NewBytesFile([]byte("hello from ipfs 101 in Kubo")))
233 - if err != nil {
234 - panic(fmt.Errorf("could not add File: %s", err))
235 - }
236 -
237 - fmt.Printf("Added file to peer with CID %s\n", peerCidFile.String())
238 -
239 - // Spawn a node using a temporary path, creating a temporary repo for the run
221 fmt.Println("Spawning Kubo node on a temporary repo")
222 ipfsB, _, err := spawnEphemeral(ctx)
223 if err != nil {
@@ -245,6 +226,27 @@ func main() {
226
227 fmt.Println("IPFS node is running")
228
229 + // Connect nodeB to nodeA before adding content. This lets the connection
230 + // finish its setup during the Add below, so the fetch in Part IV is fast.
231 + peerAddrs, err := ipfsA.Swarm().LocalAddrs(ctx)
232 + if err != nil {
233 + panic(fmt.Errorf("could not get peer addresses: %s", err))
234 + }
235 + peerMa := peerAddrs[0].String() + "/p2p/" + nodeA.Identity.String()
236 + fmt.Println("Connecting to peer...")
237 + if err := connectToPeers(ctx, ipfsB, []string{peerMa}); err != nil {
238 + panic(fmt.Errorf("failed to connect to peer: %s", err))
239 + }
240 + fmt.Println("Connected to peer")
241 +
242 + peerCidFile, err := ipfsA.Unixfs().Add(ctx,
243 + files.NewBytesFile([]byte("hello from ipfs 101 in Kubo")))
244 + if err != nil {
245 + panic(fmt.Errorf("could not add File: %s", err))
246 + }
247 +
248 + fmt.Printf("Added file to peer with CID %s\n", peerCidFile.String())
249 +
250 /// --- Part II: Adding a file and a directory to IPFS
251
252 fmt.Println("\n-- Adding and getting back files & directories --")
@@ -313,29 +315,7 @@ func main() {
315
316 /// --- Part IV: Getting a file from another IPFS node
317
316 - fmt.Println("\n-- Connecting to nodeA and fetching content via bitswap --")
317 -
318 - // Get nodeA's actual listening address dynamically.
319 - // We configured TCP-only on 127.0.0.1 with random port, so this will be a TCP address.
320 - peerAddrs, err := ipfsA.Swarm().LocalAddrs(ctx)
321 - if err != nil {
322 - panic(fmt.Errorf("could not get peer addresses: %s", err))
323 - }
324 - peerMa := peerAddrs[0].String() + "/p2p/" + nodeA.Identity.String()
325 -
326 - bootstrapNodes := []string{
327 - // In production, use real bootstrap peers like:
328 - // "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
329 - // For this example, we only connect to nodeA which has our test content.
330 - peerMa,
331 - }
332 -
333 - fmt.Println("Connecting to peer...")
334 - err = connectToPeers(ctx, ipfsB, bootstrapNodes)
335 - if err != nil {
336 - panic(fmt.Errorf("failed to connect to peers: %s", err))
337 - }
338 - fmt.Println("Connected to peer")
318 + fmt.Println("\n-- Fetching content from nodeA via bitswap --")
319
320 exampleCIDStr := peerCidFile.RootCid().String()
321