p2p/discover, p2p/enode: rework endpoint proof handling, packet logging (#18963)

This change resolves multiple issues around handling of endpoint proofs.
The proof is now done separately for each IP and completing the proof
requires a matching ping hash.

Also remove waitping because it's equivalent to sleep. waitping was
slightly more efficient, but that may cause issues with findnode if
packets are reordered and the remote end sees findnode before pong.

Logging of received packets was hitherto done after handling the packet,
which meant that sent replies were logged before the packet that
generated them. This change splits up packet handling into 'preverify'
and 'handle'. The error from 'preverify' is logged, but 'handle' happens
after the message is logged. This fixes the order. Packet logs now
contain the node ID.
This commit is contained in:
Felix Lange 2019-01-29 17:39:20 +01:00 committed by GitHub
parent 74c38902ec
commit f0c6f92140
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 597 additions and 334 deletions

View File

@ -34,6 +34,7 @@ import (
type node struct { type node struct {
enode.Node enode.Node
addedAt time.Time // time when the node was added to the table addedAt time.Time // time when the node was added to the table
livenessChecks uint // how often liveness was checked
} }
type encPubkey [64]byte type encPubkey [64]byte

View File

@ -75,6 +75,8 @@ type Table struct {
net transport net transport
refreshReq chan chan struct{} refreshReq chan chan struct{}
initDone chan struct{} initDone chan struct{}
closeOnce sync.Once
closeReq chan struct{} closeReq chan struct{}
closed chan struct{} closed chan struct{}
@ -180,16 +182,14 @@ func (tab *Table) ReadRandomNodes(buf []*enode.Node) (n int) {
// Close terminates the network listener and flushes the node database. // Close terminates the network listener and flushes the node database.
func (tab *Table) Close() { func (tab *Table) Close() {
tab.closeOnce.Do(func() {
if tab.net != nil { if tab.net != nil {
tab.net.close() tab.net.close()
} }
// Wait for loop to end.
select { close(tab.closeReq)
case <-tab.closed: <-tab.closed
// already closed. })
case tab.closeReq <- struct{}{}:
<-tab.closed // wait for refreshLoop to end.
}
} }
// setFallbackNodes sets the initial points of contact. These nodes // setFallbackNodes sets the initial points of contact. These nodes
@ -290,31 +290,39 @@ func (tab *Table) lookup(targetKey encPubkey, refreshIfEmpty bool) []*node {
// we have asked all closest nodes, stop the search // we have asked all closest nodes, stop the search
break break
} }
// wait for the next reply select {
for _, n := range <-reply { case nodes := <-reply:
for _, n := range nodes {
if n != nil && !seen[n.ID()] { if n != nil && !seen[n.ID()] {
seen[n.ID()] = true seen[n.ID()] = true
result.push(n, bucketSize) result.push(n, bucketSize)
} }
} }
case <-tab.closeReq:
return nil // shutdown, no need to continue.
}
pendingQueries-- pendingQueries--
} }
return result.entries return result.entries
} }
func (tab *Table) findnode(n *node, targetKey encPubkey, reply chan<- []*node) { func (tab *Table) findnode(n *node, targetKey encPubkey, reply chan<- []*node) {
fails := tab.db.FindFails(n.ID()) fails := tab.db.FindFails(n.ID(), n.IP())
r, err := tab.net.findnode(n.ID(), n.addr(), targetKey) r, err := tab.net.findnode(n.ID(), n.addr(), targetKey)
if err != nil || len(r) == 0 { if err == errClosed {
// Avoid recording failures on shutdown.
reply <- nil
return
} else if err != nil || len(r) == 0 {
fails++ fails++
tab.db.UpdateFindFails(n.ID(), fails) tab.db.UpdateFindFails(n.ID(), n.IP(), fails)
log.Trace("Findnode failed", "id", n.ID(), "failcount", fails, "err", err) log.Trace("Findnode failed", "id", n.ID(), "failcount", fails, "err", err)
if fails >= maxFindnodeFailures { if fails >= maxFindnodeFailures {
log.Trace("Too many findnode failures, dropping", "id", n.ID(), "failcount", fails) log.Trace("Too many findnode failures, dropping", "id", n.ID(), "failcount", fails)
tab.delete(n) tab.delete(n)
} }
} else if fails > 0 { } else if fails > 0 {
tab.db.UpdateFindFails(n.ID(), fails-1) tab.db.UpdateFindFails(n.ID(), n.IP(), fails-1)
} }
// Grab as many nodes as possible. Some of them might not be alive anymore, but we'll // Grab as many nodes as possible. Some of them might not be alive anymore, but we'll
@ -329,7 +337,7 @@ func (tab *Table) refresh() <-chan struct{} {
done := make(chan struct{}) done := make(chan struct{})
select { select {
case tab.refreshReq <- done: case tab.refreshReq <- done:
case <-tab.closed: case <-tab.closeReq:
close(done) close(done)
} }
return done return done
@ -433,7 +441,7 @@ func (tab *Table) loadSeedNodes() {
seeds = append(seeds, tab.nursery...) seeds = append(seeds, tab.nursery...)
for i := range seeds { for i := range seeds {
seed := seeds[i] seed := seeds[i]
age := log.Lazy{Fn: func() interface{} { return time.Since(tab.db.LastPongReceived(seed.ID())) }} age := log.Lazy{Fn: func() interface{} { return time.Since(tab.db.LastPongReceived(seed.ID(), seed.IP())) }}
log.Trace("Found seed node in database", "id", seed.ID(), "addr", seed.addr(), "age", age) log.Trace("Found seed node in database", "id", seed.ID(), "addr", seed.addr(), "age", age)
tab.add(seed) tab.add(seed)
} }
@ -458,16 +466,17 @@ func (tab *Table) doRevalidate(done chan<- struct{}) {
b := tab.buckets[bi] b := tab.buckets[bi]
if err == nil { if err == nil {
// The node responded, move it to the front. // The node responded, move it to the front.
log.Debug("Revalidated node", "b", bi, "id", last.ID()) last.livenessChecks++
log.Debug("Revalidated node", "b", bi, "id", last.ID(), "checks", last.livenessChecks)
b.bump(last) b.bump(last)
return return
} }
// No reply received, pick a replacement or delete the node if there aren't // No reply received, pick a replacement or delete the node if there aren't
// any replacements. // any replacements.
if r := tab.replace(b, last); r != nil { if r := tab.replace(b, last); r != nil {
log.Debug("Replaced dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "r", r.ID(), "rip", r.IP()) log.Debug("Replaced dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "checks", last.livenessChecks, "r", r.ID(), "rip", r.IP())
} else { } else {
log.Debug("Removed dead node", "b", bi, "id", last.ID(), "ip", last.IP()) log.Debug("Removed dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "checks", last.livenessChecks)
} }
} }
@ -502,7 +511,7 @@ func (tab *Table) copyLiveNodes() {
now := time.Now() now := time.Now()
for _, b := range &tab.buckets { for _, b := range &tab.buckets {
for _, n := range b.entries { for _, n := range b.entries {
if now.Sub(n.addedAt) >= seedMinTableTime { if n.livenessChecks > 0 && now.Sub(n.addedAt) >= seedMinTableTime {
tab.db.UpdateNode(unwrapNode(n)) tab.db.UpdateNode(unwrapNode(n))
} }
} }
@ -518,9 +527,11 @@ func (tab *Table) closest(target enode.ID, nresults int) *nodesByDistance {
close := &nodesByDistance{target: target} close := &nodesByDistance{target: target}
for _, b := range &tab.buckets { for _, b := range &tab.buckets {
for _, n := range b.entries { for _, n := range b.entries {
if n.livenessChecks > 0 {
close.push(n, nresults) close.push(n, nresults)
} }
} }
}
return close return close
} }
@ -572,23 +583,6 @@ func (tab *Table) addThroughPing(n *node) {
tab.add(n) tab.add(n)
} }
// stuff 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 (tab *Table) stuff(nodes []*node) {
tab.mutex.Lock()
defer tab.mutex.Unlock()
for _, n := range nodes {
if n.ID() == tab.self().ID() {
continue // don't add self
}
b := tab.bucket(n.ID())
if len(b.entries) < bucketSize {
tab.bumpOrAdd(b, n)
}
}
}
// delete removes an entry from the node table. It is used to evacuate dead nodes. // delete removes an entry from the node table. It is used to evacuate dead nodes.
func (tab *Table) delete(node *node) { func (tab *Table) delete(node *node) {
tab.mutex.Lock() tab.mutex.Lock()

View File

@ -50,8 +50,8 @@ func TestTable_pingReplace(t *testing.T) {
func testPingReplace(t *testing.T, newNodeIsResponding, lastInBucketIsResponding bool) { func testPingReplace(t *testing.T, newNodeIsResponding, lastInBucketIsResponding bool) {
transport := newPingRecorder() transport := newPingRecorder()
tab, db := newTestTable(transport) tab, db := newTestTable(transport)
defer tab.Close()
defer db.Close() defer db.Close()
defer tab.Close()
<-tab.initDone <-tab.initDone
@ -137,8 +137,8 @@ func TestBucket_bumpNoDuplicates(t *testing.T) {
func TestTable_IPLimit(t *testing.T) { func TestTable_IPLimit(t *testing.T) {
transport := newPingRecorder() transport := newPingRecorder()
tab, db := newTestTable(transport) tab, db := newTestTable(transport)
defer tab.Close()
defer db.Close() defer db.Close()
defer tab.Close()
for i := 0; i < tableIPLimit+1; i++ { for i := 0; i < tableIPLimit+1; i++ {
n := nodeAtDistance(tab.self().ID(), i, net.IP{172, 0, 1, byte(i)}) n := nodeAtDistance(tab.self().ID(), i, net.IP{172, 0, 1, byte(i)})
@ -153,8 +153,8 @@ func TestTable_IPLimit(t *testing.T) {
func TestTable_BucketIPLimit(t *testing.T) { func TestTable_BucketIPLimit(t *testing.T) {
transport := newPingRecorder() transport := newPingRecorder()
tab, db := newTestTable(transport) tab, db := newTestTable(transport)
defer tab.Close()
defer db.Close() defer db.Close()
defer tab.Close()
d := 3 d := 3
for i := 0; i < bucketIPLimit+1; i++ { for i := 0; i < bucketIPLimit+1; i++ {
@ -173,9 +173,9 @@ func TestTable_closest(t *testing.T) {
// for any node table, Target and N // for any node table, Target and N
transport := newPingRecorder() transport := newPingRecorder()
tab, db := newTestTable(transport) tab, db := newTestTable(transport)
defer tab.Close()
defer db.Close() defer db.Close()
tab.stuff(test.All) defer tab.Close()
fillTable(tab, test.All)
// check that closest(Target, N) returns nodes // check that closest(Target, N) returns nodes
result := tab.closest(test.Target, test.N).entries result := tab.closest(test.Target, test.N).entries
@ -234,13 +234,13 @@ func TestTable_ReadRandomNodesGetAll(t *testing.T) {
test := func(buf []*enode.Node) bool { test := func(buf []*enode.Node) bool {
transport := newPingRecorder() transport := newPingRecorder()
tab, db := newTestTable(transport) tab, db := newTestTable(transport)
defer tab.Close()
defer db.Close() defer db.Close()
defer tab.Close()
<-tab.initDone <-tab.initDone
for i := 0; i < len(buf); i++ { for i := 0; i < len(buf); i++ {
ld := cfg.Rand.Intn(len(tab.buckets)) ld := cfg.Rand.Intn(len(tab.buckets))
tab.stuff([]*node{nodeAtDistance(tab.self().ID(), ld, intIP(ld))}) fillTable(tab, []*node{nodeAtDistance(tab.self().ID(), ld, intIP(ld))})
} }
gotN := tab.ReadRandomNodes(buf) gotN := tab.ReadRandomNodes(buf)
if gotN != tab.len() { if gotN != tab.len() {
@ -272,16 +272,19 @@ func (*closeTest) Generate(rand *rand.Rand, size int) reflect.Value {
N: rand.Intn(bucketSize), N: rand.Intn(bucketSize),
} }
for _, id := range gen([]enode.ID{}, rand).([]enode.ID) { for _, id := range gen([]enode.ID{}, rand).([]enode.ID) {
n := enode.SignNull(new(enr.Record), id) r := new(enr.Record)
t.All = append(t.All, wrapNode(n)) r.Set(enr.IP(genIP(rand)))
n := wrapNode(enode.SignNull(r, id))
n.livenessChecks = 1
t.All = append(t.All, n)
} }
return reflect.ValueOf(t) return reflect.ValueOf(t)
} }
func TestTable_Lookup(t *testing.T) { func TestTable_Lookup(t *testing.T) {
tab, db := newTestTable(lookupTestnet) tab, db := newTestTable(lookupTestnet)
defer tab.Close()
defer db.Close() defer db.Close()
defer tab.Close()
// lookup on empty table returns no nodes // lookup on empty table returns no nodes
if results := tab.lookup(lookupTestnet.target, false); len(results) > 0 { if results := tab.lookup(lookupTestnet.target, false); len(results) > 0 {
@ -289,8 +292,9 @@ func TestTable_Lookup(t *testing.T) {
} }
// seed table with initial node (otherwise lookup will terminate immediately) // seed table with initial node (otherwise lookup will terminate immediately)
seedKey, _ := decodePubkey(lookupTestnet.dists[256][0]) seedKey, _ := decodePubkey(lookupTestnet.dists[256][0])
seed := wrapNode(enode.NewV4(seedKey, net.IP{}, 0, 256)) seed := wrapNode(enode.NewV4(seedKey, net.IP{127, 0, 0, 1}, 0, 256))
tab.stuff([]*node{seed}) seed.livenessChecks = 1
fillTable(tab, []*node{seed})
results := tab.lookup(lookupTestnet.target, true) results := tab.lookup(lookupTestnet.target, true)
t.Logf("results:") t.Logf("results:")
@ -578,6 +582,12 @@ func gen(typ interface{}, rand *rand.Rand) interface{} {
return v.Interface() return v.Interface()
} }
func genIP(rand *rand.Rand) net.IP {
ip := make(net.IP, 4)
rand.Read(ip)
return ip
}
func quickcfg() *quick.Config { func quickcfg() *quick.Config {
return &quick.Config{ return &quick.Config{
MaxCount: 5000, MaxCount: 5000,

View File

@ -83,6 +83,23 @@ func fillBucket(tab *Table, n *node) (last *node) {
return b.entries[bucketSize-1] return b.entries[bucketSize-1]
} }
// 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) {
tab.mutex.Lock()
defer tab.mutex.Unlock()
for _, n := range nodes {
if n.ID() == tab.self().ID() {
continue // don't add self
}
b := tab.bucket(n.ID())
if len(b.entries) < bucketSize {
tab.bumpOrAdd(b, n)
}
}
}
type pingRecorder struct { type pingRecorder struct {
mu sync.Mutex mu sync.Mutex
dead, pinged map[enode.ID]bool dead, pinged map[enode.ID]bool
@ -109,10 +126,6 @@ func (t *pingRecorder) findnode(toid enode.ID, toaddr *net.UDPAddr, target encPu
return nil, nil return nil, nil
} }
func (t *pingRecorder) waitping(from enode.ID) error {
return nil // remote always pings
}
func (t *pingRecorder) ping(toid enode.ID, toaddr *net.UDPAddr) error { func (t *pingRecorder) ping(toid enode.ID, toaddr *net.UDPAddr) error {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()

View File

@ -67,6 +67,8 @@ const (
// RPC request structures // RPC request structures
type ( type (
ping struct { ping struct {
senderKey *ecdsa.PublicKey // filled in by preverify
Version uint Version uint
From, To rpcEndpoint From, To rpcEndpoint
Expiration uint64 Expiration uint64
@ -155,8 +157,13 @@ func nodeToRPC(n *node) rpcNode {
return rpcNode{ID: ekey, IP: n.IP(), UDP: uint16(n.UDP()), TCP: uint16(n.TCP())} return rpcNode{ID: ekey, IP: n.IP(), UDP: uint16(n.UDP()), TCP: uint16(n.TCP())}
} }
// packet is implemented by all protocol messages.
type packet interface { type packet interface {
handle(t *udp, from *net.UDPAddr, fromKey encPubkey, mac []byte) error // preverify checks whether the packet is valid and should be handled at all.
preverify(t *udp, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error
// handle handles the packet.
handle(t *udp, from *net.UDPAddr, fromID enode.ID, mac []byte)
// name returns the name of the packet for logging purposes.
name() string name() string
} }
@ -177,43 +184,48 @@ type udp struct {
tab *Table tab *Table
wg sync.WaitGroup wg sync.WaitGroup
addpending chan *pending addReplyMatcher chan *replyMatcher
gotreply chan reply gotreply chan reply
closing chan struct{} closing chan struct{}
} }
// pending represents a pending reply. // pending represents a pending reply.
// //
// some implementations of the protocol wish to send more than one // Some implementations of the protocol wish to send more than one
// reply packet to findnode. in general, any neighbors packet cannot // reply packet to findnode. In general, any neighbors packet cannot
// be matched up with a specific findnode packet. // be matched up with a specific findnode packet.
// //
// our implementation handles this by storing a callback function for // Our implementation handles this by storing a callback function for
// each pending reply. incoming packets from a node are dispatched // each pending reply. Incoming packets from a node are dispatched
// to all the callback functions for that node. // to all callback functions for that node.
type pending struct { type replyMatcher struct {
// these fields must match in the reply. // these fields must match in the reply.
from enode.ID from enode.ID
ip net.IP
ptype byte ptype byte
// time when the request must complete // time when the request must complete
deadline time.Time deadline time.Time
// callback is called when a matching reply arrives. if it returns // callback is called when a matching reply arrives. If it returns matched == true, the
// true, the callback is removed from the pending reply queue. // reply was acceptable. The second return value indicates whether the callback should
// if it returns false, the reply is considered incomplete and // be removed from the pending reply queue. If it returns false, the reply is considered
// the callback will be invoked again for the next matching reply. // incomplete and the callback will be invoked again for the next matching reply.
callback func(resp interface{}) (done bool) callback replyMatchFunc
// errc receives nil when the callback indicates completion or an // errc receives nil when the callback indicates completion or an
// error if no further reply is received within the timeout. // error if no further reply is received within the timeout.
errc chan<- error errc chan<- error
} }
type replyMatchFunc func(interface{}) (matched bool, requestDone bool)
type reply struct { type reply struct {
from enode.ID from enode.ID
ip net.IP
ptype byte ptype byte
data interface{} data packet
// loop indicates whether there was // loop indicates whether there was
// a matching request by sending on this channel. // a matching request by sending on this channel.
matched chan<- bool matched chan<- bool
@ -254,7 +266,7 @@ func newUDP(c conn, ln *enode.LocalNode, cfg Config) (*Table, *udp, error) {
db: ln.Database(), db: ln.Database(),
closing: make(chan struct{}), closing: make(chan struct{}),
gotreply: make(chan reply), gotreply: make(chan reply),
addpending: make(chan *pending), addReplyMatcher: make(chan *replyMatcher),
} }
tab, err := newTable(udp, ln.Database(), cfg.Bootnodes) tab, err := newTable(udp, ln.Database(), cfg.Bootnodes)
if err != nil { if err != nil {
@ -304,35 +316,37 @@ func (t *udp) sendPing(toid enode.ID, toaddr *net.UDPAddr, callback func()) <-ch
errc <- err errc <- err
return errc return errc
} }
errc := t.pending(toid, pongPacket, func(p interface{}) bool { // Add a matcher for the reply to the pending reply queue. Pongs are matched if they
ok := bytes.Equal(p.(*pong).ReplyTok, hash) // reference the ping we're about to send.
if ok && callback != nil { errc := t.pending(toid, toaddr.IP, pongPacket, func(p interface{}) (matched bool, requestDone bool) {
matched = bytes.Equal(p.(*pong).ReplyTok, hash)
if matched && callback != nil {
callback() callback()
} }
return ok return matched, matched
}) })
// Send the packet.
t.localNode.UDPContact(toaddr) t.localNode.UDPContact(toaddr)
t.write(toaddr, req.name(), packet) t.write(toaddr, toid, req.name(), packet)
return errc return errc
} }
func (t *udp) waitping(from enode.ID) error {
return <-t.pending(from, pingPacket, func(interface{}) bool { return true })
}
// findnode sends a findnode request to the given node and waits until // findnode sends a findnode request to the given node and waits until
// the node has sent up to k neighbors. // the node has sent up to k neighbors.
func (t *udp) findnode(toid enode.ID, toaddr *net.UDPAddr, target encPubkey) ([]*node, error) { func (t *udp) findnode(toid enode.ID, toaddr *net.UDPAddr, target encPubkey) ([]*node, error) {
// If we haven't seen a ping from the destination node for a while, it won't remember // If we haven't seen a ping from the destination node for a while, it won't remember
// our endpoint proof and reject findnode. Solicit a ping first. // our endpoint proof and reject findnode. Solicit a ping first.
if time.Since(t.db.LastPingReceived(toid)) > bondExpiration { if time.Since(t.db.LastPingReceived(toid, toaddr.IP)) > bondExpiration {
t.ping(toid, toaddr) t.ping(toid, toaddr)
t.waitping(toid) // Wait for them to ping back and process our pong.
time.Sleep(respTimeout)
} }
// 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([]*node, 0, bucketSize)
nreceived := 0 nreceived := 0
errc := t.pending(toid, neighborsPacket, func(r interface{}) bool { errc := t.pending(toid, toaddr.IP, neighborsPacket, func(r interface{}) (matched bool, requestDone bool) {
reply := r.(*neighbors) reply := r.(*neighbors)
for _, rn := range reply.Nodes { for _, rn := range reply.Nodes {
nreceived++ nreceived++
@ -343,22 +357,22 @@ func (t *udp) findnode(toid enode.ID, toaddr *net.UDPAddr, target encPubkey) ([]
} }
nodes = append(nodes, n) nodes = append(nodes, n)
} }
return nreceived >= bucketSize return true, nreceived >= bucketSize
}) })
t.send(toaddr, findnodePacket, &findnode{ t.send(toaddr, toid, findnodePacket, &findnode{
Target: target, Target: target,
Expiration: uint64(time.Now().Add(expiration).Unix()), Expiration: uint64(time.Now().Add(expiration).Unix()),
}) })
return nodes, <-errc return nodes, <-errc
} }
// pending adds a reply callback to the pending reply queue. // pending adds a reply matcher to the pending reply queue.
// see the documentation of type pending for a detailed explanation. // see the documentation of type replyMatcher for a detailed explanation.
func (t *udp) pending(id enode.ID, ptype byte, callback func(interface{}) bool) <-chan error { func (t *udp) pending(id enode.ID, ip net.IP, ptype byte, callback replyMatchFunc) <-chan error {
ch := make(chan error, 1) ch := make(chan error, 1)
p := &pending{from: id, ptype: ptype, callback: callback, errc: ch} p := &replyMatcher{from: id, ip: ip, ptype: ptype, callback: callback, errc: ch}
select { select {
case t.addpending <- p: case t.addReplyMatcher <- p:
// loop will handle it // loop will handle it
case <-t.closing: case <-t.closing:
ch <- errClosed ch <- errClosed
@ -366,10 +380,12 @@ func (t *udp) pending(id enode.ID, ptype byte, callback func(interface{}) bool)
return ch return ch
} }
func (t *udp) handleReply(from enode.ID, ptype byte, req packet) bool { // handleReply dispatches a reply packet, invoking reply matchers. It returns
// whether any matcher considered the packet acceptable.
func (t *udp) handleReply(from enode.ID, fromIP net.IP, ptype byte, req packet) bool {
matched := make(chan bool, 1) matched := make(chan bool, 1)
select { select {
case t.gotreply <- reply{from, ptype, req, matched}: case t.gotreply <- reply{from, fromIP, ptype, req, matched}:
// loop will handle it // loop will handle it
return <-matched return <-matched
case <-t.closing: case <-t.closing:
@ -385,7 +401,7 @@ func (t *udp) loop() {
var ( var (
plist = list.New() plist = list.New()
timeout = time.NewTimer(0) timeout = time.NewTimer(0)
nextTimeout *pending // head of plist when timeout was last reset nextTimeout *replyMatcher // head of plist when timeout was last reset
contTimeouts = 0 // number of continuous timeouts to do NTP checks contTimeouts = 0 // number of continuous timeouts to do NTP checks
ntpWarnTime = time.Unix(0, 0) ntpWarnTime = time.Unix(0, 0)
) )
@ -399,7 +415,7 @@ func (t *udp) loop() {
// Start the timer so it fires when the next pending reply has expired. // Start the timer so it fires when the next pending reply has expired.
now := time.Now() now := time.Now()
for el := plist.Front(); el != nil; el = el.Next() { for el := plist.Front(); el != nil; el = el.Next() {
nextTimeout = el.Value.(*pending) nextTimeout = el.Value.(*replyMatcher)
if dist := nextTimeout.deadline.Sub(now); dist < 2*respTimeout { if dist := nextTimeout.deadline.Sub(now); dist < 2*respTimeout {
timeout.Reset(dist) timeout.Reset(dist)
return return
@ -420,25 +436,23 @@ func (t *udp) loop() {
select { select {
case <-t.closing: case <-t.closing:
for el := plist.Front(); el != nil; el = el.Next() { for el := plist.Front(); el != nil; el = el.Next() {
el.Value.(*pending).errc <- errClosed el.Value.(*replyMatcher).errc <- errClosed
} }
return return
case p := <-t.addpending: case p := <-t.addReplyMatcher:
p.deadline = time.Now().Add(respTimeout) p.deadline = time.Now().Add(respTimeout)
plist.PushBack(p) plist.PushBack(p)
case r := <-t.gotreply: case r := <-t.gotreply:
var matched bool var matched bool // whether any replyMatcher considered the reply acceptable.
for el := plist.Front(); el != nil; el = el.Next() { for el := plist.Front(); el != nil; el = el.Next() {
p := el.Value.(*pending) p := el.Value.(*replyMatcher)
if p.from == r.from && p.ptype == r.ptype { if p.from == r.from && p.ptype == r.ptype && p.ip.Equal(r.ip) {
matched = true ok, requestDone := p.callback(r.data)
// Remove the matcher if its callback indicates matched = matched || ok
// that all replies have been received. This is // Remove the matcher if callback indicates that all replies have been received.
// required for packet types that expect multiple if requestDone {
// reply packets.
if p.callback(r.data) {
p.errc <- nil p.errc <- nil
plist.Remove(el) plist.Remove(el)
} }
@ -453,7 +467,7 @@ func (t *udp) loop() {
// Notify and remove callbacks whose deadline is in the past. // Notify and remove callbacks whose deadline is in the past.
for el := plist.Front(); el != nil; el = el.Next() { for el := plist.Front(); el != nil; el = el.Next() {
p := el.Value.(*pending) p := el.Value.(*replyMatcher)
if now.After(p.deadline) || now.Equal(p.deadline) { if now.After(p.deadline) || now.Equal(p.deadline) {
p.errc <- errTimeout p.errc <- errTimeout
plist.Remove(el) plist.Remove(el)
@ -504,17 +518,17 @@ func init() {
} }
} }
func (t *udp) send(toaddr *net.UDPAddr, ptype byte, req packet) ([]byte, error) { func (t *udp) send(toaddr *net.UDPAddr, toid enode.ID, ptype byte, req packet) ([]byte, error) {
packet, hash, err := encodePacket(t.priv, ptype, req) packet, hash, err := encodePacket(t.priv, ptype, req)
if err != nil { if err != nil {
return hash, err return hash, err
} }
return hash, t.write(toaddr, req.name(), packet) return hash, t.write(toaddr, toid, req.name(), packet)
} }
func (t *udp) write(toaddr *net.UDPAddr, what string, packet []byte) error { func (t *udp) write(toaddr *net.UDPAddr, toid enode.ID, what string, packet []byte) error {
_, err := t.conn.WriteToUDP(packet, toaddr) _, err := t.conn.WriteToUDP(packet, toaddr)
log.Trace(">> "+what, "addr", toaddr, "err", err) log.Trace(">> "+what, "id", toid, "addr", toaddr, "err", err)
return err return err
} }
@ -573,13 +587,19 @@ func (t *udp) readLoop(unhandled chan<- ReadPacket) {
} }
func (t *udp) handlePacket(from *net.UDPAddr, buf []byte) error { func (t *udp) handlePacket(from *net.UDPAddr, buf []byte) error {
packet, fromID, hash, err := decodePacket(buf) packet, fromKey, hash, err := decodePacket(buf)
if err != nil { if err != nil {
log.Debug("Bad discv4 packet", "addr", from, "err", err) log.Debug("Bad discv4 packet", "addr", from, "err", err)
return err return err
} }
err = packet.handle(t, from, fromID, hash) fromID := fromKey.id()
log.Trace("<< "+packet.name(), "addr", from, "err", err) if err == nil {
err = packet.preverify(t, from, fromID, fromKey)
}
log.Trace("<< "+packet.name(), "id", fromID, "addr", from, "err", err)
if err == nil {
packet.handle(t, from, fromID, hash)
}
return err return err
} }
@ -615,54 +635,67 @@ func decodePacket(buf []byte) (packet, encPubkey, []byte, error) {
return req, fromKey, hash, err return req, fromKey, hash, err
} }
func (req *ping) handle(t *udp, from *net.UDPAddr, fromKey encPubkey, mac []byte) error { // Packet Handlers
func (req *ping) preverify(t *udp, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error {
if expired(req.Expiration) { if expired(req.Expiration) {
return errExpired return errExpired
} }
key, err := decodePubkey(fromKey) key, err := decodePubkey(fromKey)
if err != nil { if err != nil {
return fmt.Errorf("invalid public key: %v", err) return errors.New("invalid public key")
} }
t.send(from, pongPacket, &pong{ req.senderKey = key
return nil
}
func (req *ping) handle(t *udp, from *net.UDPAddr, fromID enode.ID, mac []byte) {
// Reply.
t.send(from, fromID, pongPacket, &pong{
To: makeEndpoint(from, req.From.TCP), To: makeEndpoint(from, req.From.TCP),
ReplyTok: mac, ReplyTok: mac,
Expiration: uint64(time.Now().Add(expiration).Unix()), Expiration: uint64(time.Now().Add(expiration).Unix()),
}) })
n := wrapNode(enode.NewV4(key, from.IP, int(req.From.TCP), from.Port))
t.handleReply(n.ID(), pingPacket, req) // Ping back if our last pong on file is too far in the past.
if time.Since(t.db.LastPongReceived(n.ID())) > bondExpiration { n := wrapNode(enode.NewV4(req.senderKey, from.IP, int(req.From.TCP), from.Port))
t.sendPing(n.ID(), from, func() { t.tab.addThroughPing(n) }) if time.Since(t.db.LastPongReceived(n.ID(), from.IP)) > bondExpiration {
t.sendPing(fromID, from, func() {
t.tab.addThroughPing(n)
})
} else { } else {
t.tab.addThroughPing(n) t.tab.addThroughPing(n)
} }
// Update node database and endpoint predictor.
t.db.UpdateLastPingReceived(n.ID(), from.IP, time.Now())
t.localNode.UDPEndpointStatement(from, &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)}) t.localNode.UDPEndpointStatement(from, &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)})
t.db.UpdateLastPingReceived(n.ID(), time.Now())
return nil
} }
func (req *ping) name() string { return "PING/v4" } func (req *ping) name() string { return "PING/v4" }
func (req *pong) handle(t *udp, from *net.UDPAddr, fromKey encPubkey, mac []byte) error { func (req *pong) preverify(t *udp, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error {
if expired(req.Expiration) { if expired(req.Expiration) {
return errExpired return errExpired
} }
fromID := fromKey.id() if !t.handleReply(fromID, from.IP, pongPacket, req) {
if !t.handleReply(fromID, pongPacket, req) {
return errUnsolicitedReply return errUnsolicitedReply
} }
t.localNode.UDPEndpointStatement(from, &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)})
t.db.UpdateLastPongReceived(fromID, time.Now())
return nil return nil
} }
func (req *pong) handle(t *udp, from *net.UDPAddr, fromID enode.ID, mac []byte) {
t.localNode.UDPEndpointStatement(from, &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)})
t.db.UpdateLastPongReceived(fromID, from.IP, time.Now())
}
func (req *pong) name() string { return "PONG/v4" } func (req *pong) name() string { return "PONG/v4" }
func (req *findnode) handle(t *udp, from *net.UDPAddr, fromKey encPubkey, mac []byte) error { func (req *findnode) preverify(t *udp, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error {
if expired(req.Expiration) { if expired(req.Expiration) {
return errExpired return errExpired
} }
fromID := fromKey.id() if time.Since(t.db.LastPongReceived(fromID, from.IP)) > bondExpiration {
if time.Since(t.db.LastPongReceived(fromID)) > bondExpiration {
// No endpoint proof pong exists, we don't process the packet. This prevents an // No endpoint proof pong exists, we don't process the packet. This prevents an
// attack vector where the discovery protocol could be used to amplify traffic in a // attack vector where the discovery protocol could be used to amplify traffic in a
// DDOS attack. A malicious actor would send a findnode request with the IP address // DDOS attack. A malicious actor would send a findnode request with the IP address
@ -671,43 +704,50 @@ func (req *findnode) handle(t *udp, from *net.UDPAddr, fromKey encPubkey, mac []
// findnode) to the victim. // findnode) to the victim.
return errUnknownNode return errUnknownNode
} }
return nil
}
func (req *findnode) handle(t *udp, from *net.UDPAddr, fromID enode.ID, mac []byte) {
// Determine closest nodes.
target := enode.ID(crypto.Keccak256Hash(req.Target[:])) target := enode.ID(crypto.Keccak256Hash(req.Target[:]))
t.tab.mutex.Lock() t.tab.mutex.Lock()
closest := t.tab.closest(target, bucketSize).entries closest := t.tab.closest(target, bucketSize).entries
t.tab.mutex.Unlock() t.tab.mutex.Unlock()
p := neighbors{Expiration: uint64(time.Now().Add(expiration).Unix())}
var sent bool
// Send neighbors in chunks with at most maxNeighbors per packet // Send neighbors in chunks with at most maxNeighbors per packet
// to stay below the 1280 byte limit. // to stay below the 1280 byte limit.
p := neighbors{Expiration: uint64(time.Now().Add(expiration).Unix())}
var sent bool
for _, n := range closest { for _, n := range closest {
if netutil.CheckRelayIP(from.IP, n.IP()) == nil { if netutil.CheckRelayIP(from.IP, n.IP()) == nil {
p.Nodes = append(p.Nodes, nodeToRPC(n)) p.Nodes = append(p.Nodes, nodeToRPC(n))
} }
if len(p.Nodes) == maxNeighbors { if len(p.Nodes) == maxNeighbors {
t.send(from, neighborsPacket, &p) t.send(from, fromID, neighborsPacket, &p)
p.Nodes = p.Nodes[:0] p.Nodes = p.Nodes[:0]
sent = true sent = true
} }
} }
if len(p.Nodes) > 0 || !sent { if len(p.Nodes) > 0 || !sent {
t.send(from, neighborsPacket, &p) t.send(from, fromID, neighborsPacket, &p)
} }
return nil
} }
func (req *findnode) name() string { return "FINDNODE/v4" } func (req *findnode) name() string { return "FINDNODE/v4" }
func (req *neighbors) handle(t *udp, from *net.UDPAddr, fromKey encPubkey, mac []byte) error { func (req *neighbors) preverify(t *udp, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error {
if expired(req.Expiration) { if expired(req.Expiration) {
return errExpired return errExpired
} }
if !t.handleReply(fromKey.id(), neighborsPacket, req) { if !t.handleReply(fromID, from.IP, neighborsPacket, req) {
return errUnsolicitedReply return errUnsolicitedReply
} }
return nil return nil
} }
func (req *neighbors) handle(t *udp, from *net.UDPAddr, fromID enode.ID, mac []byte) {
}
func (req *neighbors) name() string { return "NEIGHBORS/v4" } func (req *neighbors) name() string { return "NEIGHBORS/v4" }
func expired(ts uint64) bool { func expired(ts uint64) bool {

View File

@ -19,6 +19,7 @@ package discover
import ( import (
"bytes" "bytes"
"crypto/ecdsa" "crypto/ecdsa"
crand "crypto/rand"
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"errors" "errors"
@ -57,6 +58,7 @@ type udpTest struct {
t *testing.T t *testing.T
pipe *dgramPipe pipe *dgramPipe
table *Table table *Table
db *enode.DB
udp *udp udp *udp
sent [][]byte sent [][]byte
localkey, remotekey *ecdsa.PrivateKey localkey, remotekey *ecdsa.PrivateKey
@ -71,22 +73,32 @@ func newUDPTest(t *testing.T) *udpTest {
remotekey: newkey(), remotekey: newkey(),
remoteaddr: &net.UDPAddr{IP: net.IP{10, 0, 1, 99}, Port: 30303}, remoteaddr: &net.UDPAddr{IP: net.IP{10, 0, 1, 99}, Port: 30303},
} }
db, _ := enode.OpenDB("") test.db, _ = enode.OpenDB("")
ln := enode.NewLocalNode(db, test.localkey) ln := enode.NewLocalNode(test.db, test.localkey)
test.table, test.udp, _ = newUDP(test.pipe, ln, Config{PrivateKey: test.localkey}) test.table, test.udp, _ = newUDP(test.pipe, ln, Config{PrivateKey: test.localkey})
// Wait for initial refresh so the table doesn't send unexpected findnode. // Wait for initial refresh so the table doesn't send unexpected findnode.
<-test.table.initDone <-test.table.initDone
return test return test
} }
func (test *udpTest) close() {
test.table.Close()
test.db.Close()
}
// handles a packet as if it had been sent to the transport. // handles a packet as if it had been sent to the transport.
func (test *udpTest) packetIn(wantError error, ptype byte, data packet) error { func (test *udpTest) packetIn(wantError error, ptype byte, data packet) error {
enc, _, err := encodePacket(test.remotekey, ptype, data) return test.packetInFrom(wantError, test.remotekey, test.remoteaddr, ptype, 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 packet) error {
enc, _, err := encodePacket(key, ptype, data)
if err != nil { if err != nil {
return test.errorf("packet (%d) encode error: %v", ptype, err) return test.errorf("packet (%d) encode error: %v", ptype, err)
} }
test.sent = append(test.sent, enc) test.sent = append(test.sent, enc)
if err = test.udp.handlePacket(test.remoteaddr, enc); err != wantError { if err = test.udp.handlePacket(addr, enc); err != wantError {
return test.errorf("error mismatch: got %q, want %q", err, wantError) return test.errorf("error mismatch: got %q, want %q", err, wantError)
} }
return nil return nil
@ -94,19 +106,19 @@ func (test *udpTest) packetIn(wantError error, ptype byte, data packet) error {
// waits for a packet to be sent by the transport. // waits for a packet to be sent by the transport.
// validate should have type func(*udpTest, X) error, where X is a packet type. // validate should have type func(*udpTest, X) error, where X is a packet type.
func (test *udpTest) waitPacketOut(validate interface{}) ([]byte, error) { func (test *udpTest) waitPacketOut(validate interface{}) (*net.UDPAddr, []byte, error) {
dgram := test.pipe.waitPacketOut() dgram := test.pipe.waitPacketOut()
p, _, hash, err := decodePacket(dgram) p, _, hash, err := decodePacket(dgram.data)
if err != nil { if err != nil {
return hash, test.errorf("sent packet decode error: %v", err) return &dgram.to, hash, test.errorf("sent packet decode error: %v", err)
} }
fn := reflect.ValueOf(validate) fn := reflect.ValueOf(validate)
exptype := fn.Type().In(0) exptype := fn.Type().In(0)
if reflect.TypeOf(p) != exptype { if reflect.TypeOf(p) != exptype {
return hash, test.errorf("sent packet type mismatch, got: %v, want: %v", reflect.TypeOf(p), exptype) return &dgram.to, hash, test.errorf("sent packet type mismatch, got: %v, want: %v", reflect.TypeOf(p), exptype)
} }
fn.Call([]reflect.Value{reflect.ValueOf(p)}) fn.Call([]reflect.Value{reflect.ValueOf(p)})
return hash, nil return &dgram.to, hash, nil
} }
func (test *udpTest) errorf(format string, args ...interface{}) error { func (test *udpTest) errorf(format string, args ...interface{}) error {
@ -125,7 +137,7 @@ func (test *udpTest) errorf(format string, args ...interface{}) error {
func TestUDP_packetErrors(t *testing.T) { func TestUDP_packetErrors(t *testing.T) {
test := newUDPTest(t) test := newUDPTest(t)
defer test.table.Close() defer test.close()
test.packetIn(errExpired, pingPacket, &ping{From: testRemote, To: testLocalAnnounced, Version: 4}) test.packetIn(errExpired, pingPacket, &ping{From: testRemote, To: testLocalAnnounced, Version: 4})
test.packetIn(errUnsolicitedReply, pongPacket, &pong{ReplyTok: []byte{}, Expiration: futureExp}) test.packetIn(errUnsolicitedReply, pongPacket, &pong{ReplyTok: []byte{}, Expiration: futureExp})
@ -136,7 +148,7 @@ func TestUDP_packetErrors(t *testing.T) {
func TestUDP_pingTimeout(t *testing.T) { func TestUDP_pingTimeout(t *testing.T) {
t.Parallel() t.Parallel()
test := newUDPTest(t) test := newUDPTest(t)
defer test.table.Close() defer test.close()
toaddr := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222} toaddr := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222}
toid := enode.ID{1, 2, 3, 4} toid := enode.ID{1, 2, 3, 4}
@ -148,7 +160,7 @@ func TestUDP_pingTimeout(t *testing.T) {
func TestUDP_responseTimeouts(t *testing.T) { func TestUDP_responseTimeouts(t *testing.T) {
t.Parallel() t.Parallel()
test := newUDPTest(t) test := newUDPTest(t)
defer test.table.Close() defer test.close()
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
randomDuration := func(max time.Duration) time.Duration { randomDuration := func(max time.Duration) time.Duration {
@ -166,20 +178,20 @@ func TestUDP_responseTimeouts(t *testing.T) {
// with ptype <= 128 will not get a reply and should time out. // with ptype <= 128 will not get a reply and should time out.
// For all other requests, a reply is scheduled to arrive // For all other requests, a reply is scheduled to arrive
// within the timeout window. // within the timeout window.
p := &pending{ p := &replyMatcher{
ptype: byte(rand.Intn(255)), ptype: byte(rand.Intn(255)),
callback: func(interface{}) bool { return true }, callback: func(interface{}) (bool, bool) { return true, true },
} }
binary.BigEndian.PutUint64(p.from[:], uint64(i)) binary.BigEndian.PutUint64(p.from[:], uint64(i))
if p.ptype <= 128 { if p.ptype <= 128 {
p.errc = timeoutErr p.errc = timeoutErr
test.udp.addpending <- p test.udp.addReplyMatcher <- p
nTimeouts++ nTimeouts++
} else { } else {
p.errc = nilErr p.errc = nilErr
test.udp.addpending <- p test.udp.addReplyMatcher <- p
time.AfterFunc(randomDuration(60*time.Millisecond), func() { time.AfterFunc(randomDuration(60*time.Millisecond), func() {
if !test.udp.handleReply(p.from, p.ptype, nil) { if !test.udp.handleReply(p.from, p.ip, p.ptype, nil) {
t.Logf("not matched: %v", p) t.Logf("not matched: %v", p)
} }
}) })
@ -220,7 +232,7 @@ func TestUDP_responseTimeouts(t *testing.T) {
func TestUDP_findnodeTimeout(t *testing.T) { func TestUDP_findnodeTimeout(t *testing.T) {
t.Parallel() t.Parallel()
test := newUDPTest(t) test := newUDPTest(t)
defer test.table.Close() defer test.close()
toaddr := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222} toaddr := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222}
toid := enode.ID{1, 2, 3, 4} toid := enode.ID{1, 2, 3, 4}
@ -236,50 +248,65 @@ func TestUDP_findnodeTimeout(t *testing.T) {
func TestUDP_findnode(t *testing.T) { func TestUDP_findnode(t *testing.T) {
test := newUDPTest(t) test := newUDPTest(t)
defer test.table.Close() defer test.close()
// put a few nodes into the table. their exact // put a few nodes into the table. their exact
// distribution shouldn't matter much, although we need to // distribution shouldn't matter much, although we need to
// take care not to overflow any bucket. // take care not to overflow any bucket.
nodes := &nodesByDistance{target: testTarget.id()} nodes := &nodesByDistance{target: testTarget.id()}
for i := 0; i < bucketSize; i++ { live := make(map[enode.ID]bool)
numCandidates := 2 * bucketSize
for i := 0; i < numCandidates; i++ {
key := newkey() key := newkey()
n := wrapNode(enode.NewV4(&key.PublicKey, net.IP{10, 13, 0, 1}, 0, i)) ip := net.IP{10, 13, 0, byte(i)}
nodes.push(n, bucketSize) n := wrapNode(enode.NewV4(&key.PublicKey, ip, 0, 2000))
// Ensure half of table content isn't verified live yet.
if i > numCandidates/2 {
n.livenessChecks = 1
live[n.ID()] = true
} }
test.table.stuff(nodes.entries) nodes.push(n, numCandidates)
}
fillTable(test.table, nodes.entries)
// ensure there's a bond with the test node, // ensure there's a bond with the test node,
// findnode won't be accepted otherwise. // findnode won't be accepted otherwise.
remoteID := encodePubkey(&test.remotekey.PublicKey).id() remoteID := encodePubkey(&test.remotekey.PublicKey).id()
test.table.db.UpdateLastPongReceived(remoteID, time.Now()) test.table.db.UpdateLastPongReceived(remoteID, test.remoteaddr.IP, time.Now())
// check that closest neighbors are returned. // check that closest neighbors are returned.
test.packetIn(nil, findnodePacket, &findnode{Target: testTarget, Expiration: futureExp})
expected := test.table.closest(testTarget.id(), bucketSize) expected := test.table.closest(testTarget.id(), bucketSize)
test.packetIn(nil, findnodePacket, &findnode{Target: testTarget, Expiration: futureExp})
waitNeighbors := func(want []*node) { waitNeighbors := func(want []*node) {
test.waitPacketOut(func(p *neighbors) { test.waitPacketOut(func(p *neighbors) {
if len(p.Nodes) != len(want) { if len(p.Nodes) != len(want) {
t.Errorf("wrong number of results: got %d, want %d", len(p.Nodes), bucketSize) t.Errorf("wrong number of results: got %d, want %d", len(p.Nodes), bucketSize)
} }
for i := range p.Nodes { for i, n := range p.Nodes {
if p.Nodes[i].ID.id() != want[i].ID() { if n.ID.id() != want[i].ID() {
t.Errorf("result mismatch at %d:\n got: %v\n want: %v", i, p.Nodes[i], expected.entries[i]) t.Errorf("result mismatch at %d:\n got: %v\n want: %v", i, n, expected.entries[i])
}
if !live[n.ID.id()] {
t.Errorf("result includes dead node %v", n.ID.id())
} }
} }
}) })
} }
waitNeighbors(expected.entries[:maxNeighbors]) // Receive replies.
waitNeighbors(expected.entries[maxNeighbors:]) want := expected.entries
if len(want) > maxNeighbors {
waitNeighbors(want[:maxNeighbors])
want = want[maxNeighbors:]
}
waitNeighbors(want)
} }
func TestUDP_findnodeMultiReply(t *testing.T) { func TestUDP_findnodeMultiReply(t *testing.T) {
test := newUDPTest(t) test := newUDPTest(t)
defer test.table.Close() defer test.close()
rid := enode.PubkeyToIDV4(&test.remotekey.PublicKey) rid := enode.PubkeyToIDV4(&test.remotekey.PublicKey)
test.table.db.UpdateLastPingReceived(rid, time.Now()) test.table.db.UpdateLastPingReceived(rid, test.remoteaddr.IP, time.Now())
// queue a pending findnode request // queue a pending findnode request
resultc, errc := make(chan []*node), make(chan error) resultc, errc := make(chan []*node), make(chan error)
@ -329,11 +356,40 @@ func TestUDP_findnodeMultiReply(t *testing.T) {
} }
} }
func TestUDP_pingMatch(t *testing.T) {
test := newUDPTest(t)
defer test.close()
randToken := make([]byte, 32)
crand.Read(randToken)
test.packetIn(nil, pingPacket, &ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
test.waitPacketOut(func(*pong) error { return nil })
test.waitPacketOut(func(*ping) error { return nil })
test.packetIn(errUnsolicitedReply, pongPacket, &pong{ReplyTok: randToken, To: testLocalAnnounced, Expiration: futureExp})
}
func TestUDP_pingMatchIP(t *testing.T) {
test := newUDPTest(t)
defer test.close()
test.packetIn(nil, pingPacket, &ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
test.waitPacketOut(func(*pong) error { return nil })
_, hash, _ := test.waitPacketOut(func(*ping) error { return nil })
wrongAddr := &net.UDPAddr{IP: net.IP{33, 44, 1, 2}, Port: 30000}
test.packetInFrom(errUnsolicitedReply, test.remotekey, wrongAddr, pongPacket, &pong{
ReplyTok: hash,
To: testLocalAnnounced,
Expiration: futureExp,
})
}
func TestUDP_successfulPing(t *testing.T) { func TestUDP_successfulPing(t *testing.T) {
test := newUDPTest(t) test := newUDPTest(t)
added := make(chan *node, 1) added := make(chan *node, 1)
test.table.nodeAddedHook = func(n *node) { added <- n } test.table.nodeAddedHook = func(n *node) { added <- n }
defer test.table.Close() defer test.close()
// The remote side sends a ping packet to initiate the exchange. // The remote side sends a ping packet to initiate the exchange.
go test.packetIn(nil, pingPacket, &ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp}) go test.packetIn(nil, pingPacket, &ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
@ -356,7 +412,7 @@ func TestUDP_successfulPing(t *testing.T) {
}) })
// remote is unknown, the table pings back. // remote is unknown, the table pings back.
hash, _ := test.waitPacketOut(func(p *ping) error { _, hash, _ := test.waitPacketOut(func(p *ping) error {
if !reflect.DeepEqual(p.From, test.udp.ourEndpoint()) { if !reflect.DeepEqual(p.From, test.udp.ourEndpoint()) {
t.Errorf("got ping.From %#v, want %#v", p.From, test.udp.ourEndpoint()) t.Errorf("got ping.From %#v, want %#v", p.From, test.udp.ourEndpoint())
} }
@ -510,7 +566,12 @@ type dgramPipe struct {
cond *sync.Cond cond *sync.Cond
closing chan struct{} closing chan struct{}
closed bool closed bool
queue [][]byte queue []dgram
}
type dgram struct {
to net.UDPAddr
data []byte
} }
func newpipe() *dgramPipe { func newpipe() *dgramPipe {
@ -531,7 +592,7 @@ func (c *dgramPipe) WriteToUDP(b []byte, to *net.UDPAddr) (n int, err error) {
if c.closed { if c.closed {
return 0, errors.New("closed") return 0, errors.New("closed")
} }
c.queue = append(c.queue, msg) c.queue = append(c.queue, dgram{*to, b})
c.cond.Signal() c.cond.Signal()
return len(b), nil return len(b), nil
} }
@ -556,7 +617,7 @@ func (c *dgramPipe) LocalAddr() net.Addr {
return &net.UDPAddr{IP: testLocal.IP, Port: int(testLocal.UDP)} return &net.UDPAddr{IP: testLocal.IP, Port: int(testLocal.UDP)}
} }
func (c *dgramPipe) waitPacketOut() []byte { func (c *dgramPipe) waitPacketOut() dgram {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
for len(c.queue) == 0 { for len(c.queue) == 0 {

View File

@ -21,11 +21,11 @@ import (
"crypto/rand" "crypto/rand"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"net"
"os" "os"
"sync" "sync"
"time" "time"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/errors" "github.com/syndtr/goleveldb/leveldb/errors"
@ -38,23 +38,30 @@ import (
// Keys in the node database. // Keys in the node database.
const ( const (
dbVersionKey = "version" // Version of the database to flush if changes dbVersionKey = "version" // Version of the database to flush if changes
dbItemPrefix = "n:" // Identifier to prefix node entries with dbNodePrefix = "n:" // Identifier to prefix node entries with
dbLocalPrefix = "local:"
dbDiscoverRoot = "v4"
dbDiscoverRoot = ":discover" // These fields are stored per ID and IP, the full key is "n:<ID>:v4:<IP>:findfail".
dbDiscoverSeq = dbDiscoverRoot + ":seq" // Use nodeItemKey to create those keys.
dbDiscoverPing = dbDiscoverRoot + ":lastping" dbNodeFindFails = "findfail"
dbDiscoverPong = dbDiscoverRoot + ":lastpong" dbNodePing = "lastping"
dbDiscoverFindFails = dbDiscoverRoot + ":findfail" dbNodePong = "lastpong"
dbLocalRoot = ":local" dbNodeSeq = "seq"
dbLocalSeq = dbLocalRoot + ":seq"
// Local information is keyed by ID only, the full key is "local:<ID>:seq".
// Use localItemKey to create those keys.
dbLocalSeq = "seq"
) )
var ( const (
dbNodeExpiration = 24 * time.Hour // Time after which an unseen node should be dropped. dbNodeExpiration = 24 * time.Hour // Time after which an unseen node should be dropped.
dbCleanupCycle = time.Hour // Time period for running the expiration task. dbCleanupCycle = time.Hour // Time period for running the expiration task.
dbVersion = 7 dbVersion = 8
) )
var zeroIP = make(net.IP, 16)
// DB is the node database, storing previously seen nodes and any collected metadata about // DB is the node database, storing previously seen nodes and any collected metadata about
// them for QoS purposes. // them for QoS purposes.
type DB struct { type DB struct {
@ -119,27 +126,58 @@ func newPersistentDB(path string) (*DB, error) {
return &DB{lvl: db, quit: make(chan struct{})}, nil return &DB{lvl: db, quit: make(chan struct{})}, nil
} }
// makeKey generates the leveldb key-blob from a node id and its particular // nodeKey returns the database key for a node record.
// field of interest. func nodeKey(id ID) []byte {
func makeKey(id ID, field string) []byte { key := append([]byte(dbNodePrefix), id[:]...)
if (id == ID{}) { key = append(key, ':')
return []byte(field) key = append(key, dbDiscoverRoot...)
} return key
return append([]byte(dbItemPrefix), append(id[:], field...)...)
} }
// splitKey tries to split a database key into a node id and a field part. // splitNodeKey returns the node ID of a key created by nodeKey.
func splitKey(key []byte) (id ID, field string) { func splitNodeKey(key []byte) (id ID, rest []byte) {
// If the key is not of a node, return it plainly if !bytes.HasPrefix(key, []byte(dbNodePrefix)) {
if !bytes.HasPrefix(key, []byte(dbItemPrefix)) { return ID{}, nil
return ID{}, string(key)
} }
// Otherwise split the id and field item := key[len(dbNodePrefix):]
item := key[len(dbItemPrefix):]
copy(id[:], item[:len(id)]) copy(id[:], item[:len(id)])
field = string(item[len(id):]) return id, item[len(id)+1:]
}
return id, field // nodeItemKey returns the database key for a node metadata field.
func nodeItemKey(id ID, ip net.IP, field string) []byte {
ip16 := ip.To16()
if ip16 == nil {
panic(fmt.Errorf("invalid IP (length %d)", len(ip)))
}
return bytes.Join([][]byte{nodeKey(id), ip16, []byte(field)}, []byte{':'})
}
// splitNodeItemKey returns the components of a key created by nodeItemKey.
func splitNodeItemKey(key []byte) (id ID, ip net.IP, field string) {
id, key = splitNodeKey(key)
// Skip discover root.
if string(key) == dbDiscoverRoot {
return id, nil, ""
}
key = key[len(dbDiscoverRoot)+1:]
// Split out the IP.
ip = net.IP(key[:16])
if ip4 := ip.To4(); ip4 != nil {
ip = ip4
}
key = key[16+1:]
// Field is the remainder of key.
field = string(key)
return id, ip, field
}
// localItemKey returns the key of a local node item.
func localItemKey(id ID, field string) []byte {
key := append([]byte(dbLocalPrefix), id[:]...)
key = append(key, ':')
key = append(key, field...)
return key
} }
// fetchInt64 retrieves an integer associated with a particular key. // fetchInt64 retrieves an integer associated with a particular key.
@ -181,7 +219,7 @@ func (db *DB) storeUint64(key []byte, n uint64) error {
// Node retrieves a node with a given id from the database. // Node retrieves a node with a given id from the database.
func (db *DB) Node(id ID) *Node { func (db *DB) Node(id ID) *Node {
blob, err := db.lvl.Get(makeKey(id, dbDiscoverRoot), nil) blob, err := db.lvl.Get(nodeKey(id), nil)
if err != nil { if err != nil {
return nil return nil
} }
@ -207,15 +245,15 @@ func (db *DB) UpdateNode(node *Node) error {
if err != nil { if err != nil {
return err return err
} }
if err := db.lvl.Put(makeKey(node.ID(), dbDiscoverRoot), blob, nil); err != nil { if err := db.lvl.Put(nodeKey(node.ID()), blob, nil); err != nil {
return err return err
} }
return db.storeUint64(makeKey(node.ID(), dbDiscoverSeq), node.Seq()) return db.storeUint64(nodeItemKey(node.ID(), zeroIP, dbNodeSeq), node.Seq())
} }
// NodeSeq returns the stored record sequence number of the given node. // NodeSeq returns the stored record sequence number of the given node.
func (db *DB) NodeSeq(id ID) uint64 { func (db *DB) NodeSeq(id ID) uint64 {
return db.fetchUint64(makeKey(id, dbDiscoverSeq)) return db.fetchUint64(nodeItemKey(id, zeroIP, dbNodeSeq))
} }
// Resolve returns the stored record of the node if it has a larger sequence // Resolve returns the stored record of the node if it has a larger sequence
@ -227,15 +265,17 @@ func (db *DB) Resolve(n *Node) *Node {
return db.Node(n.ID()) return db.Node(n.ID())
} }
// DeleteNode deletes all information/keys associated with a node. // DeleteNode deletes all information associated with a node.
func (db *DB) DeleteNode(id ID) error { func (db *DB) DeleteNode(id ID) {
deleter := db.lvl.NewIterator(util.BytesPrefix(makeKey(id, "")), nil) deleteRange(db.lvl, nodeKey(id))
for deleter.Next() { }
if err := db.lvl.Delete(deleter.Key(), nil); err != nil {
return err func deleteRange(db *leveldb.DB, prefix []byte) {
it := db.NewIterator(util.BytesPrefix(prefix), nil)
defer it.Release()
for it.Next() {
db.Delete(it.Key(), nil)
} }
}
return nil
} }
// ensureExpirer is a small helper method ensuring that the data expiration // ensureExpirer is a small helper method ensuring that the data expiration
@ -259,9 +299,7 @@ func (db *DB) expirer() {
for { for {
select { select {
case <-tick.C: case <-tick.C:
if err := db.expireNodes(); err != nil { db.expireNodes()
log.Error("Failed to expire nodedb items", "err", err)
}
case <-db.quit: case <-db.quit:
return return
} }
@ -269,71 +307,85 @@ func (db *DB) expirer() {
} }
// expireNodes iterates over the database and deletes all nodes that have not // expireNodes iterates over the database and deletes all nodes that have not
// been seen (i.e. received a pong from) for some allotted time. // been seen (i.e. received a pong from) for some time.
func (db *DB) expireNodes() error { func (db *DB) expireNodes() {
threshold := time.Now().Add(-dbNodeExpiration) it := db.lvl.NewIterator(util.BytesPrefix([]byte(dbNodePrefix)), nil)
// Find discovered nodes that are older than the allowance
it := db.lvl.NewIterator(nil, nil)
defer it.Release() defer it.Release()
if !it.Next() {
return
}
for it.Next() { var (
// Skip the item if not a discovery node threshold = time.Now().Add(-dbNodeExpiration).Unix()
id, field := splitKey(it.Key()) youngestPong int64
if field != dbDiscoverRoot { atEnd = false
continue )
for !atEnd {
id, ip, field := splitNodeItemKey(it.Key())
if field == dbNodePong {
time, _ := binary.Varint(it.Value())
if time > youngestPong {
youngestPong = time
} }
// Skip the node if not expired yet (and not self) if time < threshold {
if seen := db.LastPongReceived(id); seen.After(threshold) { // Last pong from this IP older than threshold, remove fields belonging to it.
continue deleteRange(db.lvl, nodeItemKey(id, ip, ""))
}
}
atEnd = !it.Next()
nextID, _ := splitNodeKey(it.Key())
if atEnd || nextID != id {
// We've moved beyond the last entry of the current ID.
// Remove everything if there was no recent enough pong.
if youngestPong > 0 && youngestPong < threshold {
deleteRange(db.lvl, nodeKey(id))
}
youngestPong = 0
} }
// Otherwise delete all associated information
db.DeleteNode(id)
} }
return nil
} }
// LastPingReceived retrieves the time of the last ping packet received from // LastPingReceived retrieves the time of the last ping packet received from
// a remote node. // a remote node.
func (db *DB) LastPingReceived(id ID) time.Time { func (db *DB) LastPingReceived(id ID, ip net.IP) time.Time {
return time.Unix(db.fetchInt64(makeKey(id, dbDiscoverPing)), 0) return time.Unix(db.fetchInt64(nodeItemKey(id, ip, dbNodePing)), 0)
} }
// UpdateLastPingReceived updates the last time we tried contacting a remote node. // UpdateLastPingReceived updates the last time we tried contacting a remote node.
func (db *DB) UpdateLastPingReceived(id ID, instance time.Time) error { func (db *DB) UpdateLastPingReceived(id ID, ip net.IP, instance time.Time) error {
return db.storeInt64(makeKey(id, dbDiscoverPing), instance.Unix()) return db.storeInt64(nodeItemKey(id, ip, dbNodePing), instance.Unix())
} }
// LastPongReceived retrieves the time of the last successful pong from remote node. // LastPongReceived retrieves the time of the last successful pong from remote node.
func (db *DB) LastPongReceived(id ID) time.Time { func (db *DB) LastPongReceived(id ID, ip net.IP) time.Time {
// Launch expirer // Launch expirer
db.ensureExpirer() db.ensureExpirer()
return time.Unix(db.fetchInt64(makeKey(id, dbDiscoverPong)), 0) return time.Unix(db.fetchInt64(nodeItemKey(id, ip, dbNodePong)), 0)
} }
// UpdateLastPongReceived updates the last pong time of a node. // UpdateLastPongReceived updates the last pong time of a node.
func (db *DB) UpdateLastPongReceived(id ID, instance time.Time) error { func (db *DB) UpdateLastPongReceived(id ID, ip net.IP, instance time.Time) error {
return db.storeInt64(makeKey(id, dbDiscoverPong), instance.Unix()) return db.storeInt64(nodeItemKey(id, ip, dbNodePong), instance.Unix())
} }
// FindFails retrieves the number of findnode failures since bonding. // FindFails retrieves the number of findnode failures since bonding.
func (db *DB) FindFails(id ID) int { func (db *DB) FindFails(id ID, ip net.IP) int {
return int(db.fetchInt64(makeKey(id, dbDiscoverFindFails))) return int(db.fetchInt64(nodeItemKey(id, ip, dbNodeFindFails)))
} }
// UpdateFindFails updates the number of findnode failures since bonding. // UpdateFindFails updates the number of findnode failures since bonding.
func (db *DB) UpdateFindFails(id ID, fails int) error { func (db *DB) UpdateFindFails(id ID, ip net.IP, fails int) error {
return db.storeInt64(makeKey(id, dbDiscoverFindFails), int64(fails)) return db.storeInt64(nodeItemKey(id, ip, dbNodeFindFails), int64(fails))
} }
// LocalSeq retrieves the local record sequence counter. // LocalSeq retrieves the local record sequence counter.
func (db *DB) localSeq(id ID) uint64 { func (db *DB) localSeq(id ID) uint64 {
return db.fetchUint64(makeKey(id, dbLocalSeq)) return db.fetchUint64(nodeItemKey(id, zeroIP, dbLocalSeq))
} }
// storeLocalSeq stores the local record sequence counter. // storeLocalSeq stores the local record sequence counter.
func (db *DB) storeLocalSeq(id ID, n uint64) { func (db *DB) storeLocalSeq(id ID, n uint64) {
db.storeUint64(makeKey(id, dbLocalSeq), n) db.storeUint64(nodeItemKey(id, zeroIP, dbLocalSeq), n)
} }
// QuerySeeds retrieves random nodes to be used as potential seed nodes // QuerySeeds retrieves random nodes to be used as potential seed nodes
@ -355,14 +407,14 @@ seek:
ctr := id[0] ctr := id[0]
rand.Read(id[:]) rand.Read(id[:])
id[0] = ctr + id[0]%16 id[0] = ctr + id[0]%16
it.Seek(makeKey(id, dbDiscoverRoot)) it.Seek(nodeKey(id))
n := nextNode(it) n := nextNode(it)
if n == nil { if n == nil {
id[0] = 0 id[0] = 0
continue seek // iterator exhausted continue seek // iterator exhausted
} }
if now.Sub(db.LastPongReceived(n.ID())) > maxAge { if now.Sub(db.LastPongReceived(n.ID(), n.IP())) > maxAge {
continue seek continue seek
} }
for i := range nodes { for i := range nodes {
@ -379,8 +431,8 @@ seek:
// database entries. // database entries.
func nextNode(it iterator.Iterator) *Node { func nextNode(it iterator.Iterator) *Node {
for end := false; !end; end = !it.Next() { for end := false; !end; end = !it.Next() {
id, field := splitKey(it.Key()) id, rest := splitNodeKey(it.Key())
if field != dbDiscoverRoot { if string(rest) != dbDiscoverRoot {
continue continue
} }
return mustDecodeNode(id[:], it.Value()) return mustDecodeNode(id[:], it.Value())

View File

@ -28,42 +28,54 @@ import (
"time" "time"
) )
var nodeDBKeyTests = []struct { var keytestID = HexID("51232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439")
id ID
field string func TestDBNodeKey(t *testing.T) {
key []byte enc := nodeKey(keytestID)
}{ want := []byte{
{ 'n', ':',
id: ID{},
field: "version",
key: []byte{0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e}, // field
},
{
id: HexID("51232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
field: ":discover",
key: []byte{
0x6e, 0x3a, // prefix
0x51, 0x23, 0x2b, 0x8d, 0x78, 0x21, 0x61, 0x7d, // node id 0x51, 0x23, 0x2b, 0x8d, 0x78, 0x21, 0x61, 0x7d, // node id
0x2b, 0x29, 0xb5, 0x4b, 0x81, 0xcd, 0xef, 0xb9, // 0x2b, 0x29, 0xb5, 0x4b, 0x81, 0xcd, 0xef, 0xb9, //
0xb3, 0xe9, 0xc3, 0x7d, 0x7f, 0xd5, 0xf6, 0x32, // 0xb3, 0xe9, 0xc3, 0x7d, 0x7f, 0xd5, 0xf6, 0x32, //
0x70, 0xbc, 0xc9, 0xe1, 0xa6, 0xf6, 0xa4, 0x39, // 0x70, 0xbc, 0xc9, 0xe1, 0xa6, 0xf6, 0xa4, 0x39, //
0x3a, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, // field ':', 'v', '4',
}, }
}, if !bytes.Equal(enc, want) {
t.Errorf("wrong encoded key:\ngot %q\nwant %q", enc, want)
}
id, _ := splitNodeKey(enc)
if id != keytestID {
t.Errorf("wrong ID from splitNodeKey")
}
} }
func TestDBKeys(t *testing.T) { func TestDBNodeItemKey(t *testing.T) {
for i, tt := range nodeDBKeyTests { wantIP := net.IP{127, 0, 0, 3}
if key := makeKey(tt.id, tt.field); !bytes.Equal(key, tt.key) { wantField := "foobar"
t.Errorf("make test %d: key mismatch: have 0x%x, want 0x%x", i, key, tt.key) enc := nodeItemKey(keytestID, wantIP, wantField)
want := []byte{
'n', ':',
0x51, 0x23, 0x2b, 0x8d, 0x78, 0x21, 0x61, 0x7d, // node id
0x2b, 0x29, 0xb5, 0x4b, 0x81, 0xcd, 0xef, 0xb9, //
0xb3, 0xe9, 0xc3, 0x7d, 0x7f, 0xd5, 0xf6, 0x32, //
0x70, 0xbc, 0xc9, 0xe1, 0xa6, 0xf6, 0xa4, 0x39, //
':', 'v', '4', ':',
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // IP
0x00, 0x00, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x03, //
':', 'f', 'o', 'o', 'b', 'a', 'r',
} }
id, field := splitKey(tt.key) if !bytes.Equal(enc, want) {
if !bytes.Equal(id[:], tt.id[:]) { t.Errorf("wrong encoded key:\ngot %q\nwant %q", enc, want)
t.Errorf("split test %d: id mismatch: have 0x%x, want 0x%x", i, id, tt.id)
} }
if field != tt.field { id, ip, field := splitNodeItemKey(enc)
t.Errorf("split test %d: field mismatch: have 0x%x, want 0x%x", i, field, tt.field) if id != keytestID {
t.Errorf("splitNodeItemKey returned wrong ID: %v", id)
} }
if !bytes.Equal(ip, wantIP) {
t.Errorf("splitNodeItemKey returned wrong IP: %v", ip)
}
if field != wantField {
t.Errorf("splitNodeItemKey returned wrong field: %q", field)
} }
} }
@ -113,33 +125,33 @@ func TestDBFetchStore(t *testing.T) {
defer db.Close() defer db.Close()
// Check fetch/store operations on a node ping object // Check fetch/store operations on a node ping object
if stored := db.LastPingReceived(node.ID()); stored.Unix() != 0 { if stored := db.LastPingReceived(node.ID(), node.IP()); stored.Unix() != 0 {
t.Errorf("ping: non-existing object: %v", stored) t.Errorf("ping: non-existing object: %v", stored)
} }
if err := db.UpdateLastPingReceived(node.ID(), inst); err != nil { if err := db.UpdateLastPingReceived(node.ID(), node.IP(), inst); err != nil {
t.Errorf("ping: failed to update: %v", err) t.Errorf("ping: failed to update: %v", err)
} }
if stored := db.LastPingReceived(node.ID()); stored.Unix() != inst.Unix() { if stored := db.LastPingReceived(node.ID(), node.IP()); stored.Unix() != inst.Unix() {
t.Errorf("ping: value mismatch: have %v, want %v", stored, inst) t.Errorf("ping: value mismatch: have %v, want %v", stored, inst)
} }
// Check fetch/store operations on a node pong object // Check fetch/store operations on a node pong object
if stored := db.LastPongReceived(node.ID()); stored.Unix() != 0 { if stored := db.LastPongReceived(node.ID(), node.IP()); stored.Unix() != 0 {
t.Errorf("pong: non-existing object: %v", stored) t.Errorf("pong: non-existing object: %v", stored)
} }
if err := db.UpdateLastPongReceived(node.ID(), inst); err != nil { if err := db.UpdateLastPongReceived(node.ID(), node.IP(), inst); err != nil {
t.Errorf("pong: failed to update: %v", err) t.Errorf("pong: failed to update: %v", err)
} }
if stored := db.LastPongReceived(node.ID()); stored.Unix() != inst.Unix() { if stored := db.LastPongReceived(node.ID(), node.IP()); stored.Unix() != inst.Unix() {
t.Errorf("pong: value mismatch: have %v, want %v", stored, inst) t.Errorf("pong: value mismatch: have %v, want %v", stored, inst)
} }
// Check fetch/store operations on a node findnode-failure object // Check fetch/store operations on a node findnode-failure object
if stored := db.FindFails(node.ID()); stored != 0 { if stored := db.FindFails(node.ID(), node.IP()); stored != 0 {
t.Errorf("find-node fails: non-existing object: %v", stored) t.Errorf("find-node fails: non-existing object: %v", stored)
} }
if err := db.UpdateFindFails(node.ID(), num); err != nil { if err := db.UpdateFindFails(node.ID(), node.IP(), num); err != nil {
t.Errorf("find-node fails: failed to update: %v", err) t.Errorf("find-node fails: failed to update: %v", err)
} }
if stored := db.FindFails(node.ID()); stored != num { if stored := db.FindFails(node.ID(), node.IP()); stored != num {
t.Errorf("find-node fails: value mismatch: have %v, want %v", stored, num) t.Errorf("find-node fails: value mismatch: have %v, want %v", stored, num)
} }
// Check fetch/store operations on an actual node object // Check fetch/store operations on an actual node object
@ -256,7 +268,7 @@ func testSeedQuery() error {
if err := db.UpdateNode(seed.node); err != nil { if err := db.UpdateNode(seed.node); err != nil {
return fmt.Errorf("node %d: failed to insert: %v", i, err) return fmt.Errorf("node %d: failed to insert: %v", i, err)
} }
if err := db.UpdateLastPongReceived(seed.node.ID(), seed.pong); err != nil { if err := db.UpdateLastPongReceived(seed.node.ID(), seed.node.IP(), seed.pong); err != nil {
return fmt.Errorf("node %d: failed to insert bondTime: %v", i, err) return fmt.Errorf("node %d: failed to insert bondTime: %v", i, err)
} }
} }
@ -323,8 +335,10 @@ func TestDBPersistency(t *testing.T) {
var nodeDBExpirationNodes = []struct { var nodeDBExpirationNodes = []struct {
node *Node node *Node
pong time.Time pong time.Time
storeNode bool
exp bool exp bool
}{ }{
// Node has new enough pong time and isn't expired:
{ {
node: NewV4( node: NewV4(
hexPubkey("8d110e2ed4b446d9b5fb50f117e5f37fb7597af455e1dab0e6f045a6eeaa786a6781141659020d38bdc5e698ed3d4d2bafa8b5061810dfa63e8ac038db2e9b67"), hexPubkey("8d110e2ed4b446d9b5fb50f117e5f37fb7597af455e1dab0e6f045a6eeaa786a6781141659020d38bdc5e698ed3d4d2bafa8b5061810dfa63e8ac038db2e9b67"),
@ -332,15 +346,77 @@ var nodeDBExpirationNodes = []struct {
30303, 30303,
30303, 30303,
), ),
storeNode: true,
pong: time.Now().Add(-dbNodeExpiration + time.Minute), pong: time.Now().Add(-dbNodeExpiration + time.Minute),
exp: false, exp: false,
}, { },
// Node with pong time before expiration is removed:
{
node: NewV4( node: NewV4(
hexPubkey("913a205579c32425b220dfba999d215066e5bdbf900226b11da1907eae5e93eb40616d47412cf819664e9eacbdfcca6b0c6e07e09847a38472d4be46ab0c3672"), hexPubkey("913a205579c32425b220dfba999d215066e5bdbf900226b11da1907eae5e93eb40616d47412cf819664e9eacbdfcca6b0c6e07e09847a38472d4be46ab0c3672"),
net.IP{127, 0, 0, 2}, net.IP{127, 0, 0, 2},
30303, 30303,
30303, 30303,
), ),
storeNode: true,
pong: time.Now().Add(-dbNodeExpiration - time.Minute),
exp: true,
},
// Just pong time, no node stored:
{
node: NewV4(
hexPubkey("b56670e0b6bad2c5dab9f9fe6f061a16cf78d68b6ae2cfda3144262d08d97ce5f46fd8799b6d1f709b1abe718f2863e224488bd7518e5e3b43809ac9bd1138ca"),
net.IP{127, 0, 0, 3},
30303,
30303,
),
storeNode: false,
pong: time.Now().Add(-dbNodeExpiration - time.Minute),
exp: true,
},
// Node with multiple pong times, all older than expiration.
{
node: NewV4(
hexPubkey("29f619cebfd32c9eab34aec797ed5e3fe15b9b45be95b4df3f5fe6a9ae892f433eb08d7698b2ef3621568b0fb70d57b515ab30d4e72583b798298e0f0a66b9d1"),
net.IP{127, 0, 0, 4},
30303,
30303,
),
storeNode: true,
pong: time.Now().Add(-dbNodeExpiration - time.Minute),
exp: true,
},
{
node: NewV4(
hexPubkey("29f619cebfd32c9eab34aec797ed5e3fe15b9b45be95b4df3f5fe6a9ae892f433eb08d7698b2ef3621568b0fb70d57b515ab30d4e72583b798298e0f0a66b9d1"),
net.IP{127, 0, 0, 5},
30303,
30303,
),
storeNode: false,
pong: time.Now().Add(-dbNodeExpiration - 2*time.Minute),
exp: true,
},
// Node with multiple pong times, one newer, one older than expiration.
{
node: NewV4(
hexPubkey("3b73a9e5f4af6c4701c57c73cc8cfa0f4802840b24c11eba92aac3aef65644a3728b4b2aec8199f6d72bd66be2c65861c773129039bd47daa091ca90a6d4c857"),
net.IP{127, 0, 0, 6},
30303,
30303,
),
storeNode: true,
pong: time.Now().Add(-dbNodeExpiration + time.Minute),
exp: false,
},
{
node: NewV4(
hexPubkey("3b73a9e5f4af6c4701c57c73cc8cfa0f4802840b24c11eba92aac3aef65644a3728b4b2aec8199f6d72bd66be2c65861c773129039bd47daa091ca90a6d4c857"),
net.IP{127, 0, 0, 7},
30303,
30303,
),
storeNode: false,
pong: time.Now().Add(-dbNodeExpiration - time.Minute), pong: time.Now().Add(-dbNodeExpiration - time.Minute),
exp: true, exp: true,
}, },
@ -350,23 +426,39 @@ func TestDBExpiration(t *testing.T) {
db, _ := OpenDB("") db, _ := OpenDB("")
defer db.Close() defer db.Close()
// Add all the test nodes and set their last pong time // Add all the test nodes and set their last pong time.
for i, seed := range nodeDBExpirationNodes { for i, seed := range nodeDBExpirationNodes {
if seed.storeNode {
if err := db.UpdateNode(seed.node); err != nil { if err := db.UpdateNode(seed.node); err != nil {
t.Fatalf("node %d: failed to insert: %v", i, err) t.Fatalf("node %d: failed to insert: %v", i, err)
} }
if err := db.UpdateLastPongReceived(seed.node.ID(), seed.pong); err != nil { }
if err := db.UpdateLastPongReceived(seed.node.ID(), seed.node.IP(), seed.pong); err != nil {
t.Fatalf("node %d: failed to update bondTime: %v", i, err) t.Fatalf("node %d: failed to update bondTime: %v", i, err)
} }
} }
// Expire some of them, and check the rest
if err := db.expireNodes(); err != nil { db.expireNodes()
t.Fatalf("failed to expire nodes: %v", err)
} // Check that expired entries have been removed.
unixZeroTime := time.Unix(0, 0)
for i, seed := range nodeDBExpirationNodes { for i, seed := range nodeDBExpirationNodes {
node := db.Node(seed.node.ID()) node := db.Node(seed.node.ID())
if (node == nil && !seed.exp) || (node != nil && seed.exp) { pong := db.LastPongReceived(seed.node.ID(), seed.node.IP())
t.Errorf("node %d: expiration mismatch: have %v, want %v", i, node, seed.exp) if seed.exp {
if seed.storeNode && node != nil {
t.Errorf("node %d (%s) shouldn't be present after expiration", i, seed.node.ID().TerminalString())
}
if !pong.Equal(unixZeroTime) {
t.Errorf("pong time %d (%s %v) shouldn't be present after expiration", i, seed.node.ID().TerminalString(), seed.node.IP())
}
} else {
if seed.storeNode && node == nil {
t.Errorf("node %d (%s) should be present after expiration", i, seed.node.ID().TerminalString())
}
if !pong.Equal(seed.pong.Truncate(1 * time.Second)) {
t.Errorf("pong time %d (%s) should be %v after expiration, but is %v", i, seed.node.ID().TerminalString(), seed.pong, pong)
}
} }
} }
} }