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