@cryptotaxi247 / kubo / commits / cec743204

feat: fast provide support in `dag import` (#11058)

* fix(add): respect Provide config in fast-provide-root fast-provide-root should honor the same config settings as the regular provide system: - skip when Provide.Enabled is false - skip when Provide.DHT.Interval is 0 - respect Provide.Strategy (all/pinned/roots/mfs/combinations) This ensures fast-provide only runs when appropriate based on user configuration and the nature of the content being added (pinned vs unpinned, added to MFS or not). * feat(config): options to adjust global defaults Add Import.FastProvideRoot and Import.FastProvideWait configuration options to control default behavior of fast-provide-root and fast-provide-wait flags in ipfs add command. Users can now set global defaults in config while maintaining per-command flag overrides. - Add Import.FastProvideRoot (default: true) - Add Import.FastProvideWait (default: false) - Add ResolveBoolFromConfig helper for config resolution - Update docs with configuration details - Add log-based tests verifying actual behavior * refactor: extract fast-provide logic into reusable functions Extract fast-provide logic from add command into reusable components: - Add config.ShouldProvideForStrategy helper for strategy matching - Add ExecuteFastProvide function reusable across add and dag import commands - Move DefaultFastProvideTimeout constant to config/provide.go - Simplify add.go from 72 lines to 6 lines for fast-provide - Move fast-provide tests to dedicated TestAddFastProvide function Benefits: - cleaner API: callers only pass content characteristics - all strategy logic centralized in one place - better separation of concerns - easier to add fast-provide to other commands in future * feat(dag): add fast-provide support for dag import Adds --fast-provide-root and --fast-provide-wait flags to `ipfs dag import`, mirroring the fast-provide functionality available in `ipfs add`. Changes: - Add --fast-provide-root and --fast-provide-wait flags to dag import command - Implement fast-provide logic for all root CIDs in imported CAR files - Works even when --pin-roots=false (strategy checked internally) - Share ExecuteFastProvide implementation between add and dag import - Move ExecuteFastProvide to cmdenv package to avoid import cycles - Add logging when fast-provide is disabled - Conditional error handling: return error when wait=true, warn when wait=false - Update config docs to mention both ipfs add and ipfs dag import - Update changelog to use "provide" terminology and include dag import examples - Add comprehensive test coverage (TestDagImportFastProvide with 6 test cases) The fast-provide feature allows immediate DHT announcement of root CIDs for faster content discovery, bypassing the regular background queue. * docs: improve fast-provide documentation Refine documentation to better explain fast-provide and sweep provider working together, and highlight the performance improvement. Changelog: - add fast-provide to sweep provider features list - explain performance improvement: root CIDs discoverable in <1s vs 30+ seconds - note this uses optimistic DHT operations (faster with sweep provider) - simplify examples, point to --help for details Config docs: - fix: --fast-provide-roots should be --fast-provide-root (singular) - clarify Import.FastProvideRoot focuses on root CIDs while sweep handles all blocks - simplify Import.FastProvideWait description Command help: - ipfs add: explain sweep provider context upfront - ipfs dag import: add fast-provide explanation section - both explain the split: fast-provide for roots, sweep for all blocks * test: add tests for ShouldProvideForStrategy add tests covering all provide strategy combinations with focus on bitflag OR logic (the else-if bug fix). organized by behavior: - all strategy always provides - single strategies match only their flag - combined strategies use OR logic - zero strategy never provides * refactor: error cmd on error and wait=true change ExecuteFastProvide() to return error, enabling proper error propagation when --fast-provide-wait=true. in sync mode, provide failures now error the command as expected. in async mode (default), always returns nil with errors logged in background goroutine. also remove duplicate ExecuteFastProvide() from provide.go (75 lines), keeping single implementation in cmdenv/env.go for reuse across add and dag import commands. call sites simplified: - add.go: check and propagate error from ExecuteFastProvide - dag/import.go: return error from ForEach callback, remove confusing conditional error handling semantics: - precondition skips (DHT unavailable, etc): return nil (not failure) - async mode (wait=false): return nil, log errors in goroutine - sync mode (wait=true): return wrapped error on provide failure

Marcin Rataj committed Nov 15, 2025 at 06:06 UTC cec74320436a525cfaba448b20f06c7fe65289e0
12 files changed +740 -96
config/import.go
+4
@@ -16,6 +16,8 @@ const (
16 DefaultUnixFSRawLeaves = false
17 DefaultUnixFSChunker = "size-262144"
18 DefaultHashFunction = "sha2-256"
19 + DefaultFastProvideRoot = true
20 + DefaultFastProvideWait = false
21
22 DefaultUnixFSHAMTDirectorySizeThreshold = 262144 // 256KiB - https://github.com/ipfs/boxo/blob/6c5a07602aed248acc86598f30ab61923a54a83e/ipld/unixfs/io/directory.go#L26
23
@@ -48,6 +50,8 @@ type Import struct {
50 UnixFSHAMTDirectorySizeThreshold OptionalBytes
51 BatchMaxNodes OptionalInteger
52 BatchMaxSize OptionalInteger
53 + FastProvideRoot Flag
54 + FastProvideWait Flag
55 }
56
57 // ValidateImportConfig validates the Import configuration according to UnixFS spec requirements.
config/provide.go
+27
@@ -22,6 +22,11 @@ const (
22 DefaultProvideDHTMaxProvideConnsPerWorker = 20
23 DefaultProvideDHTKeystoreBatchSize = 1 << 14 // ~544 KiB per batch (1 multihash = 34 bytes)
24 DefaultProvideDHTOfflineDelay = 2 * time.Hour
25 +
26 + // DefaultFastProvideTimeout is the maximum time allowed for fast-provide operations.
27 + // Prevents hanging on network issues when providing root CID.
28 + // 10 seconds is sufficient for DHT operations with sweep provider or accelerated client.
29 + DefaultFastProvideTimeout = 10 * time.Second
30 )
31
32 type ProvideStrategy int
@@ -175,3 +180,25 @@ func ValidateProvideConfig(cfg *Provide) error {
180
181 return nil
182 }
183 +
184 +// ShouldProvideForStrategy determines if content should be provided based on the provide strategy
185 +// and content characteristics (pinned status, root status, MFS status).
186 +func ShouldProvideForStrategy(strategy ProvideStrategy, isPinned bool, isPinnedRoot bool, isMFS bool) bool {
187 + if strategy == ProvideStrategyAll {
188 + // 'all' strategy: always provide
189 + return true
190 + }
191 +
192 + // For combined strategies, check each component
193 + if strategy&ProvideStrategyPinned != 0 && isPinned {
194 + return true
195 + }
196 + if strategy&ProvideStrategyRoots != 0 && isPinnedRoot {
197 + return true
198 + }
199 + if strategy&ProvideStrategyMFS != 0 && isMFS {
200 + return true
201 + }
202 +
203 + return false
204 +}
config/provide_test.go
+84
@@ -105,3 +105,87 @@ func TestValidateProvideConfig_MaxWorkers(t *testing.T) {
105 })
106 }
107 }
108 +
109 +func TestShouldProvideForStrategy(t *testing.T) {
110 + t.Run("all strategy always provides", func(t *testing.T) {
111 + // ProvideStrategyAll should return true regardless of flags
112 + testCases := []struct{ pinned, pinnedRoot, mfs bool }{
113 + {false, false, false},
114 + {true, true, true},
115 + {true, false, false},
116 + }
117 +
118 + for _, tc := range testCases {
119 + assert.True(t, ShouldProvideForStrategy(
120 + ProvideStrategyAll, tc.pinned, tc.pinnedRoot, tc.mfs))
121 + }
122 + })
123 +
124 + t.Run("single strategies match only their flag", func(t *testing.T) {
125 + tests := []struct {
126 + name string
127 + strategy ProvideStrategy
128 + pinned, pinnedRoot, mfs bool
129 + want bool
130 + }{
131 + {"pinned: matches when pinned=true", ProvideStrategyPinned, true, false, false, true},
132 + {"pinned: ignores other flags", ProvideStrategyPinned, false, true, true, false},
133 +
134 + {"roots: matches when pinnedRoot=true", ProvideStrategyRoots, false, true, false, true},
135 + {"roots: ignores other flags", ProvideStrategyRoots, true, false, true, false},
136 +
137 + {"mfs: matches when mfs=true", ProvideStrategyMFS, false, false, true, true},
138 + {"mfs: ignores other flags", ProvideStrategyMFS, true, true, false, false},
139 + }
140 +
141 + for _, tt := range tests {
142 + t.Run(tt.name, func(t *testing.T) {
143 + got := ShouldProvideForStrategy(tt.strategy, tt.pinned, tt.pinnedRoot, tt.mfs)
144 + assert.Equal(t, tt.want, got)
145 + })
146 + }
147 + })
148 +
149 + t.Run("combined strategies use OR logic (else-if bug fix)", func(t *testing.T) {
150 + // CRITICAL: Tests the fix where bitflag combinations (pinned+mfs) didn't work
151 + // because of else-if instead of separate if statements
152 + tests := []struct {
153 + name string
154 + strategy ProvideStrategy
155 + pinned, pinnedRoot, mfs bool
156 + want bool
157 + }{
158 + // pinned|mfs: provide if EITHER matches
159 + {"pinned|mfs when pinned", ProvideStrategyPinned | ProvideStrategyMFS, true, false, false, true},
160 + {"pinned|mfs when mfs", ProvideStrategyPinned | ProvideStrategyMFS, false, false, true, true},
161 + {"pinned|mfs when both", ProvideStrategyPinned | ProvideStrategyMFS, true, false, true, true},
162 + {"pinned|mfs when neither", ProvideStrategyPinned | ProvideStrategyMFS, false, false, false, false},
163 +
164 + // roots|mfs
165 + {"roots|mfs when root", ProvideStrategyRoots | ProvideStrategyMFS, false, true, false, true},
166 + {"roots|mfs when mfs", ProvideStrategyRoots | ProvideStrategyMFS, false, false, true, true},
167 + {"roots|mfs when neither", ProvideStrategyRoots | ProvideStrategyMFS, false, false, false, false},
168 +
169 + // pinned|roots
170 + {"pinned|roots when pinned", ProvideStrategyPinned | ProvideStrategyRoots, true, false, false, true},
171 + {"pinned|roots when root", ProvideStrategyPinned | ProvideStrategyRoots, false, true, false, true},
172 + {"pinned|roots when neither", ProvideStrategyPinned | ProvideStrategyRoots, false, false, false, false},
173 +
174 + // triple combination
175 + {"all-three when any matches", ProvideStrategyPinned | ProvideStrategyRoots | ProvideStrategyMFS, false, false, true, true},
176 + {"all-three when none match", ProvideStrategyPinned | ProvideStrategyRoots | ProvideStrategyMFS, false, false, false, false},
177 + }
178 +
179 + for _, tt := range tests {
180 + t.Run(tt.name, func(t *testing.T) {
181 + got := ShouldProvideForStrategy(tt.strategy, tt.pinned, tt.pinnedRoot, tt.mfs)
182 + assert.Equal(t, tt.want, got)
183 + })
184 + }
185 + })
186 +
187 + t.Run("zero strategy never provides", func(t *testing.T) {
188 + assert.False(t, ShouldProvideForStrategy(ProvideStrategy(0), false, false, false))
189 + assert.False(t, ShouldProvideForStrategy(ProvideStrategy(0), true, true, true))
190 + })
191 +}
config/types.go
+10
@@ -117,6 +117,16 @@ func (f Flag) String() string {
117 }
118 }
119
120 +// ResolveBoolFromConfig returns the resolved boolean value based on:
121 +// - If userSet is true, returns userValue (user explicitly set the flag)
122 +// - Otherwise, uses configFlag.WithDefault(defaultValue) (respects config or falls back to default)
123 +func ResolveBoolFromConfig(userValue bool, userSet bool, configFlag Flag, defaultValue bool) bool {
124 + if userSet {
125 + return userValue
126 + }
127 + return configFlag.WithDefault(defaultValue)
128 +}
129 +
130 var (
131 _ json.Unmarshaler = (*Flag)(nil)
132 _ json.Marshaler = (*Flag)(nil)
core/commands/add.go
+27 -81
@@ -1,7 +1,6 @@
1 package commands
2
3 import (
4 - "context"
4 "errors"
5 "fmt"
6 "io"
@@ -9,7 +8,6 @@ import (
8 gopath "path"
9 "strconv"
10 "strings"
12 - "time"
11
12 "github.com/ipfs/kubo/config"
13 "github.com/ipfs/kubo/core/commands/cmdenv"
@@ -74,11 +72,6 @@ const (
72
73 const (
74 adderOutChanSize = 8
77 -
78 - // fastProvideTimeout is the maximum time allowed for async fast-provide operations.
79 - // Prevents hanging on network issues when providing root CID in background.
80 - // 10 seconds is sufficient for DHT operations with sweep provider or accelerated client.
81 - fastProvideTimeout = 10 * time.Second
75 )
76
77 var AddCmd = &cmds.Command{
@@ -89,21 +82,21 @@ Adds the content of <path> to IPFS. Use -r to add directories (recursively).
82
83 FAST PROVIDE OPTIMIZATION:
84
92 -When you add content to IPFS, it gets queued for announcement on the DHT.
93 -The background queue can take some time to process, meaning other peers
94 -won't find your content immediately after 'ipfs add' completes.
85 +When you add content to IPFS, the sweep provider queues it for efficient
86 +DHT provides over time. While this is resource-efficient, other peers won't
87 +find your content immediately after 'ipfs add' completes.
88
96 -To make sharing faster, 'ipfs add' does an extra immediate announcement
97 -of just the root CID to the DHT. This lets other peers start discovering
98 -your content right away, while the regular background queue still handles
99 -announcing all the blocks later.
89 +To make sharing faster, 'ipfs add' does an immediate provide of the root CID
90 +to the DHT in addition to the regular queue. This complements the sweep provider:
91 +fast-provide handles the urgent case (root CIDs that users share and reference),
92 +while the sweep provider efficiently provides all blocks according to
93 +Provide.Strategy over time.
94
101 -By default, this extra announcement runs in the background without slowing
102 -down the command. If you need to be certain the root CID is discoverable
103 -before the command returns (for example, sharing a link immediately),
104 -use --fast-provide-wait to wait for the announcement to complete.
105 -Use --fast-provide-root=false to skip this optimization and rely only on
106 -the background queue (controlled by Provide.Strategy and Provide.DHT.Interval).
95 +By default, this immediate provide runs in the background without blocking
96 +the command. If you need certainty that the root CID is discoverable before
97 +the command returns (e.g., sharing a link immediately), use --fast-provide-wait
98 +to wait for the provide to complete. Use --fast-provide-root=false to skip
99 +this optimization.
100
101 This works best with the sweep provider and accelerated DHT client.
102 Automatically skipped when DHT is not available.
@@ -245,8 +238,8 @@ https://github.com/ipfs/kubo/blob/master/docs/config.md#import
238 cmds.UintOption(modeOptionName, "Custom POSIX file mode to store in created UnixFS entries. WARNING: experimental, forces dag-pb for root block, disables raw-leaves"),
239 cmds.Int64Option(mtimeOptionName, "Custom POSIX modification time to store in created UnixFS entries (seconds before or after the Unix Epoch). WARNING: experimental, forces dag-pb for root block, disables raw-leaves"),
240 cmds.UintOption(mtimeNsecsOptionName, "Custom POSIX modification time (optional time fraction in nanoseconds)"),
248 - cmds.BoolOption(fastProvideRootOptionName, "Immediately provide root CID to DHT for fast content discovery. When disabled, root CID is queued for background providing instead.").WithDefault(true),
249 - cmds.BoolOption(fastProvideWaitOptionName, "Wait for fast-provide-root to complete before returning. Ensures root CID is discoverable when command finishes.").WithDefault(false),
241 + cmds.BoolOption(fastProvideRootOptionName, "Immediately provide root CID to DHT in addition to regular queue, for faster discovery. Default: Import.FastProvideRoot"),
242 + cmds.BoolOption(fastProvideWaitOptionName, "Block until the immediate provide completes before returning. Default: Import.FastProvideWait"),
243 },
244 PreRun: func(req *cmds.Request, env cmds.Environment) error {
245 quiet, _ := req.Options[quietOptionName].(bool)
@@ -317,8 +310,8 @@ https://github.com/ipfs/kubo/blob/master/docs/config.md#import
310 mode, _ := req.Options[modeOptionName].(uint)
311 mtime, _ := req.Options[mtimeOptionName].(int64)
312 mtimeNsecs, _ := req.Options[mtimeNsecsOptionName].(uint)
320 - fastProvideRoot, _ := req.Options[fastProvideRootOptionName].(bool)
321 - fastProvideWait, _ := req.Options[fastProvideWaitOptionName].(bool)
313 + fastProvideRoot, fastProvideRootSet := req.Options[fastProvideRootOptionName].(bool)
314 + fastProvideWait, fastProvideWaitSet := req.Options[fastProvideWaitOptionName].(bool)
315
316 if chunker == "" {
317 chunker = cfg.Import.UnixFSChunker.WithDefault(config.DefaultUnixFSChunker)
@@ -355,6 +348,9 @@ https://github.com/ipfs/kubo/blob/master/docs/config.md#import
348 maxHAMTFanout = int(cfg.Import.UnixFSHAMTDirectoryMaxFanout.WithDefault(config.DefaultUnixFSHAMTDirectoryMaxFanout))
349 }
350
351 + fastProvideRoot = config.ResolveBoolFromConfig(fastProvideRoot, fastProvideRootSet, cfg.Import.FastProvideRoot, config.DefaultFastProvideRoot)
352 + fastProvideWait = config.ResolveBoolFromConfig(fastProvideWait, fastProvideWaitSet, cfg.Import.FastProvideWait, config.DefaultFastProvideWait)
353 +
354 // Storing optional mode or mtime (UnixFS 1.5) requires root block
355 // to always be 'dag-pb' and not 'raw'. Below adjusts raw-leaves setting, if possible.
356 if preserveMode || preserveMtime || mode != 0 || mtime != 0 {
@@ -606,65 +602,15 @@ https://github.com/ipfs/kubo/blob/master/docs/config.md#import
602 if err != nil {
603 return err
604 }
609 -
610 - // Parse the provide strategy to check if we should provide based on pin/MFS status
611 - strategyStr := cfg.Provide.Strategy.WithDefault(config.DefaultProvideStrategy)
612 - strategy := config.ParseProvideStrategy(strategyStr)
613 -
614 - // Determine if we should provide based on strategy
615 - shouldProvide := false
616 - if strategy == config.ProvideStrategyAll {
617 - // 'all' strategy: always provide
618 - shouldProvide = true
619 - } else {
620 - // For combined strategies (pinned+mfs), check each component
621 - if strategy&config.ProvideStrategyPinned != 0 && dopin {
622 - shouldProvide = true
623 - } else if strategy&config.ProvideStrategyRoots != 0 && dopin {
624 - shouldProvide = true
625 - } else if strategy&config.ProvideStrategyMFS != 0 && toFilesSet {
626 - shouldProvide = true
627 - }
605 + if err := cmdenv.ExecuteFastProvide(req.Context, ipfsNode, cfg, lastRootCid.RootCid(), fastProvideWait, dopin, dopin, toFilesSet); err != nil {
606 + return err
607 }
629 -
630 - switch {
631 - case !cfg.Provide.Enabled.WithDefault(config.DefaultProvideEnabled):
632 - log.Debugw("fast-provide-root: skipped", "reason", "Provide.Enabled is false")
633 - case cfg.Provide.DHT.Interval.WithDefault(config.DefaultProvideDHTInterval) == 0:
634 - log.Debugw("fast-provide-root: skipped", "reason", "Provide.DHT.Interval is 0")
635 - case !shouldProvide:
636 - log.Debugw("fast-provide-root: skipped", "reason", "strategy does not match content", "strategy", strategyStr, "pinned", dopin, "to-files", toFilesSet)
637 - case !ipfsNode.HasActiveDHTClient():
638 - log.Debugw("fast-provide-root: skipped", "reason", "DHT not available")
639 - default:
640 - rootCid := lastRootCid.RootCid()
641 -
642 - if fastProvideWait {
643 - // Synchronous mode: block until provide completes
644 - log.Debugw("fast-provide-root: providing synchronously", "cid", rootCid)
645 - if err := provideCIDSync(req.Context, ipfsNode.DHTClient, rootCid); err != nil {
646 - log.Warnw("fast-provide-root: sync provide failed", "cid", rootCid, "error", err)
647 - } else {
648 - log.Debugw("fast-provide-root: sync provide completed", "cid", rootCid)
649 - }
650 - } else {
651 - // Asynchronous mode (default): fire-and-forget, don't block
652 - log.Debugw("fast-provide-root: providing asynchronously", "cid", rootCid)
653 - go func() {
654 - // Use detached context with timeout to prevent hanging on network issues
655 - ctx, cancel := context.WithTimeout(context.Background(), fastProvideTimeout)
656 - defer cancel()
657 - if err := provideCIDSync(ctx, ipfsNode.DHTClient, rootCid); err != nil {
658 - log.Warnw("fast-provide-root: async provide failed", "cid", rootCid, "error", err)
659 - } else {
660 - log.Debugw("fast-provide-root: async provide completed", "cid", rootCid)
661 - }
662 - }()
663 - }
608 + } else if !fastProvideRoot {
609 + if fastProvideWait {
610 + log.Debugw("fast-provide-root: skipped", "reason", "disabled by flag or config", "wait-flag-ignored", true)
611 + } else {
612 + log.Debugw("fast-provide-root: skipped", "reason", "disabled by flag or config")
613 }
665 - } else if fastProvideWait && !fastProvideRoot {
666 - // Log that wait flag is ignored when provide-root is disabled
667 - log.Debugw("fast-provide-root: wait flag ignored", "reason", "fast-provide-root disabled")
614 }
615
616 return nil
core/commands/cmdenv/env.go
+107 -3
@@ -1,15 +1,19 @@
1 package cmdenv
2
3 import (
4 + "context"
5 "fmt"
6 "strconv"
7 "strings"
8
8 - "github.com/ipfs/kubo/commands"
9 - "github.com/ipfs/kubo/core"
10 -
9 + "github.com/ipfs/go-cid"
10 cmds "github.com/ipfs/go-ipfs-cmds"
11 logging "github.com/ipfs/go-log/v2"
12 + routing "github.com/libp2p/go-libp2p/core/routing"
13 +
14 + "github.com/ipfs/kubo/commands"
15 + "github.com/ipfs/kubo/config"
16 + "github.com/ipfs/kubo/core"
17 coreiface "github.com/ipfs/kubo/core/coreiface"
18 options "github.com/ipfs/kubo/core/coreiface/options"
19 )
@@ -86,3 +90,103 @@ func needEscape(s string) bool {
90 }
91 return false
92 }
93 +
94 +// provideCIDSync performs a synchronous/blocking provide operation to announce
95 +// the given CID to the DHT.
96 +//
97 +// - If the accelerated DHT client is used, a DHT lookup isn't needed, we
98 +// directly allocate provider records to closest peers.
99 +// - If Provide.DHT.SweepEnabled=true or OptimisticProvide=true, we make an
100 +// optimistic provide call.
101 +// - Else we make a standard provide call (much slower).
102 +//
103 +// IMPORTANT: The caller MUST verify DHT availability using HasActiveDHTClient()
104 +// before calling this function. Calling with a nil or invalid router will cause
105 +// a panic - this is the caller's responsibility to prevent.
106 +func provideCIDSync(ctx context.Context, router routing.Routing, c cid.Cid) error {
107 + return router.Provide(ctx, c, true)
108 +}
109 +
110 +// ExecuteFastProvide immediately provides a root CID to the DHT, bypassing the regular
111 +// provide queue for faster content discovery. This function is reusable across commands
112 +// that add or import content, such as ipfs add and ipfs dag import.
113 +//
114 +// Parameters:
115 +// - ctx: context for synchronous provides
116 +// - ipfsNode: the IPFS node instance
117 +// - cfg: node configuration
118 +// - rootCid: the CID to provide
119 +// - wait: whether to block until provide completes (sync mode)
120 +// - isPinned: whether content is pinned
121 +// - isPinnedRoot: whether this is a pinned root CID
122 +// - isMFS: whether content is in MFS
123 +//
124 +// Return value:
125 +// - Returns nil if operation succeeded or was skipped (preconditions not met)
126 +// - Returns error only in sync mode (wait=true) when provide operation fails
127 +// - In async mode (wait=false), always returns nil (errors logged in goroutine)
128 +//
129 +// The function handles all precondition checks (Provide.Enabled, DHT availability,
130 +// strategy matching) and logs appropriately. In async mode, it launches a goroutine
131 +// with a detached context and timeout.
132 +func ExecuteFastProvide(
133 + ctx context.Context,
134 + ipfsNode *core.IpfsNode,
135 + cfg *config.Config,
136 + rootCid cid.Cid,
137 + wait bool,
138 + isPinned bool,
139 + isPinnedRoot bool,
140 + isMFS bool,
141 +) error {
142 + log.Debugw("fast-provide-root: enabled", "wait", wait)
143 +
144 + // Check preconditions for providing
145 + switch {
146 + case !cfg.Provide.Enabled.WithDefault(config.DefaultProvideEnabled):
147 + log.Debugw("fast-provide-root: skipped", "reason", "Provide.Enabled is false")
148 + return nil
149 + case cfg.Provide.DHT.Interval.WithDefault(config.DefaultProvideDHTInterval) == 0:
150 + log.Debugw("fast-provide-root: skipped", "reason", "Provide.DHT.Interval is 0")
151 + return nil
152 + case !ipfsNode.HasActiveDHTClient():
153 + log.Debugw("fast-provide-root: skipped", "reason", "DHT not available")
154 + return nil
155 + }
156 +
157 + // Check if strategy allows providing this content
158 + strategyStr := cfg.Provide.Strategy.WithDefault(config.DefaultProvideStrategy)
159 + strategy := config.ParseProvideStrategy(strategyStr)
160 + shouldProvide := config.ShouldProvideForStrategy(strategy, isPinned, isPinnedRoot, isMFS)
161 +
162 + if !shouldProvide {
163 + log.Debugw("fast-provide-root: skipped", "reason", "strategy does not match content", "strategy", strategyStr, "pinned", isPinned, "pinnedRoot", isPinnedRoot, "mfs", isMFS)
164 + return nil
165 + }
166 +
167 + // Execute provide operation
168 + if wait {
169 + // Synchronous mode: block until provide completes, return error on failure
170 + log.Debugw("fast-provide-root: providing synchronously", "cid", rootCid)
171 + if err := provideCIDSync(ctx, ipfsNode.DHTClient, rootCid); err != nil {
172 + log.Warnw("fast-provide-root: sync provide failed", "cid", rootCid, "error", err)
173 + return fmt.Errorf("fast-provide: %w", err)
174 + }
175 + log.Debugw("fast-provide-root: sync provide completed", "cid", rootCid)
176 + return nil
177 + }
178 +
179 + // Asynchronous mode (default): fire-and-forget, don't block, always return nil
180 + log.Debugw("fast-provide-root: providing asynchronously", "cid", rootCid)
181 + go func() {
182 + // Use detached context with timeout to prevent hanging on network issues
183 + ctx, cancel := context.WithTimeout(context.Background(), config.DefaultFastProvideTimeout)
184 + defer cancel()
185 + if err := provideCIDSync(ctx, ipfsNode.DHTClient, rootCid); err != nil {
186 + log.Warnw("fast-provide-root: async provide failed", "cid", rootCid, "error", err)
187 + } else {
188 + log.Debugw("fast-provide-root: async provide completed", "cid", rootCid)
189 + }
190 + }()
191 + return nil
192 +}
core/commands/dag/dag.go
+20 -4
@@ -16,10 +16,12 @@ import (
16 )
17
18 const (
19 - pinRootsOptionName = "pin-roots"
20 - progressOptionName = "progress"
21 - silentOptionName = "silent"
22 - statsOptionName = "stats"
19 + pinRootsOptionName = "pin-roots"
20 + progressOptionName = "progress"
21 + silentOptionName = "silent"
22 + statsOptionName = "stats"
23 + fastProvideRootOptionName = "fast-provide-root"
24 + fastProvideWaitOptionName = "fast-provide-wait"
25 )
26
27 // DagCmd provides a subset of commands for interacting with ipld dag objects
@@ -189,6 +191,18 @@ Note:
191 currently present in the blockstore does not represent a complete DAG,
192 pinning of that individual root will fail.
193
194 +FAST PROVIDE OPTIMIZATION:
195 +
196 +Root CIDs from CAR headers are immediately provided to the DHT in addition
197 +to the regular provide queue, allowing other peers to discover your content
198 +right away. This complements the sweep provider, which efficiently provides
199 +all blocks according to Provide.Strategy over time.
200 +
201 +By default, the provide happens in the background without blocking the
202 +command. Use --fast-provide-wait to wait for the provide to complete, or
203 +--fast-provide-root=false to skip it. Works even with --pin-roots=false.
204 +Automatically skipped when DHT is not available.
205 +
206 Maximum supported CAR version: 2
207 Specification of CAR formats: https://ipld.io/specs/transport/car/
208 `,
@@ -200,6 +214,8 @@ Specification of CAR formats: https://ipld.io/specs/transport/car/
214 cmds.BoolOption(pinRootsOptionName, "Pin optional roots listed in the .car headers after importing.").WithDefault(true),
215 cmds.BoolOption(silentOptionName, "No output."),
216 cmds.BoolOption(statsOptionName, "Output stats."),
217 + cmds.BoolOption(fastProvideRootOptionName, "Immediately provide root CIDs to DHT in addition to regular queue, for faster discovery. Default: Import.FastProvideRoot"),
218 + cmds.BoolOption(fastProvideWaitOptionName, "Block until the immediate provide completes before returning. Default: Import.FastProvideWait"),
219 cmdutils.AllowBigBlockOption,
220 },
221 Type: CarImportOutput{},
core/commands/dag/import.go
+25
@@ -11,6 +11,7 @@ import (
11 cmds "github.com/ipfs/go-ipfs-cmds"
12 ipld "github.com/ipfs/go-ipld-format"
13 ipldlegacy "github.com/ipfs/go-ipld-legacy"
14 + logging "github.com/ipfs/go-log/v2"
15 "github.com/ipfs/kubo/config"
16 "github.com/ipfs/kubo/core/coreiface/options"
17 gocarv2 "github.com/ipld/go-car/v2"
@@ -19,6 +20,8 @@ import (
20 "github.com/ipfs/kubo/core/commands/cmdutils"
21 )
22
23 +var log = logging.Logger("core/commands")
24 +
25 func dagImport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
26 node, err := cmdenv.GetNode(env)
27 if err != nil {
@@ -47,6 +50,12 @@ func dagImport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment
50
51 doPinRoots, _ := req.Options[pinRootsOptionName].(bool)
52
53 + fastProvideRoot, fastProvideRootSet := req.Options[fastProvideRootOptionName].(bool)
54 + fastProvideWait, fastProvideWaitSet := req.Options[fastProvideWaitOptionName].(bool)
55 +
56 + fastProvideRoot = config.ResolveBoolFromConfig(fastProvideRoot, fastProvideRootSet, cfg.Import.FastProvideRoot, config.DefaultFastProvideRoot)
57 + fastProvideWait = config.ResolveBoolFromConfig(fastProvideWait, fastProvideWaitSet, cfg.Import.FastProvideWait, config.DefaultFastProvideWait)
58 +
59 // grab a pinlock ( which doubles as a GC lock ) so that regardless of the
60 // size of the streamed-in cars nothing will disappear on us before we had
61 // a chance to roots that may show up at the very end
@@ -191,5 +200,21 @@ func dagImport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment
200 }
201 }
202
203 + // Fast-provide roots for faster discovery
204 + if fastProvideRoot {
205 + err = roots.ForEach(func(c cid.Cid) error {
206 + return cmdenv.ExecuteFastProvide(req.Context, node, cfg, c, fastProvideWait, doPinRoots, doPinRoots, false)
207 + })
208 + if err != nil {
209 + return err
210 + }
211 + } else {
212 + if fastProvideWait {
213 + log.Debugw("fast-provide-root: skipped", "reason", "disabled by flag or config", "wait-flag-ignored", true)
214 + } else {
215 + log.Debugw("fast-provide-root: skipped", "reason", "disabled by flag or config")
216 + }
217 + }
218 +
219 return nil
220 }
docs/changelogs/v0.39.md
+13 -8
@@ -48,26 +48,31 @@ The Amino DHT Sweep provider system, introduced as experimental in v0.38, is now
48 - Automatic resume after restarts with persistent state ([see below](#provider-resume-cycle-for-improved-reproviding-reliability))
49 - Proactive alerts when reproviding falls behind ([see below](#-sweep-provider-slow-reprovide-warnings))
50 - Better metrics for monitoring (`provider_provides_total`) ([see below](#-metric-rename-provider_provides_total))
51 +- Fast optimistic provide of new root CIDs ([see below](#-fast-root-cid-providing-for-immediate-content-discovery))
52
53 For background on the sweep provider design and motivations, see [`Provide.DHT.SweepEnabled`](https://github.com/ipfs/kubo/blob/master/docs/config.md#providedhtsweepenabled) and [ipshipyard.com#8](https://github.com/ipshipyard/ipshipyard.com/pull/8).
54
55 #### ⚡ Fast root CID providing for immediate content discovery
56
56 -When you add content to IPFS, it normally gets queued for announcement on the DHT. This background queue can take time to process, meaning other peers won't find your content immediately after `ipfs add` completes.
57 +When you add content to IPFS, the sweep provider queues it for efficient DHT provides over time. While this is resource-efficient, other peers won't find your content immediately after `ipfs add` or `ipfs dag import` completes.
58
58 -To make sharing faster, `ipfs add` now does an extra immediate announcement of just the root CID to the DHT (controlled by the new `--fast-provide-root` flag, enabled by default). This lets other peers start discovering your content right away, while the regular background queue still handles announcing all the blocks later.
59 +To make sharing faster, `ipfs add` and `ipfs dag import` now do an immediate provide of root CIDs to the DHT in addition to the regular queue (controlled by the new `--fast-provide-root` flag, enabled by default). This complements the sweep provider system: fast-provide handles the urgent case (root CIDs that users share and reference), while the sweep provider efficiently provides all blocks according to `Provide.Strategy` over time.
60
60 -By default, this extra announcement runs in the background without slowing down the command. For use cases requiring guaranteed discoverability before the command returns (for example, sharing a link immediately), use `--fast-provide-wait` to block until the announcement completes.
61 +This closes the gap between command completion and content shareability: root CIDs typically become discoverable on the network in under a second (compared to 30+ seconds previously). The feature uses optimistic DHT operations, which are significantly faster with the sweep provider (now enabled by default).
62
62 -**Usage examples:**
63 +By default, this immediate provide runs in the background without blocking the command. For use cases requiring guaranteed discoverability before the command returns (e.g., sharing a link immediately), use `--fast-provide-wait` to block until the provide completes.
64 +
65 +**Simple examples:**
66
67 ```bash
65 -ipfs add file.txt # Root CID provided immediately in background, independent of queue (default)
66 -ipfs add file.txt --fast-provide-wait # Blocks until root CID announcement completes (slower, guaranteed)
67 -ipfs add file.txt --fast-provide-root=false # Skip immediate announcement, use background queue only
68 +ipfs add file.txt # Root provided immediately, blocks queued for sweep provider
69 +ipfs add file.txt --fast-provide-wait # Wait for root provide to complete
70 +ipfs dag import file.car # Same for CAR imports
71 ```
72
70 -This optimization works best with the sweep provider and accelerated DHT client, where provide operations are significantly faster than traditional DHT providing. The feature is automatically skipped when DHT is unavailable (e.g., `Routing.Type=none` or delegated-only configurations).
73 +**Configuration:** Set defaults via `Import.FastProvideRoot` (default: `true`) and `Import.FastProvideWait` (default: `false`). See `ipfs add --help` and `ipfs dag import --help` for more details and examples.
74 +
75 +This optimization works best with the sweep provider and accelerated DHT client, where provide operations are significantly faster. Automatically skipped when DHT is unavailable (e.g., `Routing.Type=none` or delegated-only configurations).
76
77 #### 📊 Detailed statistics for Sweep provider with `ipfs provide stat`
78
docs/config.md
+34
@@ -230,6 +230,8 @@ config file at runtime.
230 - [`Import.UnixFSRawLeaves`](#importunixfsrawleaves)
231 - [`Import.UnixFSChunker`](#importunixfschunker)
232 - [`Import.HashFunction`](#importhashfunction)
233 + - [`Import.FastProvideRoot`](#importfastprovideroot)
234 + - [`Import.FastProvideWait`](#importfastprovidewait)
235 - [`Import.BatchMaxNodes`](#importbatchmaxnodes)
236 - [`Import.BatchMaxSize`](#importbatchmaxsize)
237 - [`Import.UnixFSFileMaxLinks`](#importunixfsfilemaxlinks)
@@ -3619,6 +3621,38 @@ Default: `sha2-256`
3621
3622 Type: `optionalString`
3623
3624 +### `Import.FastProvideRoot`
3625 +
3626 +Immediately provide root CIDs to the DHT in addition to the regular provide queue.
3627 +
3628 +This complements the sweep provider system: fast-provide handles the urgent case (root CIDs that users share and reference), while the sweep provider efficiently provides all blocks according to the `Provide.Strategy` over time. Together, they optimize for both immediate discoverability of newly imported content and efficient resource usage for complete DAG provides.
3629 +
3630 +When disabled, only the sweep provider's queue is used.
3631 +
3632 +This setting applies to both `ipfs add` and `ipfs dag import` commands and can be overridden per-command with the `--fast-provide-root` flag.
3633 +
3634 +Ignored when DHT is not available for routing (e.g., `Routing.Type=none` or delegated-only configurations).
3635 +
3636 +Default: `true`
3637 +
3638 +Type: `flag`
3639 +
3640 +### `Import.FastProvideWait`
3641 +
3642 +Wait for the immediate root CID provide to complete before returning.
3643 +
3644 +When enabled, the command blocks until the provide completes, ensuring guaranteed discoverability before returning. When disabled (default), the provide happens asynchronously in the background without blocking the command.
3645 +
3646 +Use this when you need certainty that content is discoverable before the command returns (e.g., sharing a link immediately after adding).
3647 +
3648 +This setting applies to both `ipfs add` and `ipfs dag import` commands and can be overridden per-command with the `--fast-provide-wait` flag.
3649 +
3650 +Ignored when DHT is not available for routing (e.g., `Routing.Type=none` or delegated-only configurations).
3651 +
3652 +Default: `false`
3653 +
3654 +Type: `flag`
3655 +
3656 ### `Import.BatchMaxNodes`
3657
3658 The maximum number of nodes in a write-batch. The total size of the batch is limited by `BatchMaxnodes` and `BatchMaxSize`.
test/cli/add_test.go
+189
@@ -6,6 +6,7 @@ import (
6 "path/filepath"
7 "strings"
8 "testing"
9 + "time"
10
11 "github.com/dustin/go-humanize"
12 "github.com/ipfs/kubo/config"
@@ -15,6 +16,19 @@ import (
16 "github.com/stretchr/testify/require"
17 )
18
19 +// waitForLogMessage polls a buffer for a log message, waiting up to timeout duration.
20 +// Returns true if message found, false if timeout reached.
21 +func waitForLogMessage(buffer *harness.Buffer, message string, timeout time.Duration) bool {
22 + deadline := time.Now().Add(timeout)
23 + for time.Now().Before(deadline) {
24 + if strings.Contains(buffer.String(), message) {
25 + return true
26 + }
27 + time.Sleep(100 * time.Millisecond)
28 + }
29 + return false
30 +}
31 +
32 func TestAdd(t *testing.T) {
33 t.Parallel()
34
@@ -435,7 +449,182 @@ func TestAdd(t *testing.T) {
449 require.Equal(t, 992, len(root.Links))
450 })
451 })
452 +}
453 +
454 +func TestAddFastProvide(t *testing.T) {
455 + t.Parallel()
456 +
457 + const (
458 + shortString = "hello world"
459 + shortStringCidV0 = "Qmf412jQZiuVUtdgnB36FXFX7xg5V6KEbSJ4dpQuhkLyfD" // cidv0 - dag-pb - sha2-256
460 + )
461 +
462 + t.Run("fast-provide-root disabled via config: verify skipped in logs", func(t *testing.T) {
463 + t.Parallel()
464 + node := harness.NewT(t).NewNode().Init()
465 + node.UpdateConfig(func(cfg *config.Config) {
466 + cfg.Import.FastProvideRoot = config.False
467 + })
468 +
469 + // Start daemon with debug logging
470 + node.StartDaemonWithReq(harness.RunRequest{
471 + CmdOpts: []harness.CmdOpt{
472 + harness.RunWithEnv(map[string]string{
473 + "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
474 + }),
475 + },
476 + }, "")
477 + defer node.StopDaemon()
478 +
479 + cidStr := node.IPFSAddStr(shortString)
480 + require.Equal(t, shortStringCidV0, cidStr)
481 +
482 + // Verify fast-provide-root was disabled
483 + daemonLog := node.Daemon.Stderr.String()
484 + require.Contains(t, daemonLog, "fast-provide-root: skipped")
485 + })
486 +
487 + t.Run("fast-provide-root enabled with wait=false: verify async provide", func(t *testing.T) {
488 + t.Parallel()
489 + node := harness.NewT(t).NewNode().Init()
490 + // Use default config (FastProvideRoot=true, FastProvideWait=false)
491 +
492 + node.StartDaemonWithReq(harness.RunRequest{
493 + CmdOpts: []harness.CmdOpt{
494 + harness.RunWithEnv(map[string]string{
495 + "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
496 + }),
497 + },
498 + }, "")
499 + defer node.StopDaemon()
500 +
501 + cidStr := node.IPFSAddStr(shortString)
502 + require.Equal(t, shortStringCidV0, cidStr)
503 +
504 + daemonLog := node.Daemon.Stderr
505 + // Should see async mode started
506 + require.Contains(t, daemonLog.String(), "fast-provide-root: enabled")
507 + require.Contains(t, daemonLog.String(), "fast-provide-root: providing asynchronously")
508 +
509 + // Wait for async completion or failure (up to 11 seconds - slightly more than fastProvideTimeout)
510 + // In test environment with no DHT peers, this will fail with "failed to find any peer in table"
511 + completedOrFailed := waitForLogMessage(daemonLog, "async provide completed", 11*time.Second) ||
512 + waitForLogMessage(daemonLog, "async provide failed", 11*time.Second)
513 + require.True(t, completedOrFailed, "async provide should complete or fail within timeout")
514 + })
515 +
516 + t.Run("fast-provide-root enabled with wait=true: verify sync provide", func(t *testing.T) {
517 + t.Parallel()
518 + node := harness.NewT(t).NewNode().Init()
519 + node.UpdateConfig(func(cfg *config.Config) {
520 + cfg.Import.FastProvideWait = config.True
521 + })
522 +
523 + node.StartDaemonWithReq(harness.RunRequest{
524 + CmdOpts: []harness.CmdOpt{
525 + harness.RunWithEnv(map[string]string{
526 + "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
527 + }),
528 + },
529 + }, "")
530 + defer node.StopDaemon()
531 +
532 + // Use Runner.Run with stdin to allow for expected errors
533 + res := node.Runner.Run(harness.RunRequest{
534 + Path: node.IPFSBin,
535 + Args: []string{"add", "-q"},
536 + CmdOpts: []harness.CmdOpt{
537 + harness.RunWithStdin(strings.NewReader(shortString)),
538 + },
539 + })
540 +
541 + // In sync mode (wait=true), provide errors propagate and fail the command.
542 + // Test environment uses 'test' profile with no bootstrappers, and CI has
543 + // insufficient peers for proper DHT puts, so we expect this to fail with
544 + // "failed to find any peer in table" error from the DHT.
545 + require.Equal(t, 1, res.ExitCode())
546 + require.Contains(t, res.Stderr.String(), "Error: fast-provide: failed to find any peer in table")
547 +
548 + daemonLog := node.Daemon.Stderr.String()
549 + // Should see sync mode started
550 + require.Contains(t, daemonLog, "fast-provide-root: enabled")
551 + require.Contains(t, daemonLog, "fast-provide-root: providing synchronously")
552 + require.Contains(t, daemonLog, "sync provide failed") // Verify the failure was logged
553 + })
554 +
555 + t.Run("fast-provide-wait ignored when root disabled", func(t *testing.T) {
556 + t.Parallel()
557 + node := harness.NewT(t).NewNode().Init()
558 + node.UpdateConfig(func(cfg *config.Config) {
559 + cfg.Import.FastProvideRoot = config.False
560 + cfg.Import.FastProvideWait = config.True
561 + })
562 +
563 + node.StartDaemonWithReq(harness.RunRequest{
564 + CmdOpts: []harness.CmdOpt{
565 + harness.RunWithEnv(map[string]string{
566 + "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
567 + }),
568 + },
569 + }, "")
570 + defer node.StopDaemon()
571 +
572 + cidStr := node.IPFSAddStr(shortString)
573 + require.Equal(t, shortStringCidV0, cidStr)
574 +
575 + daemonLog := node.Daemon.Stderr.String()
576 + require.Contains(t, daemonLog, "fast-provide-root: skipped")
577 + require.Contains(t, daemonLog, "wait-flag-ignored")
578 + })
579 +
580 + t.Run("CLI flag overrides config: flag=true overrides config=false", func(t *testing.T) {
581 + t.Parallel()
582 + node := harness.NewT(t).NewNode().Init()
583 + node.UpdateConfig(func(cfg *config.Config) {
584 + cfg.Import.FastProvideRoot = config.False
585 + })
586 +
587 + node.StartDaemonWithReq(harness.RunRequest{
588 + CmdOpts: []harness.CmdOpt{
589 + harness.RunWithEnv(map[string]string{
590 + "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
591 + }),
592 + },
593 + }, "")
594 + defer node.StopDaemon()
595 +
596 + cidStr := node.IPFSAddStr(shortString, "--fast-provide-root=true")
597 + require.Equal(t, shortStringCidV0, cidStr)
598 +
599 + daemonLog := node.Daemon.Stderr
600 + // Flag should enable it despite config saying false
601 + require.Contains(t, daemonLog.String(), "fast-provide-root: enabled")
602 + require.Contains(t, daemonLog.String(), "fast-provide-root: providing asynchronously")
603 + })
604
605 + t.Run("CLI flag overrides config: flag=false overrides config=true", func(t *testing.T) {
606 + t.Parallel()
607 + node := harness.NewT(t).NewNode().Init()
608 + node.UpdateConfig(func(cfg *config.Config) {
609 + cfg.Import.FastProvideRoot = config.True
610 + })
611 +
612 + node.StartDaemonWithReq(harness.RunRequest{
613 + CmdOpts: []harness.CmdOpt{
614 + harness.RunWithEnv(map[string]string{
615 + "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
616 + }),
617 + },
618 + }, "")
619 + defer node.StopDaemon()
620 +
621 + cidStr := node.IPFSAddStr(shortString, "--fast-provide-root=false")
622 + require.Equal(t, shortStringCidV0, cidStr)
623 +
624 + daemonLog := node.Daemon.Stderr.String()
625 + // Flag should disable it despite config saying true
626 + require.Contains(t, daemonLog, "fast-provide-root: skipped")
627 + })
628 }
629
630 // createDirectoryForHAMT aims to create enough files with long names for the directory block to be close to the UnixFSHAMTDirectorySizeThreshold.
test/cli/dag_test.go
+200
@@ -5,10 +5,13 @@ import (
5 "io"
6 "os"
7 "testing"
8 + "time"
9
10 + "github.com/ipfs/kubo/config"
11 "github.com/ipfs/kubo/test/cli/harness"
12 "github.com/ipfs/kubo/test/cli/testutils"
13 "github.com/stretchr/testify/assert"
14 + "github.com/stretchr/testify/require"
15 )
16
17 const (
@@ -102,3 +105,200 @@ func TestDag(t *testing.T) {
105 assert.Equal(t, content, stat.Stdout.Bytes())
106 })
107 }
108 +
109 +func TestDagImportFastProvide(t *testing.T) {
110 + t.Parallel()
111 +
112 + t.Run("fast-provide-root disabled via config: verify skipped in logs", func(t *testing.T) {
113 + t.Parallel()
114 + node := harness.NewT(t).NewNode().Init()
115 + node.UpdateConfig(func(cfg *config.Config) {
116 + cfg.Import.FastProvideRoot = config.False
117 + })
118 +
119 + // Start daemon with debug logging
120 + node.StartDaemonWithReq(harness.RunRequest{
121 + CmdOpts: []harness.CmdOpt{
122 + harness.RunWithEnv(map[string]string{
123 + "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
124 + }),
125 + },
126 + }, "")
127 + defer node.StopDaemon()
128 +
129 + // Import CAR file
130 + r, err := os.Open(fixtureFile)
131 + require.NoError(t, err)
132 + defer r.Close()
133 + err = node.IPFSDagImport(r, fixtureCid)
134 + require.NoError(t, err)
135 +
136 + // Verify fast-provide-root was disabled
137 + daemonLog := node.Daemon.Stderr.String()
138 + require.Contains(t, daemonLog, "fast-provide-root: skipped")
139 + })
140 +
141 + t.Run("fast-provide-root enabled with wait=false: verify async provide", func(t *testing.T) {
142 + t.Parallel()
143 + node := harness.NewT(t).NewNode().Init()
144 + // Use default config (FastProvideRoot=true, FastProvideWait=false)
145 +
146 + node.StartDaemonWithReq(harness.RunRequest{
147 + CmdOpts: []harness.CmdOpt{
148 + harness.RunWithEnv(map[string]string{
149 + "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
150 + }),
151 + },
152 + }, "")
153 + defer node.StopDaemon()
154 +
155 + // Import CAR file
156 + r, err := os.Open(fixtureFile)
157 + require.NoError(t, err)
158 + defer r.Close()
159 + err = node.IPFSDagImport(r, fixtureCid)
160 + require.NoError(t, err)
161 +
162 + daemonLog := node.Daemon.Stderr
163 + // Should see async mode started
164 + require.Contains(t, daemonLog.String(), "fast-provide-root: enabled")
165 + require.Contains(t, daemonLog.String(), "fast-provide-root: providing asynchronously")
166 + require.Contains(t, daemonLog.String(), fixtureCid) // Should log the specific CID being provided
167 +
168 + // Wait for async completion or failure (slightly more than DefaultFastProvideTimeout)
169 + // In test environment with no DHT peers, this will fail with "failed to find any peer in table"
170 + timeout := config.DefaultFastProvideTimeout + time.Second
171 + completedOrFailed := waitForLogMessage(daemonLog, "async provide completed", timeout) ||
172 + waitForLogMessage(daemonLog, "async provide failed", timeout)
173 + require.True(t, completedOrFailed, "async provide should complete or fail within timeout")
174 + })
175 +
176 + t.Run("fast-provide-root enabled with wait=true: verify sync provide", func(t *testing.T) {
177 + t.Parallel()
178 + node := harness.NewT(t).NewNode().Init()
179 + node.UpdateConfig(func(cfg *config.Config) {
180 + cfg.Import.FastProvideWait = config.True
181 + })
182 +
183 + node.StartDaemonWithReq(harness.RunRequest{
184 + CmdOpts: []harness.CmdOpt{
185 + harness.RunWithEnv(map[string]string{
186 + "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
187 + }),
188 + },
189 + }, "")
190 + defer node.StopDaemon()
191 +
192 + // Import CAR file - use Run instead of IPFSDagImport to handle expected error
193 + r, err := os.Open(fixtureFile)
194 + require.NoError(t, err)
195 + defer r.Close()
196 + res := node.Runner.Run(harness.RunRequest{
197 + Path: node.IPFSBin,
198 + Args: []string{"dag", "import", "--pin-roots=false"},
199 + CmdOpts: []harness.CmdOpt{
200 + harness.RunWithStdin(r),
201 + },
202 + })
203 + // In sync mode (wait=true), provide errors propagate and fail the command.
204 + // Test environment uses 'test' profile with no bootstrappers, and CI has
205 + // insufficient peers for proper DHT puts, so we expect this to fail with
206 + // "failed to find any peer in table" error from the DHT.
207 + require.Equal(t, 1, res.ExitCode())
208 + require.Contains(t, res.Stderr.String(), "Error: fast-provide: failed to find any peer in table")
209 +
210 + daemonLog := node.Daemon.Stderr.String()
211 + // Should see sync mode started
212 + require.Contains(t, daemonLog, "fast-provide-root: enabled")
213 + require.Contains(t, daemonLog, "fast-provide-root: providing synchronously")
214 + require.Contains(t, daemonLog, fixtureCid) // Should log the specific CID being provided
215 + require.Contains(t, daemonLog, "sync provide failed") // Verify the failure was logged
216 + })
217 +
218 + t.Run("fast-provide-wait ignored when root disabled", func(t *testing.T) {
219 + t.Parallel()
220 + node := harness.NewT(t).NewNode().Init()
221 + node.UpdateConfig(func(cfg *config.Config) {
222 + cfg.Import.FastProvideRoot = config.False
223 + cfg.Import.FastProvideWait = config.True
224 + })
225 +
226 + node.StartDaemonWithReq(harness.RunRequest{
227 + CmdOpts: []harness.CmdOpt{
228 + harness.RunWithEnv(map[string]string{
229 + "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
230 + }),
231 + },
232 + }, "")
233 + defer node.StopDaemon()
234 +
235 + // Import CAR file
236 + r, err := os.Open(fixtureFile)
237 + require.NoError(t, err)
238 + defer r.Close()
239 + err = node.IPFSDagImport(r, fixtureCid)
240 + require.NoError(t, err)
241 +
242 + daemonLog := node.Daemon.Stderr.String()
243 + require.Contains(t, daemonLog, "fast-provide-root: skipped")
244 + // Note: dag import doesn't log wait-flag-ignored like add does
245 + })
246 +
247 + t.Run("CLI flag overrides config: flag=true overrides config=false", func(t *testing.T) {
248 + t.Parallel()
249 + node := harness.NewT(t).NewNode().Init()
250 + node.UpdateConfig(func(cfg *config.Config) {
251 + cfg.Import.FastProvideRoot = config.False
252 + })
253 +
254 + node.StartDaemonWithReq(harness.RunRequest{
255 + CmdOpts: []harness.CmdOpt{
256 + harness.RunWithEnv(map[string]string{
257 + "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
258 + }),
259 + },
260 + }, "")
261 + defer node.StopDaemon()
262 +
263 + // Import CAR file with flag override
264 + r, err := os.Open(fixtureFile)
265 + require.NoError(t, err)
266 + defer r.Close()
267 + err = node.IPFSDagImport(r, fixtureCid, "--fast-provide-root=true")
268 + require.NoError(t, err)
269 +
270 + daemonLog := node.Daemon.Stderr
271 + // Flag should enable it despite config saying false
272 + require.Contains(t, daemonLog.String(), "fast-provide-root: enabled")
273 + require.Contains(t, daemonLog.String(), "fast-provide-root: providing asynchronously")
274 + require.Contains(t, daemonLog.String(), fixtureCid) // Should log the specific CID being provided
275 + })
276 +
277 + t.Run("CLI flag overrides config: flag=false overrides config=true", func(t *testing.T) {
278 + t.Parallel()
279 + node := harness.NewT(t).NewNode().Init()
280 + node.UpdateConfig(func(cfg *config.Config) {
281 + cfg.Import.FastProvideRoot = config.True
282 + })
283 +
284 + node.StartDaemonWithReq(harness.RunRequest{
285 + CmdOpts: []harness.CmdOpt{
286 + harness.RunWithEnv(map[string]string{
287 + "GOLOG_LOG_LEVEL": "error,core/commands=debug,core/commands/cmdenv=debug",
288 + }),
289 + },
290 + }, "")
291 + defer node.StopDaemon()
292 +
293 + // Import CAR file with flag override
294 + r, err := os.Open(fixtureFile)
295 + require.NoError(t, err)
296 + defer r.Close()
297 + err = node.IPFSDagImport(r, fixtureCid, "--fast-provide-root=false")
298 + require.NoError(t, err)
299 +
300 + daemonLog := node.Daemon.Stderr.String()
301 + // Flag should disable it despite config saying true
302 + require.Contains(t, daemonLog, "fast-provide-root: skipped")
303 + })
304 +}