-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
k-bucket support for proper kad bootstrapping #38
Changes from all commits
146e174
ae21263
ba93cb3
b797ed1
776df76
4c093b4
7f50731
33746c1
922f2ae
f014d4e
d5e5a48
155220b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package main | ||
|
||
import ( | ||
"crypto/sha256" | ||
"encoding/binary" | ||
"fmt" | ||
"os" | ||
"strings" | ||
|
||
mh "github.com/multiformats/go-multihash" | ||
) | ||
|
||
const bits = 16 | ||
const target = 1 << bits | ||
const idLen = 32 + 2 | ||
|
||
func main() { | ||
pkg := os.Getenv("GOPACKAGE") | ||
file := os.Getenv("GOFILE") | ||
targetFile := strings.TrimSuffix(file, ".go") + "_prefixmap.go" | ||
|
||
ids := new([target]uint32) | ||
found := new([target]bool) | ||
count := int32(0) | ||
|
||
out := make([]byte, 32) | ||
inp := [idLen]byte{mh.SHA2_256, 32} | ||
hasher := sha256.New() | ||
|
||
for i := uint32(0); count < target; i++ { | ||
binary.BigEndian.PutUint32(inp[2:], i) | ||
|
||
hasher.Write(inp[:]) | ||
out = hasher.Sum(out[:0]) | ||
hasher.Reset() | ||
|
||
prefix := binary.BigEndian.Uint32(out) >> (32 - bits) | ||
if !found[prefix] { | ||
found[prefix] = true | ||
ids[prefix] = i | ||
count++ | ||
} | ||
} | ||
|
||
f, err := os.Create(targetFile) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
printf := func(s string, args ...interface{}) { | ||
_, err := fmt.Fprintf(f, s, args...) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} | ||
|
||
printf("package %s\n\n", pkg) | ||
printf("// Code generated by generate/generate_map.go DO NOT EDIT\n") | ||
printf("var keyPrefixMap = [...]uint32{") | ||
for i, j := range ids[:] { | ||
if i%16 == 0 { | ||
printf("\n\t") | ||
} else { | ||
printf(" ") | ||
} | ||
printf("%d,", j) | ||
} | ||
printf("\n}") | ||
f.Close() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,13 +2,16 @@ | |
package kbucket | ||
|
||
import ( | ||
"encoding/binary" | ||
"errors" | ||
"fmt" | ||
"math/rand" | ||
"sync" | ||
"time" | ||
|
||
"github.com/libp2p/go-libp2p-core/peer" | ||
"github.com/libp2p/go-libp2p-core/peerstore" | ||
mh "github.com/multiformats/go-multihash" | ||
|
||
logging "github.com/ipfs/go-log" | ||
) | ||
|
@@ -20,7 +23,6 @@ var ErrPeerRejectedNoCapacity = errors.New("peer rejected; insufficient capacity | |
|
||
// RoutingTable defines the routing table. | ||
type RoutingTable struct { | ||
|
||
// ID of the local peer | ||
local ID | ||
|
||
|
@@ -57,6 +59,74 @@ func NewRoutingTable(bucketsize int, localID ID, latency time.Duration, m peerst | |
return rt | ||
} | ||
|
||
// GetAllBuckets is safe to call as rt.Buckets is append-only | ||
// caller SHOULD NOT modify the returned slice | ||
func (rt *RoutingTable) GetAllBuckets() []*Bucket { | ||
rt.tabLock.RLock() | ||
defer rt.tabLock.RUnlock() | ||
return rt.Buckets | ||
} | ||
|
||
// GenRandPeerID generates a random peerID in bucket=bucketID | ||
func (rt *RoutingTable) GenRandPeerID(bucketID int) peer.ID { | ||
if bucketID < 0 { | ||
panic(fmt.Sprintf("bucketID %d is not non-negative", bucketID)) | ||
} | ||
rt.tabLock.RLock() | ||
bucketLen := len(rt.Buckets) | ||
rt.tabLock.RUnlock() | ||
|
||
var targetCpl uint | ||
if bucketID > (bucketLen - 1) { | ||
targetCpl = uint(bucketLen) - 1 | ||
} else { | ||
targetCpl = uint(bucketID) | ||
} | ||
|
||
// We can only handle upto 16 bit prefixes | ||
if targetCpl > 16 { | ||
targetCpl = 16 | ||
} | ||
|
||
var targetPrefix uint16 | ||
localPrefix := binary.BigEndian.Uint16(rt.local) | ||
if targetCpl < 16 { | ||
// For host with ID `L`, an ID `K` belongs to a bucket with ID `B` ONLY IF CommonPrefixLen(L,K) is EXACTLY B. | ||
// Hence, to achieve a targetPrefix `T`, we must toggle the (T+1)th bit in L & then copy (T+1) bits from L | ||
// to our randomly generated prefix. | ||
toggledLocalPrefix := localPrefix ^ (uint16(0x8000) >> targetCpl) | ||
randPrefix := uint16(rand.Uint32()) | ||
|
||
// Combine the toggled local prefix and the random bits at the correct offset | ||
// such that ONLY the first `targetCpl` bits match the local ID. | ||
mask := (^uint16(0)) << (16 - (targetCpl + 1)) | ||
targetPrefix = (toggledLocalPrefix & mask) | (randPrefix & ^mask) | ||
} else { | ||
targetPrefix = localPrefix | ||
} | ||
|
||
// Convert to a known peer ID. | ||
key := keyPrefixMap[targetPrefix] | ||
id := [34]byte{mh.SHA2_256, 32} | ||
binary.BigEndian.PutUint32(id[2:], key) | ||
return peer.ID(id[:]) | ||
} | ||
|
||
// Returns the bucket for a given ID | ||
// should NOT modify the peer list on the returned bucket | ||
func (rt *RoutingTable) BucketForID(id ID) *Bucket { | ||
cpl := CommonPrefixLen(id, rt.local) | ||
|
||
rt.tabLock.RLock() | ||
defer rt.tabLock.RUnlock() | ||
bucketID := cpl | ||
if bucketID >= len(rt.Buckets) { | ||
bucketID = len(rt.Buckets) - 1 | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm. This is very unfortunate. Given our network size, we should have at least 16 buckets). That's 2**16 hashes to find a peer in that last bucket. I don't know how to fix this on our current DHT. Given how we've defined the protocol, we have to know the un-hashed key. Honestly, I think the best solution is to pre-compute a table with a few known-good bootstrap keys in each of the first 20 buckets. If you can write a program to do this (ideally highly parallel), I can run it on a really beefy machine for a few days. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm probably missing context here, but a couple comments:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @aschmahmann this is for libp2p/go-libp2p-kad-dht#375 (I've updated the PR description).
Due to how our DHT works, the we need to send the unhashed key with the request. That means we need to know the unhashed key. Kademlia as described by the paper assumes that all keys will be pre-hashed. If we wanted to do that, we'd have to pre-hash the keys when we store records in the datastore (e.g., peer routing information). We could modify the DHT to send both the unhashed and hash keys but that seems like a lot of extra work just to support bootstrapping. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Stebalien thx for the PR pointer. Fair enough about not wanting to do much changing to the dht message types if we don't have to. However, I'm not sure the solution of pre-generating keys helps. We can pre-generate keys for a single peer, but generating them for all peers is going to need require packaging a ton of keys (because the bucket distance is computed based on your local ID). In any event it still doesn't seem like we need the hash function inside of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, yeah, now I feel like an idiot. This isn't going to work (we'd need to ship ~1m peer IDs).
The issue is that we need to make a DHT request with this key. To make the DHT request, we need to know the non-hashed key. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Turns out this didn't need a beefy machine and we really don't need 1m peer IDs. I've created a branch with a table mapping 20 bit hash prefixes to numbers that can be used to generate peer IDs that hash to these 20 bit hash prefixes: https://github.com/libp2p/go-libp2p-kbucket/tree/feat/bucket-prefix-gen More concretely, to find a peer ID in a bucket N, we'll have to:
When this peer ID is hashed, the first N bits should match the result of step (2). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The downside is that this increases our binary size by 4MiB. But I'm not that concerned. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've reduced it to 16 bits (16 buckets) to reduce the size to 256KiB. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Stebalien This was a really cool idea ! Have rebased on top of your branch & made all the changes. Let me know what you think. PS: Made some minor changes to generate/main.go(line 31 & 58) & generated a new prefix-map so we can use it exactly as you've mentioned in the algorithm above. Basically, instead of crafting the multi-hash like this :
We craft it as:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, apologies for the delay in getting this up. Things were a bit hectic at work. |
||
|
||
return rt.Buckets[bucketID] | ||
} | ||
|
||
// Update adds or moves the given peer to the front of its respective bucket | ||
func (rt *RoutingTable) Update(p peer.ID) (evicted peer.ID, err error) { | ||
peerID := ConvertPeerID(p) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,6 +48,52 @@ func TestBucket(t *testing.T) { | |
} | ||
} | ||
|
||
func TestGenRandPeerID(t *testing.T) { | ||
nBuckets := 21 | ||
local := test.RandPeerIDFatal(t) | ||
m := pstore.NewMetrics() | ||
rt := NewRoutingTable(1, ConvertPeerID(local), time.Hour, m) | ||
|
||
// create nBuckets | ||
for i := 0; i < nBuckets; i++ { | ||
for { | ||
if p := test.RandPeerIDFatal(t); CommonPrefixLen(ConvertPeerID(local), ConvertPeerID(p)) == i { | ||
rt.Update(p) | ||
break | ||
} | ||
} | ||
} | ||
|
||
// test bucket for peer | ||
peers := rt.ListPeers() | ||
for _, p := range peers { | ||
b := rt.BucketForID(ConvertPeerID(p)) | ||
if !b.Has(p) { | ||
t.Fatalf("bucket should have peers %s", p.String()) | ||
} | ||
} | ||
|
||
// test generate rand peer ID | ||
for bucketID := 0; bucketID < nBuckets; bucketID++ { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's test this with with more buckets (40?) to check for overflow issues. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Stebalien Generating 40 buckets takes a lot of time as a new bucket is created ONLY IF the last bucket is full. This slows down the tests. So testing with 21 for now as it's still an overflow & hits all the code-paths. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It shouldn't be slow... We shouldn't have to generate 40 buckets, just ask for peer IDs in buckets 1-40 (where everything after 16 would just be random IDs. But this should be good enough. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I think I misunderstood your initial comment. So you meant simply passing in a bucketID of upto 40 to the GenRandPeerID method & not actually having 40 buckets. That makes more sense. Apologies. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. But really, anything over 16 is plenty. |
||
peerID := rt.GenRandPeerID(bucketID) | ||
|
||
// for bucketID upto maxPrefixLen of 16, CPL should be Exactly bucketID | ||
if bucketID < 16 { | ||
if CommonPrefixLen(ConvertPeerID(peerID), rt.local) != bucketID { | ||
t.Fatalf("cpl should be %d for bucket %d but got %d, generated peerID is %s", bucketID, bucketID, | ||
CommonPrefixLen(ConvertPeerID(peerID), rt.local), peerID) | ||
} | ||
} else { | ||
// from bucketID 16 onwards, CPL should be ATLEAST 16 | ||
if CommonPrefixLen(ConvertPeerID(peerID), rt.local) < 16 { | ||
t.Fatalf("cpl should be ATLEAST 16 for bucket %d but got %d, generated peerID is %s", bucketID, | ||
CommonPrefixLen(ConvertPeerID(peerID), rt.local), peerID) | ||
} | ||
} | ||
|
||
} | ||
} | ||
|
||
func TestTableCallbacks(t *testing.T) { | ||
local := test.RandPeerIDFatal(t) | ||
m := pstore.NewMetrics() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a comment explaining why this is safe (it's append-only).