Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0853595
feat(dir): dht only remote discovery
tkircsi Aug 4, 2026
6c6d7ff
fix(server): enforce SQLite foreign keys per connection
tkircsi Aug 4, 2026
2ea99c3
fix(routing): unpublished records were remotely discoverable
tkircsi Aug 4, 2026
6616342
Merge branch 'main' into feat/dht-only-remote-discovery
tkircsi Aug 5, 2026
e0cbcf2
Merge branch 'main' into feat/dht-only-remote-discovery
tkircsi Aug 5, 2026
3957c82
Merge branch 'main' into feat/dht-only-remote-discovery
tkircsi Aug 6, 2026
cec0634
Merge branch 'main' into feat/dht-only-remote-discovery
tkircsi Aug 7, 2026
c5e40fc
Merge branch 'main' into feat/dht-only-remote-discovery
tkircsi Aug 7, 2026
2626f14
Merge branch 'main' into feat/dht-only-remote-discovery
tkircsi Aug 8, 2026
6697020
Merge branch 'main' into feat/dht-only-remote-discovery
tkircsi Aug 10, 2026
3ad2626
Merge branch 'main' into feat/dht-only-remote-discovery
tkircsi Aug 11, 2026
dbed4ac
Merge remote-tracking branch 'origin/main' into feat/dht-only-remote-…
tkircsi Aug 11, 2026
54f1a27
fix: linter issue
tkircsi Aug 11, 2026
e902ff1
Merge branch 'main' into feat/dht-only-remote-discovery
tkircsi Aug 13, 2026
b49f577
Merge branch 'main' into feat/dht-only-remote-discovery
tkircsi Aug 14, 2026
dbb1cc7
Merge branch 'main' into feat/dht-only-remote-discovery
tkircsi Aug 14, 2026
1e72b63
Merge branch 'main' into feat/dht-only-remote-discovery
tkircsi Aug 14, 2026
1015493
Merge branch 'main' into feat/dht-only-remote-discovery
tkircsi Aug 17, 2026
47ff266
feat(routing): carry v1 publish state across the upgrade automatically
tkircsi Aug 17, 2026
6198493
Merge branch 'main' into feat/dht-only-remote-discovery
tkircsi Aug 17, 2026
957760b
Merge branch 'main' into feat/dht-only-remote-discovery
tkircsi Aug 17, 2026
9b36ab4
Merge branch 'main' into feat/dht-only-remote-discovery
tkircsi Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed
- **Dir**: The routing datastore now opens a protocol-versioned subdirectory of `routing.datastore_dir` — `dir-2` for this release — rather than the configured path itself. The setting is unchanged; only the directory the database lives in moves one level down. This matters if you back up or bind-mount that path directly (#1967)

### Fixed
- **Dir**: Publish state recorded by v1 is migrated onto the record rows on first start, so upgrading no longer needs the routing volume deleted or records republished. A v1 datastore left in place is read once and then untouched, and can be removed at any time with the node running (#1967)

## [v1.6.2] - 2026-07-29

### Added
Expand Down
11 changes: 0 additions & 11 deletions cli/cmd/daemon/daemon.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,6 @@ server:
# more announcement traffic and more work for every subscriber.
republish_interval: "36h"

gossipsub:
enabled: true
# DHT-based record + referrer autosync (deny-by-default; disabled unless set).
# When enabled, records announced by an allow-listed peer are pulled and
# ingested locally over the libp2p/DHT transport.
autosync:
enabled: false
# peerlist is a list of trusted source peers (by libp2p peer ID).
# peerlist:
# - peer: "12D3KooW...peerID1"
# - peer: "12D3KooW...peerID2"
# Circuit-relay v2 for NAT traversal.
# relay_service: enable a relay service on this node (only on publicly
# reachable nodes, e.g. bootstrap) so it can relay traffic for NAT'd peers.
Expand Down
4 changes: 2 additions & 2 deletions cli/cmd/doctor/bootstrap_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"
"time"

serverrouting "github.com/agntcy/dir/server/routing"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/protocol"
Expand Down Expand Up @@ -246,9 +247,8 @@ func addPeerProtocolDetails(details map[string]string, protocols []protocol.ID,

details["protocol_count"] = fmt.Sprintf("%d", len(protocolStrings))
details["protocols"] = strings.Join(protocolStrings, ",")
details["has_kad_dht_protocol"] = fmt.Sprintf("%t", hasProtocolPrefix(protocolStrings, "/ipfs/kad") || hasProtocolPrefix(protocolStrings, "dir/kad"))
details["has_kad_dht_protocol"] = fmt.Sprintf("%t", hasProtocolPrefix(protocolStrings, "/ipfs/kad") || hasProtocolPrefix(protocolStrings, serverrouting.ProtocolPrefix+"/kad"))
details["has_dir_rpc_protocol"] = fmt.Sprintf("%t", hasProtocolPrefix(protocolStrings, "/dir/rpc"))
details["has_gossipsub_protocol"] = fmt.Sprintf("%t", hasProtocolPrefix(protocolStrings, "/meshsub") || hasProtocolPrefix(protocolStrings, "/floodsub"))
}

func hasProtocolPrefix(protocols []string, prefix string) bool {
Expand Down
7 changes: 3 additions & 4 deletions cli/cmd/doctor/bootstrap_checks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,12 @@ func TestBootstrapPeerValidationHelpers(t *testing.T) {
func TestAddPeerProtocolDetails(t *testing.T) {
details := map[string]string{}

addPeerProtocolDetails(details, []protocol.ID{"/ipfs/kad/1.0.0", "/dir/rpc/0.1.0", "/meshsub/1.1.0"}, nil)
addPeerProtocolDetails(details, []protocol.ID{"/dir/2/kad/1.0.0", "/dir/rpc/2.0.0"}, nil)

assert.Equal(t, "3", details["protocol_count"])
assert.Equal(t, "2", details["protocol_count"])
assert.Equal(t, "true", details["has_kad_dht_protocol"])
assert.Equal(t, "true", details["has_dir_rpc_protocol"])
assert.Equal(t, "true", details["has_gossipsub_protocol"])
assert.True(t, hasProtocolPrefix([]string{"/dir/rpc/0.1.0"}, "/dir/rpc"))
assert.True(t, hasProtocolPrefix([]string{"/dir/rpc/2.0.0"}, "/dir/rpc"))
assert.False(t, hasProtocolPrefix([]string{"/other/1.0.0"}, "/dir/rpc"))
}

Expand Down
1 change: 0 additions & 1 deletion cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,6 @@ require (
github.com/libp2p/go-libp2p-gorpc v0.6.0 // indirect
github.com/libp2p/go-libp2p-kad-dht v0.41.0
github.com/libp2p/go-libp2p-kbucket v0.8.0 // indirect
github.com/libp2p/go-libp2p-pubsub v0.16.0 // indirect
github.com/libp2p/go-libp2p-record v0.3.1 // indirect
github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect
github.com/libp2p/go-msgio v0.3.0 // indirect
Expand Down
2 changes: 0 additions & 2 deletions cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1162,8 +1162,6 @@ github.com/libp2p/go-libp2p-kad-dht v0.41.0 h1:sDigz5SgV20Crj8ItJmJpEAM+eJrzC/Sa
github.com/libp2p/go-libp2p-kad-dht v0.41.0/go.mod h1:2qc4QGLvmIdznYbNg++FF76vp4q2SaBZyr76jHV8xgs=
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-pubsub v0.16.0 h1:j7G2C8kJwkcAQqYR7Wmq3d75d3Sgw/N0Hhiv0dVx7OY=
github.com/libp2p/go-libp2p-pubsub v0.16.0/go.mod h1:lr4oE8bFgQaifRcoc2uWhWWiK6tPdOEKpUuR408GFN4=
github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg=
github.com/libp2p/go-libp2p-record v0.3.1/go.mod h1:T8itUkLcWQLCYMqtX7Th6r7SexyUJpIyPgks757td/E=
github.com/libp2p/go-libp2p-routing-helpers v0.7.5 h1:HdwZj9NKovMx0vqq6YNPTh6aaNzey5zHD7HeLJtq6fI=
Expand Down
2 changes: 0 additions & 2 deletions docs/content/dir/dir-cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,6 @@ The daemon ships with sensible built-in defaults. To customize, pass a YAML conf
routing:
listen_address: "/ip4/0.0.0.0/tcp/8999"
datastore_dir: "routing"
gossipsub:
enabled: true
database:
type: "sqlite"
sqlite:
Expand Down
2 changes: 0 additions & 2 deletions docs/content/dir/dir-deployment-kubernetes.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,6 @@ The Agent Directory Service can be deployed using Helm or GitOps / Argo CD. Helm
listen_address: "/ip4/0.0.0.0/tcp/5555"
datastore_dir: /etc/routing/datastore
directory_api_address: "dir-apiserver.dir-dev-dir.svc.cluster.local:8888"
gossipsub:
enabled: false
sync:
auth_config:
username: "user"
Expand Down
2 changes: 0 additions & 2 deletions docs/content/dir/dir-deployment-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,6 @@ server:
datastore_dir: "routing"
bootstrap_peers:
- "/dns4/remote-dir.example.com/tcp/8999/p2p/<remote-peer-id>"
gossipsub:
enabled: true
database:
type: "sqlite"
sqlite:
Expand Down
2 changes: 0 additions & 2 deletions docs/content/dir/dir-federation-aws-eks.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,6 @@ This guide does not try to provision the AWS infrastructure from zero in the mai
key_path: /etc/routing/node.privkey
datastore_dir: /etc/routing/datastore
directory_api_address: "${DIR_API_HOST}:443"
gossipsub:
enabled: true
sync:
auth_config:
username: "user"
Expand Down
7 changes: 0 additions & 7 deletions install/charts/dir/apiserver/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,6 @@ config:
bootstrap_peers:
- /dns4/routing.ads.outshift.io/tcp/5555/p2p/12D3KooWLf9p3cedc86xGQBaqak6rAFmQk1HxKAK1yh7umHE3amu

# GossipSub configuration for efficient label announcements
# When enabled, labels are propagated via GossipSub mesh to ALL subscribed peers
# When disabled, falls back to DHT+Pull mechanism (higher bandwidth, limited reach)
# Default: true (recommended for production)
gossipsub:
enabled: true

# Sync configuration
sync:
# Authentication configuration for sync operations
Expand Down
18 changes: 0 additions & 18 deletions install/charts/dir/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -219,24 +219,6 @@ apiserver:
bootstrap_peers:
- /dns4/routing.ads.outshift.io/tcp/5555/p2p/12D3KooWLf9p3cedc86xGQBaqak6rAFmQk1HxKAK1yh7umHE3amu

# GossipSub configuration for efficient label announcements
# When enabled, labels are propagated via GossipSub mesh to ALL subscribed peers
# When disabled, falls back to DHT+Pull mechanism (higher bandwidth, limited reach)
# Default: true (recommended for production)
gossipsub:
enabled: true

# DHT-based record + referrer autosync.
# Deny-by-default: when enabled, only records announced by a peer in
# peerlist are pulled and ingested locally over the libp2p/DHT transport.
# peerlist is a list of trusted source peers (by libp2p peer ID) and is
# configured via values/config only (not a single environment variable).
autosync:
enabled: false
# peerlist:
# - peer: "12D3KooW...peerID1"
# - peer: "12D3KooW...peerID2"

# Circuit-relay v2 for NAT traversal.
# relay_service: run a relay service on this node so it can relay traffic
# for NAT'd peers. Enable only on publicly-reachable nodes (e.g. the
Expand Down
1 change: 0 additions & 1 deletion install/docker/apiserver.env
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ DIRECTORY_SERVER_ROUTING_DIRECTORY_API_ADDRESS=
DIRECTORY_SERVER_ROUTING_BOOTSTRAP_PEERS=
DIRECTORY_SERVER_ROUTING_KEY_PATH=
DIRECTORY_SERVER_ROUTING_DATASTORE_DIR=
DIRECTORY_SERVER_ROUTING_GOSSIPSUB_ENABLED=true

# Database Configuration (PostgreSQL)
DIRECTORY_SERVER_DATABASE_TYPE=postgres
Expand Down
99 changes: 84 additions & 15 deletions reconciler/tasks/metrics/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package metrics
import (
"context"
"fmt"
"sync"
"time"

"github.com/agntcy/dir/server/types"
Expand All @@ -20,6 +21,26 @@ import (

var logger = logging.Logger("reconciler/metrics")

// providerCountWorkers bounds how many provider lookups are in flight at once.
const providerCountWorkers = 8

// maxProviderCount is an upper bound for a stored provider count. A real DHT
// lookup returns orders of magnitude fewer peers; the cap exists so the
// conversion to the column's width cannot wrap.
const maxProviderCount = 1 << 20

func clampProviderCount(count int) uint32 {
if count < 0 {
return 0
}

if count > maxProviderCount {
return maxProviderCount
}

return uint32(count)
}

// ProviderCounterAPI is the minimal interface required by the metrics task to
// query provider counts. It is satisfied by types.RoutingAPI (daemon mode, where
// the routing layer is shared in-process) and by GRPCProviderCounter (standalone
Expand Down Expand Up @@ -96,28 +117,21 @@ func (t *Task) refreshProviderCounts(ctx context.Context) error {

var updated, failed int

for _, cid := range cids {
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled: %w", ctx.Err())
default:
}

count, err := t.counters.GetProviderCount(ctx, cid)
if err != nil {
logger.Warn("Failed to get provider count", "cid", cid, "error", err)
// Persist serially while the lookups run concurrently. The lookups are the
// slow part; the metrics table serialises writers regardless.
for res := range t.countProviders(ctx, cids) {
if res.err != nil {
logger.Warn("Failed to get provider count", "cid", res.cid, "error", res.err)

failed++

continue
}

if count < 0 {
count = 0
}
count := clampProviderCount(res.count)

if err := t.db.SetProviderCount(cid, uint32(count)); err != nil {
logger.Warn("Failed to set provider count", "cid", cid, "count", count, "error", err)
if err := t.db.SetProviderCount(res.cid, count); err != nil {
logger.Warn("Failed to set provider count", "cid", res.cid, "count", count, "error", err)

failed++

Expand All @@ -127,7 +141,62 @@ func (t *Task) refreshProviderCounts(ctx context.Context) error {
updated++
}

if err := ctx.Err(); err != nil {
return fmt.Errorf("context cancelled: %w", err)
}

logger.Info("Provider count refresh complete", "updated", updated, "failed", failed)

return nil
}

type providerCountResult struct {
cid string
count int
err error
}

// countProviders fans the CID list out over a bounded worker pool and streams
// results back as they land. Each GetProviderCount is a DHT lookup, so running
// one CID at a time would exceed the reconciliation interval on any sizeable
// corpus.
func (t *Task) countProviders(ctx context.Context, cids []string) <-chan providerCountResult {
jobs := make(chan string)
results := make(chan providerCountResult)
workers := min(providerCountWorkers, len(cids))

var wg sync.WaitGroup

for range workers {
wg.Go(func() {
for cid := range jobs {
count, err := t.counters.GetProviderCount(ctx, cid)

select {
case results <- providerCountResult{cid: cid, count: count, err: err}:
case <-ctx.Done():
return
}
}
})
}

go func() {
defer close(jobs)

for _, cid := range cids {
select {
case jobs <- cid:
case <-ctx.Done():
return
}
}
}()

go func() {
wg.Wait()
close(results)
}()

return results
}
Loading
Loading