Skip to content

Commit 3bb649c

Browse files
committed
routing/http/contentrouter: add support for GetClosestPeers
1 parent c70f108 commit 3bb649c

File tree

8 files changed

+161
-10
lines changed

8 files changed

+161
-10
lines changed

examples/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ require (
7575
github.com/libp2p/go-libp2p-kad-dht v0.34.0 // indirect
7676
github.com/libp2p/go-libp2p-kbucket v0.7.0 // indirect
7777
github.com/libp2p/go-libp2p-record v0.3.1 // indirect
78-
github.com/libp2p/go-libp2p-routing-helpers v0.7.5 // indirect
78+
github.com/libp2p/go-libp2p-routing-helpers v0.7.6-0.20250903125449-17ee6fbf872c // indirect
7979
github.com/libp2p/go-msgio v0.3.0 // indirect
8080
github.com/libp2p/go-netroute v0.2.2 // indirect
8181
github.com/libp2p/go-reuseport v0.4.0 // indirect

examples/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,8 @@ github.com/libp2p/go-libp2p-kbucket v0.7.0 h1:vYDvRjkyJPeWunQXqcW2Z6E93Ywx7fX0jg
229229
github.com/libp2p/go-libp2p-kbucket v0.7.0/go.mod h1:blOINGIj1yiPYlVEX0Rj9QwEkmVnz3EP8LK1dRKBC6g=
230230
github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg=
231231
github.com/libp2p/go-libp2p-record v0.3.1/go.mod h1:T8itUkLcWQLCYMqtX7Th6r7SexyUJpIyPgks757td/E=
232-
github.com/libp2p/go-libp2p-routing-helpers v0.7.5 h1:HdwZj9NKovMx0vqq6YNPTh6aaNzey5zHD7HeLJtq6fI=
233-
github.com/libp2p/go-libp2p-routing-helpers v0.7.5/go.mod h1:3YaxrwP0OBPDD7my3D0KxfR89FlcX/IEbxDEDfAmj98=
232+
github.com/libp2p/go-libp2p-routing-helpers v0.7.6-0.20250903125449-17ee6fbf872c h1:oWvPNbSi3yoJMDe04qvICNpwrKoub+x0EDb3bjc5cxs=
233+
github.com/libp2p/go-libp2p-routing-helpers v0.7.6-0.20250903125449-17ee6fbf872c/go.mod h1:Q1VSaOawgsvaa3hGl/PejADIhl2deiqSEsQDpB3Ggss=
234234
github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA=
235235
github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg=
236236
github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0=

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ require (
3939
github.com/libp2p/go-libp2p v0.43.0
4040
github.com/libp2p/go-libp2p-kad-dht v0.34.0
4141
github.com/libp2p/go-libp2p-record v0.3.1
42-
github.com/libp2p/go-libp2p-routing-helpers v0.7.5
42+
github.com/libp2p/go-libp2p-routing-helpers v0.7.6-0.20250903125449-17ee6fbf872c
4343
github.com/libp2p/go-libp2p-testing v0.12.0
4444
github.com/libp2p/go-msgio v0.3.0
4545
github.com/miekg/dns v1.1.68

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,8 @@ github.com/libp2p/go-libp2p-kbucket v0.7.0 h1:vYDvRjkyJPeWunQXqcW2Z6E93Ywx7fX0jg
230230
github.com/libp2p/go-libp2p-kbucket v0.7.0/go.mod h1:blOINGIj1yiPYlVEX0Rj9QwEkmVnz3EP8LK1dRKBC6g=
231231
github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg=
232232
github.com/libp2p/go-libp2p-record v0.3.1/go.mod h1:T8itUkLcWQLCYMqtX7Th6r7SexyUJpIyPgks757td/E=
233-
github.com/libp2p/go-libp2p-routing-helpers v0.7.5 h1:HdwZj9NKovMx0vqq6YNPTh6aaNzey5zHD7HeLJtq6fI=
234-
github.com/libp2p/go-libp2p-routing-helpers v0.7.5/go.mod h1:3YaxrwP0OBPDD7my3D0KxfR89FlcX/IEbxDEDfAmj98=
233+
github.com/libp2p/go-libp2p-routing-helpers v0.7.6-0.20250903125449-17ee6fbf872c h1:oWvPNbSi3yoJMDe04qvICNpwrKoub+x0EDb3bjc5cxs=
234+
github.com/libp2p/go-libp2p-routing-helpers v0.7.6-0.20250903125449-17ee6fbf872c/go.mod h1:Q1VSaOawgsvaa3hGl/PejADIhl2deiqSEsQDpB3Ggss=
235235
github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA=
236236
github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg=
237237
github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0=

routing/http/contentrouter/contentrouter.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ type Client interface {
3030
FindPeers(ctx context.Context, pid peer.ID) (peers iter.ResultIter[*types.PeerRecord], err error)
3131
GetIPNS(ctx context.Context, name ipns.Name) (*ipns.Record, error)
3232
PutIPNS(ctx context.Context, name ipns.Name, record *ipns.Record) error
33+
// GetClosestPeers returns the DHT closest peers to the given peer ID.
34+
// If empty, it will use the content router's peer ID (self). `closerThan` (optional) forces resulting records to be closer to `PeerID` than to `closerThan`. `count` specifies how many records to return ([1,100], with 20 as default when set to 0).
35+
GetClosestPeers(ctx context.Context, peerID, closerThan peer.ID, count int) (iter.ResultIter[*types.PeerRecord], error)
3336
}
3437

3538
type contentRouter struct {
@@ -44,6 +47,7 @@ var (
4447
_ routing.ValueStore = (*contentRouter)(nil)
4548
_ routinghelpers.ProvideManyRouter = (*contentRouter)(nil)
4649
_ routinghelpers.ReadyAbleRouter = (*contentRouter)(nil)
50+
_ routinghelpers.DHTRouter = (*contentRouter)(nil)
4751
)
4852

4953
type option func(c *contentRouter)
@@ -303,3 +307,45 @@ func (c *contentRouter) SearchValue(ctx context.Context, key string, opts ...rou
303307

304308
return ch, nil
305309
}
310+
311+
func (c *contentRouter) GetClosestPeers(ctx context.Context, pid, closerThan peer.ID, count int) (<-chan peer.AddrInfo, error) {
312+
iter, err := c.client.GetClosestPeers(ctx, pid, closerThan, count)
313+
if err != nil {
314+
return nil, err
315+
}
316+
defer iter.Close()
317+
318+
infos := make(chan peer.AddrInfo)
319+
go func() {
320+
defer close(infos)
321+
for iter.Next() {
322+
res := iter.Val()
323+
if res.Err != nil {
324+
logger.Warnf("error iterating peer responses: %s", res.Err)
325+
continue
326+
}
327+
328+
var addrs []multiaddr.Multiaddr
329+
for _, a := range res.Val.Addrs {
330+
addrs = append(addrs, a.Multiaddr)
331+
}
332+
333+
// If there are no addresses there's nothing of value to return
334+
if len(addrs) == 0 {
335+
continue
336+
}
337+
338+
select {
339+
case <-ctx.Done():
340+
logger.Warnf("aborting GetClosestPeers: %s", ctx.Err())
341+
return
342+
case infos <- peer.AddrInfo{
343+
ID: *res.Val.ID,
344+
Addrs: addrs,
345+
}:
346+
}
347+
}
348+
}()
349+
350+
return infos, nil
351+
}

routing/http/contentrouter/contentrouter_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ func (m *mockClient) PutIPNS(ctx context.Context, name ipns.Name, record *ipns.R
5353
return args.Error(0)
5454
}
5555

56+
func (m *mockClient) GetClosestPeers(ctx context.Context, peerID, closerThan peer.ID, count int) (iter.ResultIter[*types.PeerRecord], error) {
57+
args := m.Called(ctx, peerID, closerThan, count)
58+
return args.Get(0).(iter.ResultIter[*types.PeerRecord]), args.Error(1)
59+
}
60+
5661
func TestProvide(t *testing.T) {
5762
for _, c := range []struct {
5863
name string
@@ -258,6 +263,108 @@ func TestFindPeerNoPeer(t *testing.T) {
258263
require.ErrorIs(t, err, routing.ErrNotFound)
259264
}
260265

266+
func TestGetClosestPeers(t *testing.T) {
267+
t.Run("returns a channel and can read all results", func(t *testing.T) {
268+
ctx := context.Background()
269+
client := &mockClient{}
270+
crc := NewContentRoutingClient(client)
271+
272+
peerID := peer.ID("test-peer")
273+
closerThan := peer.ID("test-peer-2")
274+
count := 2
275+
276+
// Mock response with two peer records
277+
peer1 := peer.ID("peer1")
278+
peer2 := peer.ID("peer2")
279+
addr1 := multiaddr.StringCast("/ip4/1.2.3.4/tcp/1234")
280+
addr2 := multiaddr.StringCast("/ip4/5.6.7.8/tcp/5678")
281+
addrs1 := []types.Multiaddr{{Multiaddr: addr1}}
282+
addrs2 := []types.Multiaddr{{Multiaddr: addr2}}
283+
peerRec1 := &types.PeerRecord{
284+
Schema: types.SchemaPeer,
285+
ID: &peer1,
286+
Addrs: addrs1,
287+
Protocols: []string{"transport-bitswap"},
288+
}
289+
peerRec2 := &types.PeerRecord{
290+
Schema: types.SchemaPeer,
291+
ID: &peer2,
292+
Addrs: addrs2,
293+
Protocols: []string{"transport-bitswap"},
294+
}
295+
296+
peerIter := iter.ToResultIter[*types.PeerRecord](iter.FromSlice([]*types.PeerRecord{peerRec1, peerRec2}))
297+
298+
client.On("GetClosestPeers", ctx, peerID, closerThan, count).Return(peerIter, nil)
299+
300+
infos, err := crc.GetClosestPeers(ctx, peerID, closerThan, count)
301+
require.NoError(t, err)
302+
303+
var actual []peer.AddrInfo
304+
for info := range infos {
305+
actual = append(actual, info)
306+
}
307+
308+
expected := []peer.AddrInfo{
309+
{ID: peer1, Addrs: []multiaddr.Multiaddr{addr1}},
310+
{ID: peer2, Addrs: []multiaddr.Multiaddr{addr2}},
311+
}
312+
313+
assert.Equal(t, expected, actual)
314+
})
315+
316+
t.Run("returns no results if addrs is empty", func(t *testing.T) {
317+
ctx := context.Background()
318+
client := &mockClient{}
319+
crc := NewContentRoutingClient(client)
320+
321+
peerID := peer.ID("test-peer")
322+
closerThan := peer.ID("closer-than")
323+
count := 1
324+
325+
peer1 := peer.ID("peer1")
326+
peerRec1 := &types.PeerRecord{
327+
Schema: types.SchemaPeer,
328+
ID: &peer1,
329+
Protocols: []string{"transport-bitswap"},
330+
// no addresses
331+
}
332+
333+
// Mock response with an empty iterator
334+
peerIter := iter.ToResultIter[*types.PeerRecord](iter.FromSlice([]*types.PeerRecord{peerRec1}))
335+
336+
client.On("GetClosestPeers", ctx, peerID, closerThan, count).Return(peerIter, nil)
337+
338+
infos, err := crc.GetClosestPeers(ctx, peerID, closerThan, count)
339+
require.NoError(t, err)
340+
341+
var actual []peer.AddrInfo
342+
for info := range infos {
343+
actual = append(actual, info)
344+
}
345+
346+
assert.Empty(t, actual)
347+
})
348+
349+
t.Run("returns an error if call errors", func(t *testing.T) {
350+
ctx := context.Background()
351+
client := &mockClient{}
352+
crc := NewContentRoutingClient(client)
353+
354+
peerID := peer.ID("test-peer")
355+
closerThan := peer.ID("closer-than")
356+
count := 1
357+
358+
// Mock error response
359+
peerIter := iter.ToResultIter[*types.PeerRecord](iter.FromSlice([]*types.PeerRecord{}))
360+
client.On("GetClosestPeers", ctx, peerID, closerThan, count).Return(peerIter, assert.AnError)
361+
362+
infos, err := crc.GetClosestPeers(ctx, peerID, closerThan, count)
363+
require.ErrorIs(t, err, assert.AnError)
364+
assert.Nil(t, infos)
365+
})
366+
}
367+
261368
func makeName(t *testing.T) (crypto.PrivKey, ipns.Name) {
262369
sk, _, err := crypto.GenerateEd25519Key(rand.Reader)
263370
require.NoError(t, err)

routing/http/server/server.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -619,9 +619,7 @@ func (s *server) getClosestPeers(w http.ResponseWriter, r *http.Request) {
619619
return
620620
}
621621

622-
var (
623-
handlerFunc func(w http.ResponseWriter, provIter iter.ResultIter[*types.PeerRecord])
624-
)
622+
var handlerFunc func(w http.ResponseWriter, provIter iter.ResultIter[*types.PeerRecord])
625623

626624
if mediaType == mediaTypeNDJSON {
627625
handlerFunc = s.getClosestPeersNDJSON

routing/http/server/server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -683,7 +683,7 @@ func TestGetClosestPeers(t *testing.T) {
683683
requireCloseToNow(t, resp.Header.Get("Last-Modified"))
684684
})
685685

686-
t.Run("GET /routing/v1/dht/closest/peers/{cid-libp2p-key-peer-id}?count=? is bewteen[0-100] per spec", func(t *testing.T) {
686+
t.Run("GET /routing/v1/dht/closest/peers/{cid-libp2p-key-peer-id}?count=? is between[0-100] per spec", func(t *testing.T) {
687687
t.Parallel()
688688

689689
_, pid := makeEd25519PeerID(t)

0 commit comments

Comments
 (0)