4
"context"
5
"errors"
6
"fmt"
7
+ "os"
8
+ "path/filepath"
9
"time"
10
11
"github.com/ipfs/boxo/blockstore"
16
"github.com/ipfs/boxo/provider"
17
"github.com/ipfs/go-cid"
18
"github.com/ipfs/go-datastore"
19
+ "github.com/ipfs/go-datastore/mount"
20
"github.com/ipfs/go-datastore/namespace"
21
"github.com/ipfs/go-datastore/query"
22
log "github.com/ipfs/go-log/v2"
23
"github.com/ipfs/kubo/config"
24
"github.com/ipfs/kubo/repo"
25
+ "github.com/ipfs/kubo/repo/fsrepo"
26
irouting "github.com/ipfs/kubo/routing"
27
dht "github.com/libp2p/go-libp2p-kad-dht"
28
"github.com/libp2p/go-libp2p-kad-dht/amino"
52
// Datastore key used to store previous reprovide strategy.
53
reprovideStrategyKey = "/reprovideStrategy"
54
51
- // Datastore namespace prefix for provider data.
52
- providerDatastorePrefix = "provider"
53
- // Datastore path for the provider keystore.
54
- keystoreDatastorePath = "keystore"
55
+ // KeystoreDatastorePath is the base directory for the provider keystore datastores.
56
+ KeystoreDatastorePath = "provider-keystore"
57
+)
58
+
59
+var (
60
+ // Datastore namespace key for provider data.
61
+ providerDatastoreKey = datastore.NewKey("provider")
62
+ // Datastore namespace key for provider keystore data.
63
+ keystoreDatastoreKey = datastore.NewKey("keystore")
64
)
65
66
var errAcceleratedDHTNotReady = errors.New("AcceleratedDHTClient: routing table not ready")
67
68
+// validateKeystoreSuffix rejects any suffix other than "0" or "1".
69
+// The upstream library uses these two values as alternating namespace
70
+// identifiers. Validating here prevents accidental deletion of unrelated
71
+// directories via os.RemoveAll if the upstream ever changes its scheme.
72
+func validateKeystoreSuffix(suffix string) error {
73
+ if suffix != "0" && suffix != "1" {
74
+ return fmt.Errorf("unexpected keystore suffix %q, expected \"0\" or \"1\"", suffix)
75
+ }
76
+ return nil
77
+}
78
+
79
// Interval between reprovide queue monitoring checks for slow reprovide alerts.
80
// Used when Provide.DHT.SweepEnabled=true
81
const reprovideAlertPollInterval = 15 * time.Minute
389
FilteredAddrs() []ma.Multiaddr
390
}
391
392
+// findRootDatastoreSpec extracts the leaf datastore spec for the root ("/")
393
+// mount from the repo's Datastore.Spec config. It unwraps mount (picks the "/"
394
+// mountpoint), measure, and log wrappers to find the actual backend spec
395
+// (e.g., levelds, pebbleds).
396
+func findRootDatastoreSpec(spec map[string]any) map[string]any {
397
+ if spec == nil {
398
+ return nil
399
+ }
400
+ switch spec["type"] {
401
+ case "mount":
402
+ mounts, ok := spec["mounts"].([]any)
403
+ if !ok {
404
+ return spec
405
+ }
406
+ for _, m := range mounts {
407
+ mnt, ok := m.(map[string]any)
408
+ if !ok {
409
+ continue
410
+ }
411
+ if mnt["mountpoint"] == "/" {
412
+ return findRootDatastoreSpec(mnt)
413
+ }
414
+ }
415
+ // No root mount found; return nil so callers fall back gracefully
416
+ // (in-memory datastore or skip mounting) rather than passing a
417
+ // mount-type spec to openDatastoreAt which expects a leaf backend.
418
+ return nil
419
+ case "measure", "log":
420
+ if child, ok := spec["child"].(map[string]any); ok {
421
+ return findRootDatastoreSpec(child)
422
+ }
423
+ return spec
424
+ default:
425
+ if _, hasChild := spec["child"]; hasChild {
426
+ logger.Warnw("unrecognized datastore wrapper type, using as-is",
427
+ "type", spec["type"])
428
+ }
429
+ return spec
430
+ }
431
+}
432
+
433
+// MountKeystoreDatastores opens any provider keystore datastores that exist on
434
+// disk and returns them as mount.Mount entries ready to be combined with the
435
+// main repo datastore. The caller must call the returned cleanup function when
436
+// done. Returns nil mounts and a no-op closer if no keystores exist.
437
+func MountKeystoreDatastores(repo repo.Repo) ([]mount.Mount, func(), error) {
438
+ cfg, err := repo.Config()
439
+ if err != nil {
440
+ return nil, nil, fmt.Errorf("reading repo config: %w", err)
441
+ }
442
+
443
+ rootSpec := findRootDatastoreSpec(cfg.Datastore.Spec)
444
+ if rootSpec == nil {
445
+ return nil, func() {}, nil
446
+ }
447
+
448
+ keystoreBasePath := filepath.Join(repo.Path(), KeystoreDatastorePath)
449
+ var mounts []mount.Mount
450
+ var closers []func()
451
+
452
+ for _, suffix := range []string{"0", "1"} {
453
+ dir := filepath.Join(keystoreBasePath, suffix)
454
+ if _, err := os.Stat(dir); err != nil {
455
+ continue
456
+ }
457
+ ds, err := openDatastoreAt(rootSpec, dir)
458
+ if err != nil {
459
+ for _, c := range closers {
460
+ c()
461
+ }
462
+ return nil, nil, err
463
+ }
464
+ prefix := providerDatastoreKey.Child(keystoreDatastoreKey).ChildString(suffix)
465
+ mounts = append(mounts, mount.Mount{Prefix: prefix, Datastore: ds})
466
+ closers = append(closers, func() { ds.Close() })
467
+ }
468
+
469
+ closer := func() {
470
+ for _, c := range closers {
471
+ c()
472
+ }
473
+ }
474
+ return mounts, closer, nil
475
+}
476
+
477
+// openDatastoreAt opens a datastore using the given spec at the specified path.
478
+// It deep-copies the spec to avoid mutating the original.
479
+func openDatastoreAt(rootSpec map[string]any, path string) (datastore.Batching, error) {
480
+ spec := copySpec(rootSpec)
481
+ spec["path"] = path
482
+ dsc, err := fsrepo.AnyDatastoreConfig(spec)
483
+ if err != nil {
484
+ return nil, fmt.Errorf("creating datastore config for %s: %w", path, err)
485
+ }
486
+ return dsc.Create("")
487
+}
488
+
489
+// copySpec deep-copies a datastore spec map so modifications (e.g., changing
490
+// the path) don't affect the original.
491
+func copySpec(spec map[string]any) map[string]any {
492
+ if spec == nil {
493
+ return nil
494
+ }
495
+ cp := make(map[string]any, len(spec))
496
+ for k, v := range spec {
497
+ switch val := v.(type) {
498
+ case map[string]any:
499
+ cp[k] = copySpec(val)
500
+ case []any:
501
+ s := make([]any, len(val))
502
+ for i, elem := range val {
503
+ if m, ok := elem.(map[string]any); ok {
504
+ s[i] = copySpec(m)
505
+ } else {
506
+ s[i] = elem
507
+ }
508
+ }
509
+ cp[k] = s
510
+ default:
511
+ cp[k] = v
512
+ }
513
+ }
514
+ return cp
515
+}
516
+
517
+// purgeBatchSize is the number of keys deleted per batch commit during
518
+// orphaned keystore cleanup. Each commit is a cancellation checkpoint.
519
+const purgeBatchSize = 1 << 12 // 4096
520
+
521
+// purgeOrphanedKeystoreData deletes all keys under /provider/keystore/ from the
522
+// shared repo datastore. These were written by older Kubo versions that stored
523
+// provider keystore data inline in the shared datastore. The new code uses
524
+// separate filesystem datastores under <repo>/{KeystoreDatastorePath}/ instead.
525
+//
526
+// The operation is idempotent and safe to interrupt: partial completion is
527
+// fine because already-deleted keys are no-ops on re-run.
528
+func purgeOrphanedKeystoreData(ctx context.Context, ds datastore.Batching) error {
529
+ orphanedPrefix := providerDatastoreKey.Child(keystoreDatastoreKey).String()
530
+ syncKey := datastore.NewKey(orphanedPrefix)
531
+
532
+ results, err := ds.Query(ctx, query.Query{
533
+ Prefix: orphanedPrefix,
534
+ KeysOnly: true,
535
+ })
536
+ if err != nil {
537
+ return fmt.Errorf("querying orphaned keystore data: %w", err)
538
+ }
539
+ defer results.Close()
540
+
541
+ var batch datastore.Batch
542
+ var count, pending int
543
+ for result := range results.Next() {
544
+ if ctx.Err() != nil {
545
+ return ctx.Err()
546
+ }
547
+ if result.Error != nil {
548
+ return fmt.Errorf("iterating orphaned keystore data: %w", result.Error)
549
+ }
550
+ if batch == nil {
551
+ batch, err = ds.Batch(ctx)
552
+ if err != nil {
553
+ return fmt.Errorf("creating batch for orphaned keystore cleanup: %w", err)
554
+ }
555
+ }
556
+ if err := batch.Delete(ctx, datastore.NewKey(result.Key)); err != nil {
557
+ return fmt.Errorf("batch deleting orphaned key %s: %w", result.Key, err)
558
+ }
559
+ count++
560
+ pending++
561
+ if pending >= purgeBatchSize {
562
+ if err := batch.Commit(ctx); err != nil {
563
+ return fmt.Errorf("committing orphaned keystore cleanup batch: %w", err)
564
+ }
565
+ if err := ds.Sync(ctx, syncKey); err != nil {
566
+ return fmt.Errorf("syncing orphaned keystore cleanup: %w", err)
567
+ }
568
+ batch = nil
569
+ pending = 0
570
+ }
571
+ }
572
+ if pending > 0 {
573
+ if err := batch.Commit(ctx); err != nil {
574
+ return fmt.Errorf("committing orphaned keystore cleanup batch: %w", err)
575
+ }
576
+ if err := ds.Sync(ctx, syncKey); err != nil {
577
+ return fmt.Errorf("syncing orphaned keystore cleanup: %w", err)
578
+ }
579
+ }
580
+ if count > 0 {
581
+ logger.Infow("purged orphaned provider keystore data from shared datastore", "keys", count)
582
+ }
583
+ return nil
584
+}
585
+
586
func SweepingProviderOpt(cfg *config.Config) fx.Option {
587
reprovideInterval := cfg.Provide.DHT.Interval.WithDefault(config.DefaultProvideDHTInterval)
588
type providerInput struct {
589
fx.In
590
DHT routing.Routing `name:"dhtc"`
591
Repo repo.Repo
592
+ Lc fx.Lifecycle
593
}
594
sweepingReprovider := fx.Provide(func(in providerInput) (DHTProvider, *keystore.ResettableKeystore, error) {
380
- ds := namespace.Wrap(in.Repo.Datastore(), datastore.NewKey(providerDatastorePrefix))
381
- ks, err := keystore.NewResettableKeystore(ds,
382
- keystore.WithPrefixBits(16),
383
- keystore.WithDatastorePath(keystoreDatastorePath),
384
- keystore.WithBatchSize(int(cfg.Provide.DHT.KeystoreBatchSize.WithDefault(config.DefaultProvideDHTKeystoreBatchSize))),
595
+ ds := namespace.Wrap(in.Repo.Datastore(), providerDatastoreKey)
596
+
597
+ // Get repo path and config to determine datastore type
598
+ repoPath := in.Repo.Path()
599
+ repoCfg, err := in.Repo.Config()
600
+ if err != nil {
601
+ return nil, nil, fmt.Errorf("getting repo config: %w", err)
602
+ }
603
+
604
+ // Find the root datastore type (levelds, pebbleds, etc.)
605
+ rootSpec := findRootDatastoreSpec(repoCfg.Datastore.Spec)
606
+
607
+ // Keystore datastores live at <repo>/{KeystoreDatastorePath}/<suffix>
608
+ keystoreBasePath := filepath.Join(repoPath, KeystoreDatastorePath)
609
+
610
+ createDs := func(suffix string) (datastore.Batching, error) {
611
+ if err := validateKeystoreSuffix(suffix); err != nil {
612
+ return nil, err
613
+ }
614
+ // When no datastore spec is configured (e.g., test/mock repos),
615
+ // fall back to an in-memory datastore.
616
+ if rootSpec == nil {
617
+ return datastore.NewMapDatastore(), nil
618
+ }
619
+ if err := os.MkdirAll(keystoreBasePath, 0o755); err != nil {
620
+ return nil, fmt.Errorf("creating keystore base directory: %w", err)
621
+ }
622
+ ds, err := openDatastoreAt(rootSpec, filepath.Join(keystoreBasePath, suffix))
623
+ if err != nil {
624
+ return nil, err
625
+ }
626
+ logger.Infow("provider keystore: opened datastore", "suffix", suffix, "path", filepath.Join(keystoreBasePath, suffix))
627
+ return ds, nil
628
+ }
629
+
630
+ destroyDs := func(suffix string) error {
631
+ if err := validateKeystoreSuffix(suffix); err != nil {
632
+ return err
633
+ }
634
+ logger.Infow("provider keystore: removing datastore from disk", "suffix", suffix, "path", filepath.Join(keystoreBasePath, suffix))
635
+ return os.RemoveAll(filepath.Join(keystoreBasePath, suffix))
636
+ }
637
+
638
+ // One-time cleanup of stale keystore data left by older Kubo in the
639
+ // shared repo datastore under /provider/keystore/. New code stores
640
+ // bulk key data in separate filesystem datastores under
641
+ // <repo>/{KeystoreDatastorePath}/ while still using the same
642
+ // /provider/keystore/ namespace in the shared datastore for metadata.
643
+ //
644
+ // The absence of the keystoreBasePath directory signals a first run
645
+ // after upgrade: the directory is created later by createDs on first
646
+ // use, so it doubles as a "cleanup done" flag. If the process dies
647
+ // mid-purge the directory still won't exist and the cleanup re-runs
648
+ // on next start (it is idempotent). Must run synchronously before
649
+ // NewResettableKeystore to avoid racing with reads on the same
650
+ // namespace.
651
+ if _, statErr := os.Stat(keystoreBasePath); os.IsNotExist(statErr) {
652
+ logger.Infow("migrating provider keystore data from shared datastore to separate filesystem datastores", "path", keystoreBasePath)
653
+ // Create a cancellable context for the purge. The OnStop hook
654
+ // below calls purgeCancel when the node receives a shutdown
655
+ // signal (e.g., SIGINT), which interrupts the purge loop
656
+ // instead of blocking indefinitely.
657
+ purgeCtx, purgeCancel := context.WithCancel(context.Background())
658
+ in.Lc.Append(fx.Hook{
659
+ OnStop: func(_ context.Context) error {
660
+ purgeCancel()
661
+ return nil
662
+ },
663
+ })
664
+ if purgeErr := purgeOrphanedKeystoreData(purgeCtx, in.Repo.Datastore()); purgeErr != nil {
665
+ if purgeCtx.Err() != nil {
666
+ logger.Infow("provider keystore migration interrupted by shutdown, will resume on next start")
667
+ } else {
668
+ logger.Warnw("provider keystore migration failed, will retry on next start", "error", purgeErr)
669
+ }
670
+ } else {
671
+ logger.Infow("provider keystore migration completed")
672
+ }
673
+ purgeCancel()
674
+ }
675
+
676
+ keystoreDs := namespace.Wrap(ds, keystoreDatastoreKey)
677
+ ks, err := keystore.NewResettableKeystore(keystoreDs,
678
+ keystore.WithDatastoreFactory(createDs, destroyDs),
679
+ keystore.KeystoreOption(
680
+ keystore.WithPrefixBits(16),
681
+ keystore.WithBatchSize(int(cfg.Provide.DHT.KeystoreBatchSize.WithDefault(config.DefaultProvideDHTKeystoreBatchSize))),
682
+ ),
683
)
684
if err != nil {
685
return nil, nil, err