From 442a44d20d0f5ccab4cd3bb66c2e20011cd9c566 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Fri, 13 Feb 2026 15:34:10 +0100 Subject: [PATCH 01/20] fix(provider): purge keystore datastore after reset --- core/node/provider.go | 97 ++++++++++++++++++++++++-- docs/examples/kubo-as-a-library/go.mod | 2 +- docs/examples/kubo-as-a-library/go.sum | 4 +- go.mod | 2 +- go.sum | 4 +- test/cli/provider_test.go | 73 +++++++++++++++++++ test/dependencies/go.mod | 2 +- test/dependencies/go.sum | 4 +- 8 files changed, 173 insertions(+), 15 deletions(-) diff --git a/core/node/provider.go b/core/node/provider.go index fba012422d6..a4e7038064d 100644 --- a/core/node/provider.go +++ b/core/node/provider.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "os" + "path/filepath" "time" "github.com/ipfs/boxo/blockstore" @@ -19,6 +21,7 @@ import ( log "github.com/ipfs/go-log/v2" "github.com/ipfs/kubo/config" "github.com/ipfs/kubo/repo" + "github.com/ipfs/kubo/repo/fsrepo" irouting "github.com/ipfs/kubo/routing" dht "github.com/libp2p/go-libp2p-kad-dht" "github.com/libp2p/go-libp2p-kad-dht/amino" @@ -50,8 +53,8 @@ const ( // Datastore namespace prefix for provider data. providerDatastorePrefix = "provider" - // Datastore path for the provider keystore. - keystoreDatastorePath = "keystore" + // Base directory for the provider keystore datastores. + keystoreDatastorePath = "provider-keystore" ) var errAcceleratedDHTNotReady = errors.New("AcceleratedDHTClient: routing table not ready") @@ -369,6 +372,58 @@ type addrsFilter interface { FilteredAddrs() []ma.Multiaddr } +// findRootDatastoreSpec extracts the leaf datastore spec for the root ("/") +// mount from the repo's Datastore.Spec config. It unwraps mount (picks the "/" +// mountpoint), measure, and log wrappers to find the actual backend spec +// (e.g., levelds, pebbleds). +func findRootDatastoreSpec(spec map[string]any) map[string]any { + if spec == nil { + return nil + } + switch spec["type"] { + case "mount": + mounts, ok := spec["mounts"].([]any) + if !ok { + return spec + } + for _, m := range mounts { + mount, ok := m.(map[string]any) + if !ok { + continue + } + if mount["mountpoint"] == "/" { + return findRootDatastoreSpec(mount) + } + } + // No root mount found, return as-is + return spec + case "measure", "log": + if child, ok := spec["child"].(map[string]any); ok { + return findRootDatastoreSpec(child) + } + return spec + default: + return spec + } +} + +// copySpec deep-copies a datastore spec map so modifications (e.g., changing +// the path) don't affect the original. +func copySpec(spec map[string]any) map[string]any { + if spec == nil { + return nil + } + cp := make(map[string]any, len(spec)) + for k, v := range spec { + if m, ok := v.(map[string]any); ok { + cp[k] = copySpec(m) + } else { + cp[k] = v + } + } + return cp +} + func SweepingProviderOpt(cfg *config.Config) fx.Option { reprovideInterval := cfg.Provide.DHT.Interval.WithDefault(config.DefaultProvideDHTInterval) type providerInput struct { @@ -378,10 +433,40 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { } sweepingReprovider := fx.Provide(func(in providerInput) (DHTProvider, *keystore.ResettableKeystore, error) { ds := namespace.Wrap(in.Repo.Datastore(), datastore.NewKey(providerDatastorePrefix)) - ks, err := keystore.NewResettableKeystore(ds, - keystore.WithPrefixBits(16), - keystore.WithDatastorePath(keystoreDatastorePath), - keystore.WithBatchSize(int(cfg.Provide.DHT.KeystoreBatchSize.WithDefault(config.DefaultProvideDHTKeystoreBatchSize))), + + // Get repo path and config to determine datastore type + repoPath := in.Repo.Path() + repoCfg, err := in.Repo.Config() + if err != nil { + return nil, nil, fmt.Errorf("getting repo config: %w", err) + } + + // Find the root datastore type (levelds, pebbleds, etc.) + rootSpec := findRootDatastoreSpec(repoCfg.Datastore.Spec) + + // Keystore datastores live at /provider-keystore/ + keystoreBasePath := filepath.Join(repoPath, keystoreDatastorePath) + + createDs := func(suffix string) (datastore.Batching, error) { + spec := copySpec(rootSpec) + spec["path"] = filepath.Join(keystoreBasePath, suffix) + dsc, err := fsrepo.AnyDatastoreConfig(spec) + if err != nil { + return nil, fmt.Errorf("creating keystore datastore config: %w", err) + } + return dsc.Create("") + } + + destroyDs := func(suffix string) error { + return os.RemoveAll(filepath.Join(keystoreBasePath, suffix)) + } + + ks, err := keystore.NewResettableKeystore( + keystore.WithDatastoreFactory(createDs, destroyDs), + keystore.KeystoreOption( + keystore.WithPrefixBits(16), + keystore.WithBatchSize(int(cfg.Provide.DHT.KeystoreBatchSize.WithDefault(config.DefaultProvideDHTKeystoreBatchSize))), + ), ) if err != nil { return nil, nil, err diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index 7ee90a3fccf..0db8796bbdf 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -116,7 +116,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 // indirect github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260211161343-a9412b283883 // indirect + github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4 // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-pubsub v0.15.0 // indirect github.com/libp2p/go-libp2p-pubsub-router v0.6.0 // indirect diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 18b451148ba..060aa02c713 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -405,8 +405,8 @@ github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl9 github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g= github.com/libp2p/go-libp2p-core v0.3.0/go.mod h1:ACp3DmS3/N64c2jDzcV429ukDpicbL6+TrrxANBjPGw= -github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260211161343-a9412b283883 h1:cEbqqo3yrRk/K0sfro5FIo5udSwNH4Y1N+8MFfp7bz0= -github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260211161343-a9412b283883/go.mod h1:aZuF2qipKhprKkt8Xw3lGJlysEjpZXDIUyGdm1/KGA8= +github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4 h1:CSdNKDR+OIl+vh1KMzNKcgKV/yxbElIcrbFAcnfDhG0= +github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4/go.mod h1:aZuF2qipKhprKkt8Xw3lGJlysEjpZXDIUyGdm1/KGA8= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= diff --git a/go.mod b/go.mod index 2fa5829d253..9a060ebb5f3 100644 --- a/go.mod +++ b/go.mod @@ -52,7 +52,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 github.com/libp2p/go-libp2p v0.47.0 github.com/libp2p/go-libp2p-http v0.5.0 - github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260211161343-a9412b283883 + github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4 github.com/libp2p/go-libp2p-kbucket v0.8.0 github.com/libp2p/go-libp2p-pubsub v0.15.0 github.com/libp2p/go-libp2p-pubsub-router v0.6.0 diff --git a/go.sum b/go.sum index fb32c3ccfe2..8769747f2e3 100644 --- a/go.sum +++ b/go.sum @@ -493,8 +493,8 @@ github.com/libp2p/go-libp2p-gostream v0.6.0 h1:QfAiWeQRce6pqnYfmIVWJFXNdDyfiR/qk github.com/libp2p/go-libp2p-gostream v0.6.0/go.mod h1:Nywu0gYZwfj7Jc91PQvbGU8dIpqbQQkjWgDuOrFaRdA= github.com/libp2p/go-libp2p-http v0.5.0 h1:+x0AbLaUuLBArHubbbNRTsgWz0RjNTy6DJLOxQ3/QBc= github.com/libp2p/go-libp2p-http v0.5.0/go.mod h1:glh87nZ35XCQyFsdzZps6+F4HYI6DctVFY5u1fehwSg= -github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260211161343-a9412b283883 h1:cEbqqo3yrRk/K0sfro5FIo5udSwNH4Y1N+8MFfp7bz0= -github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260211161343-a9412b283883/go.mod h1:aZuF2qipKhprKkt8Xw3lGJlysEjpZXDIUyGdm1/KGA8= +github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4 h1:CSdNKDR+OIl+vh1KMzNKcgKV/yxbElIcrbFAcnfDhG0= +github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4/go.mod h1:aZuF2qipKhprKkt8Xw3lGJlysEjpZXDIUyGdm1/KGA8= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= diff --git a/test/cli/provider_test.go b/test/cli/provider_test.go index a62ce99446e..72a6f0f4e77 100644 --- a/test/cli/provider_test.go +++ b/test/cli/provider_test.go @@ -6,6 +6,8 @@ import ( "fmt" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "sync/atomic" "testing" @@ -842,3 +844,74 @@ func TestHTTPOnlyProviderWithSweepEnabled(t *testing.T) { assert.Contains(t, statRes.Stdout.String(), "TotalReprovides:", "should show legacy provider stats") } + +// TestProviderKeystoreDatastoreCompaction verifies that the SweepingProvider's +// keystore uses a datastore factory that creates separate physical datastores +// and reclaims disk space by deleting old datastores after each reset cycle. +// +// The keystore uses two alternating namespaces ("0" and "1") plus a "meta" +// namespace. The lifecycle is: +// 1. First start: namespace "0" is created as the initial active datastore +// 2. First reset (keystore sync at startup): "1" is created, data is written, +// namespaces swap, "0" is destroyed from disk via os.RemoveAll +// 3. Restart: "1" and "meta" survive on disk +// 4. Second reset: "0" is recreated, namespaces swap, "1" is destroyed +func TestProviderKeystoreDatastorePurge(t *testing.T) { + t.Parallel() + + h := harness.NewT(t) + node := h.NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) + node.SetIPFSConfig("Provide.Enabled", true) + node.SetIPFSConfig("Bootstrap", []string{}) + + // Add content offline so the keystore has something to sync on startup. + for i := range 5 { + node.IPFSAddStr(fmt.Sprintf("keystore-compaction-test-%d", i)) + } + + keystoreBase := filepath.Join(node.Dir, "provider-keystore") + ns0 := filepath.Join(keystoreBase, "0") + ns1 := filepath.Join(keystoreBase, "1") + meta := filepath.Join(keystoreBase, "meta") + + // Directory should not exist before starting the daemon. + _, err := os.Stat(keystoreBase) + require.True(t, os.IsNotExist(err), "provider-keystore should not exist before daemon start") + + // --- First start: triggers keystore sync (ResetCids) --- + // Init creates "0", then reset swaps to "1" and destroys "0". + node.StartDaemon() + + require.Eventually(t, func() bool { + return dirExists(ns1) && !dirExists(ns0) + }, 30*time.Second, 200*time.Millisecond, + "after first reset: ns1 should exist, ns0 should be destroyed") + + assert.True(t, dirExists(meta), "meta should exist after first reset") + + // --- Restart: triggers a second keystore sync (ResetCids) --- + // Reset swaps back to "0" and destroys "1". + node.StopDaemon() + + // Between restarts: ns1 and meta survive on disk, ns0 does not. + assert.True(t, dirExists(ns1), "ns1 should survive shutdown") + assert.True(t, dirExists(meta), "meta should survive shutdown") + assert.False(t, dirExists(ns0), "ns0 should not reappear between restarts") + + node.StartDaemon() + + require.Eventually(t, func() bool { + return dirExists(ns0) && !dirExists(ns1) + }, 30*time.Second, 200*time.Millisecond, + "after second reset: ns0 should exist, ns1 should be destroyed") + + assert.True(t, dirExists(meta), "meta should still exist after second reset") + + node.StopDaemon() +} + +func dirExists(path string) bool { + info, err := os.Stat(path) + return err == nil && info.IsDir() +} diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod index d6957d28acc..29a50e1d737 100644 --- a/test/dependencies/go.mod +++ b/test/dependencies/go.mod @@ -183,7 +183,7 @@ require ( github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p v0.47.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260211161343-a9412b283883 // indirect + github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4 // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-record v0.3.1 // indirect github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum index 23a0f6a95ba..9a35ea8f19e 100644 --- a/test/dependencies/go.sum +++ b/test/dependencies/go.sum @@ -421,8 +421,8 @@ github.com/libp2p/go-libp2p v0.47.0 h1:qQpBjSCWNQFF0hjBbKirMXE9RHLtSuzTDkTfr1rw0 github.com/libp2p/go-libp2p v0.47.0/go.mod h1:s8HPh7mMV933OtXzONaGFseCg/BE//m1V34p3x4EUOY= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= -github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260211161343-a9412b283883 h1:cEbqqo3yrRk/K0sfro5FIo5udSwNH4Y1N+8MFfp7bz0= -github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260211161343-a9412b283883/go.mod h1:aZuF2qipKhprKkt8Xw3lGJlysEjpZXDIUyGdm1/KGA8= +github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4 h1:CSdNKDR+OIl+vh1KMzNKcgKV/yxbElIcrbFAcnfDhG0= +github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4/go.mod h1:aZuF2qipKhprKkt8Xw3lGJlysEjpZXDIUyGdm1/KGA8= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg= From b4a877eab66acfad585ac881283032579f23a18f Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Fri, 13 Feb 2026 15:51:36 +0100 Subject: [PATCH 02/20] changelog --- docs/changelogs/v0.41.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/changelogs/v0.41.md b/docs/changelogs/v0.41.md index 41b761e2e23..56531e3b7b2 100644 --- a/docs/changelogs/v0.41.md +++ b/docs/changelogs/v0.41.md @@ -10,6 +10,7 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team. - [Overview](#overview) - [๐Ÿ”ฆ Highlights](#-highlights) + - [๐Ÿ—‘๏ธ Provider Keystore Disk Reclamation](#-provider-keystore-disk-reclamation) - [๐Ÿ“ Changelog](#-changelog) - [๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributors](#-contributors) @@ -17,6 +18,19 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team. ### ๐Ÿ”ฆ Highlights +#### ๐Ÿ—‘๏ธ Provider Keystore Disk Reclamation + +The SweepingProvider's keystore now uses physically separate datastores instead +of namespacing within the shared repo datastore. When the keystore resets +during a reprovide cycle, the old datastore is removed from disk entirely +(`os.RemoveAll`) rather than being emptied key-by-key. This eliminates disk +bloat from stale tombstones that previously lingered until the storage engine's +background compaction ran. The new keystores live under +`/provider-keystore/` and automatically match the repo's configured +backend (LevelDB, Pebble, etc.). See +[go-libp2p-kad-dht#1233](https://github.com/libp2p/go-libp2p-kad-dht/pull/1233) +for the upstream change. + ### ๐Ÿ“ Changelog ### ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributors From 5ba73598302c3ab8cd48e88b049ecb042c5f9c90 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Fri, 13 Feb 2026 16:05:41 +0100 Subject: [PATCH 03/20] use MapDatastore if no datastore is configured --- core/node/provider.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/node/provider.go b/core/node/provider.go index a4e7038064d..85650491d90 100644 --- a/core/node/provider.go +++ b/core/node/provider.go @@ -448,6 +448,14 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { keystoreBasePath := filepath.Join(repoPath, keystoreDatastorePath) createDs := func(suffix string) (datastore.Batching, error) { + // When no datastore spec is configured (e.g., test/mock repos), + // fall back to an in-memory datastore. + if rootSpec == nil { + return datastore.NewMapDatastore(), nil + } + if err := os.MkdirAll(keystoreBasePath, 0o755); err != nil { + return nil, fmt.Errorf("creating keystore base directory: %w", err) + } spec := copySpec(rootSpec) spec["path"] = filepath.Join(keystoreBasePath, suffix) dsc, err := fsrepo.AnyDatastoreConfig(spec) From 76916a676a55b5f90ce47e70fbfc0c9efd30905f Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Tue, 17 Feb 2026 10:30:48 +0100 Subject: [PATCH 04/20] bump kad-dht to latest commit --- docs/examples/kubo-as-a-library/go.mod | 6 +++--- docs/examples/kubo-as-a-library/go.sum | 12 ++++++------ go.mod | 6 +++--- go.sum | 12 ++++++------ test/dependencies/go.mod | 6 +++--- test/dependencies/go.sum | 12 ++++++------ 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index 0db8796bbdf..2b9eb03d8fc 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -77,7 +77,7 @@ require ( github.com/ipfs/go-block-format v0.2.3 // indirect github.com/ipfs/go-cid v0.6.0 // indirect github.com/ipfs/go-cidutil v0.1.1 // indirect - github.com/ipfs/go-datastore v0.9.0 // indirect + github.com/ipfs/go-datastore v0.9.1 // indirect github.com/ipfs/go-ds-badger v0.3.4 // indirect github.com/ipfs/go-ds-flatfs v0.6.0 // indirect github.com/ipfs/go-ds-leveldb v0.5.2 // indirect @@ -116,7 +116,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 // indirect github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4 // indirect + github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-pubsub v0.15.0 // indirect github.com/libp2p/go-libp2p-pubsub-router v0.6.0 // indirect @@ -152,7 +152,7 @@ require ( github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect - github.com/pion/dtls/v3 v3.1.0 // indirect + github.com/pion/dtls/v3 v3.1.1 // indirect github.com/pion/ice/v4 v4.0.10 // indirect github.com/pion/interceptor v0.1.40 // indirect github.com/pion/logging v0.2.4 // indirect diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 060aa02c713..4170d2bc6ea 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -283,8 +283,8 @@ github.com/ipfs/go-cidutil v0.1.1 h1:COuby6H8C2ml0alvHYX3WdbFM4F07YtbY0UlT5j+sgI github.com/ipfs/go-cidutil v0.1.1/go.mod h1:SCoUftGEUgoXe5Hjeyw5CiLZF8cwYn/TbtpFQXJCP6k= github.com/ipfs/go-datastore v0.1.0/go.mod h1:d4KVXhMt913cLBEI/PXAy6ko+W7e9AhyAKBGh803qeE= github.com/ipfs/go-datastore v0.1.1/go.mod h1:w38XXW9kVFNp57Zj5knbKWM2T+KOZCGDRVNdgPHtbHw= -github.com/ipfs/go-datastore v0.9.0 h1:WocriPOayqalEsueHv6SdD4nPVl4rYMfYGLD4bqCZ+w= -github.com/ipfs/go-datastore v0.9.0/go.mod h1:uT77w/XEGrvJWwHgdrMr8bqCN6ZTW9gzmi+3uK+ouHg= +github.com/ipfs/go-datastore v0.9.1 h1:67Po2epre/o0UxrmkzdS9ZTe2GFGODgTd2odx8Wh6Yo= +github.com/ipfs/go-datastore v0.9.1/go.mod h1:zi07Nvrpq1bQwSkEnx3bfjz+SQZbdbWyCNvyxMh9pN0= github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= github.com/ipfs/go-ds-badger v0.0.7/go.mod h1:qt0/fWzZDoPW6jpQeqUjR5kBfhDNB65jd9YlmAvpQBk= @@ -405,8 +405,8 @@ github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl9 github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g= github.com/libp2p/go-libp2p-core v0.3.0/go.mod h1:ACp3DmS3/N64c2jDzcV429ukDpicbL6+TrrxANBjPGw= -github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4 h1:CSdNKDR+OIl+vh1KMzNKcgKV/yxbElIcrbFAcnfDhG0= -github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4/go.mod h1:aZuF2qipKhprKkt8Xw3lGJlysEjpZXDIUyGdm1/KGA8= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c h1:dC2Dt3wa9SB+kWKXNCy4Y06EJS3Dz7Cbq0b8nE7B8bk= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= @@ -547,8 +547,8 @@ github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oL github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/dtls/v3 v3.1.0 h1:bz3alDjKL1DDGe8GETGcq5rDKjXFQX9mniuUo36Up0E= -github.com/pion/dtls/v3 v3.1.0/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8= +github.com/pion/dtls/v3 v3.1.1 h1:wSLMam9Kf7DL1A74hnqRvEb9OT+aXPAsQ5VS+BdXOJ0= +github.com/pion/dtls/v3 v3.1.1/go.mod h1:7FGvVYpHsUV6+aywaFpG7aE4Vz8nBOx74odPRFue6cI= github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= diff --git a/go.mod b/go.mod index 9a060ebb5f3..2615fa4af47 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/ipfs/go-block-format v0.2.3 github.com/ipfs/go-cid v0.6.0 github.com/ipfs/go-cidutil v0.1.1 - github.com/ipfs/go-datastore v0.9.0 + github.com/ipfs/go-datastore v0.9.1 github.com/ipfs/go-detect-race v0.0.1 github.com/ipfs/go-ds-badger v0.3.4 github.com/ipfs/go-ds-flatfs v0.6.0 @@ -52,7 +52,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 github.com/libp2p/go-libp2p v0.47.0 github.com/libp2p/go-libp2p-http v0.5.0 - github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4 + github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c github.com/libp2p/go-libp2p-kbucket v0.8.0 github.com/libp2p/go-libp2p-pubsub v0.15.0 github.com/libp2p/go-libp2p-pubsub-router v0.6.0 @@ -196,7 +196,7 @@ require ( github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect - github.com/pion/dtls/v3 v3.1.0 // indirect + github.com/pion/dtls/v3 v3.1.1 // indirect github.com/pion/ice/v4 v4.0.10 // indirect github.com/pion/interceptor v0.1.40 // indirect github.com/pion/logging v0.2.4 // indirect diff --git a/go.sum b/go.sum index 8769747f2e3..4ac954696ab 100644 --- a/go.sum +++ b/go.sum @@ -353,8 +353,8 @@ github.com/ipfs/go-cidutil v0.1.1 h1:COuby6H8C2ml0alvHYX3WdbFM4F07YtbY0UlT5j+sgI github.com/ipfs/go-cidutil v0.1.1/go.mod h1:SCoUftGEUgoXe5Hjeyw5CiLZF8cwYn/TbtpFQXJCP6k= github.com/ipfs/go-datastore v0.1.0/go.mod h1:d4KVXhMt913cLBEI/PXAy6ko+W7e9AhyAKBGh803qeE= github.com/ipfs/go-datastore v0.1.1/go.mod h1:w38XXW9kVFNp57Zj5knbKWM2T+KOZCGDRVNdgPHtbHw= -github.com/ipfs/go-datastore v0.9.0 h1:WocriPOayqalEsueHv6SdD4nPVl4rYMfYGLD4bqCZ+w= -github.com/ipfs/go-datastore v0.9.0/go.mod h1:uT77w/XEGrvJWwHgdrMr8bqCN6ZTW9gzmi+3uK+ouHg= +github.com/ipfs/go-datastore v0.9.1 h1:67Po2epre/o0UxrmkzdS9ZTe2GFGODgTd2odx8Wh6Yo= +github.com/ipfs/go-datastore v0.9.1/go.mod h1:zi07Nvrpq1bQwSkEnx3bfjz+SQZbdbWyCNvyxMh9pN0= github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= github.com/ipfs/go-ds-badger v0.0.7/go.mod h1:qt0/fWzZDoPW6jpQeqUjR5kBfhDNB65jd9YlmAvpQBk= @@ -493,8 +493,8 @@ github.com/libp2p/go-libp2p-gostream v0.6.0 h1:QfAiWeQRce6pqnYfmIVWJFXNdDyfiR/qk github.com/libp2p/go-libp2p-gostream v0.6.0/go.mod h1:Nywu0gYZwfj7Jc91PQvbGU8dIpqbQQkjWgDuOrFaRdA= github.com/libp2p/go-libp2p-http v0.5.0 h1:+x0AbLaUuLBArHubbbNRTsgWz0RjNTy6DJLOxQ3/QBc= github.com/libp2p/go-libp2p-http v0.5.0/go.mod h1:glh87nZ35XCQyFsdzZps6+F4HYI6DctVFY5u1fehwSg= -github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4 h1:CSdNKDR+OIl+vh1KMzNKcgKV/yxbElIcrbFAcnfDhG0= -github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4/go.mod h1:aZuF2qipKhprKkt8Xw3lGJlysEjpZXDIUyGdm1/KGA8= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c h1:dC2Dt3wa9SB+kWKXNCy4Y06EJS3Dz7Cbq0b8nE7B8bk= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= @@ -655,8 +655,8 @@ github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oL github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/dtls/v3 v3.1.0 h1:bz3alDjKL1DDGe8GETGcq5rDKjXFQX9mniuUo36Up0E= -github.com/pion/dtls/v3 v3.1.0/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8= +github.com/pion/dtls/v3 v3.1.1 h1:wSLMam9Kf7DL1A74hnqRvEb9OT+aXPAsQ5VS+BdXOJ0= +github.com/pion/dtls/v3 v3.1.1/go.mod h1:7FGvVYpHsUV6+aywaFpG7aE4Vz8nBOx74odPRFue6cI= github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod index 29a50e1d737..fa861c4824a 100644 --- a/test/dependencies/go.mod +++ b/test/dependencies/go.mod @@ -139,7 +139,7 @@ require ( github.com/ipfs/go-bitfield v1.1.0 // indirect github.com/ipfs/go-block-format v0.2.3 // indirect github.com/ipfs/go-cid v0.6.0 // indirect - github.com/ipfs/go-datastore v0.9.0 // indirect + github.com/ipfs/go-datastore v0.9.1 // indirect github.com/ipfs/go-dsqueue v0.2.0 // indirect github.com/ipfs/go-ipfs-cmds v0.15.1-0.20260204204540-af9bcbaf5709 // indirect github.com/ipfs/go-ipfs-redirects-file v0.1.2 // indirect @@ -183,7 +183,7 @@ require ( github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p v0.47.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4 // indirect + github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-record v0.3.1 // indirect github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect @@ -227,7 +227,7 @@ require ( github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect - github.com/pion/dtls/v3 v3.1.0 // indirect + github.com/pion/dtls/v3 v3.1.1 // indirect github.com/pion/ice/v4 v4.0.10 // indirect github.com/pion/interceptor v0.1.40 // indirect github.com/pion/logging v0.2.4 // indirect diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum index 9a35ea8f19e..2cd3bf135af 100644 --- a/test/dependencies/go.sum +++ b/test/dependencies/go.sum @@ -306,8 +306,8 @@ github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30= github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ= github.com/ipfs/go-cidutil v0.1.1 h1:COuby6H8C2ml0alvHYX3WdbFM4F07YtbY0UlT5j+sgI= github.com/ipfs/go-cidutil v0.1.1/go.mod h1:SCoUftGEUgoXe5Hjeyw5CiLZF8cwYn/TbtpFQXJCP6k= -github.com/ipfs/go-datastore v0.9.0 h1:WocriPOayqalEsueHv6SdD4nPVl4rYMfYGLD4bqCZ+w= -github.com/ipfs/go-datastore v0.9.0/go.mod h1:uT77w/XEGrvJWwHgdrMr8bqCN6ZTW9gzmi+3uK+ouHg= +github.com/ipfs/go-datastore v0.9.1 h1:67Po2epre/o0UxrmkzdS9ZTe2GFGODgTd2odx8Wh6Yo= +github.com/ipfs/go-datastore v0.9.1/go.mod h1:zi07Nvrpq1bQwSkEnx3bfjz+SQZbdbWyCNvyxMh9pN0= github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= github.com/ipfs/go-ds-leveldb v0.5.2 h1:6nmxlQ2zbp4LCNdJVsmHfs9GP0eylfBNxpmY1csp0x0= @@ -421,8 +421,8 @@ github.com/libp2p/go-libp2p v0.47.0 h1:qQpBjSCWNQFF0hjBbKirMXE9RHLtSuzTDkTfr1rw0 github.com/libp2p/go-libp2p v0.47.0/go.mod h1:s8HPh7mMV933OtXzONaGFseCg/BE//m1V34p3x4EUOY= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= -github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4 h1:CSdNKDR+OIl+vh1KMzNKcgKV/yxbElIcrbFAcnfDhG0= -github.com/libp2p/go-libp2p-kad-dht v0.37.2-0.20260212142733-97ce04b37df4/go.mod h1:aZuF2qipKhprKkt8Xw3lGJlysEjpZXDIUyGdm1/KGA8= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c h1:dC2Dt3wa9SB+kWKXNCy4Y06EJS3Dz7Cbq0b8nE7B8bk= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg= @@ -548,8 +548,8 @@ github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oL github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/dtls/v3 v3.1.0 h1:bz3alDjKL1DDGe8GETGcq5rDKjXFQX9mniuUo36Up0E= -github.com/pion/dtls/v3 v3.1.0/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8= +github.com/pion/dtls/v3 v3.1.1 h1:wSLMam9Kf7DL1A74hnqRvEb9OT+aXPAsQ5VS+BdXOJ0= +github.com/pion/dtls/v3 v3.1.1/go.mod h1:7FGvVYpHsUV6+aywaFpG7aE4Vz8nBOx74odPRFue6cI= github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= From 0f1d23783881fab0f019ebf11789b0e904710c03 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Tue, 17 Feb 2026 11:10:31 +0100 Subject: [PATCH 05/20] purge orphaned keystore migration --- core/node/provider.go | 57 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/core/node/provider.go b/core/node/provider.go index 85650491d90..386466ef2cb 100644 --- a/core/node/provider.go +++ b/core/node/provider.go @@ -424,6 +424,49 @@ func copySpec(spec map[string]any) map[string]any { return cp } +// purgeOrphanedKeystoreData deletes all keys under /provider/keystore/ from the +// shared repo datastore. These were written by older Kubo versions that stored +// provider keystore data inline in the shared datastore. The new code uses +// separate filesystem datastores under /{keystoreDatastorePath}/ instead. +func purgeOrphanedKeystoreData(ctx context.Context, ds datastore.Batching) error { + orphanedPrefix := datastore.NewKey(providerDatastorePrefix).ChildString("keystore").String() + + results, err := ds.Query(ctx, query.Query{ + Prefix: orphanedPrefix, + KeysOnly: true, + }) + if err != nil { + return fmt.Errorf("querying orphaned keystore data: %w", err) + } + defer results.Close() + + batch, err := ds.Batch(ctx) + if err != nil { + return fmt.Errorf("creating batch for orphaned keystore cleanup: %w", err) + } + + count := 0 + for result := range results.Next() { + if result.Error != nil { + return fmt.Errorf("iterating orphaned keystore data: %w", result.Error) + } + if err := batch.Delete(ctx, datastore.NewKey(result.Key)); err != nil { + return fmt.Errorf("batch deleting orphaned key %s: %w", result.Key, err) + } + count++ + } + if count > 0 { + if err := batch.Commit(ctx); err != nil { + return fmt.Errorf("committing orphaned keystore cleanup batch: %w", err) + } + if err := ds.Sync(ctx, datastore.NewKey(orphanedPrefix)); err != nil { + return fmt.Errorf("syncing orphaned keystore cleanup: %w", err) + } + logger.Infow("purged orphaned provider keystore data from shared datastore", "keys", count) + } + return nil +} + func SweepingProviderOpt(cfg *config.Config) fx.Option { reprovideInterval := cfg.Provide.DHT.Interval.WithDefault(config.DefaultProvideDHTInterval) type providerInput struct { @@ -444,7 +487,7 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { // Find the root datastore type (levelds, pebbleds, etc.) rootSpec := findRootDatastoreSpec(repoCfg.Datastore.Spec) - // Keystore datastores live at /provider-keystore/ + // Keystore datastores live at /{keystoreDatastorePath}/ keystoreBasePath := filepath.Join(repoPath, keystoreDatastorePath) createDs := func(suffix string) (datastore.Batching, error) { @@ -469,6 +512,18 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { return os.RemoveAll(filepath.Join(keystoreBasePath, suffix)) } + // One-time migration: purge orphaned keystore data from the shared repo + // datastore. Old Kubo versions stored keystore data at /provider/keystore/ + // inside the shared monolithic datastore. New code uses separate + // filesystem datastores under /{keystoreDatastorePath}/. On first + // start with the new code, detect the upgrade (dir doesn't exist yet) and + // delete the stale keys. + if _, statErr := os.Stat(keystoreBasePath); os.IsNotExist(statErr) { + if purgeErr := purgeOrphanedKeystoreData(context.Background(), in.Repo.Datastore()); purgeErr != nil { + logger.Warnw("failed to purge orphaned provider keystore data from shared datastore", "error", purgeErr) + } + } + ks, err := keystore.NewResettableKeystore( keystore.WithDatastoreFactory(createDs, destroyDs), keystore.KeystoreOption( From 6ef1b99e217d6eabecb3750607148d9efc1553c1 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Tue, 17 Feb 2026 13:52:57 +0100 Subject: [PATCH 06/20] bump kad-dht --- docs/examples/kubo-as-a-library/go.mod | 2 +- docs/examples/kubo-as-a-library/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- test/dependencies/go.mod | 2 +- test/dependencies/go.sum | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index 440a0b596a9..1f653252e32 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -116,7 +116,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 // indirect github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c // indirect + github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-pubsub v0.15.0 // indirect github.com/libp2p/go-libp2p-pubsub-router v0.6.0 // indirect diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 670085b49cb..8b1800c1f87 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -486,8 +486,8 @@ github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl9 github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g= github.com/libp2p/go-libp2p-core v0.3.0/go.mod h1:ACp3DmS3/N64c2jDzcV429ukDpicbL6+TrrxANBjPGw= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c h1:dC2Dt3wa9SB+kWKXNCy4Y06EJS3Dz7Cbq0b8nE7B8bk= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd h1:Ty72X3AyijGw48euNGljHeA21pYOqsgUiLZWdxMj5T4= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= diff --git a/go.mod b/go.mod index 055e596fb67..44c34e9bc5e 100644 --- a/go.mod +++ b/go.mod @@ -52,7 +52,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 github.com/libp2p/go-libp2p v0.47.0 github.com/libp2p/go-libp2p-http v0.5.0 - github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c + github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd github.com/libp2p/go-libp2p-kbucket v0.8.0 github.com/libp2p/go-libp2p-pubsub v0.15.0 github.com/libp2p/go-libp2p-pubsub-router v0.6.0 diff --git a/go.sum b/go.sum index 5c9efb7a776..9765fd5e659 100644 --- a/go.sum +++ b/go.sum @@ -543,8 +543,8 @@ github.com/libp2p/go-libp2p-gostream v0.6.0 h1:QfAiWeQRce6pqnYfmIVWJFXNdDyfiR/qk github.com/libp2p/go-libp2p-gostream v0.6.0/go.mod h1:Nywu0gYZwfj7Jc91PQvbGU8dIpqbQQkjWgDuOrFaRdA= github.com/libp2p/go-libp2p-http v0.5.0 h1:+x0AbLaUuLBArHubbbNRTsgWz0RjNTy6DJLOxQ3/QBc= github.com/libp2p/go-libp2p-http v0.5.0/go.mod h1:glh87nZ35XCQyFsdzZps6+F4HYI6DctVFY5u1fehwSg= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c h1:dC2Dt3wa9SB+kWKXNCy4Y06EJS3Dz7Cbq0b8nE7B8bk= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd h1:Ty72X3AyijGw48euNGljHeA21pYOqsgUiLZWdxMj5T4= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod index 97380c5fe75..08981140f56 100644 --- a/test/dependencies/go.mod +++ b/test/dependencies/go.mod @@ -183,7 +183,7 @@ require ( github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p v0.47.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c // indirect + github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-record v0.3.1 // indirect github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum index 26119b41d03..bd7ed90f51e 100644 --- a/test/dependencies/go.sum +++ b/test/dependencies/go.sum @@ -578,8 +578,8 @@ github.com/libp2p/go-libp2p v0.47.0 h1:qQpBjSCWNQFF0hjBbKirMXE9RHLtSuzTDkTfr1rw0 github.com/libp2p/go-libp2p v0.47.0/go.mod h1:s8HPh7mMV933OtXzONaGFseCg/BE//m1V34p3x4EUOY= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c h1:dC2Dt3wa9SB+kWKXNCy4Y06EJS3Dz7Cbq0b8nE7B8bk= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217092601-81a726dc0c1c/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd h1:Ty72X3AyijGw48euNGljHeA21pYOqsgUiLZWdxMj5T4= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg= From 86d814bfd3d0a1d2662ae80757f358cc3060db56 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Tue, 17 Feb 2026 14:05:25 +0100 Subject: [PATCH 07/20] use main datastore for keystore "meta" store --- core/node/provider.go | 3 ++- test/cli/provider_test.go | 8 +------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/core/node/provider.go b/core/node/provider.go index 386466ef2cb..ac2cac0e26a 100644 --- a/core/node/provider.go +++ b/core/node/provider.go @@ -524,7 +524,8 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { } } - ks, err := keystore.NewResettableKeystore( + keystoreDs := namespace.Wrap(ds, datastore.NewKey("keystore")) + ks, err := keystore.NewResettableKeystore(keystoreDs, keystore.WithDatastoreFactory(createDs, destroyDs), keystore.KeystoreOption( keystore.WithPrefixBits(16), diff --git a/test/cli/provider_test.go b/test/cli/provider_test.go index 72a6f0f4e77..7088f2b363b 100644 --- a/test/cli/provider_test.go +++ b/test/cli/provider_test.go @@ -873,7 +873,6 @@ func TestProviderKeystoreDatastorePurge(t *testing.T) { keystoreBase := filepath.Join(node.Dir, "provider-keystore") ns0 := filepath.Join(keystoreBase, "0") ns1 := filepath.Join(keystoreBase, "1") - meta := filepath.Join(keystoreBase, "meta") // Directory should not exist before starting the daemon. _, err := os.Stat(keystoreBase) @@ -888,15 +887,12 @@ func TestProviderKeystoreDatastorePurge(t *testing.T) { }, 30*time.Second, 200*time.Millisecond, "after first reset: ns1 should exist, ns0 should be destroyed") - assert.True(t, dirExists(meta), "meta should exist after first reset") - // --- Restart: triggers a second keystore sync (ResetCids) --- // Reset swaps back to "0" and destroys "1". node.StopDaemon() - // Between restarts: ns1 and meta survive on disk, ns0 does not. + // Between restarts: ns1 survives on disk, ns0 does not. assert.True(t, dirExists(ns1), "ns1 should survive shutdown") - assert.True(t, dirExists(meta), "meta should survive shutdown") assert.False(t, dirExists(ns0), "ns0 should not reappear between restarts") node.StartDaemon() @@ -906,8 +902,6 @@ func TestProviderKeystoreDatastorePurge(t *testing.T) { }, 30*time.Second, 200*time.Millisecond, "after second reset: ns0 should exist, ns1 should be destroyed") - assert.True(t, dirExists(meta), "meta should still exist after second reset") - node.StopDaemon() } From d8e7d7836673f583707a49c073c87d96480eb9b9 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Tue, 17 Feb 2026 16:18:46 +0100 Subject: [PATCH 08/20] add provider/keystore/0 and /1 to ipfs diag command mount keystore datastores to /provider/keystore/0 and /1 so that they are included in the ipfs diag datastore command --- core/commands/diag.go | 59 ++++++++++++++++++---- core/node/provider.go | 89 ++++++++++++++++++++++++++------- test/cli/diag_datastore_test.go | 67 +++++++++++++++++++++++++ 3 files changed, 186 insertions(+), 29 deletions(-) diff --git a/core/commands/diag.go b/core/commands/diag.go index 777e9445fb3..cc8663c3254 100644 --- a/core/commands/diag.go +++ b/core/commands/diag.go @@ -7,9 +7,11 @@ import ( "io" "github.com/ipfs/go-datastore" + "github.com/ipfs/go-datastore/mount" "github.com/ipfs/go-datastore/query" cmds "github.com/ipfs/go-ipfs-cmds" oldcmds "github.com/ipfs/kubo/commands" + node "github.com/ipfs/kubo/core/node" fsrepo "github.com/ipfs/kubo/repo/fsrepo" ) @@ -41,7 +43,11 @@ in production workflows. The datastore format may change between versions. The daemon must not be running when calling these commands. -EXAMPLE +When the provider keystore datastores exist on disk (nodes with +Provide.DHT.SweepEnabled=true), they are automatically mounted into the +datastore view under /provider/keystore/0/ and /provider/keystore/1/. + +EXAMPLES Inspecting pubsub seqno validator state: @@ -51,6 +57,11 @@ Inspecting pubsub seqno validator state: Key: /pubsub/seqno/12D3KooW... Hex Dump: 00000000 18 81 81 c8 91 c0 ea f6 |........| + +Inspecting provider keystore (requires SweepEnabled): + + $ ipfs diag datastore count /provider/keystore/0/ + $ ipfs diag datastore count /provider/keystore/1/ `, }, Subcommands: map[string]*cmds.Command{ @@ -67,6 +78,36 @@ type diagDatastoreGetResult struct { HexDump string `json:"hex_dump,omitempty"` } +// openDiagDatastore opens the repo datastore and conditionally mounts any +// provider keystore datastores that exist on disk. It returns the composite +// datastore and a cleanup function that must be called when done. +func openDiagDatastore(env cmds.Environment) (datastore.Datastore, func(), error) { + cctx := env.(*oldcmds.Context) + repo, err := fsrepo.Open(cctx.ConfigRoot) + if err != nil { + return nil, nil, fmt.Errorf("failed to open repo: %w", err) + } + + extraMounts, extraCloser, err := node.MountKeystoreDatastores(repo) + if err != nil { + repo.Close() + return nil, nil, err + } + + closer := func() { + extraCloser() + repo.Close() + } + + if len(extraMounts) == 0 { + return repo.Datastore(), closer, nil + } + + mounts := []mount.Mount{{Prefix: datastore.NewKey("/"), Datastore: repo.Datastore()}} + mounts = append(mounts, extraMounts...) + return mount.New(mounts), closer, nil +} + var diagDatastoreGetCmd = &cmds.Command{ Status: cmds.Experimental, Helptext: cmds.HelpText{ @@ -89,16 +130,14 @@ WARNING: FOR DEBUGGING/TESTING ONLY NoRemote: true, PreRun: DaemonNotRunning, Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { - cctx := env.(*oldcmds.Context) - repo, err := fsrepo.Open(cctx.ConfigRoot) + ds, closer, err := openDiagDatastore(env) if err != nil { - return fmt.Errorf("failed to open repo: %w", err) + return err } - defer repo.Close() + defer closer() keyStr := req.Arguments[0] key := datastore.NewKey(keyStr) - ds := repo.Datastore() val, err := ds.Get(req.Context, key) if err != nil { @@ -156,15 +195,13 @@ WARNING: FOR DEBUGGING/TESTING ONLY NoRemote: true, PreRun: DaemonNotRunning, Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { - cctx := env.(*oldcmds.Context) - repo, err := fsrepo.Open(cctx.ConfigRoot) + ds, closer, err := openDiagDatastore(env) if err != nil { - return fmt.Errorf("failed to open repo: %w", err) + return err } - defer repo.Close() + defer closer() prefix := req.Arguments[0] - ds := repo.Datastore() q := query.Query{ Prefix: prefix, diff --git a/core/node/provider.go b/core/node/provider.go index ac2cac0e26a..11b5c513066 100644 --- a/core/node/provider.go +++ b/core/node/provider.go @@ -16,6 +16,7 @@ import ( "github.com/ipfs/boxo/provider" "github.com/ipfs/go-cid" "github.com/ipfs/go-datastore" + "github.com/ipfs/go-datastore/mount" "github.com/ipfs/go-datastore/namespace" "github.com/ipfs/go-datastore/query" log "github.com/ipfs/go-log/v2" @@ -51,10 +52,15 @@ const ( // Datastore key used to store previous reprovide strategy. reprovideStrategyKey = "/reprovideStrategy" - // Datastore namespace prefix for provider data. - providerDatastorePrefix = "provider" - // Base directory for the provider keystore datastores. - keystoreDatastorePath = "provider-keystore" + // KeystoreDatastorePath is the base directory for the provider keystore datastores. + KeystoreDatastorePath = "provider-keystore" +) + +var ( + // Datastore namespace key for provider data. + providerDatastoreKey = datastore.NewKey("provider") + // Datastore namespace key for provider keystore data. + keystoreDatastoreKey = datastore.NewKey("keystore") ) var errAcceleratedDHTNotReady = errors.New("AcceleratedDHTClient: routing table not ready") @@ -407,6 +413,59 @@ func findRootDatastoreSpec(spec map[string]any) map[string]any { } } +// MountKeystoreDatastores opens any provider keystore datastores that exist on +// disk and returns them as mount.Mount entries ready to be combined with the +// main repo datastore. The caller must call the returned cleanup function when +// done. Returns nil mounts and a no-op closer if no keystores exist. +func MountKeystoreDatastores(repo repo.Repo) ([]mount.Mount, func(), error) { + cfg, err := repo.Config() + if err != nil { + return nil, nil, fmt.Errorf("reading repo config: %w", err) + } + + rootSpec := findRootDatastoreSpec(cfg.Datastore.Spec) + if rootSpec == nil { + return nil, func() {}, nil + } + + keystoreBasePath := filepath.Join(repo.Path(), KeystoreDatastorePath) + var mounts []mount.Mount + var closers []func() + + for _, suffix := range []string{"0", "1"} { + dir := filepath.Join(keystoreBasePath, suffix) + if _, err := os.Stat(dir); err != nil { + continue + } + ds, err := openDatastoreAt(rootSpec, dir) + if err != nil { + return nil, nil, err + } + prefix := providerDatastoreKey.Child(keystoreDatastoreKey).ChildString(suffix) + mounts = append(mounts, mount.Mount{Prefix: prefix, Datastore: ds}) + closers = append(closers, func() { ds.Close() }) + } + + closer := func() { + for _, c := range closers { + c() + } + } + return mounts, closer, nil +} + +// openDatastoreAt opens a datastore using the given spec at the specified path. +// It deep-copies the spec to avoid mutating the original. +func openDatastoreAt(rootSpec map[string]any, path string) (datastore.Batching, error) { + spec := copySpec(rootSpec) + spec["path"] = path + dsc, err := fsrepo.AnyDatastoreConfig(spec) + if err != nil { + return nil, fmt.Errorf("creating datastore config for %s: %w", path, err) + } + return dsc.Create("") +} + // copySpec deep-copies a datastore spec map so modifications (e.g., changing // the path) don't affect the original. func copySpec(spec map[string]any) map[string]any { @@ -427,9 +486,9 @@ func copySpec(spec map[string]any) map[string]any { // purgeOrphanedKeystoreData deletes all keys under /provider/keystore/ from the // shared repo datastore. These were written by older Kubo versions that stored // provider keystore data inline in the shared datastore. The new code uses -// separate filesystem datastores under /{keystoreDatastorePath}/ instead. +// separate filesystem datastores under /{KeystoreDatastorePath}/ instead. func purgeOrphanedKeystoreData(ctx context.Context, ds datastore.Batching) error { - orphanedPrefix := datastore.NewKey(providerDatastorePrefix).ChildString("keystore").String() + orphanedPrefix := providerDatastoreKey.Child(keystoreDatastoreKey).String() results, err := ds.Query(ctx, query.Query{ Prefix: orphanedPrefix, @@ -475,7 +534,7 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { Repo repo.Repo } sweepingReprovider := fx.Provide(func(in providerInput) (DHTProvider, *keystore.ResettableKeystore, error) { - ds := namespace.Wrap(in.Repo.Datastore(), datastore.NewKey(providerDatastorePrefix)) + ds := namespace.Wrap(in.Repo.Datastore(), providerDatastoreKey) // Get repo path and config to determine datastore type repoPath := in.Repo.Path() @@ -487,8 +546,8 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { // Find the root datastore type (levelds, pebbleds, etc.) rootSpec := findRootDatastoreSpec(repoCfg.Datastore.Spec) - // Keystore datastores live at /{keystoreDatastorePath}/ - keystoreBasePath := filepath.Join(repoPath, keystoreDatastorePath) + // Keystore datastores live at /{KeystoreDatastorePath}/ + keystoreBasePath := filepath.Join(repoPath, KeystoreDatastorePath) createDs := func(suffix string) (datastore.Batching, error) { // When no datastore spec is configured (e.g., test/mock repos), @@ -499,13 +558,7 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { if err := os.MkdirAll(keystoreBasePath, 0o755); err != nil { return nil, fmt.Errorf("creating keystore base directory: %w", err) } - spec := copySpec(rootSpec) - spec["path"] = filepath.Join(keystoreBasePath, suffix) - dsc, err := fsrepo.AnyDatastoreConfig(spec) - if err != nil { - return nil, fmt.Errorf("creating keystore datastore config: %w", err) - } - return dsc.Create("") + return openDatastoreAt(rootSpec, filepath.Join(keystoreBasePath, suffix)) } destroyDs := func(suffix string) error { @@ -515,7 +568,7 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { // One-time migration: purge orphaned keystore data from the shared repo // datastore. Old Kubo versions stored keystore data at /provider/keystore/ // inside the shared monolithic datastore. New code uses separate - // filesystem datastores under /{keystoreDatastorePath}/. On first + // filesystem datastores under /{KeystoreDatastorePath}/. On first // start with the new code, detect the upgrade (dir doesn't exist yet) and // delete the stale keys. if _, statErr := os.Stat(keystoreBasePath); os.IsNotExist(statErr) { @@ -524,7 +577,7 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { } } - keystoreDs := namespace.Wrap(ds, datastore.NewKey("keystore")) + keystoreDs := namespace.Wrap(ds, keystoreDatastoreKey) ks, err := keystore.NewResettableKeystore(keystoreDs, keystore.WithDatastoreFactory(createDs, destroyDs), keystore.KeystoreOption( diff --git a/test/cli/diag_datastore_test.go b/test/cli/diag_datastore_test.go index 2a69f60cc57..a8e950da433 100644 --- a/test/cli/diag_datastore_test.go +++ b/test/cli/diag_datastore_test.go @@ -2,6 +2,8 @@ package cli import ( "encoding/json" + "os" + "path/filepath" "testing" "github.com/ipfs/kubo/test/cli/harness" @@ -144,4 +146,69 @@ func TestDiagDatastore(t *testing.T) { assert.Error(t, res.Err, "count should fail when daemon is running") assert.Contains(t, res.Stderr.String(), "ipfs daemon is running") }) + + t.Run("provider keystore datastores are visible in unified view", func(t *testing.T) { + t.Parallel() + node := harness.NewT(t).NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) + node.SetIPFSConfig("Provide.Enabled", true) + + // Start daemon to create the provider-keystore datastores, then add data + node.StartDaemon() + cid := node.IPFSAddStr("data for provider keystore test") + node.IPFS("pin", "add", cid) + node.StopDaemon() + + // Verify the provider-keystore directory was created + keystorePath := filepath.Join(node.Dir, "provider-keystore") + _, err := os.Stat(keystorePath) + require.NoError(t, err, "provider-keystore directory should exist after sweep-enabled daemon ran") + + // Count entries in each keystore namespace via the unified view + for _, prefix := range []string{"/provider/keystore/0/", "/provider/keystore/1/"} { + res := node.IPFS("diag", "datastore", "count", prefix) + assert.NoError(t, res.Err) + t.Logf("count %s: %s", prefix, res.Stdout.String()) + } + + // The total count under /provider/keystore/ should include entries + // from both keystore instances (0 and 1) + count := node.DatastoreCount("/provider/keystore/") + t.Logf("total /provider/keystore/ entries: %d", count) + assert.Greater(t, count, int64(0), "should have provider keystore entries") + }) + + t.Run("provider keystore count JSON output", func(t *testing.T) { + t.Parallel() + node := harness.NewT(t).NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) + node.SetIPFSConfig("Provide.Enabled", true) + + node.StartDaemon() + node.StopDaemon() + + res := node.IPFS("diag", "datastore", "count", "/provider/keystore/0/", "--enc=json") + assert.NoError(t, res.Err) + + var result struct { + Prefix string `json:"prefix"` + Count int64 `json:"count"` + } + err := json.Unmarshal(res.Stdout.Bytes(), &result) + require.NoError(t, err) + assert.Equal(t, "/provider/keystore/0/", result.Prefix) + assert.GreaterOrEqual(t, result.Count, int64(0), "count should be non-negative") + }) + + t.Run("works without provider keystore", func(t *testing.T) { + t.Parallel() + node := harness.NewT(t).NewNode().Init() + + // No sweep enabled, no provider-keystore dirs โ€” should still work fine + count := node.DatastoreCount("/provider/keystore/0/") + assert.Zero(t, count) + + count = node.DatastoreCount("/") + assert.Greater(t, count, int64(0)) + }) } From 4c0732ae649f03e58bc865278e7058eccfe1ad91 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 18 Mar 2026 02:27:58 +0100 Subject: [PATCH 09/20] fix(provider): reject unexpected keystore suffix to prevent stray deletions destroyDs calls os.RemoveAll with a suffix from the upstream library. If suffix were ever ".." or empty, this could delete wrong directories. Validate that suffix is "0" or "1" in both createDs and destroyDs. --- core/node/provider.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/node/provider.go b/core/node/provider.go index 11b5c513066..e05f98330da 100644 --- a/core/node/provider.go +++ b/core/node/provider.go @@ -65,6 +65,17 @@ var ( var errAcceleratedDHTNotReady = errors.New("AcceleratedDHTClient: routing table not ready") +// validateKeystoreSuffix rejects any suffix other than "0" or "1". +// The upstream library uses these two values as alternating namespace +// identifiers. Validating here prevents accidental deletion of unrelated +// directories via os.RemoveAll if the upstream ever changes its scheme. +func validateKeystoreSuffix(suffix string) error { + if suffix != "0" && suffix != "1" { + return fmt.Errorf("unexpected keystore suffix %q, expected \"0\" or \"1\"", suffix) + } + return nil +} + // Interval between reprovide queue monitoring checks for slow reprovide alerts. // Used when Provide.DHT.SweepEnabled=true const reprovideAlertPollInterval = 15 * time.Minute @@ -550,6 +561,9 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { keystoreBasePath := filepath.Join(repoPath, KeystoreDatastorePath) createDs := func(suffix string) (datastore.Batching, error) { + if err := validateKeystoreSuffix(suffix); err != nil { + return nil, err + } // When no datastore spec is configured (e.g., test/mock repos), // fall back to an in-memory datastore. if rootSpec == nil { @@ -562,6 +576,9 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { } destroyDs := func(suffix string) error { + if err := validateKeystoreSuffix(suffix); err != nil { + return err + } return os.RemoveAll(filepath.Join(keystoreBasePath, suffix)) } From 727df428aed74179112aed2d6a2f729f961fc2cc Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 18 Mar 2026 02:28:30 +0100 Subject: [PATCH 10/20] fix(provider): close opened datastores when mounting partially fails If opening datastore "0" succeeds but "1" fails, MountKeystoreDatastores returned an error without closing "0". --- core/node/provider.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/node/provider.go b/core/node/provider.go index e05f98330da..407fd8f16bb 100644 --- a/core/node/provider.go +++ b/core/node/provider.go @@ -450,6 +450,9 @@ func MountKeystoreDatastores(repo repo.Repo) ([]mount.Mount, func(), error) { } ds, err := openDatastoreAt(rootSpec, dir) if err != nil { + for _, c := range closers { + c() + } return nil, nil, err } prefix := providerDatastoreKey.Child(keystoreDatastoreKey).ChildString(suffix) From 813f77ed0493335a5859893d1806ca673b63f981 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 18 Mar 2026 02:28:58 +0100 Subject: [PATCH 11/20] fix(provider): defer batch creation in orphan purge until keys are found Avoids allocating a datastore batch when no orphaned keys exist. --- core/node/provider.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/core/node/provider.go b/core/node/provider.go index 407fd8f16bb..ecad164d011 100644 --- a/core/node/provider.go +++ b/core/node/provider.go @@ -513,16 +513,18 @@ func purgeOrphanedKeystoreData(ctx context.Context, ds datastore.Batching) error } defer results.Close() - batch, err := ds.Batch(ctx) - if err != nil { - return fmt.Errorf("creating batch for orphaned keystore cleanup: %w", err) - } - + var batch datastore.Batch count := 0 for result := range results.Next() { if result.Error != nil { return fmt.Errorf("iterating orphaned keystore data: %w", result.Error) } + if batch == nil { + batch, err = ds.Batch(ctx) + if err != nil { + return fmt.Errorf("creating batch for orphaned keystore cleanup: %w", err) + } + } if err := batch.Delete(ctx, datastore.NewKey(result.Key)); err != nil { return fmt.Errorf("batch deleting orphaned key %s: %w", result.Key, err) } From a47cba0f63d0f39992580058a06dffde44012fd4 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 18 Mar 2026 02:29:23 +0100 Subject: [PATCH 12/20] fix(provider): warn on unrecognized datastore wrapper types findRootDatastoreSpec silently returns wrapper specs it doesn't know about. If a plugin adds a wrapper with a "child" field, openDatastoreAt gets the wrapper instead of the leaf backend and fails confusingly. Log a warning so operators can spot the issue. --- core/node/provider.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/node/provider.go b/core/node/provider.go index ecad164d011..3e75d4509ed 100644 --- a/core/node/provider.go +++ b/core/node/provider.go @@ -420,6 +420,10 @@ func findRootDatastoreSpec(spec map[string]any) map[string]any { } return spec default: + if _, hasChild := spec["child"]; hasChild { + logger.Warnw("unrecognized datastore wrapper type, using as-is", + "type", spec["type"]) + } return spec } } From b7b82867aa7cf309072fcb0a822ce0afc2bf2559 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 18 Mar 2026 02:29:51 +0100 Subject: [PATCH 13/20] docs: document keystore migration behavior on upgrade and downgrade - explain why context.Background() is used in the migration code - add changelog note about the provide cycle restarting on upgrade - add downgrade caveat about orphaned provider-keystore directory --- core/node/provider.go | 5 +++++ docs/changelogs/v0.41.md | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/core/node/provider.go b/core/node/provider.go index 3e75d4509ed..3c3b58ba037 100644 --- a/core/node/provider.go +++ b/core/node/provider.go @@ -597,6 +597,11 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { // filesystem datastores under /{KeystoreDatastorePath}/. On first // start with the new code, detect the upgrade (dir doesn't exist yet) and // delete the stale keys. + // + // Safe against partial completion: this runs before createDs creates the + // keystoreBasePath directory, so if the process dies mid-purge, the + // directory won't exist and the purge re-runs on next start. The purge + // is idempotent (deleting already-deleted keys is a no-op). if _, statErr := os.Stat(keystoreBasePath); os.IsNotExist(statErr) { if purgeErr := purgeOrphanedKeystoreData(context.Background(), in.Repo.Datastore()); purgeErr != nil { logger.Warnw("failed to purge orphaned provider keystore data from shared datastore", "error", purgeErr) diff --git a/docs/changelogs/v0.41.md b/docs/changelogs/v0.41.md index 56531e3b7b2..7702d8d1cd0 100644 --- a/docs/changelogs/v0.41.md +++ b/docs/changelogs/v0.41.md @@ -31,6 +31,17 @@ backend (LevelDB, Pebble, etc.). See [go-libp2p-kad-dht#1233](https://github.com/libp2p/go-libp2p-kad-dht/pull/1233) for the upstream change. +On first start after upgrading from v0.40, keystore data is migrated out +of the shared repo datastore. The provide cycle restarts: the node +re-walks all content matching the provide strategy to rebuild the +keystore in the new location. On large nodes this may take some time. + +> **Downgrade note**: If you revert to a pre-v0.41 release after running v0.41, +> the old code will not find the new `provider-keystore/` directory and will +> rebuild the keystore from scratch at next startup, restarting the provide +> cycle. The orphaned `/provider-keystore/` directory can be safely +> deleted to reclaim disk space. + ### ๐Ÿ“ Changelog ### ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributors From c22c16d9e5c36ea25da5a91aa9315cb46b248240 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 18 Mar 2026 03:21:43 +0100 Subject: [PATCH 14/20] chore(deps): bump go-libp2p-kad-dht to latest keystore factory commit --- docs/examples/kubo-as-a-library/go.mod | 2 +- docs/examples/kubo-as-a-library/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- test/dependencies/go.mod | 2 +- test/dependencies/go.sum | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index 4f155bd065a..b6f43c4b476 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -116,7 +116,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 // indirect github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd // indirect + github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-pubsub v0.15.0 // indirect github.com/libp2p/go-libp2p-pubsub-router v0.6.0 // indirect diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 6c6e40de713..87f51d0d995 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -486,8 +486,8 @@ github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl9 github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g= github.com/libp2p/go-libp2p-core v0.3.0/go.mod h1:ACp3DmS3/N64c2jDzcV429ukDpicbL6+TrrxANBjPGw= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd h1:Ty72X3AyijGw48euNGljHeA21pYOqsgUiLZWdxMj5T4= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de h1:WtCElgv08kbIkGrL4LfIh6ItMqaPMN5Jse6ZjLDkraU= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= diff --git a/go.mod b/go.mod index 58fd4d2eaee..56b423c8873 100644 --- a/go.mod +++ b/go.mod @@ -52,7 +52,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 github.com/libp2p/go-libp2p v0.47.0 github.com/libp2p/go-libp2p-http v0.5.0 - github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd + github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de github.com/libp2p/go-libp2p-kbucket v0.8.0 github.com/libp2p/go-libp2p-pubsub v0.15.0 github.com/libp2p/go-libp2p-pubsub-router v0.6.0 diff --git a/go.sum b/go.sum index 49270897738..ca3708dce1b 100644 --- a/go.sum +++ b/go.sum @@ -543,8 +543,8 @@ github.com/libp2p/go-libp2p-gostream v0.6.0 h1:QfAiWeQRce6pqnYfmIVWJFXNdDyfiR/qk github.com/libp2p/go-libp2p-gostream v0.6.0/go.mod h1:Nywu0gYZwfj7Jc91PQvbGU8dIpqbQQkjWgDuOrFaRdA= github.com/libp2p/go-libp2p-http v0.5.0 h1:+x0AbLaUuLBArHubbbNRTsgWz0RjNTy6DJLOxQ3/QBc= github.com/libp2p/go-libp2p-http v0.5.0/go.mod h1:glh87nZ35XCQyFsdzZps6+F4HYI6DctVFY5u1fehwSg= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd h1:Ty72X3AyijGw48euNGljHeA21pYOqsgUiLZWdxMj5T4= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de h1:WtCElgv08kbIkGrL4LfIh6ItMqaPMN5Jse6ZjLDkraU= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod index a5b550ab71d..b47f5a91149 100644 --- a/test/dependencies/go.mod +++ b/test/dependencies/go.mod @@ -183,7 +183,7 @@ require ( github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p v0.47.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd // indirect + github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-record v0.3.1 // indirect github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum index c7ce48a20ad..617b83acdb6 100644 --- a/test/dependencies/go.sum +++ b/test/dependencies/go.sum @@ -578,8 +578,8 @@ github.com/libp2p/go-libp2p v0.47.0 h1:qQpBjSCWNQFF0hjBbKirMXE9RHLtSuzTDkTfr1rw0 github.com/libp2p/go-libp2p v0.47.0/go.mod h1:s8HPh7mMV933OtXzONaGFseCg/BE//m1V34p3x4EUOY= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd h1:Ty72X3AyijGw48euNGljHeA21pYOqsgUiLZWdxMj5T4= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260217124839-5b45a65966fd/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de h1:WtCElgv08kbIkGrL4LfIh6ItMqaPMN5Jse6ZjLDkraU= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg= From 188addcf23c5dcb480ca3b730899150646265807 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 18 Mar 2026 18:56:37 +0100 Subject: [PATCH 15/20] fix(provider): harden keystore migration and spec handling - chunk orphan purge into 4096-key batches to bound memory and match existing batching patterns in the same file - cancel the purge context via fx.Lifecycle OnStop so SIGINT during startup does not block indefinitely - deep-copy slices in copySpec (not just maps) so the function matches its documented "deep-copy" contract - return nil from findRootDatastoreSpec when no "/" mount exists, so callers fall back to in-memory instead of passing a mount-type spec to openDatastoreAt - rename local variable to avoid shadowing the mount package import --- core/node/provider.go | 105 +++++++++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 23 deletions(-) diff --git a/core/node/provider.go b/core/node/provider.go index 3c3b58ba037..3e0bab058c0 100644 --- a/core/node/provider.go +++ b/core/node/provider.go @@ -404,16 +404,18 @@ func findRootDatastoreSpec(spec map[string]any) map[string]any { return spec } for _, m := range mounts { - mount, ok := m.(map[string]any) + mnt, ok := m.(map[string]any) if !ok { continue } - if mount["mountpoint"] == "/" { - return findRootDatastoreSpec(mount) + if mnt["mountpoint"] == "/" { + return findRootDatastoreSpec(mnt) } } - // No root mount found, return as-is - return spec + // No root mount found; return nil so callers fall back gracefully + // (in-memory datastore or skip mounting) rather than passing a + // mount-type spec to openDatastoreAt which expects a leaf backend. + return nil case "measure", "log": if child, ok := spec["child"].(map[string]any); ok { return findRootDatastoreSpec(child) @@ -492,21 +494,40 @@ func copySpec(spec map[string]any) map[string]any { } cp := make(map[string]any, len(spec)) for k, v := range spec { - if m, ok := v.(map[string]any); ok { - cp[k] = copySpec(m) - } else { + switch val := v.(type) { + case map[string]any: + cp[k] = copySpec(val) + case []any: + s := make([]any, len(val)) + for i, elem := range val { + if m, ok := elem.(map[string]any); ok { + s[i] = copySpec(m) + } else { + s[i] = elem + } + } + cp[k] = s + default: cp[k] = v } } return cp } +// purgeBatchSize is the number of keys deleted per batch commit during +// orphaned keystore cleanup. Each commit is a cancellation checkpoint. +const purgeBatchSize = 1 << 12 // 4096 + // purgeOrphanedKeystoreData deletes all keys under /provider/keystore/ from the // shared repo datastore. These were written by older Kubo versions that stored // provider keystore data inline in the shared datastore. The new code uses // separate filesystem datastores under /{KeystoreDatastorePath}/ instead. +// +// The operation is idempotent and safe to interrupt: partial completion is +// fine because already-deleted keys are no-ops on re-run. func purgeOrphanedKeystoreData(ctx context.Context, ds datastore.Batching) error { orphanedPrefix := providerDatastoreKey.Child(keystoreDatastoreKey).String() + syncKey := datastore.NewKey(orphanedPrefix) results, err := ds.Query(ctx, query.Query{ Prefix: orphanedPrefix, @@ -518,8 +539,11 @@ func purgeOrphanedKeystoreData(ctx context.Context, ds datastore.Batching) error defer results.Close() var batch datastore.Batch - count := 0 + var count, pending int for result := range results.Next() { + if ctx.Err() != nil { + return ctx.Err() + } if result.Error != nil { return fmt.Errorf("iterating orphaned keystore data: %w", result.Error) } @@ -533,14 +557,27 @@ func purgeOrphanedKeystoreData(ctx context.Context, ds datastore.Batching) error return fmt.Errorf("batch deleting orphaned key %s: %w", result.Key, err) } count++ + pending++ + if pending >= purgeBatchSize { + if err := batch.Commit(ctx); err != nil { + return fmt.Errorf("committing orphaned keystore cleanup batch: %w", err) + } + if err := ds.Sync(ctx, syncKey); err != nil { + return fmt.Errorf("syncing orphaned keystore cleanup: %w", err) + } + batch = nil + pending = 0 + } } - if count > 0 { + if pending > 0 { if err := batch.Commit(ctx); err != nil { return fmt.Errorf("committing orphaned keystore cleanup batch: %w", err) } - if err := ds.Sync(ctx, datastore.NewKey(orphanedPrefix)); err != nil { + if err := ds.Sync(ctx, syncKey); err != nil { return fmt.Errorf("syncing orphaned keystore cleanup: %w", err) } + } + if count > 0 { logger.Infow("purged orphaned provider keystore data from shared datastore", "keys", count) } return nil @@ -552,6 +589,7 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { fx.In DHT routing.Routing `name:"dhtc"` Repo repo.Repo + Lc fx.Lifecycle } sweepingReprovider := fx.Provide(func(in providerInput) (DHTProvider, *keystore.ResettableKeystore, error) { ds := namespace.Wrap(in.Repo.Datastore(), providerDatastoreKey) @@ -591,21 +629,42 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { return os.RemoveAll(filepath.Join(keystoreBasePath, suffix)) } - // One-time migration: purge orphaned keystore data from the shared repo - // datastore. Old Kubo versions stored keystore data at /provider/keystore/ - // inside the shared monolithic datastore. New code uses separate - // filesystem datastores under /{KeystoreDatastorePath}/. On first - // start with the new code, detect the upgrade (dir doesn't exist yet) and - // delete the stale keys. + // One-time cleanup of stale keystore data left by older Kubo in the + // shared repo datastore under /provider/keystore/. New code stores + // bulk key data in separate filesystem datastores under + // /{KeystoreDatastorePath}/ while still using the same + // /provider/keystore/ namespace in the shared datastore for metadata. // - // Safe against partial completion: this runs before createDs creates the - // keystoreBasePath directory, so if the process dies mid-purge, the - // directory won't exist and the purge re-runs on next start. The purge - // is idempotent (deleting already-deleted keys is a no-op). + // The absence of the keystoreBasePath directory signals a first run + // after upgrade: the directory is created later by createDs on first + // use, so it doubles as a "cleanup done" flag. If the process dies + // mid-purge the directory still won't exist and the cleanup re-runs + // on next start (it is idempotent). Must run synchronously before + // NewResettableKeystore to avoid racing with reads on the same + // namespace. if _, statErr := os.Stat(keystoreBasePath); os.IsNotExist(statErr) { - if purgeErr := purgeOrphanedKeystoreData(context.Background(), in.Repo.Datastore()); purgeErr != nil { - logger.Warnw("failed to purge orphaned provider keystore data from shared datastore", "error", purgeErr) + logger.Infow("migrating provider keystore data from shared datastore to separate filesystem datastores", "path", keystoreBasePath) + // Create a cancellable context for the purge. The OnStop hook + // below calls purgeCancel when the node receives a shutdown + // signal (e.g., SIGINT), which interrupts the purge loop + // instead of blocking indefinitely. + purgeCtx, purgeCancel := context.WithCancel(context.Background()) + in.Lc.Append(fx.Hook{ + OnStop: func(_ context.Context) error { + purgeCancel() + return nil + }, + }) + if purgeErr := purgeOrphanedKeystoreData(purgeCtx, in.Repo.Datastore()); purgeErr != nil { + if purgeCtx.Err() != nil { + logger.Infow("provider keystore migration interrupted by shutdown, will resume on next start") + } else { + logger.Warnw("provider keystore migration failed, will retry on next start", "error", purgeErr) + } + } else { + logger.Infow("provider keystore migration completed") } + purgeCancel() } keystoreDs := namespace.Wrap(ds, keystoreDatastoreKey) From 9dd5898a56a78ea1f4fb951b6992eedc43f2e061 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 18 Mar 2026 20:19:13 +0100 Subject: [PATCH 16/20] test(provider): add migration purge test and diag datastore put command - add `ipfs diag datastore put` subcommand for writing arbitrary key-value pairs to the datastore (offline, experimental) - add DatastorePut harness helper for CLI tests - add TestProviderKeystoreMigrationPurge: seeds orphaned keystore keys via `put`, starts the daemon to trigger migration, verifies the orphaned keys are purged and provider-keystore/ dir is created - add put/get roundtrip test for diag datastore --- core/commands/commands_test.go | 1 + core/commands/diag.go | 41 +++++++++++++++++++++++++ test/cli/diag_datastore_test.go | 12 ++++++++ test/cli/harness/node.go | 6 ++++ test/cli/provider_test.go | 53 +++++++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+) diff --git a/core/commands/commands_test.go b/core/commands/commands_test.go index 04ee581e0a1..80e49f23cc0 100644 --- a/core/commands/commands_test.go +++ b/core/commands/commands_test.go @@ -79,6 +79,7 @@ func TestCommands(t *testing.T) { "/diag/datastore", "/diag/datastore/count", "/diag/datastore/get", + "/diag/datastore/put", "/diag/profile", "/diag/sys", "/files", diff --git a/core/commands/diag.go b/core/commands/diag.go index cc8663c3254..c8a48e90c58 100644 --- a/core/commands/diag.go +++ b/core/commands/diag.go @@ -58,6 +58,10 @@ Inspecting pubsub seqno validator state: Hex Dump: 00000000 18 81 81 c8 91 c0 ea f6 |........| +Writing a test key (debugging only): + + $ ipfs diag datastore put /test/mykey "hello" + Inspecting provider keystore (requires SweepEnabled): $ ipfs diag datastore count /provider/keystore/0/ @@ -66,6 +70,7 @@ Inspecting provider keystore (requires SweepEnabled): }, Subcommands: map[string]*cmds.Command{ "get": diagDatastoreGetCmd, + "put": diagDatastorePutCmd, "count": diagDatastoreCountCmd, }, } @@ -172,6 +177,42 @@ WARNING: FOR DEBUGGING/TESTING ONLY }, } +var diagDatastorePutCmd = &cmds.Command{ + Status: cmds.Experimental, + Helptext: cmds.HelpText{ + Tagline: "Write a raw key-value pair to the datastore.", + ShortDescription: ` +Stores the given value at the specified datastore key. + +The daemon must not be running when using this command. + +WARNING: FOR DEBUGGING/TESTING ONLY +`, + }, + Arguments: []cmds.Argument{ + cmds.StringArg("key", true, false, "Datastore key (e.g., /test/mykey)"), + cmds.StringArg("value", true, false, "Value to store (as a string)"), + }, + NoRemote: true, + PreRun: DaemonNotRunning, + Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { + ds, closer, err := openDiagDatastore(env) + if err != nil { + return err + } + defer closer() + + key := datastore.NewKey(req.Arguments[0]) + if err := ds.Put(req.Context, key, []byte(req.Arguments[1])); err != nil { + return fmt.Errorf("failed to put key: %w", err) + } + if err := ds.Sync(req.Context, key); err != nil { + return fmt.Errorf("failed to sync: %w", err) + } + return nil + }, +} + type diagDatastoreCountResult struct { Prefix string `json:"prefix"` Count int64 `json:"count"` diff --git a/test/cli/diag_datastore_test.go b/test/cli/diag_datastore_test.go index a8e950da433..d1b429d3376 100644 --- a/test/cli/diag_datastore_test.go +++ b/test/cli/diag_datastore_test.go @@ -132,6 +132,18 @@ func TestDiagDatastore(t *testing.T) { assert.Contains(t, res.Stderr.String(), "key not found") }) + t.Run("diag datastore put and get roundtrip", func(t *testing.T) { + t.Parallel() + node := harness.NewT(t).NewNode().Init() + + node.DatastorePut("/test/roundtrip", "hello world") + assert.True(t, node.DatastoreHasKey("/test/roundtrip")) + assert.Equal(t, []byte("hello world"), node.DatastoreGet("/test/roundtrip")) + + count := node.DatastoreCount("/test/") + assert.Equal(t, int64(1), count) + }) + t.Run("diag datastore commands require daemon to be stopped", func(t *testing.T) { t.Parallel() node := harness.NewT(t).NewNode().Init().StartDaemon() diff --git a/test/cli/harness/node.go b/test/cli/harness/node.go index a4ee71f937f..afce2fb0b76 100644 --- a/test/cli/harness/node.go +++ b/test/cli/harness/node.go @@ -739,6 +739,12 @@ func (n *Node) DatastoreCount(prefix string) int64 { return count } +// DatastorePut writes a key-value pair to the datastore. +// Requires the daemon to be stopped. +func (n *Node) DatastorePut(key, value string) { + n.IPFS("diag", "datastore", "put", key, value) +} + // DatastoreGet retrieves the value at the given key. // Requires the daemon to be stopped. Returns nil if key not found. func (n *Node) DatastoreGet(key string) []byte { diff --git a/test/cli/provider_test.go b/test/cli/provider_test.go index 7088f2b363b..9f6c63bcbd1 100644 --- a/test/cli/provider_test.go +++ b/test/cli/provider_test.go @@ -905,6 +905,59 @@ func TestProviderKeystoreDatastorePurge(t *testing.T) { node.StopDaemon() } +// TestProviderKeystoreMigrationPurge verifies that orphaned keystore data +// left in the shared repo datastore by older Kubo versions is purged on +// the first sweep-enabled daemon start. The migration is triggered by the +// absence of the /provider-keystore/ directory. +func TestProviderKeystoreMigrationPurge(t *testing.T) { + t.Parallel() + + h := harness.NewT(t) + node := h.NewNode().Init() + node.SetIPFSConfig("Provide.DHT.SweepEnabled", true) + node.SetIPFSConfig("Provide.Enabled", true) + node.SetIPFSConfig("Bootstrap", []string{}) + + keystoreBase := filepath.Join(node.Dir, "provider-keystore") + + // Pre-seed orphaned keystore data into the shared datastore, simulating + // the layout produced by older Kubo that stored keystore entries inline. + const numOrphans = 10 + for i := range numOrphans { + node.DatastorePut( + fmt.Sprintf("/provider/keystore/%d/fake-key-%d", i%2, i), + fmt.Sprintf("orphan-%d", i), + ) + } + + // The orphaned keys should be visible via diag datastore. + count := node.DatastoreCount("/provider/keystore/") + require.Equal(t, int64(numOrphans), count, "orphaned keys should be present before migration") + + // The provider-keystore directory must not exist yet (its absence + // triggers the migration). + require.False(t, dirExists(keystoreBase), + "provider-keystore/ should not exist before first sweep-enabled start") + + // Start the daemon: this triggers the one-time migration purge. + node.StartDaemon() + node.StopDaemon() + + // After migration the seeded orphaned keys should be gone from the + // shared datastore. The diag datastore count command mounts the + // separate provider-keystore datastores, so we check for the specific + // fake keys we seeded to confirm they were purged. + for i := range numOrphans { + key := fmt.Sprintf("/provider/keystore/%d/fake-key-%d", i%2, i) + assert.False(t, node.DatastoreHasKey(key), + "orphaned key %s should be purged after migration", key) + } + + // The provider-keystore directory should now exist. + assert.True(t, dirExists(keystoreBase), + "provider-keystore/ should exist after sweep-enabled daemon ran") +} + func dirExists(path string) bool { info, err := os.Stat(path) return err == nil && info.IsDir() From 0da88af99698b73bcd8460b308203c7d7b38ff08 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 18 Mar 2026 20:21:46 +0100 Subject: [PATCH 17/20] chore(deps): bump go-libp2p-kad-dht to 1bede74b8246 --- docs/examples/kubo-as-a-library/go.mod | 2 +- docs/examples/kubo-as-a-library/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- test/dependencies/go.mod | 2 +- test/dependencies/go.sum | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index b6f43c4b476..f78317b9d35 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -116,7 +116,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 // indirect github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de // indirect + github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-pubsub v0.15.0 // indirect github.com/libp2p/go-libp2p-pubsub-router v0.6.0 // indirect diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 87f51d0d995..ead406796b2 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -486,8 +486,8 @@ github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl9 github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g= github.com/libp2p/go-libp2p-core v0.3.0/go.mod h1:ACp3DmS3/N64c2jDzcV429ukDpicbL6+TrrxANBjPGw= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de h1:WtCElgv08kbIkGrL4LfIh6ItMqaPMN5Jse6ZjLDkraU= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d h1:/T1QPIn+5C0s6v7cLM6ldY1Ot5imedIC27vhZ0r3OW4= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= diff --git a/go.mod b/go.mod index 56b423c8873..f25e40fce09 100644 --- a/go.mod +++ b/go.mod @@ -52,7 +52,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 github.com/libp2p/go-libp2p v0.47.0 github.com/libp2p/go-libp2p-http v0.5.0 - github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de + github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d github.com/libp2p/go-libp2p-kbucket v0.8.0 github.com/libp2p/go-libp2p-pubsub v0.15.0 github.com/libp2p/go-libp2p-pubsub-router v0.6.0 diff --git a/go.sum b/go.sum index ca3708dce1b..5679ac255c8 100644 --- a/go.sum +++ b/go.sum @@ -543,8 +543,8 @@ github.com/libp2p/go-libp2p-gostream v0.6.0 h1:QfAiWeQRce6pqnYfmIVWJFXNdDyfiR/qk github.com/libp2p/go-libp2p-gostream v0.6.0/go.mod h1:Nywu0gYZwfj7Jc91PQvbGU8dIpqbQQkjWgDuOrFaRdA= github.com/libp2p/go-libp2p-http v0.5.0 h1:+x0AbLaUuLBArHubbbNRTsgWz0RjNTy6DJLOxQ3/QBc= github.com/libp2p/go-libp2p-http v0.5.0/go.mod h1:glh87nZ35XCQyFsdzZps6+F4HYI6DctVFY5u1fehwSg= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de h1:WtCElgv08kbIkGrL4LfIh6ItMqaPMN5Jse6ZjLDkraU= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d h1:/T1QPIn+5C0s6v7cLM6ldY1Ot5imedIC27vhZ0r3OW4= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod index b47f5a91149..0fb82cf52de 100644 --- a/test/dependencies/go.mod +++ b/test/dependencies/go.mod @@ -183,7 +183,7 @@ require ( github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p v0.47.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de // indirect + github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-record v0.3.1 // indirect github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum index 617b83acdb6..a19056f4823 100644 --- a/test/dependencies/go.sum +++ b/test/dependencies/go.sum @@ -578,8 +578,8 @@ github.com/libp2p/go-libp2p v0.47.0 h1:qQpBjSCWNQFF0hjBbKirMXE9RHLtSuzTDkTfr1rw0 github.com/libp2p/go-libp2p v0.47.0/go.mod h1:s8HPh7mMV933OtXzONaGFseCg/BE//m1V34p3x4EUOY= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de h1:WtCElgv08kbIkGrL4LfIh6ItMqaPMN5Jse6ZjLDkraU= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318021441-7f11bf3a21de/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d h1:/T1QPIn+5C0s6v7cLM6ldY1Ot5imedIC27vhZ0r3OW4= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg= From 010e0c870fc54be409081a1beae429ec20419079 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 18 Mar 2026 20:58:51 +0100 Subject: [PATCH 18/20] fix(provider): log keystore datastore create and destroy operations --- core/node/provider.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/node/provider.go b/core/node/provider.go index 3e0bab058c0..da80849c9e1 100644 --- a/core/node/provider.go +++ b/core/node/provider.go @@ -619,13 +619,19 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option { if err := os.MkdirAll(keystoreBasePath, 0o755); err != nil { return nil, fmt.Errorf("creating keystore base directory: %w", err) } - return openDatastoreAt(rootSpec, filepath.Join(keystoreBasePath, suffix)) + ds, err := openDatastoreAt(rootSpec, filepath.Join(keystoreBasePath, suffix)) + if err != nil { + return nil, err + } + logger.Infow("provider keystore: opened datastore", "suffix", suffix, "path", filepath.Join(keystoreBasePath, suffix)) + return ds, nil } destroyDs := func(suffix string) error { if err := validateKeystoreSuffix(suffix); err != nil { return err } + logger.Infow("provider keystore: removing datastore from disk", "suffix", suffix, "path", filepath.Join(keystoreBasePath, suffix)) return os.RemoveAll(filepath.Join(keystoreBasePath, suffix)) } From a16ddc09abc9f90e1b9e45325b73e8e9506118b8 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 18 Mar 2026 21:38:00 +0100 Subject: [PATCH 19/20] docs: rewrite provider keystore changelog to focus on user impact --- docs/changelogs/v0.41.md | 45 +++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/docs/changelogs/v0.41.md b/docs/changelogs/v0.41.md index 95177247c0e..f47fdb0550c 100644 --- a/docs/changelogs/v0.41.md +++ b/docs/changelogs/v0.41.md @@ -10,7 +10,7 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team. - [Overview](#overview) - [๐Ÿ”ฆ Highlights](#-highlights) - - [๐Ÿ—‘๏ธ Provider Keystore Disk Reclamation](#-provider-keystore-disk-reclamation) + - [๐Ÿ—‘๏ธ Faster Provide Queue Disk Reclamation](#-faster-provide-queue-disk-reclamation) - [๐Ÿ–ฅ๏ธ WebUI Improvements](#-webui-improvements) - [๐Ÿ”ง Correct provider addresses for custom HTTP routing](#-correct-provider-addresses-for-custom-http-routing) - [๐Ÿ“ฆ๏ธ Dependency updates](#-dependency-updates) @@ -21,29 +21,26 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team. ### ๐Ÿ”ฆ Highlights -#### ๐Ÿ—‘๏ธ Provider Keystore Disk Reclamation - -The SweepingProvider's keystore now uses physically separate datastores instead -of namespacing within the shared repo datastore. When the keystore resets -during a reprovide cycle, the old datastore is removed from disk entirely -(`os.RemoveAll`) rather than being emptied key-by-key. This eliminates disk -bloat from stale tombstones that previously lingered until the storage engine's -background compaction ran. The new keystores live under -`/provider-keystore/` and automatically match the repo's configured -backend (LevelDB, Pebble, etc.). See -[go-libp2p-kad-dht#1233](https://github.com/libp2p/go-libp2p-kad-dht/pull/1233) -for the upstream change. - -On first start after upgrading from v0.40, keystore data is migrated out -of the shared repo datastore. The provide cycle restarts: the node -re-walks all content matching the provide strategy to rebuild the -keystore in the new location. On large nodes this may take some time. - -> **Downgrade note**: If you revert to a pre-v0.41 release after running v0.41, -> the old code will not find the new `provider-keystore/` directory and will -> rebuild the keystore from scratch at next startup, restarting the provide -> cycle. The orphaned `/provider-keystore/` directory can be safely -> deleted to reclaim disk space. +#### ๐Ÿ—‘๏ธ Faster Provide Queue Disk Reclamation + +Nodes with significant amount of data and DHT provide sweep enabled +(`Provide.DHT.SweepEnabled`, the default since Kubo 0.39) could see their +`datastore/` directory grow continuously. +Each reprovide cycle rewrote the provider keystore inside the shared repo +datastore, generating tombstones faster than the storage engine could compact +them, and in default configuration Kubo was slow to reclaim this space. + +The provider keystore now lives in a dedicated datastore under +`$IPFS_PATH/provider-keystore/`. After each reprovide cycle the old datastore +is removed from disk entirely, so space is reclaimed immediately regardless +of storage backend. + +On first start after upgrading, stale keystore data is cleaned up from the +shared datastore automatically. + +To learn more, see [kubo#11096](https://github.com/ipfs/kubo/issues/11096), +[kubo#11198](https://github.com/ipfs/kubo/pull/11198), and +[go-libp2p-kad-dht#1233](https://github.com/libp2p/go-libp2p-kad-dht/pull/1233). #### ๐Ÿ–ฅ๏ธ WebUI Improvements From 72f4a5f45bcb1b80c2cfb835dbc7862c23d17b99 Mon Sep 17 00:00:00 2001 From: guillaumemichel Date: Thu, 19 Mar 2026 11:02:13 +0100 Subject: [PATCH 20/20] bump kad-dht@master --- docs/examples/kubo-as-a-library/go.mod | 2 +- docs/examples/kubo-as-a-library/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- test/dependencies/go.mod | 2 +- test/dependencies/go.sum | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index f78317b9d35..19b0208ec3f 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -116,7 +116,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 // indirect github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d // indirect + github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260319095041-7ba6b28e4b29 // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-pubsub v0.15.0 // indirect github.com/libp2p/go-libp2p-pubsub-router v0.6.0 // indirect diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index ead406796b2..e54766c6991 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -486,8 +486,8 @@ github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl9 github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-core v0.2.4/go.mod h1:STh4fdfa5vDYr0/SzYYeqnt+E6KfEV5VxfIrm0bcI0g= github.com/libp2p/go-libp2p-core v0.3.0/go.mod h1:ACp3DmS3/N64c2jDzcV429ukDpicbL6+TrrxANBjPGw= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d h1:/T1QPIn+5C0s6v7cLM6ldY1Ot5imedIC27vhZ0r3OW4= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260319095041-7ba6b28e4b29 h1:ZSi0kAWeDUeDPnoiVZ75Hyun7+wksJpQxFiz6aWNrys= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260319095041-7ba6b28e4b29/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= diff --git a/go.mod b/go.mod index f25e40fce09..3ebc1a11f1c 100644 --- a/go.mod +++ b/go.mod @@ -52,7 +52,7 @@ require ( github.com/libp2p/go-doh-resolver v0.5.0 github.com/libp2p/go-libp2p v0.47.0 github.com/libp2p/go-libp2p-http v0.5.0 - github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d + github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260319095041-7ba6b28e4b29 github.com/libp2p/go-libp2p-kbucket v0.8.0 github.com/libp2p/go-libp2p-pubsub v0.15.0 github.com/libp2p/go-libp2p-pubsub-router v0.6.0 diff --git a/go.sum b/go.sum index 5679ac255c8..9d9e94d694c 100644 --- a/go.sum +++ b/go.sum @@ -543,8 +543,8 @@ github.com/libp2p/go-libp2p-gostream v0.6.0 h1:QfAiWeQRce6pqnYfmIVWJFXNdDyfiR/qk github.com/libp2p/go-libp2p-gostream v0.6.0/go.mod h1:Nywu0gYZwfj7Jc91PQvbGU8dIpqbQQkjWgDuOrFaRdA= github.com/libp2p/go-libp2p-http v0.5.0 h1:+x0AbLaUuLBArHubbbNRTsgWz0RjNTy6DJLOxQ3/QBc= github.com/libp2p/go-libp2p-http v0.5.0/go.mod h1:glh87nZ35XCQyFsdzZps6+F4HYI6DctVFY5u1fehwSg= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d h1:/T1QPIn+5C0s6v7cLM6ldY1Ot5imedIC27vhZ0r3OW4= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260319095041-7ba6b28e4b29 h1:ZSi0kAWeDUeDPnoiVZ75Hyun7+wksJpQxFiz6aWNrys= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260319095041-7ba6b28e4b29/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod index 0fb82cf52de..a5931816196 100644 --- a/test/dependencies/go.mod +++ b/test/dependencies/go.mod @@ -183,7 +183,7 @@ require ( github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p v0.47.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d // indirect + github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260319095041-7ba6b28e4b29 // indirect github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect github.com/libp2p/go-libp2p-record v0.3.1 // indirect github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum index a19056f4823..02a6a8002b4 100644 --- a/test/dependencies/go.sum +++ b/test/dependencies/go.sum @@ -578,8 +578,8 @@ github.com/libp2p/go-libp2p v0.47.0 h1:qQpBjSCWNQFF0hjBbKirMXE9RHLtSuzTDkTfr1rw0 github.com/libp2p/go-libp2p v0.47.0/go.mod h1:s8HPh7mMV933OtXzONaGFseCg/BE//m1V34p3x4EUOY= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d h1:/T1QPIn+5C0s6v7cLM6ldY1Ot5imedIC27vhZ0r3OW4= -github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260318194436-7b44139b899d/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260319095041-7ba6b28e4b29 h1:ZSi0kAWeDUeDPnoiVZ75Hyun7+wksJpQxFiz6aWNrys= +github.com/libp2p/go-libp2p-kad-dht v0.38.1-0.20260319095041-7ba6b28e4b29/go.mod h1:g/CefQilAnCMyUH52A6tUGbe17NgQ8q26MaZCA968iI= github.com/libp2p/go-libp2p-kbucket v0.8.0 h1:QAK7RzKJpYe+EuSEATAaaHYMYLkPDGC18m9jxPLnU8s= github.com/libp2p/go-libp2p-kbucket v0.8.0/go.mod h1:JMlxqcEyKwO6ox716eyC0hmiduSWZZl6JY93mGaaqc4= github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg=