@cryptotaxi247 / kubo / commits / 0c00fc389

fix(filestore): respect Provide.Strategy (#11243)

* fix: check provide strategy before providing newly added filestore blocks currently, all blocks added into filestore is provided regardless of which provide strategy is selected, which makes little sense when the current strategy is not "all" (new files might not be pinned with --pin=false or might not be added to MFS). Added a similar check as the blockstore to make the behavior identical between blockstore and filestore. * docs: improve filestore provide strategy changelog entry - reword heading and description for clarity - link to filestore and urlstore experiment docs - mention both experiments since both use the same code path * test: verify filestore respects Provide.Strategy - add positive/negative test pair for filestore provide gating - positive: filestore + "all" strategy provides root and leaf CIDs - negative: filestore + "roots" strategy with --pin=false does not - increase providerTimeout to 30s for CI reliability - replace fixed 500ms DHT sleep with waitForProviderReady: polls 'ipfs provide stat' for SweepingProvider connectivity, then runs a canary provide+findprovs round-trip - add expectNoneProvided for parallel negative assertions to avoid sequential timeout accumulation * docs(changelog): trim filestore section --------- Co-authored-by: Marcin Rataj <lidel@lidel.org>

Haruka committed Apr 10, 2026 at 10:46 UTC 0c00fc38972dc180fa791e9a4c40a2a9cb62af84
4 files changed +125 -9
core/node/groups.go
+3 -1
@@ -243,7 +243,9 @@ func Storage(bcfg *BuildCfg, cfg *config.Config) fx.Option {
243
244 finalBstore := fx.Provide(GcBlockstoreCtor)
245 if cfg.Experimental.FilestoreEnabled || cfg.Experimental.UrlstoreEnabled {
246 - finalBstore = fx.Provide(FilestoreBlockstoreCtor)
246 + finalBstore = fx.Provide(FilestoreBlockstoreCtor(
247 + cfg.Provide.Strategy.WithDefault(config.DefaultProvideStrategy),
248 + ))
249 }
250
251 return fx.Options(
core/node/storage.go
+20 -8
@@ -7,6 +7,7 @@ import (
7 "go.uber.org/fx"
8
9 "github.com/ipfs/boxo/filestore"
10 + "github.com/ipfs/boxo/provider"
11 "github.com/ipfs/kubo/core/node/helpers"
12 "github.com/ipfs/kubo/repo"
13 "github.com/ipfs/kubo/thirdparty/verifbs"
@@ -77,14 +78,25 @@ func GcBlockstoreCtor(bb BaseBlocks) (gclocker blockstore.GCLocker, gcbs blockst
78 }
79
80 // FilestoreBlockstoreCtor wraps GcBlockstore and adds Filestore support
80 -func FilestoreBlockstoreCtor(repo repo.Repo, bb BaseBlocks, prov DHTProvider) (gclocker blockstore.GCLocker, gcbs blockstore.GCBlockstore, bs blockstore.Blockstore, fstore *filestore.Filestore) {
81 - gclocker = blockstore.NewGCLocker()
81 +func FilestoreBlockstoreCtor(
82 + providingStrategy string,
83 +) func(repo repo.Repo, bb BaseBlocks, prov DHTProvider) (gclocker blockstore.GCLocker, gcbs blockstore.GCBlockstore, bs blockstore.Blockstore, fstore *filestore.Filestore) {
84 + return func(repo repo.Repo, bb BaseBlocks, prov DHTProvider) (gclocker blockstore.GCLocker, gcbs blockstore.GCBlockstore, bs blockstore.Blockstore, fstore *filestore.Filestore) {
85 + gclocker = blockstore.NewGCLocker()
86
83 - // hash security
84 - fstore = filestore.NewFilestore(bb, repo.FileManager(), prov)
85 - gcbs = blockstore.NewGCBlockstore(fstore, gclocker)
86 - gcbs = &verifbs.VerifBSGC{GCBlockstore: gcbs}
87 + var fstoreProv provider.MultihashProvider
88 + strategyFlag := config.MustParseProvideStrategy(providingStrategy)
89 + if strategyFlag&config.ProvideStrategyAll != 0 {
90 + fstoreProv = prov
91 + }
92
88 - bs = gcbs
89 - return
93 + fstore = filestore.NewFilestore(bb, repo.FileManager(), fstoreProv)
94 +
95 + // hash security
96 + gcbs = blockstore.NewGCBlockstore(fstore, gclocker)
97 + gcbs = &verifbs.VerifBSGC{GCBlockstore: gcbs}
98 +
99 + bs = gcbs
100 + return
101 + }
102 }
docs/changelogs/v0.41.md
+5
@@ -18,6 +18,7 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
18 - [📌 `pin add` and `pin update` now fast-provide root CID](#-pin-add-and-pin-update-now-fast-provide-root-cid)
19 - [🌳 New `--fast-provide-dag` flag for fine-tuned provide control](#-new---fast-provide-dag-flag-for-fine-tuned-provide-control)
20 - [🛡️ Hardened `Provide.Strategy` parsing](#-hardened-providestrategy-parsing)
21 + - [🔧 Filestore now respects `Provide.Strategy`](#-filestore-now-respects-providestrategy)
22 - [🛡️ `ipfs object patch` validates UnixFS node types](#-ipfs-object-patch-validates-unixfs-node-types)
23 - [🔗 MFS: fixed CidBuilder preservation](#-mfs-fixed-cidbuilder-preservation)
24 - [📂 FUSE Mount Improvements](#-fuse-mount-improvements)
@@ -120,6 +121,10 @@ Pass `--fast-provide-dag=true` (or set [`Import.FastProvideDAG`](https://github.
121
122 Unknown strategy tokens (e.g. typo `"uniuqe"`), malformed delimiters (`"pinned+"`), and invalid combinations (`"all+pinned"`) now produce a clear error at startup instead of being silently ignored.
123
124 +#### 🔧 Filestore now respects `Provide.Strategy`
125 +
126 +Blocks added via the [filestore](https://github.com/ipfs/kubo/blob/master/docs/experimental-features.md#ipfs-filestore) or [urlstore](https://github.com/ipfs/kubo/blob/master/docs/experimental-features.md#ipfs-urlstore) (`ipfs add --nocopy`) used to ignore [`Provide.Strategy`](https://github.com/ipfs/kubo/blob/master/docs/config.md#providestrategy) and were always announced at write time. The filestore is now gated on the strategy the same way the regular blockstore is, so selective strategies get the same [fast-provide knobs](#-new---fast-provide-dag-flag-for-fine-tuned-provide-control) for filestore-backed content that they already had for regular `ipfs add`.
127 +
128 #### 🛡️ `ipfs object patch` validates UnixFS node types
129
130 As part of the ongoing deprecation of the legacy `ipfs object` API (which
test/cli/provider_test.go
+97
@@ -472,6 +472,103 @@ func runProviderSuite(t *testing.T, sweep bool, apply cfgApplier, awaitReprovide
472 expectNoProviders(t, cidChunk, peers...)
473 })
474
475 + // addLargeFilestoreFile writes a 2 MiB file to the publisher's
476 + // node directory and adds it via --nocopy, returning the root CID
477 + // and a chunk CID from the file's DAG links. With the configured
478 + // 1 MiB chunker the file produces multiple leaf blocks so we can
479 + // distinguish root-level from chunk-level provide behavior.
480 + addLargeFilestoreFile := func(t *testing.T, publisher *harness.Node, addArgs ...string) (cidRoot, cidChunk string) {
481 + t.Helper()
482 + filePath := filepath.Join(publisher.Dir, "filestore-"+strconv.FormatInt(time.Now().UnixNano(), 10)+".bin")
483 + require.NoError(t, os.WriteFile(filePath, random.Bytes(2*1024*1024), 0o644))
484 +
485 + args := append([]string{"add", "-q", "--nocopy"}, addArgs...)
486 + args = append(args, filePath)
487 + cidRoot = strings.TrimSpace(publisher.IPFS(args...).Stdout.String())
488 +
489 + dagOut := publisher.IPFS("dag", "get", cidRoot)
490 + var dagNode struct {
491 + Links []struct {
492 + Hash map[string]string `json:"Hash"`
493 + } `json:"Links"`
494 + }
495 + require.NoError(t, json.Unmarshal(dagOut.Stdout.Bytes(), &dagNode))
496 + require.Greater(t, len(dagNode.Links), 1, "filestore file should have multiple chunks")
497 + cidChunk = dagNode.Links[0].Hash["/"]
498 + require.NotEmpty(t, cidChunk)
499 + return cidRoot, cidChunk
500 + }
501 +
502 + t.Run("Filestore --nocopy with 'all' strategy provides every block", func(t *testing.T) {
503 + t.Parallel()
504 +
505 + nodes := initNodes(t, 2, func(n *harness.Node) {
506 + n.SetIPFSConfig("Experimental.FilestoreEnabled", true)
507 + n.SetIPFSConfig("Provide.Strategy", "all")
508 + n.SetIPFSConfig("Import.UnixFSChunker", "size-1048576") // 1 MiB chunks
509 + })
510 + defer nodes.StopDaemons()
511 + publisher, peers := nodes[0], nodes[1:]
512 +
513 + // Positive control: with the default 'all' strategy the
514 + // filestore Put path provides every block as it is written,
515 + // including non-root chunks.
516 + cidRoot, cidChunk := addLargeFilestoreFile(t, publisher)
517 +
518 + pid := publisher.PeerID().String()
519 + expectProviders(t, cidRoot, pid, peers...)
520 + expectProviders(t, cidChunk, pid, peers...)
521 + })
522 +
523 + t.Run("Filestore --nocopy with selective strategy skips write-time provide", func(t *testing.T) {
524 + t.Parallel()
525 +
526 + nodes := initNodes(t, 2, func(n *harness.Node) {
527 + n.SetIPFSConfig("Experimental.FilestoreEnabled", true)
528 + n.SetIPFSConfig("Provide.Strategy", "pinned")
529 + n.SetIPFSConfig("Import.UnixFSChunker", "size-1048576") // 1 MiB chunks
530 + })
531 + defer nodes.StopDaemons()
532 + publisher, peers := nodes[0], nodes[1:]
533 +
534 + // With a selective strategy the filestore must not eagerly
535 + // announce blocks at write time. --pin=false skips the pin
536 + // (so fast-provide-root has nothing to do) and
537 + // --fast-provide-root=false disables it explicitly, isolating
538 + // the assertion to the filestore's internal provide path.
539 + cidRoot, cidChunk := addLargeFilestoreFile(t, publisher,
540 + "--pin=false", "--fast-provide-root=false")
541 +
542 + expectNoProviders(t, cidRoot, peers...)
543 + expectNoProviders(t, cidChunk, peers...)
544 + })
545 +
546 + t.Run("Filestore --nocopy + selective strategy + --fast-provide-dag walks DAG", func(t *testing.T) {
547 + t.Parallel()
548 +
549 + nodes := initNodes(t, 2, func(n *harness.Node) {
550 + n.SetIPFSConfig("Experimental.FilestoreEnabled", true)
551 + n.SetIPFSConfig("Provide.Strategy", "pinned")
552 + n.SetIPFSConfig("Import.UnixFSChunker", "size-1048576") // 1 MiB chunks
553 + })
554 + defer nodes.StopDaemons()
555 + publisher, peers := nodes[0], nodes[1:]
556 +
557 + // The selective-strategy gate skips the filestore's write-time
558 + // provide, but the post-add ExecuteFastProvideDAG walk reads
559 + // blocks through the wrapping blockstore (which transparently
560 + // serves filestore-backed content) and announces each block,
561 + // honoring the active strategy. This is the integration test
562 + // behind the changelog claim that filestore content now plays
563 + // well with the fast-provide-dag flag.
564 + cidRoot, cidChunk := addLargeFilestoreFile(t, publisher,
565 + "--fast-provide-dag", "--fast-provide-wait")
566 +
567 + pid := publisher.PeerID().String()
568 + expectProviders(t, cidRoot, pid, peers...)
569 + expectProviders(t, cidChunk, pid, peers...)
570 + })
571 +
572 t.Run("Provide with 'roots' strategy", func(t *testing.T) {
573 t.Parallel()
574