Unverified Commit 350a87dd authored by Felix Lange's avatar Felix Lange Committed by GitHub

p2p/discover: add support for EIP-868 (v4 ENR extension) (#19540)

This change implements EIP-868. The UDPv4 transport announces support
for the extension in ping/pong and handles enrRequest messages.

There are two uses of the extension: If a remote node announces support
for EIP-868 in their pong, node revalidation pulls the node's record.
The Resolve method requests the record unconditionally.
parent 8deec2e4
......@@ -53,15 +53,17 @@ const (
bucketIPLimit, bucketSubnet = 2, 24 // at most 2 addresses from the same /24
tableIPLimit, tableSubnet = 10, 24
maxFindnodeFailures = 5 // Nodes exceeding this limit are dropped
refreshInterval = 30 * time.Minute
revalidateInterval = 10 * time.Second
copyNodesInterval = 30 * time.Second
seedMinTableTime = 5 * time.Minute
seedCount = 30
seedMaxAge = 5 * 24 * time.Hour
refreshInterval = 30 * time.Minute
revalidateInterval = 10 * time.Second
copyNodesInterval = 30 * time.Second
seedMinTableTime = 5 * time.Minute
seedCount = 30
seedMaxAge = 5 * 24 * time.Hour
)
// Table is the 'node table', a Kademlia-like index of neighbor nodes. The table keeps
// itself up-to-date by verifying the liveness of neighbors and requesting their node
// records when announcements of a new record version are received.
type Table struct {
mutex sync.Mutex // protects buckets, bucket content, nursery, rand
buckets [nBuckets]*bucket // index of known nodes by distance
......@@ -80,12 +82,13 @@ type Table struct {
nodeAddedHook func(*node) // for testing
}
// transport is implemented by UDP transports.
// transport is implemented by the UDP transports.
type transport interface {
Self() *enode.Node
lookupRandom() []*enode.Node
lookupSelf() []*enode.Node
ping(*enode.Node) error
ping(*enode.Node) (seq uint64, err error)
requestENR(*enode.Node) (*enode.Node, error)
}
// bucket contains nodes, ordered by their last activity. the entry
......@@ -175,14 +178,16 @@ func (tab *Table) ReadRandomNodes(buf []*enode.Node) (n int) {
return i + 1
}
// Resolve searches for a specific node with the given ID.
// It returns nil if the node could not be found.
func (tab *Table) Resolve(n *enode.Node) *enode.Node {
// getNode returns the node with the given ID or nil if it isn't in the table.
func (tab *Table) getNode(id enode.ID) *enode.Node {
tab.mutex.Lock()
cl := tab.closest(n.ID(), 1, false)
tab.mutex.Unlock()
if len(cl.entries) > 0 && cl.entries[0].ID() == n.ID() {
return unwrapNode(cl.entries[0])
defer tab.mutex.Unlock()
b := tab.bucket(id)
for _, e := range b.entries {
if e.ID() == id {
return unwrapNode(e)
}
}
return nil
}
......@@ -226,7 +231,7 @@ func (tab *Table) refresh() <-chan struct{} {
return done
}
// loop schedules refresh, revalidate runs and coordinates shutdown.
// loop schedules runs of doRefresh, doRevalidate and copyLiveNodes.
func (tab *Table) loop() {
var (
revalidate = time.NewTimer(tab.nextRevalidateTime())
......@@ -288,9 +293,8 @@ loop:
close(tab.closed)
}
// doRefresh performs a lookup for a random target to keep buckets
// full. seed nodes are inserted if the table is empty (initial
// bootstrap or discarded faulty peers).
// doRefresh performs a lookup for a random target to keep buckets full. seed nodes are
// inserted if the table is empty (initial bootstrap or discarded faulty peers).
func (tab *Table) doRefresh(done chan struct{}) {
defer close(done)
......@@ -324,8 +328,8 @@ func (tab *Table) loadSeedNodes() {
}
}
// doRevalidate checks that the last node in a random bucket is still live
// and replaces or deletes the node if it isn't.
// doRevalidate checks that the last node in a random bucket is still live and replaces or
// deletes the node if it isn't.
func (tab *Table) doRevalidate(done chan<- struct{}) {
defer func() { done <- struct{}{} }()
......@@ -336,7 +340,17 @@ func (tab *Table) doRevalidate(done chan<- struct{}) {
}
// Ping the selected node and wait for a pong.
err := tab.net.ping(unwrapNode(last))
remoteSeq, err := tab.net.ping(unwrapNode(last))
// Also fetch record if the node replied and returned a higher sequence number.
if last.Seq() < remoteSeq {
n, err := tab.net.requestENR(unwrapNode(last))
if err != nil {
tab.log.Debug("ENR request failed", "id", last.ID(), "addr", last.addr(), "err", err)
} else {
last = &node{Node: *n, addedAt: last.addedAt, livenessChecks: last.livenessChecks}
}
}
tab.mutex.Lock()
defer tab.mutex.Unlock()
......
......@@ -368,6 +368,34 @@ func TestTable_addSeenNode(t *testing.T) {
checkIPLimitInvariant(t, tab)
}
// This test checks that ENR updates happen during revalidation. If a node in the table
// announces a new sequence number, the new record should be pulled.
func TestTable_revalidateSyncRecord(t *testing.T) {
transport := newPingRecorder()
tab, db := newTestTable(transport)
<-tab.initDone
defer db.Close()
defer tab.close()
// Insert a node.
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.addSeenNode(n1)
// Update the node record.
r.Set(enr.WithEntry("foo", "bar"))
n2 := enode.SignNull(&r, id)
transport.updateRecord(n2)
tab.doRevalidate(make(chan struct{}, 1))
intable := tab.getNode(id)
if !reflect.DeepEqual(intable, n2) {
t.Fatalf("table contains old record with seq %d, want seq %d", intable.Seq(), n2.Seq())
}
}
// gen wraps quick.Value so it's easier to use.
// it generates a random value of the given value's type.
func gen(typ interface{}, rand *rand.Rand) interface{} {
......
......@@ -98,6 +98,7 @@ func fillTable(tab *Table, nodes []*node) {
type pingRecorder struct {
mu sync.Mutex
dead, pinged map[enode.ID]bool
records map[enode.ID]*enode.Node
n *enode.Node
}
......@@ -107,38 +108,53 @@ func newPingRecorder() *pingRecorder {
n := enode.SignNull(&r, enode.ID{})
return &pingRecorder{
dead: make(map[enode.ID]bool),
pinged: make(map[enode.ID]bool),
n: n,
dead: make(map[enode.ID]bool),
pinged: make(map[enode.ID]bool),
records: make(map[enode.ID]*enode.Node),
n: n,
}
}
func (t *pingRecorder) Self() *enode.Node {
return nullNode
// setRecord updates a node record. Future calls to ping and
// requestENR will return this record.
func (t *pingRecorder) updateRecord(n *enode.Node) {
t.mu.Lock()
defer t.mu.Unlock()
t.records[n.ID()] = n
}
func (t *pingRecorder) ping(n *enode.Node) error {
// Stubs to satisfy the transport interface.
func (t *pingRecorder) Self() *enode.Node { return nullNode }
func (t *pingRecorder) lookupSelf() []*enode.Node { return nil }
func (t *pingRecorder) lookupRandom() []*enode.Node { return nil }
func (t *pingRecorder) close() {}
// ping simulates a ping request.
func (t *pingRecorder) ping(n *enode.Node) (seq uint64, err error) {
t.mu.Lock()
defer t.mu.Unlock()
t.pinged[n.ID()] = true
if t.dead[n.ID()] {
return errTimeout
} else {
return nil
return 0, errTimeout
}
if t.records[n.ID()] != nil {
seq = t.records[n.ID()].Seq()
}
return seq, nil
}
func (t *pingRecorder) lookupSelf() []*enode.Node {
return nil
}
// requestENR simulates an ENR request.
func (t *pingRecorder) requestENR(n *enode.Node) (*enode.Node, error) {
t.mu.Lock()
defer t.mu.Unlock()
func (t *pingRecorder) lookupRandom() []*enode.Node {
return nil
if t.dead[n.ID()] || t.records[n.ID()] == nil {
return nil, errTimeout
}
return t.records[n.ID()], nil
}
func (t *pingRecorder) close() {}
func hasDuplicates(slice []*node) bool {
seen := make(map[enode.ID]bool)
for i, e := range slice {
......
This diff is collapsed.
......@@ -54,11 +54,11 @@ func TestUDPv4_Lookup(t *testing.T) {
n, key := lookupTestnet.nodeByAddr(to)
switch p.(type) {
case *pingV4:
test.packetInFrom(nil, key, to, p_pongV4, &pongV4{Expiration: futureExp, ReplyTok: hash})
test.packetInFrom(nil, key, to, &pongV4{Expiration: futureExp, ReplyTok: hash})
case *findnodeV4:
dist := enode.LogDist(n.ID(), lookupTestnet.target.id())
nodes := lookupTestnet.nodesAtDistance(dist - 1)
test.packetInFrom(nil, key, to, p_neighborsV4, &neighborsV4{Expiration: futureExp, Nodes: nodes})
test.packetInFrom(nil, key, to, &neighborsV4{Expiration: futureExp, Nodes: nodes})
}
})
}
......
......@@ -37,6 +37,7 @@ import (
"github.com/ethereum/go-ethereum/internal/testlog"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/enr"
"github.com/ethereum/go-ethereum/rlp"
)
......@@ -91,19 +92,19 @@ func (test *udpTest) close() {
}
// handles a packet as if it had been sent to the transport.
func (test *udpTest) packetIn(wantError error, ptype byte, data packetV4) {
func (test *udpTest) packetIn(wantError error, data packetV4) {
test.t.Helper()
test.packetInFrom(wantError, test.remotekey, test.remoteaddr, ptype, data)
test.packetInFrom(wantError, test.remotekey, test.remoteaddr, data)
}
// 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, ptype byte, data packetV4) {
func (test *udpTest) packetInFrom(wantError error, key *ecdsa.PrivateKey, addr *net.UDPAddr, data packetV4) {
test.t.Helper()
enc, _, err := test.udp.encode(key, ptype, data)
enc, _, err := test.udp.encode(key, data)
if err != nil {
test.t.Errorf("packet (%d) encode error: %v", ptype, err)
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 {
......@@ -139,10 +140,10 @@ func TestUDPv4_packetErrors(t *testing.T) {
test := newUDPTest(t)
defer test.close()
test.packetIn(errExpired, p_pingV4, &pingV4{From: testRemote, To: testLocalAnnounced, Version: 4})
test.packetIn(errUnsolicitedReply, p_pongV4, &pongV4{ReplyTok: []byte{}, Expiration: futureExp})
test.packetIn(errUnknownNode, p_findnodeV4, &findnodeV4{Expiration: futureExp})
test.packetIn(errUnsolicitedReply, p_neighborsV4, &neighborsV4{Expiration: futureExp})
test.packetIn(errExpired, &pingV4{From: testRemote, To: testLocalAnnounced, Version: 4})
test.packetIn(errUnsolicitedReply, &pongV4{ReplyTok: []byte{}, Expiration: futureExp})
test.packetIn(errUnknownNode, &findnodeV4{Expiration: futureExp})
test.packetIn(errUnsolicitedReply, &neighborsV4{Expiration: futureExp})
}
func TestUDPv4_pingTimeout(t *testing.T) {
......@@ -153,11 +154,21 @@ func TestUDPv4_pingTimeout(t *testing.T) {
key := newkey()
toaddr := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222}
node := enode.NewV4(&key.PublicKey, toaddr.IP, 0, toaddr.Port)
if err := test.udp.ping(node); err != errTimeout {
if _, err := test.udp.ping(node); err != errTimeout {
t.Error("expected timeout error, got", err)
}
}
type testPacket byte
func (req testPacket) kind() byte { return byte(req) }
func (req testPacket) name() string { return "" }
func (req testPacket) preverify(*UDPv4, *net.UDPAddr, enode.ID, encPubkey) error {
return nil
}
func (req testPacket) handle(*UDPv4, *net.UDPAddr, enode.ID, []byte) {
}
func TestUDPv4_responseTimeouts(t *testing.T) {
t.Parallel()
test := newUDPTest(t)
......@@ -192,7 +203,7 @@ func TestUDPv4_responseTimeouts(t *testing.T) {
p.errc = nilErr
test.udp.addReplyMatcher <- p
time.AfterFunc(randomDuration(60*time.Millisecond), func() {
if !test.udp.handleReply(p.from, p.ip, p.ptype, nil) {
if !test.udp.handleReply(p.from, p.ip, testPacket(p.ptype)) {
t.Logf("not matched: %v", p)
}
})
......@@ -277,7 +288,7 @@ func TestUDPv4_findnode(t *testing.T) {
// check that closest neighbors are returned.
expected := test.table.closest(testTarget.id(), bucketSize, true)
test.packetIn(nil, p_findnodeV4, &findnodeV4{Target: testTarget, Expiration: futureExp})
test.packetIn(nil, &findnodeV4{Target: testTarget, Expiration: futureExp})
waitNeighbors := func(want []*node) {
test.waitPacketOut(func(p *neighborsV4, to *net.UDPAddr, hash []byte) {
if len(p.Nodes) != len(want) {
......@@ -340,8 +351,8 @@ func TestUDPv4_findnodeMultiReply(t *testing.T) {
for i := range list {
rpclist[i] = nodeToRPC(list[i])
}
test.packetIn(nil, p_neighborsV4, &neighborsV4{Expiration: futureExp, Nodes: rpclist[:2]})
test.packetIn(nil, p_neighborsV4, &neighborsV4{Expiration: futureExp, Nodes: rpclist[2:]})
test.packetIn(nil, &neighborsV4{Expiration: futureExp, Nodes: rpclist[:2]})
test.packetIn(nil, &neighborsV4{Expiration: futureExp, Nodes: rpclist[2:]})
// check that the sent neighbors are all returned by findnode
select {
......@@ -357,6 +368,7 @@ func TestUDPv4_findnodeMultiReply(t *testing.T) {
}
}
// This test checks that reply matching of pong verifies the ping hash.
func TestUDPv4_pingMatch(t *testing.T) {
test := newUDPTest(t)
defer test.close()
......@@ -364,22 +376,23 @@ func TestUDPv4_pingMatch(t *testing.T) {
randToken := make([]byte, 32)
crand.Read(randToken)
test.packetIn(nil, p_pingV4, &pingV4{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
test.packetIn(nil, &pingV4{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
test.waitPacketOut(func(*pongV4, *net.UDPAddr, []byte) {})
test.waitPacketOut(func(*pingV4, *net.UDPAddr, []byte) {})
test.packetIn(errUnsolicitedReply, p_pongV4, &pongV4{ReplyTok: randToken, To: testLocalAnnounced, Expiration: futureExp})
test.packetIn(errUnsolicitedReply, &pongV4{ReplyTok: randToken, To: testLocalAnnounced, Expiration: futureExp})
}
// This test checks that reply matching of pong verifies the sender IP address.
func TestUDPv4_pingMatchIP(t *testing.T) {
test := newUDPTest(t)
defer test.close()
test.packetIn(nil, p_pingV4, &pingV4{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
test.packetIn(nil, &pingV4{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
test.waitPacketOut(func(*pongV4, *net.UDPAddr, []byte) {})
test.waitPacketOut(func(p *pingV4, to *net.UDPAddr, hash []byte) {
wrongAddr := &net.UDPAddr{IP: net.IP{33, 44, 1, 2}, Port: 30000}
test.packetInFrom(errUnsolicitedReply, test.remotekey, wrongAddr, p_pongV4, &pongV4{
test.packetInFrom(errUnsolicitedReply, test.remotekey, wrongAddr, &pongV4{
ReplyTok: hash,
To: testLocalAnnounced,
Expiration: futureExp,
......@@ -394,9 +407,9 @@ func TestUDPv4_successfulPing(t *testing.T) {
defer test.close()
// The remote side sends a ping packet to initiate the exchange.
go test.packetIn(nil, p_pingV4, &pingV4{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
go test.packetIn(nil, &pingV4{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
// the ping is replied to.
// The ping is replied to.
test.waitPacketOut(func(p *pongV4, to *net.UDPAddr, hash []byte) {
pinghash := test.sent[0][:macSize]
if !bytes.Equal(p.ReplyTok, pinghash) {
......@@ -413,7 +426,7 @@ func TestUDPv4_successfulPing(t *testing.T) {
}
})
// remote is unknown, the table pings back.
// Remote is unknown, the table pings back.
test.waitPacketOut(func(p *pingV4, to *net.UDPAddr, hash []byte) {
if !reflect.DeepEqual(p.From, test.udp.ourEndpoint()) {
t.Errorf("got ping.From %#v, want %#v", p.From, test.udp.ourEndpoint())
......@@ -427,10 +440,10 @@ func TestUDPv4_successfulPing(t *testing.T) {
if !reflect.DeepEqual(p.To, wantTo) {
t.Errorf("got ping.To %v, want %v", p.To, wantTo)
}
test.packetIn(nil, p_pongV4, &pongV4{ReplyTok: hash, Expiration: futureExp})
test.packetIn(nil, &pongV4{ReplyTok: hash, Expiration: futureExp})
})
// the node should be added to the table shortly after getting the
// The node should be added to the table shortly after getting the
// pong packet.
select {
case n := <-added:
......@@ -452,6 +465,45 @@ func TestUDPv4_successfulPing(t *testing.T) {
}
}
// This test checks that EIP-868 requests work.
func TestUDPv4_EIP868(t *testing.T) {
test := newUDPTest(t)
defer test.close()
test.udp.localNode.Set(enr.WithEntry("foo", "bar"))
wantNode := test.udp.localNode.Node()
// ENR requests aren't allowed before endpoint proof.
test.packetIn(errUnknownNode, &enrRequestV4{Expiration: futureExp})
// Perform endpoint proof and check for sequence number in packet tail.
test.packetIn(nil, &pingV4{Expiration: futureExp})
test.waitPacketOut(func(p *pongV4, addr *net.UDPAddr, hash []byte) {
if seq := seqFromTail(p.Rest); seq != wantNode.Seq() {
t.Errorf("wrong sequence number in pong: %d, want %d", seq, wantNode.Seq())
}
})
test.waitPacketOut(func(p *pingV4, addr *net.UDPAddr, hash []byte) {
if seq := seqFromTail(p.Rest); seq != wantNode.Seq() {
t.Errorf("wrong sequence number in ping: %d, want %d", seq, wantNode.Seq())
}
test.packetIn(nil, &pongV4{Expiration: futureExp, ReplyTok: hash})
})
// Request should work now.
test.packetIn(nil, &enrRequestV4{Expiration: futureExp})
test.waitPacketOut(func(p *enrResponseV4, addr *net.UDPAddr, hash []byte) {
n, err := enode.New(enode.ValidSchemes, &p.Record)
if err != nil {
t.Fatalf("invalid record: %v", err)
}
if !reflect.DeepEqual(n, wantNode) {
t.Fatalf("wrong node in enrResponse: %v", n)
}
})
}
// EIP-8 test vectors.
var testPackets = []struct {
input string
wantPacket interface{}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment