Skip to content

Commit 3efdc57

Browse files
provider: refresh schedule (libp2p#1131)
* provider: refresh schedule * address review
1 parent dd5096d commit 3efdc57

File tree

8 files changed

+486
-15
lines changed

8 files changed

+486
-15
lines changed

provider/datastore/keystore.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,62 @@ func (s *KeyStore) Get(ctx context.Context, prefix bitstr.Key) ([]mh.Multihash,
369369
return result, nil
370370
}
371371

372+
// ContainsPrefix reports whether the KeyStore currently holds at least one
373+
// multihash whose kademlia identifier (bit256.Key) starts with the provided
374+
// bit-prefix.
375+
func (s *KeyStore) ContainsPrefix(ctx context.Context, prefix bitstr.Key) (bool, error) {
376+
if s.closed() {
377+
return false, ErrKeyStoreClosed
378+
}
379+
s.lk.Lock()
380+
defer s.lk.Unlock()
381+
382+
// Case 1: shorter than bucket length — any bucket with this key-prefix is enough.
383+
if len(prefix) < s.prefixLen {
384+
rem := s.prefixLen - len(prefix)
385+
limit := 1 << rem
386+
for i := range limit {
387+
suffix := fmt.Sprintf("%0*b", rem, i)
388+
dsKey := s.dsKey(prefix + bitstr.Key(suffix))
389+
exists, err := s.ds.Has(ctx, dsKey)
390+
if err != nil {
391+
return false, err
392+
}
393+
if exists {
394+
return true, nil
395+
}
396+
}
397+
return false, nil
398+
}
399+
400+
// Case 2: at least a full bucket prefix — check that bucket's content.
401+
dsKey := s.dsKey(bitstr.Key(prefix[:s.prefixLen]))
402+
403+
// Fast path when asking exactly for a bucket prefix: existence implies non-empty.
404+
if len(prefix) == s.prefixLen {
405+
return s.ds.Has(ctx, dsKey)
406+
}
407+
408+
// Longer-than-bucket: must inspect entries and see if any starts with `prefix`.
409+
data, err := s.ds.Get(ctx, dsKey)
410+
if err != nil {
411+
if err == ds.ErrNotFound {
412+
return false, nil
413+
}
414+
return false, err
415+
}
416+
var stored []mh.Multihash
417+
if err := json.Unmarshal(data, &stored); err != nil {
418+
return false, err
419+
}
420+
for _, h := range stored {
421+
if keyspace.IsPrefix(prefix, keyspace.MhToBit256(h)) {
422+
return true, nil
423+
}
424+
}
425+
return false, nil
426+
}
427+
372428
// emptyLocked deletes all entries under the datastore prefix, assuming s.lk is
373429
// already held.
374430
func (s *KeyStore) emptyLocked(ctx context.Context) error {

provider/datastore/keystore_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ package datastore
22

33
import (
44
"context"
5+
"strings"
56
"testing"
67

78
"github.com/ipfs/go-cid"
89
ds "github.com/ipfs/go-datastore"
10+
"github.com/ipfs/go-test/random"
911
"github.com/libp2p/go-libp2p-kad-dht/provider/internal/keyspace"
1012
mh "github.com/multiformats/go-multihash"
13+
"github.com/stretchr/testify/require"
1114

1215
"github.com/probe-lab/go-libdht/kad/key"
1316
"github.com/probe-lab/go-libdht/kad/key/bitstr"
@@ -85,6 +88,60 @@ func TestKeyStoreStoreAndGet(t *testing.T) {
8588
}
8689
}
8790

91+
func genMultihashesMatchingPrefix(prefix bitstr.Key, n int) []mh.Multihash {
92+
mhs := make([]mh.Multihash, 0, n)
93+
for i := 0; len(mhs) < n; i++ {
94+
h := random.Multihashes(1)[0]
95+
k := keyspace.MhToBit256(h)
96+
if keyspace.IsPrefix(prefix, k) {
97+
mhs = append(mhs, h)
98+
}
99+
}
100+
return mhs
101+
}
102+
103+
func TestKeyStoreContainsPrefix(t *testing.T) {
104+
ctx := context.Background()
105+
store, err := NewKeyStore(ds.NewMapDatastore())
106+
require.NoError(t, err)
107+
108+
ok, err := store.ContainsPrefix(ctx, bitstr.Key("0000"))
109+
require.NoError(t, err)
110+
require.False(t, ok)
111+
112+
generated := genMultihashesMatchingPrefix(bitstr.Key(strings.Repeat("0", DefaultKeyStorePrefixBits+4)), 1)
113+
require.True(t, keyspace.IsPrefix(bitstr.Key("0000"), keyspace.MhToBit256(generated[0])))
114+
store.Put(ctx, generated...)
115+
116+
ok, err = store.ContainsPrefix(ctx, bitstr.Key("0"))
117+
require.NoError(t, err)
118+
require.True(t, ok)
119+
120+
ok, err = store.ContainsPrefix(ctx, bitstr.Key("0000"))
121+
require.NoError(t, err)
122+
require.True(t, ok)
123+
124+
ok, err = store.ContainsPrefix(ctx, bitstr.Key(strings.Repeat("0", DefaultKeyStorePrefixBits)))
125+
require.NoError(t, err)
126+
require.True(t, ok)
127+
128+
ok, err = store.ContainsPrefix(ctx, bitstr.Key(strings.Repeat("0", DefaultKeyStorePrefixBits+4)))
129+
require.NoError(t, err)
130+
require.True(t, ok)
131+
132+
ok, err = store.ContainsPrefix(ctx, bitstr.Key("1"))
133+
require.NoError(t, err)
134+
require.False(t, ok)
135+
136+
ok, err = store.ContainsPrefix(ctx, bitstr.Key("0001"))
137+
require.NoError(t, err)
138+
require.False(t, ok)
139+
140+
ok, err = store.ContainsPrefix(ctx, bitstr.Key(strings.Repeat("0", DefaultKeyStorePrefixBits+2)+"1"))
141+
require.NoError(t, err)
142+
require.False(t, ok)
143+
}
144+
88145
func TestKeyStoreReset(t *testing.T) {
89146
store, err := NewKeyStore(ds.NewMapDatastore())
90147
if err != nil {

provider/internal/keyspace/key.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,42 @@ func ShortestCoveredPrefix(target bitstr.Key, peers []peer.ID) (bitstr.Key, []pe
131131
return target[:coveredCpl], peers[:lastCoveredPeerIndex]
132132
}
133133

134+
// ExtendBinaryPrefix returns all bitstrings of length n that start with prefix.
135+
// Example: prefix="1101", n=6 -> ["110100", "110101", "110110", "110111"].
136+
func ExtendBinaryPrefix(prefix bitstr.Key, n int) []bitstr.Key {
137+
extraBits := n - len(prefix)
138+
if n < 0 || extraBits < 0 {
139+
return nil
140+
}
141+
142+
extLen := 1 << extraBits // 2^extraBits
143+
rd := make([]bitstr.Key, 0, extLen)
144+
wr := make([]bitstr.Key, 1, extLen)
145+
wr[0] = prefix
146+
147+
// Iteratively append bits until reaching length n.
148+
for range extraBits {
149+
rd, wr = wr, rd[:0]
150+
for _, s := range rd {
151+
wr = append(wr, s+"0", s+"1")
152+
}
153+
}
154+
return wr
155+
}
156+
157+
// SiblingPrefixes returns the prefixes of the sibling subtrees along the path
158+
// to key. Together with the subtree under `key` itself, these prefixes
159+
// partition the keyspace.
160+
//
161+
// For key "1100" it returns: ["0", "10", "111", "1101"].
162+
func SiblingPrefixes(key bitstr.Key) []bitstr.Key {
163+
complements := make([]bitstr.Key, len(key))
164+
for i := range key {
165+
complements[i] = FlipLastBit(key[:i+1])
166+
}
167+
return complements
168+
}
169+
134170
// PrefixAndKeys is a struct that holds a prefix and the multihashes whose
135171
// kademlia identifier share the same prefix.
136172
type PrefixAndKeys struct {

provider/internal/keyspace/key_test.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77
"testing"
88

9+
"github.com/ipfs/go-test/random"
910
kb "github.com/libp2p/go-libp2p-kbucket"
1011
"github.com/libp2p/go-libp2p/core/peer"
1112
mh "github.com/multiformats/go-multihash"
@@ -178,9 +179,7 @@ func TestShortestCoveredPrefix(t *testing.T) {
178179
for range nIterations {
179180
minCpl := KeyLen
180181
largestCplCount := 0
181-
for i := range peers {
182-
peers[i] = genRandPeerID(t)
183-
}
182+
peers = random.Peers(nPeers)
184183
peers = kb.SortClosestPeers(peers, target[:])
185184
for i := range peers {
186185
cpl = kb.CommonPrefixLen(kb.ConvertPeerID(peers[i]), target[:])
@@ -203,6 +202,30 @@ func TestShortestCoveredPrefix(t *testing.T) {
203202
require.Empty(t, coveredPeers)
204203
}
205204

205+
func TestExtendBinaryPrefix(t *testing.T) {
206+
prefix := bitstr.Key("")
207+
l := 1
208+
require.Equal(t, []bitstr.Key{"0", "1"}, ExtendBinaryPrefix(prefix, l))
209+
prefix = bitstr.Key("1101")
210+
l = 6
211+
require.Equal(t, []bitstr.Key{"110100", "110101", "110110", "110111"}, ExtendBinaryPrefix(prefix, l))
212+
}
213+
214+
func TestSiblingPrefixes(t *testing.T) {
215+
k := bitstr.Key("")
216+
require.Empty(t, SiblingPrefixes(k))
217+
k = bitstr.Key("0")
218+
require.Equal(t, []bitstr.Key{"1"}, SiblingPrefixes(k))
219+
k = bitstr.Key("1")
220+
require.Equal(t, []bitstr.Key{"0"}, SiblingPrefixes(k))
221+
k = bitstr.Key("00")
222+
require.Equal(t, []bitstr.Key{"1", "01"}, SiblingPrefixes(k))
223+
k = bitstr.Key("000")
224+
require.Equal(t, []bitstr.Key{"1", "01", "001"}, SiblingPrefixes(k))
225+
k = bitstr.Key("1100")
226+
require.Equal(t, []bitstr.Key{"0", "10", "111", "1101"}, SiblingPrefixes(k))
227+
}
228+
206229
func genMultihashes(n int) []mh.Multihash {
207230
mhs := make([]mh.Multihash, n)
208231
for i := range mhs {

provider/internal/keyspace/trie.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package keyspace
33
import (
44
"github.com/libp2p/go-libp2p/core/peer"
55
mh "github.com/multiformats/go-multihash"
6+
67
"github.com/probe-lab/go-libdht/kad"
78
"github.com/probe-lab/go-libdht/kad/key"
89
"github.com/probe-lab/go-libdht/kad/key/bit256"
@@ -180,6 +181,47 @@ func pruneSubtrieAtDepth[K0 kad.Key[K0], K1 kad.Key[K1], D any](t *trie.Trie[K0,
180181
return false
181182
}
182183

184+
// TrieGaps returns all prefixes that aren't covered by a key (prefix) in the
185+
// trie. Combining the prefixes included in the trie with the gap prefixes
186+
// results in a full keyspace coverage.
187+
//
188+
// E.g Trie: ["00", "100"], GapsInTrie: ["01", "101", "11"]
189+
func TrieGaps[D any](t *trie.Trie[bitstr.Key, D]) []bitstr.Key {
190+
if t.IsLeaf() {
191+
if t.HasKey() {
192+
return SiblingPrefixes(*t.Key())
193+
}
194+
return []bitstr.Key{""}
195+
}
196+
return trieGapsAtDepth(t, 0)
197+
}
198+
199+
func trieGapsAtDepth[D any](t *trie.Trie[bitstr.Key, D], depth int) []bitstr.Key {
200+
var gaps []bitstr.Key
201+
for i := range 2 {
202+
bstr := bitstr.Key(byte('0' + i))
203+
if b := t.Branch(i); b == nil {
204+
gaps = append(gaps, bstr)
205+
} else if b.IsLeaf() {
206+
if b.HasKey() {
207+
k := *b.Key()
208+
if len(k) > depth+1 {
209+
for _, siblingPrefix := range SiblingPrefixes(k)[depth+1:] {
210+
gaps = append(gaps, siblingPrefix[depth:])
211+
}
212+
}
213+
} else {
214+
gaps = append(gaps, bstr)
215+
}
216+
} else {
217+
for _, gap := range trieGapsAtDepth(b, depth+1) {
218+
gaps = append(gaps, bstr+gap)
219+
}
220+
}
221+
}
222+
return gaps
223+
}
224+
183225
// mapMerge merges all key-value pairs from the source map into the destination
184226
// map. Values from the source are appended to existing slices in the
185227
// destination.

0 commit comments

Comments
 (0)