p2p/discover: WIP node refactoring

This commit is contained in:
Felix Lange 2024-05-25 18:47:32 +02:00
parent cc22e0cdf0
commit b168d82446
10 changed files with 182 additions and 177 deletions

View file

@ -29,16 +29,16 @@ import (
// not need to be an actual node identifier.
type lookup struct {
tab *Table
queryfunc func(*node) ([]*node, error)
replyCh chan []*node
queryfunc queryFunc
replyCh chan []*enode.Node
cancelCh <-chan struct{}
asked, seen map[enode.ID]bool
result nodesByDistance
replyBuffer []*node
replyBuffer []*enode.Node
queries int
}
type queryFunc func(*node) ([]*node, error)
type queryFunc func(*enode.Node) ([]*enode.Node, error)
func newLookup(ctx context.Context, tab *Table, target enode.ID, q queryFunc) *lookup {
it := &lookup{
@ -47,7 +47,7 @@ func newLookup(ctx context.Context, tab *Table, target enode.ID, q queryFunc) *l
asked: make(map[enode.ID]bool),
seen: make(map[enode.ID]bool),
result: nodesByDistance{target: target},
replyCh: make(chan []*node, alpha),
replyCh: make(chan []*enode.Node, alpha),
cancelCh: ctx.Done(),
queries: -1,
}
@ -61,7 +61,7 @@ func newLookup(ctx context.Context, tab *Table, target enode.ID, q queryFunc) *l
func (it *lookup) run() []*enode.Node {
for it.advance() {
}
return unwrapNodes(it.result.entries)
return it.result.entries
}
// advance advances the lookup until any new nodes have been found.
@ -139,7 +139,7 @@ func (it *lookup) slowdown() {
}
}
func (it *lookup) query(n *node, reply chan<- []*node) {
func (it *lookup) query(n *enode.Node, reply chan<- []*enode.Node) {
r, err := it.queryfunc(n)
if !errors.Is(err, errClosed) { // avoid recording failures on shutdown.
success := len(r) > 0
@ -154,7 +154,7 @@ func (it *lookup) query(n *node, reply chan<- []*node) {
// lookupIterator performs lookup operations and iterates over all seen nodes.
// When a lookup finishes, a new one is created through nextLookup.
type lookupIterator struct {
buffer []*node
buffer []*enode.Node
nextLookup lookupFunc
ctx context.Context
cancel func()
@ -173,7 +173,7 @@ func (it *lookupIterator) Node() *enode.Node {
if len(it.buffer) == 0 {
return nil
}
return unwrapNode(it.buffer[0])
return it.buffer[0]
}
// Next moves to the next node.

View file

@ -37,8 +37,7 @@ type BucketNode struct {
Live bool `json:"live"`
}
// node represents a host on the network.
// The fields of Node may not be modified.
// node is a node table entry.
type node struct {
*enode.Node
revalList *revalidationList
@ -75,26 +74,10 @@ func (e encPubkey) id() enode.ID {
return enode.ID(crypto.Keccak256Hash(e[:]))
}
func wrapNode(n *enode.Node) *node {
return &node{Node: n}
}
func wrapNodes(ns []*enode.Node) []*node {
result := make([]*node, len(ns))
for i, n := range ns {
result[i] = wrapNode(n)
}
return result
}
func unwrapNode(n *node) *enode.Node {
return n.Node
}
func unwrapNodes(ns []*node) []*enode.Node {
result := make([]*enode.Node, len(ns))
for i, n := range ns {
result[i] = unwrapNode(n)
result[i] = n.Node
}
return result
}

View file

@ -65,7 +65,7 @@ const (
type Table struct {
mutex sync.Mutex // protects buckets, bucket content, nursery, rand
buckets [nBuckets]*bucket // index of known nodes by distance
nursery []*node // bootstrap nodes
nursery []*enode.Node // bootstrap nodes
rand reseedingRandom // source of randomness, periodically reseeded
ips netutil.DistinctNetSet
revalidation tableRevalidation
@ -108,13 +108,14 @@ type bucket struct {
}
type addNodeOp struct {
node *node
isInbound bool
node *enode.Node
isInbound bool
forceSetLive bool // for tests
}
type trackRequestOp struct {
node *node
foundNodes []*node
node *enode.Node
foundNodes []*enode.Node
success bool
}
@ -186,7 +187,7 @@ func (tab *Table) getNode(id enode.ID) *enode.Node {
b := tab.bucket(id)
for _, e := range b.entries {
if e.ID() == id {
return unwrapNode(e)
return e.Node
}
}
return nil
@ -202,7 +203,7 @@ func (tab *Table) close() {
// are used to connect to the network if the table is empty and there
// are no known nodes in the database.
func (tab *Table) setFallbackNodes(nodes []*enode.Node) error {
nursery := make([]*node, 0, len(nodes))
nursery := make([]*enode.Node, 0, len(nodes))
for _, n := range nodes {
if err := n.ValidateComplete(); err != nil {
return fmt.Errorf("bad bootstrap node %q: %v", n, err)
@ -211,7 +212,7 @@ func (tab *Table) setFallbackNodes(nodes []*enode.Node) error {
tab.log.Error("Bootstrap node filtered by netrestrict", "id", n.ID(), "ip", n.IP())
continue
}
nursery = append(nursery, wrapNode(n))
nursery = append(nursery, n)
}
tab.nursery = nursery
return nil
@ -255,9 +256,9 @@ func (tab *Table) findnodeByID(target enode.ID, nresults int, preferLive bool) *
liveNodes := &nodesByDistance{target: target}
for _, b := range &tab.buckets {
for _, n := range b.entries {
nodes.push(n, nresults)
nodes.push(n.Node, nresults)
if preferLive && n.isValidatedLive {
liveNodes.push(n, nresults)
liveNodes.push(n.Node, nresults)
}
}
}
@ -309,8 +310,8 @@ func (tab *Table) len() (n int) {
// list.
//
// The caller must not hold tab.mutex.
func (tab *Table) addFoundNode(n *node) bool {
op := addNodeOp{node: n, isInbound: false}
func (tab *Table) addFoundNode(n *enode.Node, forceSetLive bool) bool {
op := addNodeOp{node: n, isInbound: false, forceSetLive: forceSetLive}
select {
case tab.addNodeCh <- op:
return <-tab.addNodeHandled
@ -327,7 +328,7 @@ func (tab *Table) addFoundNode(n *node) bool {
// repeatedly.
//
// The caller must not hold tab.mutex.
func (tab *Table) addInboundNode(n *node) bool {
func (tab *Table) addInboundNode(n *enode.Node) bool {
op := addNodeOp{node: n, isInbound: true}
select {
case tab.addNodeCh <- op:
@ -337,7 +338,7 @@ func (tab *Table) addInboundNode(n *node) bool {
}
}
func (tab *Table) trackRequest(n *node, success bool, foundNodes []*node) {
func (tab *Table) trackRequest(n *enode.Node, success bool, foundNodes []*enode.Node) {
op := trackRequestOp{n, foundNodes, success}
select {
case tab.trackRequestCh <- op:
@ -443,13 +444,14 @@ func (tab *Table) doRefresh(done chan struct{}) {
}
func (tab *Table) loadSeedNodes() {
seeds := wrapNodes(tab.db.QuerySeeds(seedCount, seedMaxAge))
seeds := tab.db.QuerySeeds(seedCount, seedMaxAge)
seeds = append(seeds, tab.nursery...)
for i := range seeds {
seed := seeds[i]
if tab.log.Enabled(context.Background(), log.LevelTrace) {
age := time.Since(tab.db.LastPongReceived(seed.ID(), seed.IP()))
tab.log.Trace("Found seed node in database", "id", seed.ID(), "addr", seed.addr(), "age", age)
addr, _ := seed.UDPEndpoint()
tab.log.Trace("Found seed node in database", "id", seed.ID(), "addr", addr, "age", age)
}
tab.handleAddNode(addNodeOp{node: seed, isInbound: false})
}
@ -513,7 +515,7 @@ func (tab *Table) handleAddNode(req addNodeOp) bool {
}
b := tab.bucket(req.node.ID())
n, _ := tab.bumpInBucket(b, req.node.Node, req.isInbound)
n, _ := tab.bumpInBucket(b, req.node, req.isInbound)
if n != nil {
// Already in bucket.
return false
@ -529,15 +531,20 @@ func (tab *Table) handleAddNode(req addNodeOp) bool {
}
// Add to bucket.
b.entries = append(b.entries, req.node)
b.replacements = deleteNode(b.replacements, req.node)
tab.nodeAdded(b, req.node)
wn := &node{Node: req.node}
if req.forceSetLive {
wn.livenessChecks = 1
wn.isValidatedLive = true
}
b.entries = append(b.entries, wn)
b.replacements = deleteNode(b.replacements, wn.ID())
tab.nodeAdded(b, wn)
return true
}
// addReplacement adds n to the replacement cache of bucket b.
func (tab *Table) addReplacement(b *bucket, n *node) {
if contains(b.replacements, n.ID()) {
func (tab *Table) addReplacement(b *bucket, n *enode.Node) {
if containsID(b.replacements, n.ID()) {
// TODO: update ENR
return
}
@ -545,9 +552,9 @@ func (tab *Table) addReplacement(b *bucket, n *node) {
return
}
n.addedToTable = time.Now()
wn := &node{Node: n, addedToTable: time.Now()}
var removed *node
b.replacements, removed = pushNode(b.replacements, n, maxReplacements)
b.replacements, removed = pushNode(b.replacements, wn, maxReplacements)
if removed != nil {
tab.removeIP(b, removed.IP())
}
@ -672,11 +679,15 @@ func (tab *Table) handleTrackRequest(op trackRequestOp) {
// Add found nodes.
for _, n := range op.foundNodes {
tab.handleAddNode(addNodeOp{n, false})
tab.handleAddNode(addNodeOp{n, false, false})
}
}
func contains(ns []*node, id enode.ID) bool {
type nodeType interface {
ID() enode.ID
}
func containsID[N nodeType](ns []N, id enode.ID) bool {
for _, n := range ns {
if n.ID() == id {
return true
@ -697,23 +708,20 @@ func pushNode(list []*node, n *node, max int) ([]*node, *node) {
}
// deleteNode removes n from list.
func deleteNode(list []*node, n *node) []*node {
for i := range list {
if list[i].ID() == n.ID() {
return append(list[:i], list[i+1:]...)
}
}
return list
func deleteNode[N nodeType](list []N, id enode.ID) []N {
return slices.DeleteFunc(list, func(n N) bool {
return n.ID() == id
})
}
// nodesByDistance is a list of nodes, ordered by distance to target.
type nodesByDistance struct {
entries []*node
entries []*enode.Node
target enode.ID
}
// push adds the given node to the list, keeping the total size below maxElems.
func (h *nodesByDistance) push(n *node, maxElems int) {
func (h *nodesByDistance) push(n *enode.Node, maxElems int) {
ix := sort.Search(len(h.entries), func(i int) bool {
return enode.DistCmp(h.target, h.entries[i].ID(), n.ID()) > 0
})

View file

@ -22,6 +22,7 @@ import (
"math/rand"
"net"
"reflect"
"slices"
"testing"
"testing/quick"
"time"
@ -64,7 +65,7 @@ func testPingReplace(t *testing.T, newNodeIsResponding, lastInBucketIsResponding
// Fill up the sender's bucket.
replacementNodeKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
replacementNode := wrapNode(enode.NewV4(&replacementNodeKey.PublicKey, net.IP{127, 0, 0, 1}, 99, 99))
replacementNode := enode.NewV4(&replacementNodeKey.PublicKey, net.IP{127, 0, 0, 1}, 99, 99)
last := fillBucket(tab, replacementNode.ID())
tab.mutex.Lock()
nodeEvents := newNodeEventRecorder(128)
@ -78,7 +79,7 @@ func testPingReplace(t *testing.T, newNodeIsResponding, lastInBucketIsResponding
transport.dead[replacementNode.ID()] = !newNodeIsResponding
// Add replacement node to table.
tab.addFoundNode(replacementNode)
tab.addFoundNode(replacementNode, false)
t.Log("last:", last.ID())
t.Log("replacement:", replacementNode.ID())
@ -115,11 +116,11 @@ func testPingReplace(t *testing.T, newNodeIsResponding, lastInBucketIsResponding
if l := len(bucket.entries); l != wantSize {
t.Errorf("wrong bucket size after revalidation: got %d, want %d", l, wantSize)
}
if ok := contains(bucket.entries, last.ID()); ok != lastInBucketIsResponding {
if ok := containsID(bucket.entries, last.ID()); ok != lastInBucketIsResponding {
t.Errorf("revalidated node found: %t, want: %t", ok, lastInBucketIsResponding)
}
wantNewEntry := newNodeIsResponding && !lastInBucketIsResponding
if ok := contains(bucket.entries, replacementNode.ID()); ok != wantNewEntry {
if ok := containsID(bucket.entries, replacementNode.ID()); ok != wantNewEntry {
t.Errorf("replacement node found: %t, want: %t", ok, wantNewEntry)
}
}
@ -153,7 +154,7 @@ func TestTable_IPLimit(t *testing.T) {
for i := 0; i < tableIPLimit+1; i++ {
n := nodeAtDistance(tab.self().ID(), i, net.IP{172, 0, 1, byte(i)})
tab.addFoundNode(n)
tab.addFoundNode(n, false)
}
if tab.len() > tableIPLimit {
t.Errorf("too many nodes in table")
@ -171,7 +172,7 @@ func TestTable_BucketIPLimit(t *testing.T) {
d := 3
for i := 0; i < bucketIPLimit+1; i++ {
n := nodeAtDistance(tab.self().ID(), d, net.IP{172, 0, 1, byte(i)})
tab.addFoundNode(n)
tab.addFoundNode(n, false)
}
if tab.len() > bucketIPLimit {
t.Errorf("too many nodes in table")
@ -232,7 +233,7 @@ func TestTable_findnodeByID(t *testing.T) {
// check that the result nodes have minimum distance to target.
for _, b := range tab.buckets {
for _, n := range b.entries {
if contains(result, n.ID()) {
if containsID(result, n.ID()) {
continue // don't run the check below for nodes in result
}
farthestResult := result[len(result)-1].ID()
@ -255,7 +256,7 @@ func TestTable_findnodeByID(t *testing.T) {
type closeTest struct {
Self enode.ID
Target enode.ID
All []*node
All []*enode.Node
N int
}
@ -268,8 +269,7 @@ func (*closeTest) Generate(rand *rand.Rand, size int) reflect.Value {
for _, id := range gen([]enode.ID{}, rand).([]enode.ID) {
r := new(enr.Record)
r.Set(enr.IP(genIP(rand)))
n := wrapNode(enode.SignNull(r, id))
n.livenessChecks = 1
n := enode.SignNull(r, id)
t.All = append(t.All, n)
}
return reflect.ValueOf(t)
@ -284,16 +284,16 @@ func TestTable_addInboundNode(t *testing.T) {
// Insert two nodes.
n1 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 1})
n2 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 2})
tab.addFoundNode(n1)
tab.addFoundNode(n2)
checkBucketContent(t, tab, []*enode.Node{n1.Node, n2.Node})
tab.addFoundNode(n1, false)
tab.addFoundNode(n2, false)
checkBucketContent(t, tab, []*enode.Node{n1, n2})
// Add a changed version of n2. The bucket should be updated.
newrec := n2.Record()
newrec.Set(enr.IP{99, 99, 99, 99})
n2v2 := enode.SignNull(newrec, n2.ID())
tab.addInboundNode(wrapNode(n2v2))
checkBucketContent(t, tab, []*enode.Node{n1.Node, n2v2})
tab.addInboundNode(n2v2)
checkBucketContent(t, tab, []*enode.Node{n1, n2v2})
// Try updating n2 without sequence number change. The update is accepted
// because it's inbound.
@ -301,8 +301,8 @@ func TestTable_addInboundNode(t *testing.T) {
newrec.Set(enr.IP{100, 100, 100, 100})
newrec.SetSeq(n2.Seq())
n2v3 := enode.SignNull(newrec, n2.ID())
tab.addInboundNode(wrapNode(n2v3))
checkBucketContent(t, tab, []*enode.Node{n1.Node, n2v3})
tab.addInboundNode(n2v3)
checkBucketContent(t, tab, []*enode.Node{n1, n2v3})
}
func TestTable_addFoundNode(t *testing.T) {
@ -314,16 +314,16 @@ func TestTable_addFoundNode(t *testing.T) {
// Insert two nodes.
n1 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 1})
n2 := nodeAtDistance(tab.self().ID(), 256, net.IP{88, 77, 66, 2})
tab.addFoundNode(n1)
tab.addFoundNode(n2)
checkBucketContent(t, tab, []*enode.Node{n1.Node, n2.Node})
tab.addFoundNode(n1, false)
tab.addFoundNode(n2, false)
checkBucketContent(t, tab, []*enode.Node{n1, n2})
// Add a changed version of n2. The bucket should be updated.
newrec := n2.Record()
newrec.Set(enr.IP{99, 99, 99, 99})
n2v2 := enode.SignNull(newrec, n2.ID())
tab.addFoundNode(wrapNode(n2v2))
checkBucketContent(t, tab, []*enode.Node{n1.Node, n2v2})
tab.addFoundNode(n2v2, false)
checkBucketContent(t, tab, []*enode.Node{n1, n2v2})
// Try updating n2 without a sequence number change.
// The update should not be accepted.
@ -331,8 +331,8 @@ func TestTable_addFoundNode(t *testing.T) {
newrec.Set(enr.IP{100, 100, 100, 100})
newrec.SetSeq(n2.Seq())
n2v3 := enode.SignNull(newrec, n2.ID())
tab.addFoundNode(wrapNode(n2v3))
checkBucketContent(t, tab, []*enode.Node{n1.Node, n2v2})
tab.addFoundNode(n2v3, false)
checkBucketContent(t, tab, []*enode.Node{n1, n2v2})
}
// This test checks that discv4 nodes can update their own endpoint via PING.
@ -345,13 +345,13 @@ func TestTable_addInboundNodeUpdateV4Accept(t *testing.T) {
// Add a v4 node.
key, _ := crypto.HexToECDSA("dd3757a8075e88d0f2b1431e7d3c5b1562e1c0aab9643707e8cbfcc8dae5cfe3")
n1 := enode.NewV4(&key.PublicKey, net.IP{88, 77, 66, 1}, 9000, 9000)
tab.addInboundNode(wrapNode(n1))
tab.addInboundNode(n1)
checkBucketContent(t, tab, []*enode.Node{n1})
// Add an updated version with changed IP.
// The update will be accepted because it is inbound.
n1v2 := enode.NewV4(&key.PublicKey, net.IP{99, 99, 99, 99}, 9000, 9000)
tab.addInboundNode(wrapNode(n1v2))
tab.addInboundNode(n1v2)
checkBucketContent(t, tab, []*enode.Node{n1v2})
}
@ -366,13 +366,13 @@ func TestTable_addFoundNodeV4UpdateReject(t *testing.T) {
// Add a v4 node.
key, _ := crypto.HexToECDSA("dd3757a8075e88d0f2b1431e7d3c5b1562e1c0aab9643707e8cbfcc8dae5cfe3")
n1 := enode.NewV4(&key.PublicKey, net.IP{88, 77, 66, 1}, 9000, 9000)
tab.addFoundNode(wrapNode(n1))
tab.addFoundNode(n1, false)
checkBucketContent(t, tab, []*enode.Node{n1})
// Add an updated version with changed IP.
// The update won't be accepted because it isn't inbound.
n1v2 := enode.NewV4(&key.PublicKey, net.IP{99, 99, 99, 99}, 9000, 9000)
tab.addFoundNode(wrapNode(n1v2))
tab.addFoundNode(n1v2, false)
checkBucketContent(t, tab, []*enode.Node{n1})
}
@ -413,8 +413,8 @@ func TestTable_revalidateSyncRecord(t *testing.T) {
var r enr.Record
r.Set(enr.IP(net.IP{127, 0, 0, 1}))
id := enode.ID{1}
n1 := wrapNode(enode.SignNull(&r, id))
tab.addFoundNode(n1)
n1 := enode.SignNull(&r, id)
tab.addFoundNode(n1, false)
// Update the node record.
r.Set(enr.WithEntry("foo", "bar"))
@ -437,7 +437,7 @@ func TestNodesPush(t *testing.T) {
n1 := nodeAtDistance(target, 255, intIP(1))
n2 := nodeAtDistance(target, 254, intIP(2))
n3 := nodeAtDistance(target, 253, intIP(3))
perm := [][]*node{
perm := [][]*enode.Node{
{n3, n2, n1},
{n3, n1, n2},
{n2, n3, n1},
@ -452,7 +452,7 @@ func TestNodesPush(t *testing.T) {
for _, n := range nodes {
list.push(n, 3)
}
if !slicesEqual(list.entries, perm[0], nodeIDEqual) {
if !slices.EqualFunc(list.entries, perm[0], nodeIDEqual) {
t.Fatal("not equal")
}
}
@ -463,13 +463,13 @@ func TestNodesPush(t *testing.T) {
for _, n := range nodes {
list.push(n, 2)
}
if !slicesEqual(list.entries, perm[0][:2], nodeIDEqual) {
if !slices.EqualFunc(list.entries, perm[0][:2], nodeIDEqual) {
t.Fatal("not equal")
}
}
}
func nodeIDEqual(n1, n2 *node) bool {
func nodeIDEqual[N nodeType](n1, n2 N) bool {
return n1.ID() == n2.ID()
}

View file

@ -56,18 +56,18 @@ func newInactiveTestTable(t transport, cfg Config) (*Table, *enode.DB) {
}
// nodeAtDistance creates a node for which enode.LogDist(base, n.id) == ld.
func nodeAtDistance(base enode.ID, ld int, ip net.IP) *node {
func nodeAtDistance(base enode.ID, ld int, ip net.IP) *enode.Node {
var r enr.Record
r.Set(enr.IP(ip))
r.Set(enr.UDP(30303))
return wrapNode(enode.SignNull(&r, idAtDistance(base, ld)))
return enode.SignNull(&r, idAtDistance(base, ld))
}
// nodesAtDistance creates n nodes for which enode.LogDist(base, node.ID()) == ld.
func nodesAtDistance(base enode.ID, ld int, n int) []*enode.Node {
results := make([]*enode.Node, n)
for i := range results {
results[i] = unwrapNode(nodeAtDistance(base, ld, intIP(i)))
results[i] = nodeAtDistance(base, ld, intIP(i))
}
return results
}
@ -110,7 +110,7 @@ func fillBucket(tab *Table, id enode.ID) (last *node) {
b := tab.bucket(id)
for len(b.entries) < bucketSize {
node := nodeAtDistance(tab.self().ID(), ld, intIP(ld))
if !tab.addFoundNode(node) {
if !tab.addFoundNode(node, false) {
panic("node not added")
}
}
@ -119,13 +119,11 @@ func fillBucket(tab *Table, id enode.ID) (last *node) {
// fillTable adds nodes the table to the end of their corresponding bucket
// if the bucket is not full. The caller must not hold tab.mutex.
func fillTable(tab *Table, nodes []*node, setLive bool) {
func fillTable(tab *Table, nodes []*enode.Node, setLive bool) {
for _, n := range nodes {
if setLive {
n.livenessChecks = 1
n.isValidatedLive = true
}
tab.addFoundNode(n)
tab.addFoundNode(n, setLive)
}
}
@ -219,7 +217,7 @@ func (t *pingRecorder) RequestENR(n *enode.Node) (*enode.Node, error) {
return t.records[n.ID()], nil
}
func hasDuplicates(slice []*node) bool {
func hasDuplicates(slice []*enode.Node) bool {
seen := make(map[enode.ID]bool, len(slice))
for i, e := range slice {
if e == nil {
@ -261,14 +259,14 @@ func nodeEqual(n1 *enode.Node, n2 *enode.Node) bool {
return n1.ID() == n2.ID() && n1.IP().Equal(n2.IP())
}
func sortByID(nodes []*enode.Node) {
slices.SortFunc(nodes, func(a, b *enode.Node) int {
func sortByID[N nodeType](nodes []N) {
slices.SortFunc(nodes, func(a, b N) int {
return bytes.Compare(a.ID().Bytes(), b.ID().Bytes())
})
}
func sortedByDistanceTo(distbase enode.ID, slice []*node) bool {
return slices.IsSortedFunc(slice, func(a, b *node) int {
func sortedByDistanceTo(distbase enode.ID, slice []*enode.Node) bool {
return slices.IsSortedFunc(slice, func(a, b *enode.Node) int {
return enode.DistCmp(distbase, a.ID(), b.ID())
})
}

View file

@ -19,7 +19,7 @@ package discover
import (
"crypto/ecdsa"
"fmt"
"net"
"net/netip"
"slices"
"testing"
@ -40,7 +40,7 @@ func TestUDPv4_Lookup(t *testing.T) {
}
// Seed table with initial node.
fillTable(test.table, []*node{wrapNode(lookupTestnet.node(256, 0))}, true)
fillTable(test.table, []*enode.Node{lookupTestnet.node(256, 0)}, true)
// Start the lookup.
resultC := make(chan []*enode.Node, 1)
@ -70,9 +70,9 @@ func TestUDPv4_LookupIterator(t *testing.T) {
defer test.close()
// Seed table with initial nodes.
bootnodes := make([]*node, len(lookupTestnet.dists[256]))
bootnodes := make([]*enode.Node, len(lookupTestnet.dists[256]))
for i := range lookupTestnet.dists[256] {
bootnodes[i] = wrapNode(lookupTestnet.node(256, i))
bootnodes[i] = lookupTestnet.node(256, i)
}
fillTable(test.table, bootnodes, true)
go serveTestnet(test, lookupTestnet)
@ -105,9 +105,9 @@ func TestUDPv4_LookupIteratorClose(t *testing.T) {
defer test.close()
// Seed table with initial nodes.
bootnodes := make([]*node, len(lookupTestnet.dists[256]))
bootnodes := make([]*enode.Node, len(lookupTestnet.dists[256]))
for i := range lookupTestnet.dists[256] {
bootnodes[i] = wrapNode(lookupTestnet.node(256, i))
bootnodes[i] = lookupTestnet.node(256, i)
}
fillTable(test.table, bootnodes, true)
go serveTestnet(test, lookupTestnet)
@ -136,7 +136,7 @@ func TestUDPv4_LookupIteratorClose(t *testing.T) {
func serveTestnet(test *udpTest, testnet *preminedTestnet) {
for done := false; !done; {
done = test.waitPacketOut(func(p v4wire.Packet, to *net.UDPAddr, hash []byte) {
done = test.waitPacketOut(func(p v4wire.Packet, to netip.AddrPort, hash []byte) {
n, key := testnet.nodeByAddr(to)
switch p.(type) {
case *v4wire.Ping:
@ -158,10 +158,10 @@ func checkLookupResults(t *testing.T, tn *preminedTestnet, results []*enode.Node
for _, e := range results {
t.Logf(" ld=%d, %x", enode.LogDist(tn.target.id(), e.ID()), e.ID().Bytes())
}
if hasDuplicates(wrapNodes(results)) {
if hasDuplicates(results) {
t.Errorf("result set contains duplicate entries")
}
if !sortedByDistanceTo(tn.target.id(), wrapNodes(results)) {
if !sortedByDistanceTo(tn.target.id(), results) {
t.Errorf("result set not sorted by distance to target")
}
wantNodes := tn.closest(len(results))
@ -264,9 +264,10 @@ func (tn *preminedTestnet) node(dist, index int) *enode.Node {
return n
}
func (tn *preminedTestnet) nodeByAddr(addr *net.UDPAddr) (*enode.Node, *ecdsa.PrivateKey) {
dist := int(addr.IP[1])<<8 + int(addr.IP[2])
index := int(addr.IP[3])
func (tn *preminedTestnet) nodeByAddr(addr netip.AddrPort) (*enode.Node, *ecdsa.PrivateKey) {
ip := addr.Addr().As4()
dist := int(ip[1])<<8 + int(ip[2])
index := int(ip[3])
key := tn.dists[dist][index]
return tn.node(dist, index), key
}
@ -274,7 +275,7 @@ func (tn *preminedTestnet) nodeByAddr(addr *net.UDPAddr) (*enode.Node, *ecdsa.Pr
func (tn *preminedTestnet) nodesAtDistance(dist int) []v4wire.Node {
result := make([]v4wire.Node, len(tn.dists[dist]))
for i := range result {
result[i] = nodeToRPC(wrapNode(tn.node(dist, i)))
result[i] = nodeToRPC(tn.node(dist, i))
}
return result
}

View file

@ -26,6 +26,7 @@ import (
"fmt"
"io"
"net"
"net/netip"
"sync"
"time"
@ -45,6 +46,7 @@ var (
errClockWarp = errors.New("reply deadline too far in the future")
errClosed = errors.New("socket closed")
errLowPort = errors.New("low port")
errNoUDPEndpoint = errors.New("node has no UDP endpoint")
)
const (
@ -290,20 +292,25 @@ func (t *UDPv4) newRandomLookup(ctx context.Context) *lookup {
func (t *UDPv4) newLookup(ctx context.Context, targetKey encPubkey) *lookup {
target := enode.ID(crypto.Keccak256Hash(targetKey[:]))
ekey := v4wire.Pubkey(targetKey)
it := newLookup(ctx, t.tab, target, func(n *node) ([]*node, error) {
return t.findnode(n.ID(), n.addr(), ekey)
it := newLookup(ctx, t.tab, target, func(n *enode.Node) ([]*enode.Node, error) {
addr, ok := n.UDPEndpoint()
if !ok {
return nil, errNoUDPEndpoint
}
return t.findnode(n.ID(), addr, ekey)
})
return it
}
// findnode sends a findnode request to the given node and waits until
// the node has sent up to k neighbors.
func (t *UDPv4) findnode(toid enode.ID, toaddr *net.UDPAddr, target v4wire.Pubkey) ([]*node, error) {
func (t *UDPv4) findnode(toid enode.ID, toAddrPort netip.AddrPort, target v4wire.Pubkey) ([]*enode.Node, error) {
toaddr := &net.UDPAddr{IP: toAddrPort.Addr().AsSlice(), Port: int(toAddrPort.Port())}
t.ensureBond(toid, toaddr)
// Add a matcher for 'neighbours' replies to the pending reply queue. The matcher is
// active until enough nodes have been received.
nodes := make([]*node, 0, bucketSize)
nodes := make([]*enode.Node, 0, bucketSize)
nreceived := 0
rm := t.pending(toid, toaddr.IP, v4wire.NeighborsPacket, func(r v4wire.Packet) (matched bool, requestDone bool) {
reply := r.(*v4wire.Neighbors)
@ -579,7 +586,7 @@ func (t *UDPv4) ensureBond(toid enode.ID, toaddr *net.UDPAddr) {
}
}
func (t *UDPv4) nodeFromRPC(sender *net.UDPAddr, rn v4wire.Node) (*node, error) {
func (t *UDPv4) nodeFromRPC(sender *net.UDPAddr, rn v4wire.Node) (*enode.Node, error) {
if rn.UDP <= 1024 {
return nil, errLowPort
}
@ -593,12 +600,12 @@ func (t *UDPv4) nodeFromRPC(sender *net.UDPAddr, rn v4wire.Node) (*node, error)
if err != nil {
return nil, err
}
n := wrapNode(enode.NewV4(key, rn.IP, int(rn.TCP), int(rn.UDP)))
n := enode.NewV4(key, rn.IP, int(rn.TCP), int(rn.UDP))
err = n.ValidateComplete()
return n, err
}
func nodeToRPC(n *node) v4wire.Node {
func nodeToRPC(n *enode.Node) v4wire.Node {
var key ecdsa.PublicKey
var ekey v4wire.Pubkey
if err := n.Load((*enode.Secp256k1)(&key)); err == nil {
@ -670,7 +677,7 @@ func (t *UDPv4) handlePing(h *packetHandlerV4, from *net.UDPAddr, fromID enode.I
})
// Ping back if our last pong on file is too far in the past.
n := wrapNode(enode.NewV4(h.senderKey, from.IP, int(req.From.TCP), from.Port))
n := enode.NewV4(h.senderKey, from.IP, int(req.From.TCP), from.Port)
if time.Since(t.db.LastPongReceived(n.ID(), from.IP)) > bondExpiration {
t.sendPing(fromID, from, func() {
t.tab.addInboundNode(n)

View file

@ -26,6 +26,7 @@ import (
"io"
"math/rand"
"net"
"net/netip"
"reflect"
"sync"
"testing"
@ -55,7 +56,7 @@ type udpTest struct {
udp *UDPv4
sent [][]byte
localkey, remotekey *ecdsa.PrivateKey
remoteaddr *net.UDPAddr
remoteaddr netip.AddrPort
}
func newUDPTest(t *testing.T) *udpTest {
@ -64,7 +65,7 @@ func newUDPTest(t *testing.T) *udpTest {
pipe: newpipe(),
localkey: newkey(),
remotekey: newkey(),
remoteaddr: &net.UDPAddr{IP: net.IP{10, 0, 1, 99}, Port: 30303},
remoteaddr: netip.AddrPortFrom(netip.MustParseAddr("10.0.1.99"), 30303),
}
test.db, _ = enode.OpenDB("")
@ -92,7 +93,7 @@ func (test *udpTest) packetIn(wantError error, data v4wire.Packet) {
}
// handles a packet as if it had been sent to the transport by the key/endpoint.
func (test *udpTest) packetInFrom(wantError error, key *ecdsa.PrivateKey, addr *net.UDPAddr, data v4wire.Packet) {
func (test *udpTest) packetInFrom(wantError error, key *ecdsa.PrivateKey, addr netip.AddrPort, data v4wire.Packet) {
test.t.Helper()
enc, _, err := v4wire.Encode(key, data)
@ -100,7 +101,8 @@ func (test *udpTest) packetInFrom(wantError error, key *ecdsa.PrivateKey, addr *
test.t.Errorf("%s encode error: %v", data.Name(), err)
}
test.sent = append(test.sent, enc)
if err = test.udp.handlePacket(addr, enc); err != wantError {
udpaddr := &net.UDPAddr{IP: addr.Addr().AsSlice(), Port: int(addr.Port())}
if err = test.udp.handlePacket(udpaddr, enc); err != wantError {
test.t.Errorf("error mismatch: got %q, want %q", err, wantError)
}
}
@ -236,7 +238,7 @@ func TestUDPv4_findnodeTimeout(t *testing.T) {
test := newUDPTest(t)
defer test.close()
toaddr := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222}
toaddr := netip.AddrPortFrom(netip.MustParseAddr("1.2.3.4"), 2222)
toid := enode.ID{1, 2, 3, 4}
target := v4wire.Pubkey{4, 5, 6, 7}
result, err := test.udp.findnode(toid, toaddr, target)
@ -261,25 +263,24 @@ func TestUDPv4_findnode(t *testing.T) {
for i := 0; i < numCandidates; i++ {
key := newkey()
ip := net.IP{10, 13, 0, byte(i)}
n := wrapNode(enode.NewV4(&key.PublicKey, ip, 0, 2000))
n := enode.NewV4(&key.PublicKey, ip, 0, 2000)
// Ensure half of table content isn't verified live yet.
if i > numCandidates/2 {
n.isValidatedLive = true
live[n.ID()] = true
}
test.table.addFoundNode(n, live[n.ID()])
nodes.push(n, numCandidates)
}
fillTable(test.table, nodes.entries, false)
// ensure there's a bond with the test node,
// findnode won't be accepted otherwise.
remoteID := v4wire.EncodePubkey(&test.remotekey.PublicKey).ID()
test.table.db.UpdateLastPongReceived(remoteID, test.remoteaddr.IP, time.Now())
test.table.db.UpdateLastPongReceived(remoteID, test.remoteaddr.Addr().AsSlice(), time.Now())
// check that closest neighbors are returned.
expected := test.table.findnodeByID(testTarget.ID(), bucketSize, true)
test.packetIn(nil, &v4wire.Findnode{Target: testTarget, Expiration: futureExp})
waitNeighbors := func(want []*node) {
waitNeighbors := func(want []*enode.Node) {
test.waitPacketOut(func(p *v4wire.Neighbors, to *net.UDPAddr, hash []byte) {
if len(p.Nodes) != len(want) {
t.Errorf("wrong number of results: got %d, want %d", len(p.Nodes), len(want))
@ -309,10 +310,10 @@ func TestUDPv4_findnodeMultiReply(t *testing.T) {
defer test.close()
rid := enode.PubkeyToIDV4(&test.remotekey.PublicKey)
test.table.db.UpdateLastPingReceived(rid, test.remoteaddr.IP, time.Now())
test.table.db.UpdateLastPingReceived(rid, test.remoteaddr.Addr().AsSlice(), time.Now())
// queue a pending findnode request
resultc, errc := make(chan []*node, 1), make(chan error, 1)
resultc, errc := make(chan []*enode.Node, 1), make(chan error, 1)
go func() {
rid := encodePubkey(&test.remotekey.PublicKey).id()
ns, err := test.udp.findnode(rid, test.remoteaddr, testTarget)
@ -332,11 +333,11 @@ func TestUDPv4_findnodeMultiReply(t *testing.T) {
})
// send the reply as two packets.
list := []*node{
wrapNode(enode.MustParse("enode://ba85011c70bcc5c04d8607d3a0ed29aa6179c092cbdda10d5d32684fb33ed01bd94f588ca8f91ac48318087dcb02eaf36773a7a453f0eedd6742af668097b29c@10.0.1.16:30303?discport=30304")),
wrapNode(enode.MustParse("enode://81fa361d25f157cd421c60dcc28d8dac5ef6a89476633339c5df30287474520caca09627da18543d9079b5b288698b542d56167aa5c09111e55acdbbdf2ef799@10.0.1.16:30303")),
wrapNode(enode.MustParse("enode://9bffefd833d53fac8e652415f4973bee289e8b1a5c6c4cbe70abf817ce8a64cee11b823b66a987f51aaa9fba0d6a91b3e6bf0d5a5d1042de8e9eeea057b217f8@10.0.1.36:30301?discport=17")),
wrapNode(enode.MustParse("enode://1b5b4aa662d7cb44a7221bfba67302590b643028197a7d5214790f3bac7aaa4a3241be9e83c09cf1f6c69d007c634faae3dc1b1221793e8446c0b3a09de65960@10.0.1.16:30303")),
list := []*enode.Node{
enode.MustParse("enode://ba85011c70bcc5c04d8607d3a0ed29aa6179c092cbdda10d5d32684fb33ed01bd94f588ca8f91ac48318087dcb02eaf36773a7a453f0eedd6742af668097b29c@10.0.1.16:30303?discport=30304"),
enode.MustParse("enode://81fa361d25f157cd421c60dcc28d8dac5ef6a89476633339c5df30287474520caca09627da18543d9079b5b288698b542d56167aa5c09111e55acdbbdf2ef799@10.0.1.16:30303"),
enode.MustParse("enode://9bffefd833d53fac8e652415f4973bee289e8b1a5c6c4cbe70abf817ce8a64cee11b823b66a987f51aaa9fba0d6a91b3e6bf0d5a5d1042de8e9eeea057b217f8@10.0.1.36:30301?discport=17"),
enode.MustParse("enode://1b5b4aa662d7cb44a7221bfba67302590b643028197a7d5214790f3bac7aaa4a3241be9e83c09cf1f6c69d007c634faae3dc1b1221793e8446c0b3a09de65960@10.0.1.16:30303"),
}
rpclist := make([]v4wire.Node, len(list))
for i := range list {
@ -381,8 +382,8 @@ func TestUDPv4_pingMatchIP(t *testing.T) {
test.packetIn(nil, &v4wire.Ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
test.waitPacketOut(func(*v4wire.Pong, *net.UDPAddr, []byte) {})
test.waitPacketOut(func(p *v4wire.Ping, to *net.UDPAddr, hash []byte) {
wrongAddr := &net.UDPAddr{IP: net.IP{33, 44, 1, 2}, Port: 30000}
test.waitPacketOut(func(p *v4wire.Ping, to netip.AddrPort, hash []byte) {
wrongAddr := netip.MustParseAddrPort("33.44.1.2:30000")
test.packetInFrom(errUnsolicitedReply, test.remotekey, wrongAddr, &v4wire.Pong{
ReplyTok: hash,
To: testLocalAnnounced,
@ -408,7 +409,8 @@ func TestUDPv4_successfulPing(t *testing.T) {
}
wantTo := v4wire.Endpoint{
// The mirrored UDP address is the UDP packet sender
IP: test.remoteaddr.IP, UDP: uint16(test.remoteaddr.Port),
IP: test.remoteaddr.Addr().AsSlice(),
UDP: test.remoteaddr.Port(),
// The mirrored TCP port is the one from the ping packet
TCP: testRemote.TCP,
}
@ -424,8 +426,8 @@ func TestUDPv4_successfulPing(t *testing.T) {
}
wantTo := v4wire.Endpoint{
// The mirrored UDP address is the UDP packet sender.
IP: test.remoteaddr.IP,
UDP: uint16(test.remoteaddr.Port),
IP: test.remoteaddr.Addr().AsSlice(),
UDP: test.remoteaddr.Port(),
TCP: 0,
}
if !reflect.DeepEqual(p.To, wantTo) {
@ -442,10 +444,10 @@ func TestUDPv4_successfulPing(t *testing.T) {
if n.ID() != rid {
t.Errorf("node has wrong ID: got %v, want %v", n.ID(), rid)
}
if !n.IP().Equal(test.remoteaddr.IP) {
t.Errorf("node has wrong IP: got %v, want: %v", n.IP(), test.remoteaddr.IP)
if !n.IP().Equal(test.remoteaddr.Addr().AsSlice()) {
t.Errorf("node has wrong IP: got %v, want: %v", n.IP(), test.remoteaddr.Addr())
}
if n.UDP() != test.remoteaddr.Port {
if n.UDP() != int(test.remoteaddr.Port()) {
t.Errorf("node has wrong UDP port: got %v, want: %v", n.UDP(), test.remoteaddr.Port)
}
if n.TCP() != int(testRemote.TCP) {
@ -584,7 +586,7 @@ type dgramPipe struct {
}
type dgram struct {
to net.UDPAddr
to netip.AddrPort
data []byte
}
@ -606,7 +608,12 @@ func (c *dgramPipe) WriteToUDP(b []byte, to *net.UDPAddr) (n int, err error) {
if c.closed {
return 0, errors.New("closed")
}
c.queue = append(c.queue, dgram{*to, b})
addr, ok := netip.AddrFromSlice(to.IP)
if !ok {
panic(fmt.Errorf("invalid destination IP addr %v", to.IP))
}
addrPort := netip.AddrPortFrom(addr, uint16(to.Port))
c.queue = append(c.queue, dgram{addrPort, b})
c.cond.Signal()
return len(b), nil
}

View file

@ -233,7 +233,7 @@ func (t *UDPv5) AllNodes() []*enode.Node {
for _, b := range &t.tab.buckets {
for _, n := range b.entries {
nodes = append(nodes, unwrapNode(n))
nodes = append(nodes, n.Node)
}
}
return nodes
@ -314,26 +314,26 @@ func (t *UDPv5) newRandomLookup(ctx context.Context) *lookup {
}
func (t *UDPv5) newLookup(ctx context.Context, target enode.ID) *lookup {
return newLookup(ctx, t.tab, target, func(n *node) ([]*node, error) {
return newLookup(ctx, t.tab, target, func(n *enode.Node) ([]*enode.Node, error) {
return t.lookupWorker(n, target)
})
}
// lookupWorker performs FINDNODE calls against a single node during lookup.
func (t *UDPv5) lookupWorker(destNode *node, target enode.ID) ([]*node, error) {
func (t *UDPv5) lookupWorker(destNode *enode.Node, target enode.ID) ([]*enode.Node, error) {
var (
dists = lookupDistances(target, destNode.ID())
nodes = nodesByDistance{target: target}
err error
)
var r []*enode.Node
r, err = t.findnode(unwrapNode(destNode), dists)
r, err = t.findnode(destNode, dists)
if errors.Is(err, errClosed) {
return nil, err
}
for _, n := range r {
if n.ID() != t.Self().ID() {
nodes.push(wrapNode(n), findnodeResultLimit)
nodes.push(n, findnodeResultLimit)
}
}
return nodes.entries, err
@ -699,7 +699,7 @@ func (t *UDPv5) handlePacket(rawpacket []byte, fromAddr *net.UDPAddr) error {
}
if fromNode != nil {
// Handshake succeeded, add to table.
t.tab.addInboundNode(wrapNode(fromNode))
t.tab.addInboundNode(fromNode)
}
if packet.Kind() != v5wire.WhoareyouPacket {
// WHOAREYOU logged separately to report errors.

View file

@ -23,6 +23,7 @@ import (
"fmt"
"math/rand"
"net"
"net/netip"
"reflect"
"slices"
"testing"
@ -141,7 +142,7 @@ func TestUDPv5_unknownPacket(t *testing.T) {
// Make node known.
n := test.getNode(test.remotekey, test.remoteaddr).Node()
test.table.addFoundNode(wrapNode(n))
test.table.addFoundNode(n, false)
test.packetIn(&v5wire.Unknown{Nonce: nonce})
test.waitPacketOut(func(p *v5wire.Whoareyou, addr *net.UDPAddr, _ v5wire.Nonce) {
@ -159,9 +160,9 @@ func TestUDPv5_findnodeHandling(t *testing.T) {
nodes253 := nodesAtDistance(test.table.self().ID(), 253, 16)
nodes249 := nodesAtDistance(test.table.self().ID(), 249, 4)
nodes248 := nodesAtDistance(test.table.self().ID(), 248, 10)
fillTable(test.table, wrapNodes(nodes253), true)
fillTable(test.table, wrapNodes(nodes249), true)
fillTable(test.table, wrapNodes(nodes248), true)
fillTable(test.table, nodes253, true)
fillTable(test.table, nodes249, true)
fillTable(test.table, nodes248, true)
// Requesting with distance zero should return the node's own record.
test.packetIn(&v5wire.Findnode{ReqID: []byte{0}, Distances: []uint{0}})
@ -589,7 +590,7 @@ func TestUDPv5_lookup(t *testing.T) {
// Seed table with initial node.
initialNode := lookupTestnet.node(256, 0)
fillTable(test.table, []*node{wrapNode(initialNode)}, true)
fillTable(test.table, []*enode.Node{initialNode}, true)
// Start the lookup.
resultC := make(chan []*enode.Node, 1)
@ -601,7 +602,7 @@ func TestUDPv5_lookup(t *testing.T) {
// Answer lookup packets.
asked := make(map[enode.ID]bool)
for done := false; !done; {
done = test.waitPacketOut(func(p v5wire.Packet, to *net.UDPAddr, _ v5wire.Nonce) {
done = test.waitPacketOut(func(p v5wire.Packet, to netip.AddrPort, _ v5wire.Nonce) {
recipient, key := lookupTestnet.nodeByAddr(to)
switch p := p.(type) {
case *v5wire.Ping:
@ -688,9 +689,9 @@ type udpV5Test struct {
db *enode.DB
udp *UDPv5
localkey, remotekey *ecdsa.PrivateKey
remoteaddr *net.UDPAddr
remoteaddr netip.AddrPort
nodesByID map[enode.ID]*enode.LocalNode
nodesByIP map[string]*enode.LocalNode
nodesByIP map[netip.Addr]*enode.LocalNode
}
// testCodec is the packet encoding used by protocol tests. This codec does not perform encryption.
@ -778,7 +779,7 @@ func (test *udpV5Test) packetIn(packet v5wire.Packet) {
}
// handles a packet as if it had been sent to the transport by the key/endpoint.
func (test *udpV5Test) packetInFrom(key *ecdsa.PrivateKey, addr *net.UDPAddr, packet v5wire.Packet) {
func (test *udpV5Test) packetInFrom(key *ecdsa.PrivateKey, addr netip.AddrPort, packet v5wire.Packet) {
test.t.Helper()
ln := test.getNode(key, addr)
@ -793,17 +794,17 @@ func (test *udpV5Test) packetInFrom(key *ecdsa.PrivateKey, addr *net.UDPAddr, pa
}
// getNode ensures the test knows about a node at the given endpoint.
func (test *udpV5Test) getNode(key *ecdsa.PrivateKey, addr *net.UDPAddr) *enode.LocalNode {
func (test *udpV5Test) getNode(key *ecdsa.PrivateKey, addr netip.AddrPort) *enode.LocalNode {
id := encodePubkey(&key.PublicKey).id()
ln := test.nodesByID[id]
if ln == nil {
db, _ := enode.OpenDB("")
ln = enode.NewLocalNode(db, key)
ln.SetStaticIP(addr.IP)
ln.Set(enr.UDP(addr.Port))
ln.SetStaticIP(addr.Addr().AsSlice())
ln.Set(enr.UDP(addr.Port()))
test.nodesByID[id] = ln
}
test.nodesByIP[string(addr.IP)] = ln
test.nodesByIP[string(addr.Addr().String())] = ln
return ln
}