diff --git a/p2p/discover/node.go b/p2p/discover/node.go index c6d2e97661..7d00b1b90a 100644 --- a/p2p/discover/node.go +++ b/p2p/discover/node.go @@ -15,11 +15,22 @@ import ( "time" "github.com/ethereum/go-ethereum/crypto/secp256k1" + "github.com/ethereum/go-ethereum/crypto/sha3" "github.com/ethereum/go-ethereum/rlp" ) const nodeIDBits = 512 +// nodeIDHash is the hash of a NodeID +type NodeIDHash []byte + +// hashNodeID generates a nodeIDHash from a NodeID by hashing it using sha3-256 +func hashNodeID(id NodeID) NodeIDHash { + h := sha3.NewKeccak256() + h.Write(id[:]) + return NodeIDHash(h.Sum(nil)) +} + // Node represents a host on the network. type Node struct { ID NodeID @@ -29,6 +40,7 @@ type Node struct { TCPPort int // TCP listening port for RLPx active time.Time + IDHash []byte } func newNode(id NodeID, addr *net.UDPAddr) *Node { @@ -38,6 +50,7 @@ func newNode(id NodeID, addr *net.UDPAddr) *Node { DiscPort: addr.Port, TCPPort: addr.Port, active: time.Now(), + IDHash: hashNodeID(id), } } @@ -206,7 +219,7 @@ func recoverNodeID(hash, sig []byte) (id NodeID, err error) { // distcmp compares the distances a->target and b->target. // Returns -1 if a is closer to target, 1 if b is closer to target // and 0 if they are equal. -func distcmp(target, a, b NodeID) int { +func distcmp(target, a, b NodeIDHash) int { for i := range target { da := a[i] ^ target[i] db := b[i] ^ target[i] @@ -256,14 +269,14 @@ var lzcount = [256]int{ } // logdist returns the logarithmic distance between a and b, log2(a ^ b). -func logdist(a, b NodeID) int { +func logdist(a, b NodeIDHash) int { + lz := 0 for i := range a { x := a[i] ^ b[i] - if x == 0 { - lz += 8 - } else { - lz += lzcount[x] + lz += lzcount[x] + + if x != 0 { break } } diff --git a/p2p/discover/node_test.go b/p2p/discover/node_test.go index ae82ae4f11..53afb930a0 100644 --- a/p2p/discover/node_test.go +++ b/p2p/discover/node_test.go @@ -151,13 +151,17 @@ func TestNodeID_distcmp(t *testing.T) { func TestNodeID_distcmpEqual(t *testing.T) { base := NodeID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} x := NodeID{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0} - if distcmp(base, x, x) != 0 { - t.Errorf("distcmp(base, x, x) != 0") + + baseHash := hashNodeID(base) + xHash := hashNodeID(x) + + if distcmp(baseHash, xHash, xHash) != 0 { + t.Errorf("distcmp(baseHash, xHash, xHash) != 0") } } func TestNodeID_logdist(t *testing.T) { - logdistBig := func(a, b NodeID) int { + logdistBig := func(a, b NodeIDHash) int { abig, bbig := new(big.Int).SetBytes(a[:]), new(big.Int).SetBytes(b[:]) return new(big.Int).Xor(abig, bbig).BitLen() } @@ -169,8 +173,9 @@ func TestNodeID_logdist(t *testing.T) { // the random tests is likely to miss the case where they're equal. func TestNodeID_logdistEqual(t *testing.T) { x := NodeID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} - if logdist(x, x) != 0 { - t.Errorf("logdist(x, x) != 0") + xHash := hashNodeID(x) + if logdist(xHash, xHash) != 0 { + t.Errorf("logdist(xHash, xHash) != 0") } } @@ -181,7 +186,11 @@ func TestNodeID_randomID(t *testing.T) { a := gen(NodeID{}, quickrand).(NodeID) dist := quickrand.Intn(len(NodeID{}) * 8) result := randomID(a, dist) - actualdist := logdist(result, a) + + resultHash := hashNodeID(result) + aHash := hashNodeID(a) + + actualdist := logdist(resultHash, aHash) if dist != actualdist { t.Log("a: ", a) diff --git a/p2p/discover/table.go b/p2p/discover/table.go index e3bec93287..2e4b3de7f7 100644 --- a/p2p/discover/table.go +++ b/p2p/discover/table.go @@ -86,7 +86,9 @@ func (tab *Table) Lookup(target NodeID) []*Node { seen = make(map[NodeID]bool) reply = make(chan []*Node, alpha) pendingQueries = 0 + targetHash = hashNodeID(target) ) + // don't query further if we hit the target or ourself. // unlikely to happen often in practice. asked[target] = true @@ -94,7 +96,7 @@ func (tab *Table) Lookup(target NodeID) []*Node { tab.mutex.Lock() // update last lookup stamp (for refresh logic) - tab.buckets[logdist(tab.self.ID, target)].lastLookup = time.Now() + tab.buckets[logdist(tab.self.IDHash, targetHash)].lastLookup = time.Now() // generate initial result set result := tab.closest(target, bucketSize) tab.mutex.Unlock() @@ -160,7 +162,7 @@ func (tab *Table) closest(target NodeID, nresults int) *nodesByDistance { // This is a very wasteful way to find the closest nodes but // obviously correct. I believe that tree-based buckets would make // this easier to implement efficiently. - close := &nodesByDistance{target: target} + close := &nodesByDistance{target: hashNodeID(target)} for _, b := range tab.buckets { for _, n := range b.entries { close.push(n, nresults) @@ -180,7 +182,7 @@ func (tab *Table) len() (n int) { // attempts to insert the node into a bucket. The returned Node might // not be part of the table. The caller must hold tab.mutex. func (tab *Table) bumpOrAdd(node NodeID, from *net.UDPAddr) (n *Node) { - b := tab.buckets[logdist(tab.self.ID, node)] + b := tab.buckets[logdist(tab.self.IDHash, hashNodeID(node))] if n = b.bump(node); n == nil { n = newNode(node, from) if len(b.entries) == bucketSize { @@ -214,7 +216,7 @@ func (tab *Table) pingReplace(n *Node, b *bucket) { // bump updates the activity timestamp for the given node. // The caller must hold tab.mutex. func (tab *Table) bump(node NodeID) { - tab.buckets[logdist(tab.self.ID, node)].bump(node) + tab.buckets[logdist(tab.self.IDHash, hashNodeID(node))].bump(node) } // add puts the entries into the table if their corresponding @@ -227,7 +229,7 @@ outer: // input lists. continue } - bucket := tab.buckets[logdist(tab.self.ID, n.ID)] + bucket := tab.buckets[logdist(tab.self.IDHash, n.IDHash)] for i := range bucket.entries { if bucket.entries[i].ID == n.ID { // already in bucket @@ -257,13 +259,13 @@ func (b *bucket) bump(id NodeID) *Node { // distance to target. type nodesByDistance struct { entries []*Node - target NodeID + target NodeIDHash } // push adds the given node to the list, keeping the total size below maxElems. func (h *nodesByDistance) push(n *Node, maxElems int) { ix := sort.Search(len(h.entries), func(i int) bool { - return distcmp(h.target, h.entries[i].ID, n.ID) > 0 + return distcmp(h.target, h.entries[i].IDHash, n.IDHash) > 0 }) if len(h.entries) < maxElems { h.entries = append(h.entries, n) diff --git a/p2p/discover/table_test.go b/p2p/discover/table_test.go index 08faea68e9..01fa79b0e8 100644 --- a/p2p/discover/table_test.go +++ b/p2p/discover/table_test.go @@ -140,7 +140,7 @@ func TestTable_closest(t *testing.T) { t.Errorf("result contains duplicates") return false } - if !sortedByDistanceTo(test.Target, result) { + if !sortedByDistanceTo(test.TargetHash, result) { t.Errorf("result is not sorted by distance to target") return false } @@ -163,8 +163,8 @@ func TestTable_closest(t *testing.T) { if contains(result, n.ID) { continue // don't run the check below for nodes in result } - farthestResult := result[len(result)-1].ID - if distcmp(test.Target, n.ID, farthestResult) < 0 { + farthestResult := result[len(result)-1].IDHash + if distcmp(test.TargetHash, n.IDHash, farthestResult) < 0 { t.Errorf("table contains node that is closer to target but it's not in result") t.Logf(" Target: %v", test.Target) t.Logf(" Farthest Result: %v", farthestResult) @@ -183,8 +183,12 @@ func TestTable_closest(t *testing.T) { type closeTest struct { Self NodeID Target NodeID - All []*Node - N int + + SelfHash NodeIDHash + TargetHash NodeIDHash + + All []*Node + N int } func (*closeTest) Generate(rand *rand.Rand, size int) reflect.Value { @@ -193,8 +197,12 @@ func (*closeTest) Generate(rand *rand.Rand, size int) reflect.Value { Target: gen(NodeID{}, rand).(NodeID), N: rand.Intn(bucketSize), } + + t.SelfHash = hashNodeID(t.Self) + t.TargetHash = hashNodeID(t.Target) + for _, id := range gen([]NodeID{}, rand).([]NodeID) { - t.All = append(t.All, &Node{ID: id}) + t.All = append(t.All, &Node{ID: id, IDHash: hashNodeID(id)}) } return reflect.ValueOf(t) } @@ -202,6 +210,7 @@ func (*closeTest) Generate(rand *rand.Rand, size int) reflect.Value { func TestTable_Lookup(t *testing.T) { self := gen(NodeID{}, quickrand).(NodeID) target := randomID(self, 200) + targetHash := hashNodeID(target) transport := findnodeOracle{t, target} tab := newTable(transport, self, &net.UDPAddr{}) @@ -215,7 +224,7 @@ func TestTable_Lookup(t *testing.T) { results := tab.Lookup(target) t.Logf("results:") for _, e := range results { - t.Logf(" ld=%d, %v", logdist(target, e.ID), e.ID) + t.Logf(" ld=%d, %v", logdist(targetHash, e.IDHash), e.ID) } if len(results) != bucketSize { t.Errorf("wrong number of results: got %d, want %d", len(results), bucketSize) @@ -223,7 +232,7 @@ func TestTable_Lookup(t *testing.T) { if hasDuplicates(results) { t.Errorf("result set contains duplicate entries") } - if !sortedByDistanceTo(target, results) { + if !sortedByDistanceTo(targetHash, results) { t.Errorf("result set not sorted by distance to target") } if !contains(results, target) { @@ -272,13 +281,13 @@ func hasDuplicates(slice []*Node) bool { return false } -func sortedByDistanceTo(distbase NodeID, slice []*Node) bool { - var last NodeID +func sortedByDistanceTo(distbase NodeIDHash, slice []*Node) bool { + var last NodeIDHash for i, e := range slice { - if i > 0 && distcmp(distbase, e.ID, last) < 0 { + if i > 0 && distcmp(distbase, e.IDHash, last) < 0 { return false } - last = e.ID + last = e.IDHash } return true } diff --git a/p2p/discover/udp_test.go b/p2p/discover/udp_test.go index 0a8ff63589..85a0922d94 100644 --- a/p2p/discover/udp_test.go +++ b/p2p/discover/udp_test.go @@ -57,7 +57,7 @@ func TestUDP_findnode(t *testing.T) { // matter much, altough we need to take care not to overflow // any bucket. target := randomID(n1.self.ID, 100) - nodes := &nodesByDistance{target: target} + nodes := &nodesByDistance{target: hashNodeID(target)} for i := 0; i < bucketSize; i++ { n2.add([]*Node{&Node{ IP: net.IP{1, 2, 3, byte(i)},