mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-20 10:52:25 +00:00
Delete p2p directory
This commit is contained in:
parent
3d0a141460
commit
b9d1320f45
109 changed files with 0 additions and 31590 deletions
544
p2p/dial.go
544
p2p/dial.go
|
|
@ -1,544 +0,0 @@
|
||||||
// Copyright 2015 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package p2p
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
crand "crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
mrand "math/rand"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/netutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// This is the amount of time spent waiting in between redialing a certain node. The
|
|
||||||
// limit is a bit higher than inboundThrottleTime to prevent failing dials in small
|
|
||||||
// private networks.
|
|
||||||
dialHistoryExpiration = inboundThrottleTime + 5*time.Second
|
|
||||||
|
|
||||||
// Config for the "Looking for peers" message.
|
|
||||||
dialStatsLogInterval = 10 * time.Second // printed at most this often
|
|
||||||
dialStatsPeerLimit = 3 // but not if more than this many dialed peers
|
|
||||||
|
|
||||||
// Endpoint resolution is throttled with bounded backoff.
|
|
||||||
initialResolveDelay = 60 * time.Second
|
|
||||||
maxResolveDelay = time.Hour
|
|
||||||
)
|
|
||||||
|
|
||||||
// NodeDialer is used to connect to nodes in the network, typically by using
|
|
||||||
// an underlying net.Dialer but also using net.Pipe in tests.
|
|
||||||
type NodeDialer interface {
|
|
||||||
Dial(context.Context, *enode.Node) (net.Conn, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type nodeResolver interface {
|
|
||||||
Resolve(*enode.Node) *enode.Node
|
|
||||||
}
|
|
||||||
|
|
||||||
// tcpDialer implements NodeDialer using real TCP connections.
|
|
||||||
type tcpDialer struct {
|
|
||||||
d *net.Dialer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t tcpDialer) Dial(ctx context.Context, dest *enode.Node) (net.Conn, error) {
|
|
||||||
return t.d.DialContext(ctx, "tcp", nodeAddr(dest).String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func nodeAddr(n *enode.Node) net.Addr {
|
|
||||||
return &net.TCPAddr{IP: n.IP(), Port: n.TCP()}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkDial errors:
|
|
||||||
var (
|
|
||||||
errSelf = errors.New("is self")
|
|
||||||
errAlreadyDialing = errors.New("already dialing")
|
|
||||||
errAlreadyConnected = errors.New("already connected")
|
|
||||||
errRecentlyDialed = errors.New("recently dialed")
|
|
||||||
errNetRestrict = errors.New("not contained in netrestrict list")
|
|
||||||
errNoPort = errors.New("node does not provide TCP port")
|
|
||||||
)
|
|
||||||
|
|
||||||
// dialer creates outbound connections and submits them into Server.
|
|
||||||
// Two types of peer connections can be created:
|
|
||||||
//
|
|
||||||
// - static dials are pre-configured connections. The dialer attempts
|
|
||||||
// keep these nodes connected at all times.
|
|
||||||
//
|
|
||||||
// - dynamic dials are created from node discovery results. The dialer
|
|
||||||
// continuously reads candidate nodes from its input iterator and attempts
|
|
||||||
// to create peer connections to nodes arriving through the iterator.
|
|
||||||
type dialScheduler struct {
|
|
||||||
dialConfig
|
|
||||||
setupFunc dialSetupFunc
|
|
||||||
wg sync.WaitGroup
|
|
||||||
cancel context.CancelFunc
|
|
||||||
ctx context.Context
|
|
||||||
nodesIn chan *enode.Node
|
|
||||||
doneCh chan *dialTask
|
|
||||||
addStaticCh chan *enode.Node
|
|
||||||
remStaticCh chan *enode.Node
|
|
||||||
addPeerCh chan *conn
|
|
||||||
remPeerCh chan *conn
|
|
||||||
|
|
||||||
// Everything below here belongs to loop and
|
|
||||||
// should only be accessed by code on the loop goroutine.
|
|
||||||
dialing map[enode.ID]*dialTask // active tasks
|
|
||||||
peers map[enode.ID]struct{} // all connected peers
|
|
||||||
dialPeers int // current number of dialed peers
|
|
||||||
|
|
||||||
// The static map tracks all static dial tasks. The subset of usable static dial tasks
|
|
||||||
// (i.e. those passing checkDial) is kept in staticPool. The scheduler prefers
|
|
||||||
// launching random static tasks from the pool over launching dynamic dials from the
|
|
||||||
// iterator.
|
|
||||||
static map[enode.ID]*dialTask
|
|
||||||
staticPool []*dialTask
|
|
||||||
|
|
||||||
// The dial history keeps recently dialed nodes. Members of history are not dialed.
|
|
||||||
history expHeap
|
|
||||||
historyTimer *mclock.Alarm
|
|
||||||
|
|
||||||
// for logStats
|
|
||||||
lastStatsLog mclock.AbsTime
|
|
||||||
doneSinceLastLog int
|
|
||||||
}
|
|
||||||
|
|
||||||
type dialSetupFunc func(net.Conn, connFlag, *enode.Node) error
|
|
||||||
|
|
||||||
type dialConfig struct {
|
|
||||||
self enode.ID // our own ID
|
|
||||||
maxDialPeers int // maximum number of dialed peers
|
|
||||||
maxActiveDials int // maximum number of active dials
|
|
||||||
netRestrict *netutil.Netlist // IP netrestrict list, disabled if nil
|
|
||||||
resolver nodeResolver
|
|
||||||
dialer NodeDialer
|
|
||||||
log log.Logger
|
|
||||||
clock mclock.Clock
|
|
||||||
rand *mrand.Rand
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg dialConfig) withDefaults() dialConfig {
|
|
||||||
if cfg.maxActiveDials == 0 {
|
|
||||||
cfg.maxActiveDials = defaultMaxPendingPeers
|
|
||||||
}
|
|
||||||
if cfg.log == nil {
|
|
||||||
cfg.log = log.Root()
|
|
||||||
}
|
|
||||||
if cfg.clock == nil {
|
|
||||||
cfg.clock = mclock.System{}
|
|
||||||
}
|
|
||||||
if cfg.rand == nil {
|
|
||||||
seedb := make([]byte, 8)
|
|
||||||
crand.Read(seedb)
|
|
||||||
seed := int64(binary.BigEndian.Uint64(seedb))
|
|
||||||
cfg.rand = mrand.New(mrand.NewSource(seed))
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDialScheduler(config dialConfig, it enode.Iterator, setupFunc dialSetupFunc) *dialScheduler {
|
|
||||||
cfg := config.withDefaults()
|
|
||||||
d := &dialScheduler{
|
|
||||||
dialConfig: cfg,
|
|
||||||
historyTimer: mclock.NewAlarm(cfg.clock),
|
|
||||||
setupFunc: setupFunc,
|
|
||||||
dialing: make(map[enode.ID]*dialTask),
|
|
||||||
static: make(map[enode.ID]*dialTask),
|
|
||||||
peers: make(map[enode.ID]struct{}),
|
|
||||||
doneCh: make(chan *dialTask),
|
|
||||||
nodesIn: make(chan *enode.Node),
|
|
||||||
addStaticCh: make(chan *enode.Node),
|
|
||||||
remStaticCh: make(chan *enode.Node),
|
|
||||||
addPeerCh: make(chan *conn),
|
|
||||||
remPeerCh: make(chan *conn),
|
|
||||||
}
|
|
||||||
d.lastStatsLog = d.clock.Now()
|
|
||||||
d.ctx, d.cancel = context.WithCancel(context.Background())
|
|
||||||
d.wg.Add(2)
|
|
||||||
go d.readNodes(it)
|
|
||||||
go d.loop(it)
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop shuts down the dialer, canceling all current dial tasks.
|
|
||||||
func (d *dialScheduler) stop() {
|
|
||||||
d.cancel()
|
|
||||||
d.wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
// addStatic adds a static dial candidate.
|
|
||||||
func (d *dialScheduler) addStatic(n *enode.Node) {
|
|
||||||
select {
|
|
||||||
case d.addStaticCh <- n:
|
|
||||||
case <-d.ctx.Done():
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeStatic removes a static dial candidate.
|
|
||||||
func (d *dialScheduler) removeStatic(n *enode.Node) {
|
|
||||||
select {
|
|
||||||
case d.remStaticCh <- n:
|
|
||||||
case <-d.ctx.Done():
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// peerAdded updates the peer set.
|
|
||||||
func (d *dialScheduler) peerAdded(c *conn) {
|
|
||||||
select {
|
|
||||||
case d.addPeerCh <- c:
|
|
||||||
case <-d.ctx.Done():
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// peerRemoved updates the peer set.
|
|
||||||
func (d *dialScheduler) peerRemoved(c *conn) {
|
|
||||||
select {
|
|
||||||
case d.remPeerCh <- c:
|
|
||||||
case <-d.ctx.Done():
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// loop is the main loop of the dialer.
|
|
||||||
func (d *dialScheduler) loop(it enode.Iterator) {
|
|
||||||
var (
|
|
||||||
nodesCh chan *enode.Node
|
|
||||||
)
|
|
||||||
|
|
||||||
loop:
|
|
||||||
for {
|
|
||||||
// Launch new dials if slots are available.
|
|
||||||
slots := d.freeDialSlots()
|
|
||||||
slots -= d.startStaticDials(slots)
|
|
||||||
if slots > 0 {
|
|
||||||
nodesCh = d.nodesIn
|
|
||||||
} else {
|
|
||||||
nodesCh = nil
|
|
||||||
}
|
|
||||||
d.rearmHistoryTimer()
|
|
||||||
d.logStats()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case node := <-nodesCh:
|
|
||||||
if err := d.checkDial(node); err != nil {
|
|
||||||
d.log.Trace("Discarding dial candidate", "id", node.ID(), "ip", node.IP(), "reason", err)
|
|
||||||
} else {
|
|
||||||
d.startDial(newDialTask(node, dynDialedConn))
|
|
||||||
}
|
|
||||||
|
|
||||||
case task := <-d.doneCh:
|
|
||||||
id := task.dest.ID()
|
|
||||||
delete(d.dialing, id)
|
|
||||||
d.updateStaticPool(id)
|
|
||||||
d.doneSinceLastLog++
|
|
||||||
|
|
||||||
case c := <-d.addPeerCh:
|
|
||||||
if c.is(dynDialedConn) || c.is(staticDialedConn) {
|
|
||||||
d.dialPeers++
|
|
||||||
}
|
|
||||||
id := c.node.ID()
|
|
||||||
d.peers[id] = struct{}{}
|
|
||||||
// Remove from static pool because the node is now connected.
|
|
||||||
task := d.static[id]
|
|
||||||
if task != nil && task.staticPoolIndex >= 0 {
|
|
||||||
d.removeFromStaticPool(task.staticPoolIndex)
|
|
||||||
}
|
|
||||||
// TODO: cancel dials to connected peers
|
|
||||||
|
|
||||||
case c := <-d.remPeerCh:
|
|
||||||
if c.is(dynDialedConn) || c.is(staticDialedConn) {
|
|
||||||
d.dialPeers--
|
|
||||||
}
|
|
||||||
delete(d.peers, c.node.ID())
|
|
||||||
d.updateStaticPool(c.node.ID())
|
|
||||||
|
|
||||||
case node := <-d.addStaticCh:
|
|
||||||
id := node.ID()
|
|
||||||
_, exists := d.static[id]
|
|
||||||
d.log.Trace("Adding static node", "id", id, "ip", node.IP(), "added", !exists)
|
|
||||||
if exists {
|
|
||||||
continue loop
|
|
||||||
}
|
|
||||||
task := newDialTask(node, staticDialedConn)
|
|
||||||
d.static[id] = task
|
|
||||||
if d.checkDial(node) == nil {
|
|
||||||
d.addToStaticPool(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
case node := <-d.remStaticCh:
|
|
||||||
id := node.ID()
|
|
||||||
task := d.static[id]
|
|
||||||
d.log.Trace("Removing static node", "id", id, "ok", task != nil)
|
|
||||||
if task != nil {
|
|
||||||
delete(d.static, id)
|
|
||||||
if task.staticPoolIndex >= 0 {
|
|
||||||
d.removeFromStaticPool(task.staticPoolIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-d.historyTimer.C():
|
|
||||||
d.expireHistory()
|
|
||||||
|
|
||||||
case <-d.ctx.Done():
|
|
||||||
it.Close()
|
|
||||||
break loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d.historyTimer.Stop()
|
|
||||||
for range d.dialing {
|
|
||||||
<-d.doneCh
|
|
||||||
}
|
|
||||||
d.wg.Done()
|
|
||||||
}
|
|
||||||
|
|
||||||
// readNodes runs in its own goroutine and delivers nodes from
|
|
||||||
// the input iterator to the nodesIn channel.
|
|
||||||
func (d *dialScheduler) readNodes(it enode.Iterator) {
|
|
||||||
defer d.wg.Done()
|
|
||||||
|
|
||||||
for it.Next() {
|
|
||||||
select {
|
|
||||||
case d.nodesIn <- it.Node():
|
|
||||||
case <-d.ctx.Done():
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// logStats prints dialer statistics to the log. The message is suppressed when enough
|
|
||||||
// peers are connected because users should only see it while their client is starting up
|
|
||||||
// or comes back online.
|
|
||||||
func (d *dialScheduler) logStats() {
|
|
||||||
now := d.clock.Now()
|
|
||||||
if d.lastStatsLog.Add(dialStatsLogInterval) > now {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if d.dialPeers < dialStatsPeerLimit && d.dialPeers < d.maxDialPeers {
|
|
||||||
d.log.Info("Looking for peers", "peercount", len(d.peers), "tried", d.doneSinceLastLog, "static", len(d.static))
|
|
||||||
}
|
|
||||||
d.doneSinceLastLog = 0
|
|
||||||
d.lastStatsLog = now
|
|
||||||
}
|
|
||||||
|
|
||||||
// rearmHistoryTimer configures d.historyTimer to fire when the
|
|
||||||
// next item in d.history expires.
|
|
||||||
func (d *dialScheduler) rearmHistoryTimer() {
|
|
||||||
if len(d.history) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.historyTimer.Schedule(d.history.nextExpiry())
|
|
||||||
}
|
|
||||||
|
|
||||||
// expireHistory removes expired items from d.history.
|
|
||||||
func (d *dialScheduler) expireHistory() {
|
|
||||||
d.history.expire(d.clock.Now(), func(hkey string) {
|
|
||||||
var id enode.ID
|
|
||||||
copy(id[:], hkey)
|
|
||||||
d.updateStaticPool(id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// freeDialSlots returns the number of free dial slots. The result can be negative
|
|
||||||
// when peers are connected while their task is still running.
|
|
||||||
func (d *dialScheduler) freeDialSlots() int {
|
|
||||||
slots := (d.maxDialPeers - d.dialPeers) * 2
|
|
||||||
if slots > d.maxActiveDials {
|
|
||||||
slots = d.maxActiveDials
|
|
||||||
}
|
|
||||||
free := slots - len(d.dialing)
|
|
||||||
return free
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkDial returns an error if node n should not be dialed.
|
|
||||||
func (d *dialScheduler) checkDial(n *enode.Node) error {
|
|
||||||
if n.ID() == d.self {
|
|
||||||
return errSelf
|
|
||||||
}
|
|
||||||
if n.IP() != nil && n.TCP() == 0 {
|
|
||||||
// This check can trigger if a non-TCP node is found
|
|
||||||
// by discovery. If there is no IP, the node is a static
|
|
||||||
// node and the actual endpoint will be resolved later in dialTask.
|
|
||||||
return errNoPort
|
|
||||||
}
|
|
||||||
if _, ok := d.dialing[n.ID()]; ok {
|
|
||||||
return errAlreadyDialing
|
|
||||||
}
|
|
||||||
if _, ok := d.peers[n.ID()]; ok {
|
|
||||||
return errAlreadyConnected
|
|
||||||
}
|
|
||||||
if d.netRestrict != nil && !d.netRestrict.Contains(n.IP()) {
|
|
||||||
return errNetRestrict
|
|
||||||
}
|
|
||||||
if d.history.contains(string(n.ID().Bytes())) {
|
|
||||||
return errRecentlyDialed
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// startStaticDials starts n static dial tasks.
|
|
||||||
func (d *dialScheduler) startStaticDials(n int) (started int) {
|
|
||||||
for started = 0; started < n && len(d.staticPool) > 0; started++ {
|
|
||||||
idx := d.rand.Intn(len(d.staticPool))
|
|
||||||
task := d.staticPool[idx]
|
|
||||||
d.startDial(task)
|
|
||||||
d.removeFromStaticPool(idx)
|
|
||||||
}
|
|
||||||
return started
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateStaticPool attempts to move the given static dial back into staticPool.
|
|
||||||
func (d *dialScheduler) updateStaticPool(id enode.ID) {
|
|
||||||
task, ok := d.static[id]
|
|
||||||
if ok && task.staticPoolIndex < 0 && d.checkDial(task.dest) == nil {
|
|
||||||
d.addToStaticPool(task)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dialScheduler) addToStaticPool(task *dialTask) {
|
|
||||||
if task.staticPoolIndex >= 0 {
|
|
||||||
panic("attempt to add task to staticPool twice")
|
|
||||||
}
|
|
||||||
d.staticPool = append(d.staticPool, task)
|
|
||||||
task.staticPoolIndex = len(d.staticPool) - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeFromStaticPool removes the task at idx from staticPool. It does that by moving the
|
|
||||||
// current last element of the pool to idx and then shortening the pool by one.
|
|
||||||
func (d *dialScheduler) removeFromStaticPool(idx int) {
|
|
||||||
task := d.staticPool[idx]
|
|
||||||
end := len(d.staticPool) - 1
|
|
||||||
d.staticPool[idx] = d.staticPool[end]
|
|
||||||
d.staticPool[idx].staticPoolIndex = idx
|
|
||||||
d.staticPool[end] = nil
|
|
||||||
d.staticPool = d.staticPool[:end]
|
|
||||||
task.staticPoolIndex = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// startDial runs the given dial task in a separate goroutine.
|
|
||||||
func (d *dialScheduler) startDial(task *dialTask) {
|
|
||||||
d.log.Trace("Starting p2p dial", "id", task.dest.ID(), "ip", task.dest.IP(), "flag", task.flags)
|
|
||||||
hkey := string(task.dest.ID().Bytes())
|
|
||||||
d.history.add(hkey, d.clock.Now().Add(dialHistoryExpiration))
|
|
||||||
d.dialing[task.dest.ID()] = task
|
|
||||||
go func() {
|
|
||||||
task.run(d)
|
|
||||||
d.doneCh <- task
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// A dialTask generated for each node that is dialed.
|
|
||||||
type dialTask struct {
|
|
||||||
staticPoolIndex int
|
|
||||||
flags connFlag
|
|
||||||
// These fields are private to the task and should not be
|
|
||||||
// accessed by dialScheduler while the task is running.
|
|
||||||
dest *enode.Node
|
|
||||||
lastResolved mclock.AbsTime
|
|
||||||
resolveDelay time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDialTask(dest *enode.Node, flags connFlag) *dialTask {
|
|
||||||
return &dialTask{dest: dest, flags: flags, staticPoolIndex: -1}
|
|
||||||
}
|
|
||||||
|
|
||||||
type dialError struct {
|
|
||||||
error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *dialTask) run(d *dialScheduler) {
|
|
||||||
if t.needResolve() && !t.resolve(d) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := t.dial(d, t.dest)
|
|
||||||
if err != nil {
|
|
||||||
// For static nodes, resolve one more time if dialing fails.
|
|
||||||
if _, ok := err.(*dialError); ok && t.flags&staticDialedConn != 0 {
|
|
||||||
if t.resolve(d) {
|
|
||||||
t.dial(d, t.dest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *dialTask) needResolve() bool {
|
|
||||||
return t.flags&staticDialedConn != 0 && t.dest.IP() == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolve attempts to find the current endpoint for the destination
|
|
||||||
// using discovery.
|
|
||||||
//
|
|
||||||
// Resolve operations are throttled with backoff to avoid flooding the
|
|
||||||
// discovery network with useless queries for nodes that don't exist.
|
|
||||||
// The backoff delay resets when the node is found.
|
|
||||||
func (t *dialTask) resolve(d *dialScheduler) bool {
|
|
||||||
if d.resolver == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if t.resolveDelay == 0 {
|
|
||||||
t.resolveDelay = initialResolveDelay
|
|
||||||
}
|
|
||||||
if t.lastResolved > 0 && time.Duration(d.clock.Now()-t.lastResolved) < t.resolveDelay {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
resolved := d.resolver.Resolve(t.dest)
|
|
||||||
t.lastResolved = d.clock.Now()
|
|
||||||
if resolved == nil {
|
|
||||||
t.resolveDelay *= 2
|
|
||||||
if t.resolveDelay > maxResolveDelay {
|
|
||||||
t.resolveDelay = maxResolveDelay
|
|
||||||
}
|
|
||||||
d.log.Debug("Resolving node failed", "id", t.dest.ID(), "newdelay", t.resolveDelay)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// The node was found.
|
|
||||||
t.resolveDelay = initialResolveDelay
|
|
||||||
t.dest = resolved
|
|
||||||
d.log.Debug("Resolved node", "id", t.dest.ID(), "addr", &net.TCPAddr{IP: t.dest.IP(), Port: t.dest.TCP()})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// dial performs the actual connection attempt.
|
|
||||||
func (t *dialTask) dial(d *dialScheduler, dest *enode.Node) error {
|
|
||||||
dialMeter.Mark(1)
|
|
||||||
fd, err := d.dialer.Dial(d.ctx, t.dest)
|
|
||||||
if err != nil {
|
|
||||||
d.log.Trace("Dial error", "id", t.dest.ID(), "addr", nodeAddr(t.dest), "conn", t.flags, "err", cleanupDialErr(err))
|
|
||||||
dialConnectionError.Mark(1)
|
|
||||||
return &dialError{err}
|
|
||||||
}
|
|
||||||
return d.setupFunc(newMeteredConn(fd), t.flags, dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *dialTask) String() string {
|
|
||||||
id := t.dest.ID()
|
|
||||||
return fmt.Sprintf("%v %x %v:%d", t.flags, id[:8], t.dest.IP(), t.dest.TCP())
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanupDialErr(err error) error {
|
|
||||||
if netErr, ok := err.(*net.OpError); ok && netErr.Op == "dial" {
|
|
||||||
return netErr.Err
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
672
p2p/dial_test.go
672
p2p/dial_test.go
|
|
@ -1,672 +0,0 @@
|
||||||
// Copyright 2015 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package p2p
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
"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/netutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This test checks that dynamic dials are launched from discovery results.
|
|
||||||
func TestDialSchedDynDial(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
config := dialConfig{
|
|
||||||
maxActiveDials: 5,
|
|
||||||
maxDialPeers: 4,
|
|
||||||
}
|
|
||||||
runDialTest(t, config, []dialTestRound{
|
|
||||||
// 3 out of 4 peers are connected, leaving 2 dial slots.
|
|
||||||
// 9 nodes are discovered, but only 2 are dialed.
|
|
||||||
{
|
|
||||||
peersAdded: []*conn{
|
|
||||||
{flags: staticDialedConn, node: newNode(uintID(0x00), "")},
|
|
||||||
{flags: dynDialedConn, node: newNode(uintID(0x01), "")},
|
|
||||||
{flags: dynDialedConn, node: newNode(uintID(0x02), "")},
|
|
||||||
},
|
|
||||||
discovered: []*enode.Node{
|
|
||||||
newNode(uintID(0x00), "127.0.0.1:30303"), // not dialed because already connected as static peer
|
|
||||||
newNode(uintID(0x02), "127.0.0.1:30303"), // ...
|
|
||||||
newNode(uintID(0x03), "127.0.0.1:30303"),
|
|
||||||
newNode(uintID(0x04), "127.0.0.1:30303"),
|
|
||||||
newNode(uintID(0x05), "127.0.0.1:30303"), // not dialed because there are only two slots
|
|
||||||
newNode(uintID(0x06), "127.0.0.1:30303"), // ...
|
|
||||||
newNode(uintID(0x07), "127.0.0.1:30303"), // ...
|
|
||||||
newNode(uintID(0x08), "127.0.0.1:30303"), // ...
|
|
||||||
},
|
|
||||||
wantNewDials: []*enode.Node{
|
|
||||||
newNode(uintID(0x03), "127.0.0.1:30303"),
|
|
||||||
newNode(uintID(0x04), "127.0.0.1:30303"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// One dial completes, freeing one dial slot.
|
|
||||||
{
|
|
||||||
failed: []enode.ID{
|
|
||||||
uintID(0x04),
|
|
||||||
},
|
|
||||||
wantNewDials: []*enode.Node{
|
|
||||||
newNode(uintID(0x05), "127.0.0.1:30303"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Dial to 0x03 completes, filling the last remaining peer slot.
|
|
||||||
{
|
|
||||||
succeeded: []enode.ID{
|
|
||||||
uintID(0x03),
|
|
||||||
},
|
|
||||||
failed: []enode.ID{
|
|
||||||
uintID(0x05),
|
|
||||||
},
|
|
||||||
discovered: []*enode.Node{
|
|
||||||
newNode(uintID(0x09), "127.0.0.1:30303"), // not dialed because there are no free slots
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// 3 peers drop off, creating 6 dial slots. Check that 5 of those slots
|
|
||||||
// (i.e. up to maxActiveDialTasks) are used.
|
|
||||||
{
|
|
||||||
peersRemoved: []enode.ID{
|
|
||||||
uintID(0x00),
|
|
||||||
uintID(0x01),
|
|
||||||
uintID(0x02),
|
|
||||||
},
|
|
||||||
discovered: []*enode.Node{
|
|
||||||
newNode(uintID(0x0a), "127.0.0.1:30303"),
|
|
||||||
newNode(uintID(0x0b), "127.0.0.1:30303"),
|
|
||||||
newNode(uintID(0x0c), "127.0.0.1:30303"),
|
|
||||||
newNode(uintID(0x0d), "127.0.0.1:30303"),
|
|
||||||
newNode(uintID(0x0f), "127.0.0.1:30303"),
|
|
||||||
},
|
|
||||||
wantNewDials: []*enode.Node{
|
|
||||||
newNode(uintID(0x06), "127.0.0.1:30303"),
|
|
||||||
newNode(uintID(0x07), "127.0.0.1:30303"),
|
|
||||||
newNode(uintID(0x08), "127.0.0.1:30303"),
|
|
||||||
newNode(uintID(0x09), "127.0.0.1:30303"),
|
|
||||||
newNode(uintID(0x0a), "127.0.0.1:30303"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that candidates that do not match the netrestrict list are not dialed.
|
|
||||||
func TestDialSchedNetRestrict(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
nodes := []*enode.Node{
|
|
||||||
newNode(uintID(0x01), "127.0.0.1:30303"),
|
|
||||||
newNode(uintID(0x02), "127.0.0.2:30303"),
|
|
||||||
newNode(uintID(0x03), "127.0.0.3:30303"),
|
|
||||||
newNode(uintID(0x04), "127.0.0.4:30303"),
|
|
||||||
newNode(uintID(0x05), "127.0.2.5:30303"),
|
|
||||||
newNode(uintID(0x06), "127.0.2.6:30303"),
|
|
||||||
newNode(uintID(0x07), "127.0.2.7:30303"),
|
|
||||||
newNode(uintID(0x08), "127.0.2.8:30303"),
|
|
||||||
}
|
|
||||||
config := dialConfig{
|
|
||||||
netRestrict: new(netutil.Netlist),
|
|
||||||
maxActiveDials: 10,
|
|
||||||
maxDialPeers: 10,
|
|
||||||
}
|
|
||||||
config.netRestrict.Add("127.0.2.0/24")
|
|
||||||
runDialTest(t, config, []dialTestRound{
|
|
||||||
{
|
|
||||||
discovered: nodes,
|
|
||||||
wantNewDials: nodes[4:8],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
succeeded: []enode.ID{
|
|
||||||
nodes[4].ID(),
|
|
||||||
nodes[5].ID(),
|
|
||||||
nodes[6].ID(),
|
|
||||||
nodes[7].ID(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that static dials work and obey the limits.
|
|
||||||
func TestDialSchedStaticDial(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
config := dialConfig{
|
|
||||||
maxActiveDials: 5,
|
|
||||||
maxDialPeers: 4,
|
|
||||||
}
|
|
||||||
runDialTest(t, config, []dialTestRound{
|
|
||||||
// Static dials are launched for the nodes that
|
|
||||||
// aren't yet connected.
|
|
||||||
{
|
|
||||||
peersAdded: []*conn{
|
|
||||||
{flags: dynDialedConn, node: newNode(uintID(0x01), "127.0.0.1:30303")},
|
|
||||||
{flags: dynDialedConn, node: newNode(uintID(0x02), "127.0.0.2:30303")},
|
|
||||||
},
|
|
||||||
update: func(d *dialScheduler) {
|
|
||||||
// These two are not dialed because they're already connected
|
|
||||||
// as dynamic peers.
|
|
||||||
d.addStatic(newNode(uintID(0x01), "127.0.0.1:30303"))
|
|
||||||
d.addStatic(newNode(uintID(0x02), "127.0.0.2:30303"))
|
|
||||||
// These nodes will be dialed:
|
|
||||||
d.addStatic(newNode(uintID(0x03), "127.0.0.3:30303"))
|
|
||||||
d.addStatic(newNode(uintID(0x04), "127.0.0.4:30303"))
|
|
||||||
d.addStatic(newNode(uintID(0x05), "127.0.0.5:30303"))
|
|
||||||
d.addStatic(newNode(uintID(0x06), "127.0.0.6:30303"))
|
|
||||||
d.addStatic(newNode(uintID(0x07), "127.0.0.7:30303"))
|
|
||||||
d.addStatic(newNode(uintID(0x08), "127.0.0.8:30303"))
|
|
||||||
d.addStatic(newNode(uintID(0x09), "127.0.0.9:30303"))
|
|
||||||
},
|
|
||||||
wantNewDials: []*enode.Node{
|
|
||||||
newNode(uintID(0x03), "127.0.0.3:30303"),
|
|
||||||
newNode(uintID(0x04), "127.0.0.4:30303"),
|
|
||||||
newNode(uintID(0x05), "127.0.0.5:30303"),
|
|
||||||
newNode(uintID(0x06), "127.0.0.6:30303"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Dial to 0x03 completes, filling a peer slot. One slot remains,
|
|
||||||
// two dials are launched to attempt to fill it.
|
|
||||||
{
|
|
||||||
succeeded: []enode.ID{
|
|
||||||
uintID(0x03),
|
|
||||||
},
|
|
||||||
failed: []enode.ID{
|
|
||||||
uintID(0x04),
|
|
||||||
uintID(0x05),
|
|
||||||
uintID(0x06),
|
|
||||||
},
|
|
||||||
wantResolves: map[enode.ID]*enode.Node{
|
|
||||||
uintID(0x04): nil,
|
|
||||||
uintID(0x05): nil,
|
|
||||||
uintID(0x06): nil,
|
|
||||||
},
|
|
||||||
wantNewDials: []*enode.Node{
|
|
||||||
newNode(uintID(0x08), "127.0.0.8:30303"),
|
|
||||||
newNode(uintID(0x09), "127.0.0.9:30303"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Peer 0x01 drops and 0x07 connects as inbound peer.
|
|
||||||
// Only 0x01 is dialed.
|
|
||||||
{
|
|
||||||
peersAdded: []*conn{
|
|
||||||
{flags: inboundConn, node: newNode(uintID(0x07), "127.0.0.7:30303")},
|
|
||||||
},
|
|
||||||
peersRemoved: []enode.ID{
|
|
||||||
uintID(0x01),
|
|
||||||
},
|
|
||||||
wantNewDials: []*enode.Node{
|
|
||||||
newNode(uintID(0x01), "127.0.0.1:30303"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that removing static nodes stops connecting to them.
|
|
||||||
func TestDialSchedRemoveStatic(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
config := dialConfig{
|
|
||||||
maxActiveDials: 1,
|
|
||||||
maxDialPeers: 1,
|
|
||||||
}
|
|
||||||
runDialTest(t, config, []dialTestRound{
|
|
||||||
// Add static nodes.
|
|
||||||
{
|
|
||||||
update: func(d *dialScheduler) {
|
|
||||||
d.addStatic(newNode(uintID(0x01), "127.0.0.1:30303"))
|
|
||||||
d.addStatic(newNode(uintID(0x02), "127.0.0.2:30303"))
|
|
||||||
d.addStatic(newNode(uintID(0x03), "127.0.0.3:30303"))
|
|
||||||
},
|
|
||||||
wantNewDials: []*enode.Node{
|
|
||||||
newNode(uintID(0x01), "127.0.0.1:30303"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Dial to 0x01 fails.
|
|
||||||
{
|
|
||||||
failed: []enode.ID{
|
|
||||||
uintID(0x01),
|
|
||||||
},
|
|
||||||
wantResolves: map[enode.ID]*enode.Node{
|
|
||||||
uintID(0x01): nil,
|
|
||||||
},
|
|
||||||
wantNewDials: []*enode.Node{
|
|
||||||
newNode(uintID(0x02), "127.0.0.2:30303"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// All static nodes are removed. 0x01 is in history, 0x02 is being
|
|
||||||
// dialed, 0x03 is in staticPool.
|
|
||||||
{
|
|
||||||
update: func(d *dialScheduler) {
|
|
||||||
d.removeStatic(newNode(uintID(0x01), "127.0.0.1:30303"))
|
|
||||||
d.removeStatic(newNode(uintID(0x02), "127.0.0.2:30303"))
|
|
||||||
d.removeStatic(newNode(uintID(0x03), "127.0.0.3:30303"))
|
|
||||||
},
|
|
||||||
failed: []enode.ID{
|
|
||||||
uintID(0x02),
|
|
||||||
},
|
|
||||||
wantResolves: map[enode.ID]*enode.Node{
|
|
||||||
uintID(0x02): nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Since all static nodes are removed, they should not be dialed again.
|
|
||||||
{}, {}, {},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that static dials are selected at random.
|
|
||||||
func TestDialSchedManyStaticNodes(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
config := dialConfig{maxDialPeers: 2}
|
|
||||||
runDialTest(t, config, []dialTestRound{
|
|
||||||
{
|
|
||||||
peersAdded: []*conn{
|
|
||||||
{flags: dynDialedConn, node: newNode(uintID(0xFFFE), "")},
|
|
||||||
{flags: dynDialedConn, node: newNode(uintID(0xFFFF), "")},
|
|
||||||
},
|
|
||||||
update: func(d *dialScheduler) {
|
|
||||||
for id := uint16(0); id < 2000; id++ {
|
|
||||||
n := newNode(uintID(id), "127.0.0.1:30303")
|
|
||||||
d.addStatic(n)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
peersRemoved: []enode.ID{
|
|
||||||
uintID(0xFFFE),
|
|
||||||
uintID(0xFFFF),
|
|
||||||
},
|
|
||||||
wantNewDials: []*enode.Node{
|
|
||||||
newNode(uintID(0x0085), "127.0.0.1:30303"),
|
|
||||||
newNode(uintID(0x02dc), "127.0.0.1:30303"),
|
|
||||||
newNode(uintID(0x0285), "127.0.0.1:30303"),
|
|
||||||
newNode(uintID(0x00cb), "127.0.0.1:30303"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that past dials are not retried for some time.
|
|
||||||
func TestDialSchedHistory(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
config := dialConfig{
|
|
||||||
maxActiveDials: 3,
|
|
||||||
maxDialPeers: 3,
|
|
||||||
}
|
|
||||||
runDialTest(t, config, []dialTestRound{
|
|
||||||
{
|
|
||||||
update: func(d *dialScheduler) {
|
|
||||||
d.addStatic(newNode(uintID(0x01), "127.0.0.1:30303"))
|
|
||||||
d.addStatic(newNode(uintID(0x02), "127.0.0.2:30303"))
|
|
||||||
d.addStatic(newNode(uintID(0x03), "127.0.0.3:30303"))
|
|
||||||
},
|
|
||||||
wantNewDials: []*enode.Node{
|
|
||||||
newNode(uintID(0x01), "127.0.0.1:30303"),
|
|
||||||
newNode(uintID(0x02), "127.0.0.2:30303"),
|
|
||||||
newNode(uintID(0x03), "127.0.0.3:30303"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// No new tasks are launched in this round because all static
|
|
||||||
// nodes are either connected or still being dialed.
|
|
||||||
{
|
|
||||||
succeeded: []enode.ID{
|
|
||||||
uintID(0x01),
|
|
||||||
uintID(0x02),
|
|
||||||
},
|
|
||||||
failed: []enode.ID{
|
|
||||||
uintID(0x03),
|
|
||||||
},
|
|
||||||
wantResolves: map[enode.ID]*enode.Node{
|
|
||||||
uintID(0x03): nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Nothing happens in this round because we're waiting for
|
|
||||||
// node 0x3's history entry to expire.
|
|
||||||
{},
|
|
||||||
// The cache entry for node 0x03 has expired and is retried.
|
|
||||||
{
|
|
||||||
wantNewDials: []*enode.Node{
|
|
||||||
newNode(uintID(0x03), "127.0.0.3:30303"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDialSchedResolve(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
config := dialConfig{
|
|
||||||
maxActiveDials: 1,
|
|
||||||
maxDialPeers: 1,
|
|
||||||
}
|
|
||||||
node := newNode(uintID(0x01), "")
|
|
||||||
resolved := newNode(uintID(0x01), "127.0.0.1:30303")
|
|
||||||
resolved2 := newNode(uintID(0x01), "127.0.0.55:30303")
|
|
||||||
runDialTest(t, config, []dialTestRound{
|
|
||||||
{
|
|
||||||
update: func(d *dialScheduler) {
|
|
||||||
d.addStatic(node)
|
|
||||||
},
|
|
||||||
wantResolves: map[enode.ID]*enode.Node{
|
|
||||||
uintID(0x01): resolved,
|
|
||||||
},
|
|
||||||
wantNewDials: []*enode.Node{
|
|
||||||
resolved,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
failed: []enode.ID{
|
|
||||||
uintID(0x01),
|
|
||||||
},
|
|
||||||
wantResolves: map[enode.ID]*enode.Node{
|
|
||||||
uintID(0x01): resolved2,
|
|
||||||
},
|
|
||||||
wantNewDials: []*enode.Node{
|
|
||||||
resolved2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------
|
|
||||||
// Code below here is the framework for the tests above.
|
|
||||||
|
|
||||||
type dialTestRound struct {
|
|
||||||
peersAdded []*conn
|
|
||||||
peersRemoved []enode.ID
|
|
||||||
update func(*dialScheduler) // called at beginning of round
|
|
||||||
discovered []*enode.Node // newly discovered nodes
|
|
||||||
succeeded []enode.ID // dials which succeed this round
|
|
||||||
failed []enode.ID // dials which fail this round
|
|
||||||
wantResolves map[enode.ID]*enode.Node
|
|
||||||
wantNewDials []*enode.Node // dials that should be launched in this round
|
|
||||||
}
|
|
||||||
|
|
||||||
func runDialTest(t *testing.T, config dialConfig, rounds []dialTestRound) {
|
|
||||||
var (
|
|
||||||
clock = new(mclock.Simulated)
|
|
||||||
iterator = newDialTestIterator()
|
|
||||||
dialer = newDialTestDialer()
|
|
||||||
resolver = new(dialTestResolver)
|
|
||||||
peers = make(map[enode.ID]*conn)
|
|
||||||
setupCh = make(chan *conn)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Override config.
|
|
||||||
config.clock = clock
|
|
||||||
config.dialer = dialer
|
|
||||||
config.resolver = resolver
|
|
||||||
config.log = testlog.Logger(t, log.LvlTrace)
|
|
||||||
config.rand = rand.New(rand.NewSource(0x1111))
|
|
||||||
|
|
||||||
// Set up the dialer. The setup function below runs on the dialTask
|
|
||||||
// goroutine and adds the peer.
|
|
||||||
var dialsched *dialScheduler
|
|
||||||
setup := func(fd net.Conn, f connFlag, node *enode.Node) error {
|
|
||||||
conn := &conn{flags: f, node: node}
|
|
||||||
dialsched.peerAdded(conn)
|
|
||||||
setupCh <- conn
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
dialsched = newDialScheduler(config, iterator, setup)
|
|
||||||
defer dialsched.stop()
|
|
||||||
|
|
||||||
for i, round := range rounds {
|
|
||||||
// Apply peer set updates.
|
|
||||||
for _, c := range round.peersAdded {
|
|
||||||
if peers[c.node.ID()] != nil {
|
|
||||||
t.Fatalf("round %d: peer %v already connected", i, c.node.ID())
|
|
||||||
}
|
|
||||||
dialsched.peerAdded(c)
|
|
||||||
peers[c.node.ID()] = c
|
|
||||||
}
|
|
||||||
for _, id := range round.peersRemoved {
|
|
||||||
c := peers[id]
|
|
||||||
if c == nil {
|
|
||||||
t.Fatalf("round %d: can't remove non-existent peer %v", i, id)
|
|
||||||
}
|
|
||||||
dialsched.peerRemoved(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init round.
|
|
||||||
t.Logf("round %d (%d peers)", i, len(peers))
|
|
||||||
resolver.setAnswers(round.wantResolves)
|
|
||||||
if round.update != nil {
|
|
||||||
round.update(dialsched)
|
|
||||||
}
|
|
||||||
iterator.addNodes(round.discovered)
|
|
||||||
|
|
||||||
// Unblock dialTask goroutines.
|
|
||||||
if err := dialer.completeDials(round.succeeded, nil); err != nil {
|
|
||||||
t.Fatalf("round %d: %v", i, err)
|
|
||||||
}
|
|
||||||
for range round.succeeded {
|
|
||||||
conn := <-setupCh
|
|
||||||
peers[conn.node.ID()] = conn
|
|
||||||
}
|
|
||||||
if err := dialer.completeDials(round.failed, errors.New("oops")); err != nil {
|
|
||||||
t.Fatalf("round %d: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for new tasks.
|
|
||||||
if err := dialer.waitForDials(round.wantNewDials); err != nil {
|
|
||||||
t.Fatalf("round %d: %v", i, err)
|
|
||||||
}
|
|
||||||
if !resolver.checkCalls() {
|
|
||||||
t.Fatalf("unexpected calls to Resolve: %v", resolver.calls)
|
|
||||||
}
|
|
||||||
|
|
||||||
clock.Run(16 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// dialTestIterator is the input iterator for dialer tests. This works a bit like a channel
|
|
||||||
// with infinite buffer: nodes are added to the buffer with addNodes, which unblocks Next
|
|
||||||
// and returns them from the iterator.
|
|
||||||
type dialTestIterator struct {
|
|
||||||
cur *enode.Node
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
buf []*enode.Node
|
|
||||||
cond *sync.Cond
|
|
||||||
closed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDialTestIterator() *dialTestIterator {
|
|
||||||
it := &dialTestIterator{}
|
|
||||||
it.cond = sync.NewCond(&it.mu)
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
|
|
||||||
// addNodes adds nodes to the iterator buffer and unblocks Next.
|
|
||||||
func (it *dialTestIterator) addNodes(nodes []*enode.Node) {
|
|
||||||
it.mu.Lock()
|
|
||||||
defer it.mu.Unlock()
|
|
||||||
|
|
||||||
it.buf = append(it.buf, nodes...)
|
|
||||||
it.cond.Signal()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node returns the current node.
|
|
||||||
func (it *dialTestIterator) Node() *enode.Node {
|
|
||||||
return it.cur
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next moves to the next node.
|
|
||||||
func (it *dialTestIterator) Next() bool {
|
|
||||||
it.mu.Lock()
|
|
||||||
defer it.mu.Unlock()
|
|
||||||
|
|
||||||
it.cur = nil
|
|
||||||
for len(it.buf) == 0 && !it.closed {
|
|
||||||
it.cond.Wait()
|
|
||||||
}
|
|
||||||
if it.closed {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
it.cur = it.buf[0]
|
|
||||||
copy(it.buf[:], it.buf[1:])
|
|
||||||
it.buf = it.buf[:len(it.buf)-1]
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close ends the iterator, unblocking Next.
|
|
||||||
func (it *dialTestIterator) Close() {
|
|
||||||
it.mu.Lock()
|
|
||||||
defer it.mu.Unlock()
|
|
||||||
|
|
||||||
it.closed = true
|
|
||||||
it.buf = nil
|
|
||||||
it.cond.Signal()
|
|
||||||
}
|
|
||||||
|
|
||||||
// dialTestDialer is the NodeDialer used by runDialTest.
|
|
||||||
type dialTestDialer struct {
|
|
||||||
init chan *dialTestReq
|
|
||||||
blocked map[enode.ID]*dialTestReq
|
|
||||||
}
|
|
||||||
|
|
||||||
type dialTestReq struct {
|
|
||||||
n *enode.Node
|
|
||||||
unblock chan error
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDialTestDialer() *dialTestDialer {
|
|
||||||
return &dialTestDialer{
|
|
||||||
init: make(chan *dialTestReq),
|
|
||||||
blocked: make(map[enode.ID]*dialTestReq),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial implements NodeDialer.
|
|
||||||
func (d *dialTestDialer) Dial(ctx context.Context, n *enode.Node) (net.Conn, error) {
|
|
||||||
req := &dialTestReq{n: n, unblock: make(chan error, 1)}
|
|
||||||
select {
|
|
||||||
case d.init <- req:
|
|
||||||
select {
|
|
||||||
case err := <-req.unblock:
|
|
||||||
pipe, _ := net.Pipe()
|
|
||||||
return pipe, err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// waitForDials waits for calls to Dial with the given nodes as argument.
|
|
||||||
// Those calls will be held blocking until completeDials is called with the same nodes.
|
|
||||||
func (d *dialTestDialer) waitForDials(nodes []*enode.Node) error {
|
|
||||||
waitset := make(map[enode.ID]*enode.Node, len(nodes))
|
|
||||||
for _, n := range nodes {
|
|
||||||
waitset[n.ID()] = n
|
|
||||||
}
|
|
||||||
timeout := time.NewTimer(1 * time.Second)
|
|
||||||
defer timeout.Stop()
|
|
||||||
|
|
||||||
for len(waitset) > 0 {
|
|
||||||
select {
|
|
||||||
case req := <-d.init:
|
|
||||||
want, ok := waitset[req.n.ID()]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("attempt to dial unexpected node %v", req.n.ID())
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(req.n, want) {
|
|
||||||
return fmt.Errorf("ENR of dialed node %v does not match test", req.n.ID())
|
|
||||||
}
|
|
||||||
delete(waitset, req.n.ID())
|
|
||||||
d.blocked[req.n.ID()] = req
|
|
||||||
case <-timeout.C:
|
|
||||||
var waitlist []enode.ID
|
|
||||||
for id := range waitset {
|
|
||||||
waitlist = append(waitlist, id)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("timed out waiting for dials to %v", waitlist)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return d.checkUnexpectedDial()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dialTestDialer) checkUnexpectedDial() error {
|
|
||||||
select {
|
|
||||||
case req := <-d.init:
|
|
||||||
return fmt.Errorf("attempt to dial unexpected node %v", req.n.ID())
|
|
||||||
case <-time.After(150 * time.Millisecond):
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// completeDials unblocks calls to Dial for the given nodes.
|
|
||||||
func (d *dialTestDialer) completeDials(ids []enode.ID, err error) error {
|
|
||||||
for _, id := range ids {
|
|
||||||
req := d.blocked[id]
|
|
||||||
if req == nil {
|
|
||||||
return fmt.Errorf("can't complete dial to %v", id)
|
|
||||||
}
|
|
||||||
req.unblock <- err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// dialTestResolver tracks calls to resolve.
|
|
||||||
type dialTestResolver struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
calls []enode.ID
|
|
||||||
answers map[enode.ID]*enode.Node
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *dialTestResolver) setAnswers(m map[enode.ID]*enode.Node) {
|
|
||||||
t.mu.Lock()
|
|
||||||
defer t.mu.Unlock()
|
|
||||||
|
|
||||||
t.answers = m
|
|
||||||
t.calls = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *dialTestResolver) checkCalls() bool {
|
|
||||||
t.mu.Lock()
|
|
||||||
defer t.mu.Unlock()
|
|
||||||
|
|
||||||
for _, id := range t.calls {
|
|
||||||
if _, ok := t.answers[id]; !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *dialTestResolver) Resolve(n *enode.Node) *enode.Node {
|
|
||||||
t.mu.Lock()
|
|
||||||
defer t.mu.Unlock()
|
|
||||||
|
|
||||||
t.calls = append(t.calls, n.ID())
|
|
||||||
return t.answers[n.ID()]
|
|
||||||
}
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
// Copyright 2019 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
"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/p2p/netutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UDPConn is a network connection on which discovery can operate.
|
|
||||||
type UDPConn interface {
|
|
||||||
ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error)
|
|
||||||
WriteToUDP(b []byte, addr *net.UDPAddr) (n int, err error)
|
|
||||||
Close() error
|
|
||||||
LocalAddr() net.Addr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config holds settings for the discovery listener.
|
|
||||||
type Config struct {
|
|
||||||
// These settings are required and configure the UDP listener:
|
|
||||||
PrivateKey *ecdsa.PrivateKey
|
|
||||||
|
|
||||||
// All remaining settings are optional.
|
|
||||||
|
|
||||||
// Packet handling configuration:
|
|
||||||
NetRestrict *netutil.Netlist // list of allowed IP networks
|
|
||||||
Unhandled chan<- ReadPacket // unhandled packets are sent on this channel
|
|
||||||
|
|
||||||
// Node table configuration:
|
|
||||||
Bootnodes []*enode.Node // list of bootstrap nodes
|
|
||||||
PingInterval time.Duration // speed of node liveness check
|
|
||||||
RefreshInterval time.Duration // used in bucket refresh
|
|
||||||
|
|
||||||
// The options below are useful in very specific cases, like in unit tests.
|
|
||||||
V5ProtocolID *[6]byte
|
|
||||||
Log log.Logger // if set, log messages go here
|
|
||||||
ValidSchemes enr.IdentityScheme // allowed identity schemes
|
|
||||||
Clock mclock.Clock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg Config) withDefaults() Config {
|
|
||||||
// Node table configuration:
|
|
||||||
if cfg.PingInterval == 0 {
|
|
||||||
cfg.PingInterval = 10 * time.Second
|
|
||||||
}
|
|
||||||
if cfg.RefreshInterval == 0 {
|
|
||||||
cfg.RefreshInterval = 30 * time.Minute
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug/test settings:
|
|
||||||
if cfg.Log == nil {
|
|
||||||
cfg.Log = log.Root()
|
|
||||||
}
|
|
||||||
if cfg.ValidSchemes == nil {
|
|
||||||
cfg.ValidSchemes = enode.ValidSchemes
|
|
||||||
}
|
|
||||||
if cfg.Clock == nil {
|
|
||||||
cfg.Clock = mclock.System{}
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenUDP starts listening for discovery packets on the given UDP socket.
|
|
||||||
func ListenUDP(c UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv4, error) {
|
|
||||||
return ListenV4(c, ln, cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadPacket is a packet that couldn't be handled. Those packets are sent to the unhandled
|
|
||||||
// channel if configured.
|
|
||||||
type ReadPacket struct {
|
|
||||||
Data []byte
|
|
||||||
Addr *net.UDPAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
func min(x, y int) int {
|
|
||||||
if x > y {
|
|
||||||
return y
|
|
||||||
}
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
// Copyright 2019 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// lookup performs a network search for nodes close to the given target. It approaches the
|
|
||||||
// target by querying nodes that are closer to it on each iteration. The given target does
|
|
||||||
// not need to be an actual node identifier.
|
|
||||||
type lookup struct {
|
|
||||||
tab *Table
|
|
||||||
queryfunc func(*node) ([]*node, error)
|
|
||||||
replyCh chan []*node
|
|
||||||
cancelCh <-chan struct{}
|
|
||||||
asked, seen map[enode.ID]bool
|
|
||||||
result nodesByDistance
|
|
||||||
replyBuffer []*node
|
|
||||||
queries int
|
|
||||||
}
|
|
||||||
|
|
||||||
type queryFunc func(*node) ([]*node, error)
|
|
||||||
|
|
||||||
func newLookup(ctx context.Context, tab *Table, target enode.ID, q queryFunc) *lookup {
|
|
||||||
it := &lookup{
|
|
||||||
tab: tab,
|
|
||||||
queryfunc: q,
|
|
||||||
asked: make(map[enode.ID]bool),
|
|
||||||
seen: make(map[enode.ID]bool),
|
|
||||||
result: nodesByDistance{target: target},
|
|
||||||
replyCh: make(chan []*node, alpha),
|
|
||||||
cancelCh: ctx.Done(),
|
|
||||||
queries: -1,
|
|
||||||
}
|
|
||||||
// Don't query further if we hit ourself.
|
|
||||||
// Unlikely to happen often in practice.
|
|
||||||
it.asked[tab.self().ID()] = true
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
|
|
||||||
// run runs the lookup to completion and returns the closest nodes found.
|
|
||||||
func (it *lookup) run() []*enode.Node {
|
|
||||||
for it.advance() {
|
|
||||||
}
|
|
||||||
return unwrapNodes(it.result.entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
// advance advances the lookup until any new nodes have been found.
|
|
||||||
// It returns false when the lookup has ended.
|
|
||||||
func (it *lookup) advance() bool {
|
|
||||||
for it.startQueries() {
|
|
||||||
select {
|
|
||||||
case nodes := <-it.replyCh:
|
|
||||||
it.replyBuffer = it.replyBuffer[:0]
|
|
||||||
for _, n := range nodes {
|
|
||||||
if n != nil && !it.seen[n.ID()] {
|
|
||||||
it.seen[n.ID()] = true
|
|
||||||
it.result.push(n, bucketSize)
|
|
||||||
it.replyBuffer = append(it.replyBuffer, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
it.queries--
|
|
||||||
if len(it.replyBuffer) > 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
case <-it.cancelCh:
|
|
||||||
it.shutdown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *lookup) shutdown() {
|
|
||||||
for it.queries > 0 {
|
|
||||||
<-it.replyCh
|
|
||||||
it.queries--
|
|
||||||
}
|
|
||||||
it.queryfunc = nil
|
|
||||||
it.replyBuffer = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *lookup) startQueries() bool {
|
|
||||||
if it.queryfunc == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// The first query returns nodes from the local table.
|
|
||||||
if it.queries == -1 {
|
|
||||||
closest := it.tab.findnodeByID(it.result.target, bucketSize, false)
|
|
||||||
// Avoid finishing the lookup too quickly if table is empty. It'd be better to wait
|
|
||||||
// for the table to fill in this case, but there is no good mechanism for that
|
|
||||||
// yet.
|
|
||||||
if len(closest.entries) == 0 {
|
|
||||||
it.slowdown()
|
|
||||||
}
|
|
||||||
it.queries = 1
|
|
||||||
it.replyCh <- closest.entries
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ask the closest nodes that we haven't asked yet.
|
|
||||||
for i := 0; i < len(it.result.entries) && it.queries < alpha; i++ {
|
|
||||||
n := it.result.entries[i]
|
|
||||||
if !it.asked[n.ID()] {
|
|
||||||
it.asked[n.ID()] = true
|
|
||||||
it.queries++
|
|
||||||
go it.query(n, it.replyCh)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The lookup ends when no more nodes can be asked.
|
|
||||||
return it.queries > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *lookup) slowdown() {
|
|
||||||
sleep := time.NewTimer(1 * time.Second)
|
|
||||||
defer sleep.Stop()
|
|
||||||
select {
|
|
||||||
case <-sleep.C:
|
|
||||||
case <-it.tab.closeReq:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *lookup) query(n *node, reply chan<- []*node) {
|
|
||||||
fails := it.tab.db.FindFails(n.ID(), n.IP())
|
|
||||||
r, err := it.queryfunc(n)
|
|
||||||
if errors.Is(err, errClosed) {
|
|
||||||
// Avoid recording failures on shutdown.
|
|
||||||
reply <- nil
|
|
||||||
return
|
|
||||||
} else if len(r) == 0 {
|
|
||||||
fails++
|
|
||||||
it.tab.db.UpdateFindFails(n.ID(), n.IP(), fails)
|
|
||||||
// Remove the node from the local table if it fails to return anything useful too
|
|
||||||
// many times, but only if there are enough other nodes in the bucket.
|
|
||||||
dropped := false
|
|
||||||
if fails >= maxFindnodeFailures && it.tab.bucketLen(n.ID()) >= bucketSize/2 {
|
|
||||||
dropped = true
|
|
||||||
it.tab.delete(n)
|
|
||||||
}
|
|
||||||
it.tab.log.Trace("FINDNODE failed", "id", n.ID(), "failcount", fails, "dropped", dropped, "err", err)
|
|
||||||
} else if fails > 0 {
|
|
||||||
// Reset failure counter because it counts _consecutive_ failures.
|
|
||||||
it.tab.db.UpdateFindFails(n.ID(), n.IP(), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab as many nodes as possible. Some of them might not be alive anymore, but we'll
|
|
||||||
// just remove those again during revalidation.
|
|
||||||
for _, n := range r {
|
|
||||||
it.tab.addSeenNode(n)
|
|
||||||
}
|
|
||||||
reply <- r
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
nextLookup lookupFunc
|
|
||||||
ctx context.Context
|
|
||||||
cancel func()
|
|
||||||
lookup *lookup
|
|
||||||
}
|
|
||||||
|
|
||||||
type lookupFunc func(ctx context.Context) *lookup
|
|
||||||
|
|
||||||
func newLookupIterator(ctx context.Context, next lookupFunc) *lookupIterator {
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
|
||||||
return &lookupIterator{ctx: ctx, cancel: cancel, nextLookup: next}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node returns the current node.
|
|
||||||
func (it *lookupIterator) Node() *enode.Node {
|
|
||||||
if len(it.buffer) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return unwrapNode(it.buffer[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next moves to the next node.
|
|
||||||
func (it *lookupIterator) Next() bool {
|
|
||||||
// Consume next node in buffer.
|
|
||||||
if len(it.buffer) > 0 {
|
|
||||||
it.buffer = it.buffer[1:]
|
|
||||||
}
|
|
||||||
// Advance the lookup to refill the buffer.
|
|
||||||
for len(it.buffer) == 0 {
|
|
||||||
if it.ctx.Err() != nil {
|
|
||||||
it.lookup = nil
|
|
||||||
it.buffer = nil
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if it.lookup == nil {
|
|
||||||
it.lookup = it.nextLookup(it.ctx)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !it.lookup.advance() {
|
|
||||||
it.lookup = nil
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
it.buffer = it.lookup.replyBuffer
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close ends the iterator.
|
|
||||||
func (it *lookupIterator) Close() {
|
|
||||||
it.cancel()
|
|
||||||
}
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
// Copyright 2023 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/metrics"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
moduleName = "discover"
|
|
||||||
// ingressMeterName is the prefix of the per-packet inbound metrics.
|
|
||||||
ingressMeterName = moduleName + "/ingress"
|
|
||||||
|
|
||||||
// egressMeterName is the prefix of the per-packet outbound metrics.
|
|
||||||
egressMeterName = moduleName + "/egress"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
bucketsCounter []metrics.Counter
|
|
||||||
ingressTrafficMeter = metrics.NewRegisteredMeter(ingressMeterName, nil)
|
|
||||||
egressTrafficMeter = metrics.NewRegisteredMeter(egressMeterName, nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
for i := 0; i < nBuckets; i++ {
|
|
||||||
bucketsCounter = append(bucketsCounter, metrics.NewRegisteredCounter(fmt.Sprintf("%s/bucket/%d/count", moduleName, i), nil))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// meteredConn is a wrapper around a net.UDPConn that meters both the
|
|
||||||
// inbound and outbound network traffic.
|
|
||||||
type meteredUdpConn struct {
|
|
||||||
UDPConn
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMeteredConn(conn UDPConn) UDPConn {
|
|
||||||
// Short circuit if metrics are disabled
|
|
||||||
if !metrics.Enabled {
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
return &meteredUdpConn{UDPConn: conn}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read delegates a network read to the underlying connection, bumping the udp ingress traffic meter along the way.
|
|
||||||
func (c *meteredUdpConn) ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) {
|
|
||||||
n, addr, err = c.UDPConn.ReadFromUDP(b)
|
|
||||||
ingressTrafficMeter.Mark(int64(n))
|
|
||||||
return n, addr, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write delegates a network write to the underlying connection, bumping the udp egress traffic meter along the way.
|
|
||||||
func (c *meteredUdpConn) WriteToUDP(b []byte, addr *net.UDPAddr) (n int, err error) {
|
|
||||||
n, err = c.UDPConn.WriteToUDP(b, addr)
|
|
||||||
egressTrafficMeter.Mark(int64(n))
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
// Copyright 2015 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"errors"
|
|
||||||
"math/big"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// node represents a host on the network.
|
|
||||||
// The fields of Node may not be modified.
|
|
||||||
type node struct {
|
|
||||||
enode.Node
|
|
||||||
addedAt time.Time // time when the node was added to the table
|
|
||||||
livenessChecks uint // how often liveness was checked
|
|
||||||
}
|
|
||||||
|
|
||||||
type encPubkey [64]byte
|
|
||||||
|
|
||||||
func encodePubkey(key *ecdsa.PublicKey) encPubkey {
|
|
||||||
var e encPubkey
|
|
||||||
math.ReadBits(key.X, e[:len(e)/2])
|
|
||||||
math.ReadBits(key.Y, e[len(e)/2:])
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodePubkey(curve elliptic.Curve, e []byte) (*ecdsa.PublicKey, error) {
|
|
||||||
if len(e) != len(encPubkey{}) {
|
|
||||||
return nil, errors.New("wrong size public key data")
|
|
||||||
}
|
|
||||||
p := &ecdsa.PublicKey{Curve: curve, X: new(big.Int), Y: new(big.Int)}
|
|
||||||
half := len(e) / 2
|
|
||||||
p.X.SetBytes(e[:half])
|
|
||||||
p.Y.SetBytes(e[half:])
|
|
||||||
if !p.Curve.IsOnCurve(p.X, p.Y) {
|
|
||||||
return nil, errors.New("invalid curve point")
|
|
||||||
}
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *node) addr() *net.UDPAddr {
|
|
||||||
return &net.UDPAddr{IP: n.IP(), Port: n.UDP()}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *node) String() string {
|
|
||||||
return n.Node.String()
|
|
||||||
}
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
// Copyright 2016 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Contains the NTP time drift detection via the SNTP protocol:
|
|
||||||
// https://tools.ietf.org/html/rfc4330
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ntpPool = "pool.ntp.org" // ntpPool is the NTP server to query for the current time
|
|
||||||
ntpChecks = 3 // Number of measurements to do against the NTP server
|
|
||||||
)
|
|
||||||
|
|
||||||
// checkClockDrift queries an NTP server for clock drifts and warns the user if
|
|
||||||
// one large enough is detected.
|
|
||||||
func checkClockDrift() {
|
|
||||||
drift, err := sntpDrift(ntpChecks)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if drift < -driftThreshold || drift > driftThreshold {
|
|
||||||
log.Warn(fmt.Sprintf("System clock seems off by %v, which can prevent network connectivity", drift))
|
|
||||||
log.Warn("Please enable network time synchronisation in system settings.")
|
|
||||||
} else {
|
|
||||||
log.Debug("NTP sanity check done", "drift", drift)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sntpDrift does a naive time resolution against an NTP server and returns the
|
|
||||||
// measured drift. This method uses the simple version of NTP. It's not precise
|
|
||||||
// but should be fine for these purposes.
|
|
||||||
//
|
|
||||||
// Note, it executes two extra measurements compared to the number of requested
|
|
||||||
// ones to be able to discard the two extremes as outliers.
|
|
||||||
func sntpDrift(measurements int) (time.Duration, error) {
|
|
||||||
// Resolve the address of the NTP server
|
|
||||||
addr, err := net.ResolveUDPAddr("udp", ntpPool+":123")
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
// Construct the time request (empty package with only 2 fields set):
|
|
||||||
// Bits 3-5: Protocol version, 3
|
|
||||||
// Bits 6-8: Mode of operation, client, 3
|
|
||||||
request := make([]byte, 48)
|
|
||||||
request[0] = 3<<3 | 3
|
|
||||||
|
|
||||||
// Execute each of the measurements
|
|
||||||
drifts := []time.Duration{}
|
|
||||||
for i := 0; i < measurements+2; i++ {
|
|
||||||
// Dial the NTP server and send the time retrieval request
|
|
||||||
conn, err := net.DialUDP("udp", nil, addr)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
sent := time.Now()
|
|
||||||
if _, err = conn.Write(request); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
// Retrieve the reply and calculate the elapsed time
|
|
||||||
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
|
||||||
|
|
||||||
reply := make([]byte, 48)
|
|
||||||
if _, err = conn.Read(reply); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
elapsed := time.Since(sent)
|
|
||||||
|
|
||||||
// Reconstruct the time from the reply data
|
|
||||||
sec := uint64(reply[43]) | uint64(reply[42])<<8 | uint64(reply[41])<<16 | uint64(reply[40])<<24
|
|
||||||
frac := uint64(reply[47]) | uint64(reply[46])<<8 | uint64(reply[45])<<16 | uint64(reply[44])<<24
|
|
||||||
|
|
||||||
nanosec := sec*1e9 + (frac*1e9)>>32
|
|
||||||
|
|
||||||
t := time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC).Add(time.Duration(nanosec)).Local()
|
|
||||||
|
|
||||||
// Calculate the drift based on an assumed answer time of RRT/2
|
|
||||||
drifts = append(drifts, sent.Sub(t)+elapsed/2)
|
|
||||||
}
|
|
||||||
// Calculate average drift (drop two extremities to avoid outliers)
|
|
||||||
slices.Sort(drifts)
|
|
||||||
|
|
||||||
drift := time.Duration(0)
|
|
||||||
for i := 1; i < len(drifts)-1; i++ {
|
|
||||||
drift += drifts[i]
|
|
||||||
}
|
|
||||||
return drift / time.Duration(measurements), nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,754 +0,0 @@
|
||||||
// Copyright 2015 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Package discover implements the Node Discovery Protocol.
|
|
||||||
//
|
|
||||||
// The Node Discovery protocol provides a way to find RLPx nodes that
|
|
||||||
// can be connected to. It uses a Kademlia-like protocol to maintain a
|
|
||||||
// distributed database of the IDs and endpoints of all listening
|
|
||||||
// nodes.
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
crand "crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
mrand "math/rand"
|
|
||||||
"net"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/metrics"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/netutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
alpha = 3 // Kademlia concurrency factor
|
|
||||||
bucketSize = 16 // Kademlia bucket size
|
|
||||||
maxReplacements = 10 // Size of per-bucket replacement list
|
|
||||||
|
|
||||||
// We keep buckets for the upper 1/15 of distances because
|
|
||||||
// it's very unlikely we'll ever encounter a node that's closer.
|
|
||||||
hashBits = len(common.Hash{}) * 8
|
|
||||||
nBuckets = hashBits / 15 // Number of buckets
|
|
||||||
bucketMinDistance = hashBits - nBuckets // Log distance of closest bucket
|
|
||||||
|
|
||||||
// IP address limits.
|
|
||||||
bucketIPLimit, bucketSubnet = 2, 24 // at most 2 addresses from the same /24
|
|
||||||
tableIPLimit, tableSubnet = 10, 24
|
|
||||||
|
|
||||||
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
|
|
||||||
nursery []*node // bootstrap nodes
|
|
||||||
rand *mrand.Rand // source of randomness, periodically reseeded
|
|
||||||
ips netutil.DistinctNetSet
|
|
||||||
|
|
||||||
db *enode.DB // database of known nodes
|
|
||||||
net transport
|
|
||||||
cfg Config
|
|
||||||
log log.Logger
|
|
||||||
|
|
||||||
// loop channels
|
|
||||||
refreshReq chan chan struct{}
|
|
||||||
initDone chan struct{}
|
|
||||||
closeReq chan struct{}
|
|
||||||
closed chan struct{}
|
|
||||||
|
|
||||||
nodeAddedHook func(*bucket, *node)
|
|
||||||
nodeRemovedHook func(*bucket, *node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// transport is implemented by the UDP transports.
|
|
||||||
type transport interface {
|
|
||||||
Self() *enode.Node
|
|
||||||
RequestENR(*enode.Node) (*enode.Node, error)
|
|
||||||
lookupRandom() []*enode.Node
|
|
||||||
lookupSelf() []*enode.Node
|
|
||||||
ping(*enode.Node) (seq uint64, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// bucket contains nodes, ordered by their last activity. the entry
|
|
||||||
// that was most recently active is the first element in entries.
|
|
||||||
type bucket struct {
|
|
||||||
entries []*node // live entries, sorted by time of last contact
|
|
||||||
replacements []*node // recently seen nodes to be used if revalidation fails
|
|
||||||
ips netutil.DistinctNetSet
|
|
||||||
index int
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTable(t transport, db *enode.DB, cfg Config) (*Table, error) {
|
|
||||||
cfg = cfg.withDefaults()
|
|
||||||
tab := &Table{
|
|
||||||
net: t,
|
|
||||||
db: db,
|
|
||||||
cfg: cfg,
|
|
||||||
log: cfg.Log,
|
|
||||||
refreshReq: make(chan chan struct{}),
|
|
||||||
initDone: make(chan struct{}),
|
|
||||||
closeReq: make(chan struct{}),
|
|
||||||
closed: make(chan struct{}),
|
|
||||||
rand: mrand.New(mrand.NewSource(0)),
|
|
||||||
ips: netutil.DistinctNetSet{Subnet: tableSubnet, Limit: tableIPLimit},
|
|
||||||
}
|
|
||||||
if err := tab.setFallbackNodes(cfg.Bootnodes); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for i := range tab.buckets {
|
|
||||||
tab.buckets[i] = &bucket{
|
|
||||||
index: i,
|
|
||||||
ips: netutil.DistinctNetSet{Subnet: bucketSubnet, Limit: bucketIPLimit},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tab.seedRand()
|
|
||||||
tab.loadSeedNodes()
|
|
||||||
|
|
||||||
return tab, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMeteredTable(t transport, db *enode.DB, cfg Config) (*Table, error) {
|
|
||||||
tab, err := newTable(t, db, cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if metrics.Enabled {
|
|
||||||
tab.nodeAddedHook = func(b *bucket, n *node) {
|
|
||||||
bucketsCounter[b.index].Inc(1)
|
|
||||||
}
|
|
||||||
tab.nodeRemovedHook = func(b *bucket, n *node) {
|
|
||||||
bucketsCounter[b.index].Dec(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tab, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nodes returns all nodes contained in the table.
|
|
||||||
func (tab *Table) Nodes() []*enode.Node {
|
|
||||||
if !tab.isInitDone() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tab.mutex.Lock()
|
|
||||||
defer tab.mutex.Unlock()
|
|
||||||
|
|
||||||
var nodes []*enode.Node
|
|
||||||
for _, b := range &tab.buckets {
|
|
||||||
for _, n := range b.entries {
|
|
||||||
nodes = append(nodes, unwrapNode(n))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tab *Table) self() *enode.Node {
|
|
||||||
return tab.net.Self()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tab *Table) seedRand() {
|
|
||||||
var b [8]byte
|
|
||||||
crand.Read(b[:])
|
|
||||||
|
|
||||||
tab.mutex.Lock()
|
|
||||||
tab.rand.Seed(int64(binary.BigEndian.Uint64(b[:])))
|
|
||||||
tab.mutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
defer tab.mutex.Unlock()
|
|
||||||
|
|
||||||
b := tab.bucket(id)
|
|
||||||
for _, e := range b.entries {
|
|
||||||
if e.ID() == id {
|
|
||||||
return unwrapNode(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// close terminates the network listener and flushes the node database.
|
|
||||||
func (tab *Table) close() {
|
|
||||||
close(tab.closeReq)
|
|
||||||
<-tab.closed
|
|
||||||
}
|
|
||||||
|
|
||||||
// setFallbackNodes sets the initial points of contact. These nodes
|
|
||||||
// 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))
|
|
||||||
for _, n := range nodes {
|
|
||||||
if err := n.ValidateComplete(); err != nil {
|
|
||||||
return fmt.Errorf("bad bootstrap node %q: %v", n, err)
|
|
||||||
}
|
|
||||||
if tab.cfg.NetRestrict != nil && !tab.cfg.NetRestrict.Contains(n.IP()) {
|
|
||||||
tab.log.Error("Bootstrap node filtered by netrestrict", "id", n.ID(), "ip", n.IP())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
nursery = append(nursery, wrapNode(n))
|
|
||||||
}
|
|
||||||
tab.nursery = nursery
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isInitDone returns whether the table's initial seeding procedure has completed.
|
|
||||||
func (tab *Table) isInitDone() bool {
|
|
||||||
select {
|
|
||||||
case <-tab.initDone:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tab *Table) refresh() <-chan struct{} {
|
|
||||||
done := make(chan struct{})
|
|
||||||
select {
|
|
||||||
case tab.refreshReq <- done:
|
|
||||||
case <-tab.closeReq:
|
|
||||||
close(done)
|
|
||||||
}
|
|
||||||
return done
|
|
||||||
}
|
|
||||||
|
|
||||||
// loop schedules runs of doRefresh, doRevalidate and copyLiveNodes.
|
|
||||||
func (tab *Table) loop() {
|
|
||||||
var (
|
|
||||||
revalidate = time.NewTimer(tab.nextRevalidateTime())
|
|
||||||
refresh = time.NewTimer(tab.nextRefreshTime())
|
|
||||||
copyNodes = time.NewTicker(copyNodesInterval)
|
|
||||||
refreshDone = make(chan struct{}) // where doRefresh reports completion
|
|
||||||
revalidateDone chan struct{} // where doRevalidate reports completion
|
|
||||||
waiting = []chan struct{}{tab.initDone} // holds waiting callers while doRefresh runs
|
|
||||||
)
|
|
||||||
defer refresh.Stop()
|
|
||||||
defer revalidate.Stop()
|
|
||||||
defer copyNodes.Stop()
|
|
||||||
|
|
||||||
// Start initial refresh.
|
|
||||||
go tab.doRefresh(refreshDone)
|
|
||||||
|
|
||||||
loop:
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-refresh.C:
|
|
||||||
tab.seedRand()
|
|
||||||
if refreshDone == nil {
|
|
||||||
refreshDone = make(chan struct{})
|
|
||||||
go tab.doRefresh(refreshDone)
|
|
||||||
}
|
|
||||||
case req := <-tab.refreshReq:
|
|
||||||
waiting = append(waiting, req)
|
|
||||||
if refreshDone == nil {
|
|
||||||
refreshDone = make(chan struct{})
|
|
||||||
go tab.doRefresh(refreshDone)
|
|
||||||
}
|
|
||||||
case <-refreshDone:
|
|
||||||
for _, ch := range waiting {
|
|
||||||
close(ch)
|
|
||||||
}
|
|
||||||
waiting, refreshDone = nil, nil
|
|
||||||
refresh.Reset(tab.nextRefreshTime())
|
|
||||||
case <-revalidate.C:
|
|
||||||
revalidateDone = make(chan struct{})
|
|
||||||
go tab.doRevalidate(revalidateDone)
|
|
||||||
case <-revalidateDone:
|
|
||||||
revalidate.Reset(tab.nextRevalidateTime())
|
|
||||||
revalidateDone = nil
|
|
||||||
case <-copyNodes.C:
|
|
||||||
go tab.copyLiveNodes()
|
|
||||||
case <-tab.closeReq:
|
|
||||||
break loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if refreshDone != nil {
|
|
||||||
<-refreshDone
|
|
||||||
}
|
|
||||||
for _, ch := range waiting {
|
|
||||||
close(ch)
|
|
||||||
}
|
|
||||||
if revalidateDone != nil {
|
|
||||||
<-revalidateDone
|
|
||||||
}
|
|
||||||
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).
|
|
||||||
func (tab *Table) doRefresh(done chan struct{}) {
|
|
||||||
defer close(done)
|
|
||||||
|
|
||||||
// Load nodes from the database and insert
|
|
||||||
// them. This should yield a few previously seen nodes that are
|
|
||||||
// (hopefully) still alive.
|
|
||||||
tab.loadSeedNodes()
|
|
||||||
|
|
||||||
// Run self lookup to discover new neighbor nodes.
|
|
||||||
tab.net.lookupSelf()
|
|
||||||
|
|
||||||
// The Kademlia paper specifies that the bucket refresh should
|
|
||||||
// perform a lookup in the least recently used bucket. We cannot
|
|
||||||
// adhere to this because the findnode target is a 512bit value
|
|
||||||
// (not hash-sized) and it is not easily possible to generate a
|
|
||||||
// sha3 preimage that falls into a chosen bucket.
|
|
||||||
// We perform a few lookups with a random target instead.
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
tab.net.lookupRandom()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tab *Table) loadSeedNodes() {
|
|
||||||
seeds := wrapNodes(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)
|
|
||||||
}
|
|
||||||
tab.addSeenNode(seed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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{}{} }()
|
|
||||||
|
|
||||||
last, bi := tab.nodeToRevalidate()
|
|
||||||
if last == nil {
|
|
||||||
// No non-empty bucket found.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping the selected node and wait for a pong.
|
|
||||||
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()
|
|
||||||
b := tab.buckets[bi]
|
|
||||||
if err == nil {
|
|
||||||
// The node responded, move it to the front.
|
|
||||||
last.livenessChecks++
|
|
||||||
tab.log.Debug("Revalidated node", "b", bi, "id", last.ID(), "checks", last.livenessChecks)
|
|
||||||
tab.bumpInBucket(b, last)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// No reply received, pick a replacement or delete the node if there aren't
|
|
||||||
// any replacements.
|
|
||||||
if r := tab.replace(b, last); r != nil {
|
|
||||||
tab.log.Debug("Replaced dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "checks", last.livenessChecks, "r", r.ID(), "rip", r.IP())
|
|
||||||
} else {
|
|
||||||
tab.log.Debug("Removed dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "checks", last.livenessChecks)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// nodeToRevalidate returns the last node in a random, non-empty bucket.
|
|
||||||
func (tab *Table) nodeToRevalidate() (n *node, bi int) {
|
|
||||||
tab.mutex.Lock()
|
|
||||||
defer tab.mutex.Unlock()
|
|
||||||
|
|
||||||
for _, bi = range tab.rand.Perm(len(tab.buckets)) {
|
|
||||||
b := tab.buckets[bi]
|
|
||||||
if len(b.entries) > 0 {
|
|
||||||
last := b.entries[len(b.entries)-1]
|
|
||||||
return last, bi
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tab *Table) nextRevalidateTime() time.Duration {
|
|
||||||
tab.mutex.Lock()
|
|
||||||
defer tab.mutex.Unlock()
|
|
||||||
|
|
||||||
return time.Duration(tab.rand.Int63n(int64(tab.cfg.PingInterval)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tab *Table) nextRefreshTime() time.Duration {
|
|
||||||
tab.mutex.Lock()
|
|
||||||
defer tab.mutex.Unlock()
|
|
||||||
|
|
||||||
half := tab.cfg.RefreshInterval / 2
|
|
||||||
return half + time.Duration(tab.rand.Int63n(int64(half)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// copyLiveNodes adds nodes from the table to the database if they have been in the table
|
|
||||||
// longer than seedMinTableTime.
|
|
||||||
func (tab *Table) copyLiveNodes() {
|
|
||||||
tab.mutex.Lock()
|
|
||||||
defer tab.mutex.Unlock()
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
for _, b := range &tab.buckets {
|
|
||||||
for _, n := range b.entries {
|
|
||||||
if n.livenessChecks > 0 && now.Sub(n.addedAt) >= seedMinTableTime {
|
|
||||||
tab.db.UpdateNode(unwrapNode(n))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// findnodeByID returns the n nodes in the table that are closest to the given id.
|
|
||||||
// This is used by the FINDNODE/v4 handler.
|
|
||||||
//
|
|
||||||
// The preferLive parameter says whether the caller wants liveness-checked results. If
|
|
||||||
// preferLive is true and the table contains any verified nodes, the result will not
|
|
||||||
// contain unverified nodes. However, if there are no verified nodes at all, the result
|
|
||||||
// will contain unverified nodes.
|
|
||||||
func (tab *Table) findnodeByID(target enode.ID, nresults int, preferLive bool) *nodesByDistance {
|
|
||||||
tab.mutex.Lock()
|
|
||||||
defer tab.mutex.Unlock()
|
|
||||||
|
|
||||||
// Scan all buckets. There might be a better way to do this, but there aren't that many
|
|
||||||
// buckets, so this solution should be fine. The worst-case complexity of this loop
|
|
||||||
// is O(tab.len() * nresults).
|
|
||||||
nodes := &nodesByDistance{target: target}
|
|
||||||
liveNodes := &nodesByDistance{target: target}
|
|
||||||
for _, b := range &tab.buckets {
|
|
||||||
for _, n := range b.entries {
|
|
||||||
nodes.push(n, nresults)
|
|
||||||
if preferLive && n.livenessChecks > 0 {
|
|
||||||
liveNodes.push(n, nresults)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if preferLive && len(liveNodes.entries) > 0 {
|
|
||||||
return liveNodes
|
|
||||||
}
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
// appendLiveNodes adds nodes at the given distance to the result slice.
|
|
||||||
func (tab *Table) appendLiveNodes(dist uint, result []*enode.Node) []*enode.Node {
|
|
||||||
if dist > 256 {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
if dist == 0 {
|
|
||||||
return append(result, tab.self())
|
|
||||||
}
|
|
||||||
|
|
||||||
tab.mutex.Lock()
|
|
||||||
defer tab.mutex.Unlock()
|
|
||||||
for _, n := range tab.bucketAtDistance(int(dist)).entries {
|
|
||||||
if n.livenessChecks >= 1 {
|
|
||||||
node := n.Node // avoid handing out pointer to struct field
|
|
||||||
result = append(result, &node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// len returns the number of nodes in the table.
|
|
||||||
func (tab *Table) len() (n int) {
|
|
||||||
tab.mutex.Lock()
|
|
||||||
defer tab.mutex.Unlock()
|
|
||||||
|
|
||||||
for _, b := range &tab.buckets {
|
|
||||||
n += len(b.entries)
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// bucketLen returns the number of nodes in the bucket for the given ID.
|
|
||||||
func (tab *Table) bucketLen(id enode.ID) int {
|
|
||||||
tab.mutex.Lock()
|
|
||||||
defer tab.mutex.Unlock()
|
|
||||||
|
|
||||||
return len(tab.bucket(id).entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
// bucket returns the bucket for the given node ID hash.
|
|
||||||
func (tab *Table) bucket(id enode.ID) *bucket {
|
|
||||||
d := enode.LogDist(tab.self().ID(), id)
|
|
||||||
return tab.bucketAtDistance(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tab *Table) bucketAtDistance(d int) *bucket {
|
|
||||||
if d <= bucketMinDistance {
|
|
||||||
return tab.buckets[0]
|
|
||||||
}
|
|
||||||
return tab.buckets[d-bucketMinDistance-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// addSeenNode adds a node which may or may not be live to the end of a bucket. If the
|
|
||||||
// bucket has space available, adding the node succeeds immediately. Otherwise, the node is
|
|
||||||
// added to the replacements list.
|
|
||||||
//
|
|
||||||
// The caller must not hold tab.mutex.
|
|
||||||
func (tab *Table) addSeenNode(n *node) {
|
|
||||||
if n.ID() == tab.self().ID() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tab.mutex.Lock()
|
|
||||||
defer tab.mutex.Unlock()
|
|
||||||
b := tab.bucket(n.ID())
|
|
||||||
if contains(b.entries, n.ID()) {
|
|
||||||
// Already in bucket, don't add.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(b.entries) >= bucketSize {
|
|
||||||
// Bucket full, maybe add as replacement.
|
|
||||||
tab.addReplacement(b, n)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !tab.addIP(b, n.IP()) {
|
|
||||||
// Can't add: IP limit reached.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to end of bucket:
|
|
||||||
b.entries = append(b.entries, n)
|
|
||||||
b.replacements = deleteNode(b.replacements, n)
|
|
||||||
n.addedAt = time.Now()
|
|
||||||
|
|
||||||
if tab.nodeAddedHook != nil {
|
|
||||||
tab.nodeAddedHook(b, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// addVerifiedNode adds a node whose existence has been verified recently to the front of a
|
|
||||||
// bucket. If the node is already in the bucket, it is moved to the front. If the bucket
|
|
||||||
// has no space, the node is added to the replacements list.
|
|
||||||
//
|
|
||||||
// There is an additional safety measure: if the table is still initializing the node
|
|
||||||
// is not added. This prevents an attack where the table could be filled by just sending
|
|
||||||
// ping repeatedly.
|
|
||||||
//
|
|
||||||
// The caller must not hold tab.mutex.
|
|
||||||
func (tab *Table) addVerifiedNode(n *node) {
|
|
||||||
if !tab.isInitDone() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if n.ID() == tab.self().ID() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tab.mutex.Lock()
|
|
||||||
defer tab.mutex.Unlock()
|
|
||||||
b := tab.bucket(n.ID())
|
|
||||||
if tab.bumpInBucket(b, n) {
|
|
||||||
// Already in bucket, moved to front.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(b.entries) >= bucketSize {
|
|
||||||
// Bucket full, maybe add as replacement.
|
|
||||||
tab.addReplacement(b, n)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !tab.addIP(b, n.IP()) {
|
|
||||||
// Can't add: IP limit reached.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to front of bucket.
|
|
||||||
b.entries, _ = pushNode(b.entries, n, bucketSize)
|
|
||||||
b.replacements = deleteNode(b.replacements, n)
|
|
||||||
n.addedAt = time.Now()
|
|
||||||
|
|
||||||
if tab.nodeAddedHook != nil {
|
|
||||||
tab.nodeAddedHook(b, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete removes an entry from the node table. It is used to evacuate dead nodes.
|
|
||||||
func (tab *Table) delete(node *node) {
|
|
||||||
tab.mutex.Lock()
|
|
||||||
defer tab.mutex.Unlock()
|
|
||||||
|
|
||||||
tab.deleteInBucket(tab.bucket(node.ID()), node)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tab *Table) addIP(b *bucket, ip net.IP) bool {
|
|
||||||
if len(ip) == 0 {
|
|
||||||
return false // Nodes without IP cannot be added.
|
|
||||||
}
|
|
||||||
if netutil.IsLAN(ip) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if !tab.ips.Add(ip) {
|
|
||||||
tab.log.Debug("IP exceeds table limit", "ip", ip)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !b.ips.Add(ip) {
|
|
||||||
tab.log.Debug("IP exceeds bucket limit", "ip", ip)
|
|
||||||
tab.ips.Remove(ip)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tab *Table) removeIP(b *bucket, ip net.IP) {
|
|
||||||
if netutil.IsLAN(ip) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tab.ips.Remove(ip)
|
|
||||||
b.ips.Remove(ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tab *Table) addReplacement(b *bucket, n *node) {
|
|
||||||
for _, e := range b.replacements {
|
|
||||||
if e.ID() == n.ID() {
|
|
||||||
return // already in list
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !tab.addIP(b, n.IP()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var removed *node
|
|
||||||
b.replacements, removed = pushNode(b.replacements, n, maxReplacements)
|
|
||||||
if removed != nil {
|
|
||||||
tab.removeIP(b, removed.IP())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace removes n from the replacement list and replaces 'last' with it if it is the
|
|
||||||
// last entry in the bucket. If 'last' isn't the last entry, it has either been replaced
|
|
||||||
// with someone else or became active.
|
|
||||||
func (tab *Table) replace(b *bucket, last *node) *node {
|
|
||||||
if len(b.entries) == 0 || b.entries[len(b.entries)-1].ID() != last.ID() {
|
|
||||||
// Entry has moved, don't replace it.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Still the last entry.
|
|
||||||
if len(b.replacements) == 0 {
|
|
||||||
tab.deleteInBucket(b, last)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
r := b.replacements[tab.rand.Intn(len(b.replacements))]
|
|
||||||
b.replacements = deleteNode(b.replacements, r)
|
|
||||||
b.entries[len(b.entries)-1] = r
|
|
||||||
tab.removeIP(b, last.IP())
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// bumpInBucket moves the given node to the front of the bucket entry list
|
|
||||||
// if it is contained in that list.
|
|
||||||
func (tab *Table) bumpInBucket(b *bucket, n *node) bool {
|
|
||||||
for i := range b.entries {
|
|
||||||
if b.entries[i].ID() == n.ID() {
|
|
||||||
if !n.IP().Equal(b.entries[i].IP()) {
|
|
||||||
// Endpoint has changed, ensure that the new IP fits into table limits.
|
|
||||||
tab.removeIP(b, b.entries[i].IP())
|
|
||||||
if !tab.addIP(b, n.IP()) {
|
|
||||||
// It doesn't, put the previous one back.
|
|
||||||
tab.addIP(b, b.entries[i].IP())
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Move it to the front.
|
|
||||||
copy(b.entries[1:], b.entries[:i])
|
|
||||||
b.entries[0] = n
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tab *Table) deleteInBucket(b *bucket, n *node) {
|
|
||||||
// Check if the node is actually in the bucket so the removed hook
|
|
||||||
// isn't called multiple times for the same node.
|
|
||||||
if !contains(b.entries, n.ID()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
b.entries = deleteNode(b.entries, n)
|
|
||||||
tab.removeIP(b, n.IP())
|
|
||||||
if tab.nodeRemovedHook != nil {
|
|
||||||
tab.nodeRemovedHook(b, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains(ns []*node, id enode.ID) bool {
|
|
||||||
for _, n := range ns {
|
|
||||||
if n.ID() == id {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// pushNode adds n to the front of list, keeping at most max items.
|
|
||||||
func pushNode(list []*node, n *node, max int) ([]*node, *node) {
|
|
||||||
if len(list) < max {
|
|
||||||
list = append(list, nil)
|
|
||||||
}
|
|
||||||
removed := list[len(list)-1]
|
|
||||||
copy(list[1:], list)
|
|
||||||
list[0] = n
|
|
||||||
return list, removed
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// nodesByDistance is a list of nodes, ordered by distance to target.
|
|
||||||
type nodesByDistance struct {
|
|
||||||
entries []*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) {
|
|
||||||
ix := sort.Search(len(h.entries), func(i int) bool {
|
|
||||||
return enode.DistCmp(h.target, h.entries[i].ID(), n.ID()) > 0
|
|
||||||
})
|
|
||||||
|
|
||||||
end := len(h.entries)
|
|
||||||
if len(h.entries) < maxElems {
|
|
||||||
h.entries = append(h.entries, n)
|
|
||||||
}
|
|
||||||
if ix < end {
|
|
||||||
// Slide existing entries down to make room.
|
|
||||||
// This will overwrite the entry we just appended.
|
|
||||||
copy(h.entries[ix+1:], h.entries[ix:])
|
|
||||||
h.entries[ix] = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,446 +0,0 @@
|
||||||
// Copyright 2015 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"testing/quick"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/netutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTable_pingReplace(t *testing.T) {
|
|
||||||
run := func(newNodeResponding, lastInBucketResponding bool) {
|
|
||||||
name := fmt.Sprintf("newNodeResponding=%t/lastInBucketResponding=%t", newNodeResponding, lastInBucketResponding)
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
testPingReplace(t, newNodeResponding, lastInBucketResponding)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
run(true, true)
|
|
||||||
run(false, true)
|
|
||||||
run(true, false)
|
|
||||||
run(false, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPingReplace(t *testing.T, newNodeIsResponding, lastInBucketIsResponding bool) {
|
|
||||||
transport := newPingRecorder()
|
|
||||||
tab, db := newTestTable(transport)
|
|
||||||
defer db.Close()
|
|
||||||
defer tab.close()
|
|
||||||
|
|
||||||
<-tab.initDone
|
|
||||||
|
|
||||||
// Fill up the sender's bucket.
|
|
||||||
pingKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
|
|
||||||
pingSender := wrapNode(enode.NewV4(&pingKey.PublicKey, net.IP{127, 0, 0, 1}, 99, 99))
|
|
||||||
last := fillBucket(tab, pingSender)
|
|
||||||
|
|
||||||
// Add the sender as if it just pinged us. Revalidate should replace the last node in
|
|
||||||
// its bucket if it is unresponsive. Revalidate again to ensure that
|
|
||||||
transport.dead[last.ID()] = !lastInBucketIsResponding
|
|
||||||
transport.dead[pingSender.ID()] = !newNodeIsResponding
|
|
||||||
tab.addSeenNode(pingSender)
|
|
||||||
tab.doRevalidate(make(chan struct{}, 1))
|
|
||||||
tab.doRevalidate(make(chan struct{}, 1))
|
|
||||||
|
|
||||||
if !transport.pinged[last.ID()] {
|
|
||||||
// Oldest node in bucket is pinged to see whether it is still alive.
|
|
||||||
t.Error("table did not ping last node in bucket")
|
|
||||||
}
|
|
||||||
|
|
||||||
tab.mutex.Lock()
|
|
||||||
defer tab.mutex.Unlock()
|
|
||||||
wantSize := bucketSize
|
|
||||||
if !lastInBucketIsResponding && !newNodeIsResponding {
|
|
||||||
wantSize--
|
|
||||||
}
|
|
||||||
if l := len(tab.bucket(pingSender.ID()).entries); l != wantSize {
|
|
||||||
t.Errorf("wrong bucket size after bond: got %d, want %d", l, wantSize)
|
|
||||||
}
|
|
||||||
if found := contains(tab.bucket(pingSender.ID()).entries, last.ID()); found != lastInBucketIsResponding {
|
|
||||||
t.Errorf("last entry found: %t, want: %t", found, lastInBucketIsResponding)
|
|
||||||
}
|
|
||||||
wantNewEntry := newNodeIsResponding && !lastInBucketIsResponding
|
|
||||||
if found := contains(tab.bucket(pingSender.ID()).entries, pingSender.ID()); found != wantNewEntry {
|
|
||||||
t.Errorf("new entry found: %t, want: %t", found, wantNewEntry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBucket_bumpNoDuplicates(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
cfg := &quick.Config{
|
|
||||||
MaxCount: 1000,
|
|
||||||
Rand: rand.New(rand.NewSource(time.Now().Unix())),
|
|
||||||
Values: func(args []reflect.Value, rand *rand.Rand) {
|
|
||||||
// generate a random list of nodes. this will be the content of the bucket.
|
|
||||||
n := rand.Intn(bucketSize-1) + 1
|
|
||||||
nodes := make([]*node, n)
|
|
||||||
for i := range nodes {
|
|
||||||
nodes[i] = nodeAtDistance(enode.ID{}, 200, intIP(200))
|
|
||||||
}
|
|
||||||
args[0] = reflect.ValueOf(nodes)
|
|
||||||
// generate random bump positions.
|
|
||||||
bumps := make([]int, rand.Intn(100))
|
|
||||||
for i := range bumps {
|
|
||||||
bumps[i] = rand.Intn(len(nodes))
|
|
||||||
}
|
|
||||||
args[1] = reflect.ValueOf(bumps)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
prop := func(nodes []*node, bumps []int) (ok bool) {
|
|
||||||
tab, db := newTestTable(newPingRecorder())
|
|
||||||
defer db.Close()
|
|
||||||
defer tab.close()
|
|
||||||
|
|
||||||
b := &bucket{entries: make([]*node, len(nodes))}
|
|
||||||
copy(b.entries, nodes)
|
|
||||||
for i, pos := range bumps {
|
|
||||||
tab.bumpInBucket(b, b.entries[pos])
|
|
||||||
if hasDuplicates(b.entries) {
|
|
||||||
t.Logf("bucket has duplicates after %d/%d bumps:", i+1, len(bumps))
|
|
||||||
for _, n := range b.entries {
|
|
||||||
t.Logf(" %p", n)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkIPLimitInvariant(t, tab)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if err := quick.Check(prop, cfg); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This checks that the table-wide IP limit is applied correctly.
|
|
||||||
func TestTable_IPLimit(t *testing.T) {
|
|
||||||
transport := newPingRecorder()
|
|
||||||
tab, db := newTestTable(transport)
|
|
||||||
defer db.Close()
|
|
||||||
defer tab.close()
|
|
||||||
|
|
||||||
for i := 0; i < tableIPLimit+1; i++ {
|
|
||||||
n := nodeAtDistance(tab.self().ID(), i, net.IP{172, 0, 1, byte(i)})
|
|
||||||
tab.addSeenNode(n)
|
|
||||||
}
|
|
||||||
if tab.len() > tableIPLimit {
|
|
||||||
t.Errorf("too many nodes in table")
|
|
||||||
}
|
|
||||||
checkIPLimitInvariant(t, tab)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This checks that the per-bucket IP limit is applied correctly.
|
|
||||||
func TestTable_BucketIPLimit(t *testing.T) {
|
|
||||||
transport := newPingRecorder()
|
|
||||||
tab, db := newTestTable(transport)
|
|
||||||
defer db.Close()
|
|
||||||
defer tab.close()
|
|
||||||
|
|
||||||
d := 3
|
|
||||||
for i := 0; i < bucketIPLimit+1; i++ {
|
|
||||||
n := nodeAtDistance(tab.self().ID(), d, net.IP{172, 0, 1, byte(i)})
|
|
||||||
tab.addSeenNode(n)
|
|
||||||
}
|
|
||||||
if tab.len() > bucketIPLimit {
|
|
||||||
t.Errorf("too many nodes in table")
|
|
||||||
}
|
|
||||||
checkIPLimitInvariant(t, tab)
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkIPLimitInvariant checks that ip limit sets contain an entry for every
|
|
||||||
// node in the table and no extra entries.
|
|
||||||
func checkIPLimitInvariant(t *testing.T, tab *Table) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
tabset := netutil.DistinctNetSet{Subnet: tableSubnet, Limit: tableIPLimit}
|
|
||||||
for _, b := range tab.buckets {
|
|
||||||
for _, n := range b.entries {
|
|
||||||
tabset.Add(n.IP())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if tabset.String() != tab.ips.String() {
|
|
||||||
t.Errorf("table IP set is incorrect:\nhave: %v\nwant: %v", tab.ips, tabset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTable_findnodeByID(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
test := func(test *closeTest) bool {
|
|
||||||
// for any node table, Target and N
|
|
||||||
transport := newPingRecorder()
|
|
||||||
tab, db := newTestTable(transport)
|
|
||||||
defer db.Close()
|
|
||||||
defer tab.close()
|
|
||||||
fillTable(tab, test.All, true)
|
|
||||||
|
|
||||||
// check that closest(Target, N) returns nodes
|
|
||||||
result := tab.findnodeByID(test.Target, test.N, false).entries
|
|
||||||
if hasDuplicates(result) {
|
|
||||||
t.Errorf("result contains duplicates")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !sortedByDistanceTo(test.Target, result) {
|
|
||||||
t.Errorf("result is not sorted by distance to target")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// check that the number of results is min(N, tablen)
|
|
||||||
wantN := test.N
|
|
||||||
if tlen := tab.len(); tlen < test.N {
|
|
||||||
wantN = tlen
|
|
||||||
}
|
|
||||||
if len(result) != wantN {
|
|
||||||
t.Errorf("wrong number of nodes: got %d, want %d", len(result), wantN)
|
|
||||||
return false
|
|
||||||
} else if len(result) == 0 {
|
|
||||||
return true // no need to check distance
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()) {
|
|
||||||
continue // don't run the check below for nodes in result
|
|
||||||
}
|
|
||||||
farthestResult := result[len(result)-1].ID()
|
|
||||||
if enode.DistCmp(test.Target, n.ID(), farthestResult) < 0 {
|
|
||||||
t.Errorf("table contains node that is closer to target but it's not in result")
|
|
||||||
t.Logf(" Target: %v", test.Target)
|
|
||||||
t.Logf(" Farthest Result: %v", farthestResult)
|
|
||||||
t.Logf(" ID: %v", n.ID())
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if err := quick.Check(test, quickcfg()); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type closeTest struct {
|
|
||||||
Self enode.ID
|
|
||||||
Target enode.ID
|
|
||||||
All []*node
|
|
||||||
N int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*closeTest) Generate(rand *rand.Rand, size int) reflect.Value {
|
|
||||||
t := &closeTest{
|
|
||||||
Self: gen(enode.ID{}, rand).(enode.ID),
|
|
||||||
Target: gen(enode.ID{}, rand).(enode.ID),
|
|
||||||
N: rand.Intn(bucketSize),
|
|
||||||
}
|
|
||||||
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
|
|
||||||
t.All = append(t.All, n)
|
|
||||||
}
|
|
||||||
return reflect.ValueOf(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTable_addVerifiedNode(t *testing.T) {
|
|
||||||
tab, db := newTestTable(newPingRecorder())
|
|
||||||
<-tab.initDone
|
|
||||||
defer db.Close()
|
|
||||||
defer tab.close()
|
|
||||||
|
|
||||||
// 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.addSeenNode(n1)
|
|
||||||
tab.addSeenNode(n2)
|
|
||||||
|
|
||||||
// Verify bucket content:
|
|
||||||
bcontent := []*node{n1, n2}
|
|
||||||
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, bcontent) {
|
|
||||||
t.Fatalf("wrong bucket content: %v", tab.bucket(n1.ID()).entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a changed version of n2.
|
|
||||||
newrec := n2.Record()
|
|
||||||
newrec.Set(enr.IP{99, 99, 99, 99})
|
|
||||||
newn2 := wrapNode(enode.SignNull(newrec, n2.ID()))
|
|
||||||
tab.addVerifiedNode(newn2)
|
|
||||||
|
|
||||||
// Check that bucket is updated correctly.
|
|
||||||
newBcontent := []*node{newn2, n1}
|
|
||||||
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, newBcontent) {
|
|
||||||
t.Fatalf("wrong bucket content after update: %v", tab.bucket(n1.ID()).entries)
|
|
||||||
}
|
|
||||||
checkIPLimitInvariant(t, tab)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTable_addSeenNode(t *testing.T) {
|
|
||||||
tab, db := newTestTable(newPingRecorder())
|
|
||||||
<-tab.initDone
|
|
||||||
defer db.Close()
|
|
||||||
defer tab.close()
|
|
||||||
|
|
||||||
// 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.addSeenNode(n1)
|
|
||||||
tab.addSeenNode(n2)
|
|
||||||
|
|
||||||
// Verify bucket content:
|
|
||||||
bcontent := []*node{n1, n2}
|
|
||||||
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, bcontent) {
|
|
||||||
t.Fatalf("wrong bucket content: %v", tab.bucket(n1.ID()).entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a changed version of n2.
|
|
||||||
newrec := n2.Record()
|
|
||||||
newrec.Set(enr.IP{99, 99, 99, 99})
|
|
||||||
newn2 := wrapNode(enode.SignNull(newrec, n2.ID()))
|
|
||||||
tab.addSeenNode(newn2)
|
|
||||||
|
|
||||||
// Check that bucket content is unchanged.
|
|
||||||
if !reflect.DeepEqual(tab.bucket(n1.ID()).entries, bcontent) {
|
|
||||||
t.Fatalf("wrong bucket content after update: %v", tab.bucket(n1.ID()).entries)
|
|
||||||
}
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNodesPush(t *testing.T) {
|
|
||||||
var target enode.ID
|
|
||||||
n1 := nodeAtDistance(target, 255, intIP(1))
|
|
||||||
n2 := nodeAtDistance(target, 254, intIP(2))
|
|
||||||
n3 := nodeAtDistance(target, 253, intIP(3))
|
|
||||||
perm := [][]*node{
|
|
||||||
{n3, n2, n1},
|
|
||||||
{n3, n1, n2},
|
|
||||||
{n2, n3, n1},
|
|
||||||
{n2, n1, n3},
|
|
||||||
{n1, n3, n2},
|
|
||||||
{n1, n2, n3},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert all permutations into lists with size limit 3.
|
|
||||||
for _, nodes := range perm {
|
|
||||||
list := nodesByDistance{target: target}
|
|
||||||
for _, n := range nodes {
|
|
||||||
list.push(n, 3)
|
|
||||||
}
|
|
||||||
if !slicesEqual(list.entries, perm[0], nodeIDEqual) {
|
|
||||||
t.Fatal("not equal")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert all permutations into lists with size limit 2.
|
|
||||||
for _, nodes := range perm {
|
|
||||||
list := nodesByDistance{target: target}
|
|
||||||
for _, n := range nodes {
|
|
||||||
list.push(n, 2)
|
|
||||||
}
|
|
||||||
if !slicesEqual(list.entries, perm[0][:2], nodeIDEqual) {
|
|
||||||
t.Fatal("not equal")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func nodeIDEqual(n1, n2 *node) bool {
|
|
||||||
return n1.ID() == n2.ID()
|
|
||||||
}
|
|
||||||
|
|
||||||
func slicesEqual[T any](s1, s2 []T, check func(e1, e2 T) bool) bool {
|
|
||||||
if len(s1) != len(s2) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i := range s1 {
|
|
||||||
if !check(s1[i], s2[i]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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{} {
|
|
||||||
v, ok := quick.Value(reflect.TypeOf(typ), rand)
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Sprintf("couldn't generate random value of type %T", typ))
|
|
||||||
}
|
|
||||||
return v.Interface()
|
|
||||||
}
|
|
||||||
|
|
||||||
func genIP(rand *rand.Rand) net.IP {
|
|
||||||
ip := make(net.IP, 4)
|
|
||||||
rand.Read(ip)
|
|
||||||
return ip
|
|
||||||
}
|
|
||||||
|
|
||||||
func quickcfg() *quick.Config {
|
|
||||||
return &quick.Config{
|
|
||||||
MaxCount: 5000,
|
|
||||||
Rand: rand.New(rand.NewSource(time.Now().Unix())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newkey() *ecdsa.PrivateKey {
|
|
||||||
key, err := crypto.GenerateKey()
|
|
||||||
if err != nil {
|
|
||||||
panic("couldn't generate key: " + err.Error())
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
@ -1,258 +0,0 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
var nullNode *enode.Node
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
var r enr.Record
|
|
||||||
r.Set(enr.IP{0, 0, 0, 0})
|
|
||||||
nullNode = enode.SignNull(&r, enode.ID{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestTable(t transport) (*Table, *enode.DB) {
|
|
||||||
cfg := Config{}
|
|
||||||
db, _ := enode.OpenDB("")
|
|
||||||
tab, _ := newTable(t, db, cfg)
|
|
||||||
go tab.loop()
|
|
||||||
return tab, db
|
|
||||||
}
|
|
||||||
|
|
||||||
// nodeAtDistance creates a node for which enode.LogDist(base, n.id) == ld.
|
|
||||||
func nodeAtDistance(base enode.ID, ld int, ip net.IP) *node {
|
|
||||||
var r enr.Record
|
|
||||||
r.Set(enr.IP(ip))
|
|
||||||
r.Set(enr.UDP(30303))
|
|
||||||
return wrapNode(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)))
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
func nodesToRecords(nodes []*enode.Node) []*enr.Record {
|
|
||||||
records := make([]*enr.Record, len(nodes))
|
|
||||||
for i := range nodes {
|
|
||||||
records[i] = nodes[i].Record()
|
|
||||||
}
|
|
||||||
return records
|
|
||||||
}
|
|
||||||
|
|
||||||
// idAtDistance returns a random hash such that enode.LogDist(a, b) == n
|
|
||||||
func idAtDistance(a enode.ID, n int) (b enode.ID) {
|
|
||||||
if n == 0 {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
// flip bit at position n, fill the rest with random bits
|
|
||||||
b = a
|
|
||||||
pos := len(a) - n/8 - 1
|
|
||||||
bit := byte(0x01) << (byte(n%8) - 1)
|
|
||||||
if bit == 0 {
|
|
||||||
pos++
|
|
||||||
bit = 0x80
|
|
||||||
}
|
|
||||||
b[pos] = a[pos]&^bit | ^a[pos]&bit // TODO: randomize end bits
|
|
||||||
for i := pos + 1; i < len(a); i++ {
|
|
||||||
b[i] = byte(rand.Intn(255))
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func intIP(i int) net.IP {
|
|
||||||
return net.IP{byte(i), 0, 2, byte(i)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fillBucket inserts nodes into the given bucket until it is full.
|
|
||||||
func fillBucket(tab *Table, n *node) (last *node) {
|
|
||||||
ld := enode.LogDist(tab.self().ID(), n.ID())
|
|
||||||
b := tab.bucket(n.ID())
|
|
||||||
for len(b.entries) < bucketSize {
|
|
||||||
b.entries = append(b.entries, nodeAtDistance(tab.self().ID(), ld, intIP(ld)))
|
|
||||||
}
|
|
||||||
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, setLive bool) {
|
|
||||||
for _, n := range nodes {
|
|
||||||
if setLive {
|
|
||||||
n.livenessChecks = 1
|
|
||||||
}
|
|
||||||
tab.addSeenNode(n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type pingRecorder struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
dead, pinged map[enode.ID]bool
|
|
||||||
records map[enode.ID]*enode.Node
|
|
||||||
n *enode.Node
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPingRecorder() *pingRecorder {
|
|
||||||
var r enr.Record
|
|
||||||
r.Set(enr.IP{0, 0, 0, 0})
|
|
||||||
n := enode.SignNull(&r, enode.ID{})
|
|
||||||
|
|
||||||
return &pingRecorder{
|
|
||||||
dead: make(map[enode.ID]bool),
|
|
||||||
pinged: make(map[enode.ID]bool),
|
|
||||||
records: make(map[enode.ID]*enode.Node),
|
|
||||||
n: n,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateRecord 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 }
|
|
||||||
|
|
||||||
// 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 0, errTimeout
|
|
||||||
}
|
|
||||||
if t.records[n.ID()] != nil {
|
|
||||||
seq = t.records[n.ID()].Seq()
|
|
||||||
}
|
|
||||||
return seq, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestENR simulates an ENR request.
|
|
||||||
func (t *pingRecorder) RequestENR(n *enode.Node) (*enode.Node, error) {
|
|
||||||
t.mu.Lock()
|
|
||||||
defer t.mu.Unlock()
|
|
||||||
|
|
||||||
if t.dead[n.ID()] || t.records[n.ID()] == nil {
|
|
||||||
return nil, errTimeout
|
|
||||||
}
|
|
||||||
return t.records[n.ID()], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasDuplicates(slice []*node) bool {
|
|
||||||
seen := make(map[enode.ID]bool, len(slice))
|
|
||||||
for i, e := range slice {
|
|
||||||
if e == nil {
|
|
||||||
panic(fmt.Sprintf("nil *Node at %d", i))
|
|
||||||
}
|
|
||||||
if seen[e.ID()] {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
seen[e.ID()] = true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkNodesEqual checks whether the two given node lists contain the same nodes.
|
|
||||||
func checkNodesEqual(got, want []*enode.Node) error {
|
|
||||||
if len(got) == len(want) {
|
|
||||||
for i := range got {
|
|
||||||
if !nodeEqual(got[i], want[i]) {
|
|
||||||
goto NotEqual
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
|
|
||||||
NotEqual:
|
|
||||||
output := new(bytes.Buffer)
|
|
||||||
fmt.Fprintf(output, "got %d nodes:\n", len(got))
|
|
||||||
for _, n := range got {
|
|
||||||
fmt.Fprintf(output, " %v %v\n", n.ID(), n)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(output, "want %d:\n", len(want))
|
|
||||||
for _, n := range want {
|
|
||||||
fmt.Fprintf(output, " %v %v\n", n.ID(), n)
|
|
||||||
}
|
|
||||||
return errors.New(output.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
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 {
|
|
||||||
return enode.DistCmp(distbase, a.ID(), b.ID())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// hexEncPrivkey decodes h as a private key.
|
|
||||||
func hexEncPrivkey(h string) *ecdsa.PrivateKey {
|
|
||||||
b, err := hex.DecodeString(h)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
key, err := crypto.ToECDSA(b)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
// hexEncPubkey decodes h as a public key.
|
|
||||||
func hexEncPubkey(h string) (ret encPubkey) {
|
|
||||||
b, err := hex.DecodeString(h)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if len(b) != len(ret) {
|
|
||||||
panic("invalid length")
|
|
||||||
}
|
|
||||||
copy(ret[:], b)
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
@ -1,347 +0,0 @@
|
||||||
// Copyright 2019 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/discover/v4wire"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUDPv4_Lookup(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPTest(t)
|
|
||||||
|
|
||||||
// Lookup on empty table returns no nodes.
|
|
||||||
targetKey, _ := decodePubkey(crypto.S256(), lookupTestnet.target[:])
|
|
||||||
if results := test.udp.LookupPubkey(targetKey); len(results) > 0 {
|
|
||||||
t.Fatalf("lookup on empty table returned %d results: %#v", len(results), results)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed table with initial node.
|
|
||||||
fillTable(test.table, []*node{wrapNode(lookupTestnet.node(256, 0))}, true)
|
|
||||||
|
|
||||||
// Start the lookup.
|
|
||||||
resultC := make(chan []*enode.Node, 1)
|
|
||||||
go func() {
|
|
||||||
resultC <- test.udp.LookupPubkey(targetKey)
|
|
||||||
test.close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Answer lookup packets.
|
|
||||||
serveTestnet(test, lookupTestnet)
|
|
||||||
|
|
||||||
// Verify result nodes.
|
|
||||||
results := <-resultC
|
|
||||||
t.Logf("results:")
|
|
||||||
for _, e := range results {
|
|
||||||
t.Logf(" ld=%d, %x", enode.LogDist(lookupTestnet.target.id(), e.ID()), e.ID().Bytes())
|
|
||||||
}
|
|
||||||
if len(results) != bucketSize {
|
|
||||||
t.Errorf("wrong number of results: got %d, want %d", len(results), bucketSize)
|
|
||||||
}
|
|
||||||
checkLookupResults(t, lookupTestnet, results)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUDPv4_LookupIterator(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPTest(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
// Seed table with initial nodes.
|
|
||||||
bootnodes := make([]*node, len(lookupTestnet.dists[256]))
|
|
||||||
for i := range lookupTestnet.dists[256] {
|
|
||||||
bootnodes[i] = wrapNode(lookupTestnet.node(256, i))
|
|
||||||
}
|
|
||||||
fillTable(test.table, bootnodes, true)
|
|
||||||
go serveTestnet(test, lookupTestnet)
|
|
||||||
|
|
||||||
// Create the iterator and collect the nodes it yields.
|
|
||||||
iter := test.udp.RandomNodes()
|
|
||||||
seen := make(map[enode.ID]*enode.Node)
|
|
||||||
for limit := lookupTestnet.len(); iter.Next() && len(seen) < limit; {
|
|
||||||
seen[iter.Node().ID()] = iter.Node()
|
|
||||||
}
|
|
||||||
iter.Close()
|
|
||||||
|
|
||||||
// Check that all nodes in lookupTestnet were seen by the iterator.
|
|
||||||
results := make([]*enode.Node, 0, len(seen))
|
|
||||||
for _, n := range seen {
|
|
||||||
results = append(results, n)
|
|
||||||
}
|
|
||||||
sortByID(results)
|
|
||||||
want := lookupTestnet.nodes()
|
|
||||||
if err := checkNodesEqual(results, want); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestUDPv4_LookupIteratorClose checks that lookupIterator ends when its Close
|
|
||||||
// method is called.
|
|
||||||
func TestUDPv4_LookupIteratorClose(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPTest(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
// Seed table with initial nodes.
|
|
||||||
bootnodes := make([]*node, len(lookupTestnet.dists[256]))
|
|
||||||
for i := range lookupTestnet.dists[256] {
|
|
||||||
bootnodes[i] = wrapNode(lookupTestnet.node(256, i))
|
|
||||||
}
|
|
||||||
fillTable(test.table, bootnodes, true)
|
|
||||||
go serveTestnet(test, lookupTestnet)
|
|
||||||
|
|
||||||
it := test.udp.RandomNodes()
|
|
||||||
if ok := it.Next(); !ok || it.Node() == nil {
|
|
||||||
t.Fatalf("iterator didn't return any node")
|
|
||||||
}
|
|
||||||
|
|
||||||
it.Close()
|
|
||||||
|
|
||||||
ncalls := 0
|
|
||||||
for ; ncalls < 100 && it.Next(); ncalls++ {
|
|
||||||
if it.Node() == nil {
|
|
||||||
t.Error("iterator returned Node() == nil node after Next() == true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.Logf("iterator returned %d nodes after close", ncalls)
|
|
||||||
if it.Next() {
|
|
||||||
t.Errorf("Next() == true after close and %d more calls", ncalls)
|
|
||||||
}
|
|
||||||
if n := it.Node(); n != nil {
|
|
||||||
t.Errorf("iterator returned non-nil node after close and %d more calls", ncalls)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveTestnet(test *udpTest, testnet *preminedTestnet) {
|
|
||||||
for done := false; !done; {
|
|
||||||
done = test.waitPacketOut(func(p v4wire.Packet, to *net.UDPAddr, hash []byte) {
|
|
||||||
n, key := testnet.nodeByAddr(to)
|
|
||||||
switch p.(type) {
|
|
||||||
case *v4wire.Ping:
|
|
||||||
test.packetInFrom(nil, key, to, &v4wire.Pong{Expiration: futureExp, ReplyTok: hash})
|
|
||||||
case *v4wire.Findnode:
|
|
||||||
dist := enode.LogDist(n.ID(), testnet.target.id())
|
|
||||||
nodes := testnet.nodesAtDistance(dist - 1)
|
|
||||||
test.packetInFrom(nil, key, to, &v4wire.Neighbors{Expiration: futureExp, Nodes: nodes})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkLookupResults verifies that the results of a lookup are the closest nodes to
|
|
||||||
// the testnet's target.
|
|
||||||
func checkLookupResults(t *testing.T, tn *preminedTestnet, results []*enode.Node) {
|
|
||||||
t.Helper()
|
|
||||||
t.Logf("results:")
|
|
||||||
for _, e := range results {
|
|
||||||
t.Logf(" ld=%d, %x", enode.LogDist(tn.target.id(), e.ID()), e.ID().Bytes())
|
|
||||||
}
|
|
||||||
if hasDuplicates(wrapNodes(results)) {
|
|
||||||
t.Errorf("result set contains duplicate entries")
|
|
||||||
}
|
|
||||||
if !sortedByDistanceTo(tn.target.id(), wrapNodes(results)) {
|
|
||||||
t.Errorf("result set not sorted by distance to target")
|
|
||||||
}
|
|
||||||
wantNodes := tn.closest(len(results))
|
|
||||||
if err := checkNodesEqual(results, wantNodes); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is the test network for the Lookup test.
|
|
||||||
// The nodes were obtained by running lookupTestnet.mine with a random NodeID as target.
|
|
||||||
var lookupTestnet = &preminedTestnet{
|
|
||||||
target: hexEncPubkey("5d485bdcbe9bc89314a10ae9231e429d33853e3a8fa2af39f5f827370a2e4185e344ace5d16237491dad41f278f1d3785210d29ace76cd627b9147ee340b1125"),
|
|
||||||
dists: [257][]*ecdsa.PrivateKey{
|
|
||||||
251: {
|
|
||||||
hexEncPrivkey("29738ba0c1a4397d6a65f292eee07f02df8e58d41594ba2be3cf84ce0fc58169"),
|
|
||||||
hexEncPrivkey("511b1686e4e58a917f7f848e9bf5539d206a68f5ad6b54b552c2399fe7d174ae"),
|
|
||||||
hexEncPrivkey("d09e5eaeec0fd596236faed210e55ef45112409a5aa7f3276d26646080dcfaeb"),
|
|
||||||
hexEncPrivkey("c1e20dbbf0d530e50573bd0a260b32ec15eb9190032b4633d44834afc8afe578"),
|
|
||||||
hexEncPrivkey("ed5f38f5702d92d306143e5d9154fb21819777da39af325ea359f453d179e80b"),
|
|
||||||
},
|
|
||||||
252: {
|
|
||||||
hexEncPrivkey("1c9b1cafbec00848d2c174b858219914b42a7d5c9359b1ca03fd650e8239ae94"),
|
|
||||||
hexEncPrivkey("e0e1e8db4a6f13c1ffdd3e96b72fa7012293ced187c9dcdcb9ba2af37a46fa10"),
|
|
||||||
hexEncPrivkey("3d53823e0a0295cb09f3e11d16c1b44d07dd37cec6f739b8df3a590189fe9fb9"),
|
|
||||||
},
|
|
||||||
253: {
|
|
||||||
hexEncPrivkey("2d0511ae9bf590166597eeab86b6f27b1ab761761eaea8965487b162f8703847"),
|
|
||||||
hexEncPrivkey("6cfbd7b8503073fc3dbdb746a7c672571648d3bd15197ccf7f7fef3d904f53a2"),
|
|
||||||
hexEncPrivkey("a30599b12827b69120633f15b98a7f6bc9fc2e9a0fd6ae2ebb767c0e64d743ab"),
|
|
||||||
hexEncPrivkey("14a98db9b46a831d67eff29f3b85b1b485bb12ae9796aea98d91be3dc78d8a91"),
|
|
||||||
hexEncPrivkey("2369ff1fc1ff8ca7d20b17e2673adc3365c3674377f21c5d9dafaff21fe12e24"),
|
|
||||||
hexEncPrivkey("9ae91101d6b5048607f41ec0f690ef5d09507928aded2410aabd9237aa2727d7"),
|
|
||||||
hexEncPrivkey("05e3c59090a3fd1ae697c09c574a36fcf9bedd0afa8fe3946f21117319ca4973"),
|
|
||||||
hexEncPrivkey("06f31c5ea632658f718a91a1b1b9ae4b7549d7b3bc61cbc2be5f4a439039f3ad"),
|
|
||||||
},
|
|
||||||
254: {
|
|
||||||
hexEncPrivkey("dec742079ec00ff4ec1284d7905bc3de2366f67a0769431fd16f80fd68c58a7c"),
|
|
||||||
hexEncPrivkey("ff02c8861fa12fbd129d2a95ea663492ef9c1e51de19dcfbbfe1c59894a28d2b"),
|
|
||||||
hexEncPrivkey("4dded9e4eefcbce4262be4fd9e8a773670ab0b5f448f286ec97dfc8cf681444a"),
|
|
||||||
hexEncPrivkey("750d931e2a8baa2c9268cb46b7cd851f4198018bed22f4dceb09dd334a2395f6"),
|
|
||||||
hexEncPrivkey("ce1435a956a98ffec484cd11489c4f165cf1606819ab6b521cee440f0c677e9e"),
|
|
||||||
hexEncPrivkey("996e7f8d1638be92d7328b4770f47e5420fc4bafecb4324fd33b1f5d9f403a75"),
|
|
||||||
hexEncPrivkey("ebdc44e77a6cc0eb622e58cf3bb903c3da4c91ca75b447b0168505d8fc308b9c"),
|
|
||||||
hexEncPrivkey("46bd1eddcf6431bea66fc19ebc45df191c1c7d6ed552dcdc7392885009c322f0"),
|
|
||||||
},
|
|
||||||
255: {
|
|
||||||
hexEncPrivkey("da8645f90826e57228d9ea72aff84500060ad111a5d62e4af831ed8e4b5acfb8"),
|
|
||||||
hexEncPrivkey("3c944c5d9af51d4c1d43f5d0f3a1a7ef65d5e82744d669b58b5fed242941a566"),
|
|
||||||
hexEncPrivkey("5ebcde76f1d579eebf6e43b0ffe9157e65ffaa391175d5b9aa988f47df3e33da"),
|
|
||||||
hexEncPrivkey("97f78253a7d1d796e4eaabce721febcc4550dd68fb11cc818378ba807a2cb7de"),
|
|
||||||
hexEncPrivkey("a38cd7dc9b4079d1c0406afd0fdb1165c285f2c44f946eca96fc67772c988c7d"),
|
|
||||||
hexEncPrivkey("d64cbb3ffdf712c372b7a22a176308ef8f91861398d5dbaf326fd89c6eaeef1c"),
|
|
||||||
hexEncPrivkey("d269609743ef29d6446e3355ec647e38d919c82a4eb5837e442efd7f4218944f"),
|
|
||||||
hexEncPrivkey("d8f7bcc4a530efde1d143717007179e0d9ace405ddaaf151c4d863753b7fd64c"),
|
|
||||||
},
|
|
||||||
256: {
|
|
||||||
hexEncPrivkey("8c5b422155d33ea8e9d46f71d1ad3e7b24cb40051413ffa1a81cff613d243ba9"),
|
|
||||||
hexEncPrivkey("937b1af801def4e8f5a3a8bd225a8bcff1db764e41d3e177f2e9376e8dd87233"),
|
|
||||||
hexEncPrivkey("120260dce739b6f71f171da6f65bc361b5fad51db74cf02d3e973347819a6518"),
|
|
||||||
hexEncPrivkey("1fa56cf25d4b46c2bf94e82355aa631717b63190785ac6bae545a88aadc304a9"),
|
|
||||||
hexEncPrivkey("3c38c503c0376f9b4adcbe935d5f4b890391741c764f61b03cd4d0d42deae002"),
|
|
||||||
hexEncPrivkey("3a54af3e9fa162bc8623cdf3e5d9b70bf30ade1d54cc3abea8659aba6cff471f"),
|
|
||||||
hexEncPrivkey("6799a02ea1999aefdcbcc4d3ff9544478be7365a328d0d0f37c26bd95ade0cda"),
|
|
||||||
hexEncPrivkey("e24a7bc9051058f918646b0f6e3d16884b2a55a15553b89bab910d55ebc36116"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
type preminedTestnet struct {
|
|
||||||
target encPubkey
|
|
||||||
dists [hashBits + 1][]*ecdsa.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tn *preminedTestnet) len() int {
|
|
||||||
n := 0
|
|
||||||
for _, keys := range tn.dists {
|
|
||||||
n += len(keys)
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tn *preminedTestnet) nodes() []*enode.Node {
|
|
||||||
result := make([]*enode.Node, 0, tn.len())
|
|
||||||
for dist, keys := range tn.dists {
|
|
||||||
for index := range keys {
|
|
||||||
result = append(result, tn.node(dist, index))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sortByID(result)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tn *preminedTestnet) node(dist, index int) *enode.Node {
|
|
||||||
key := tn.dists[dist][index]
|
|
||||||
rec := new(enr.Record)
|
|
||||||
rec.Set(enr.IP{127, byte(dist >> 8), byte(dist), byte(index)})
|
|
||||||
rec.Set(enr.UDP(5000))
|
|
||||||
enode.SignV4(rec, key)
|
|
||||||
n, _ := enode.New(enode.ValidSchemes, rec)
|
|
||||||
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])
|
|
||||||
key := tn.dists[dist][index]
|
|
||||||
return tn.node(dist, index), key
|
|
||||||
}
|
|
||||||
|
|
||||||
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)))
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tn *preminedTestnet) neighborsAtDistances(base *enode.Node, distances []uint, elems int) []*enode.Node {
|
|
||||||
var result []*enode.Node
|
|
||||||
for d := range lookupTestnet.dists {
|
|
||||||
for i := range lookupTestnet.dists[d] {
|
|
||||||
n := lookupTestnet.node(d, i)
|
|
||||||
d := enode.LogDist(base.ID(), n.ID())
|
|
||||||
if containsUint(uint(d), distances) {
|
|
||||||
result = append(result, n)
|
|
||||||
if len(result) >= elems {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tn *preminedTestnet) closest(n int) (nodes []*enode.Node) {
|
|
||||||
for d := range tn.dists {
|
|
||||||
for i := range tn.dists[d] {
|
|
||||||
nodes = append(nodes, tn.node(d, i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
slices.SortFunc(nodes, func(a, b *enode.Node) int {
|
|
||||||
return enode.DistCmp(tn.target.id(), a.ID(), b.ID())
|
|
||||||
})
|
|
||||||
return nodes[:n]
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = (*preminedTestnet).mine // avoid linter warning about mine being dead code.
|
|
||||||
|
|
||||||
// mine generates a testnet struct literal with nodes at
|
|
||||||
// various distances to the network's target.
|
|
||||||
func (tn *preminedTestnet) mine() {
|
|
||||||
// Clear existing slices first (useful when re-mining).
|
|
||||||
for i := range tn.dists {
|
|
||||||
tn.dists[i] = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
targetSha := tn.target.id()
|
|
||||||
found, need := 0, 40
|
|
||||||
for found < need {
|
|
||||||
k := newkey()
|
|
||||||
ld := enode.LogDist(targetSha, encodePubkey(&k.PublicKey).id())
|
|
||||||
if len(tn.dists[ld]) < 8 {
|
|
||||||
tn.dists[ld] = append(tn.dists[ld], k)
|
|
||||||
found++
|
|
||||||
fmt.Printf("found ID with ld %d (%d/%d)\n", ld, found, need)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Printf("&preminedTestnet{\n")
|
|
||||||
fmt.Printf(" target: hexEncPubkey(\"%x\"),\n", tn.target[:])
|
|
||||||
fmt.Printf(" dists: [%d][]*ecdsa.PrivateKey{\n", len(tn.dists))
|
|
||||||
for ld, ns := range tn.dists {
|
|
||||||
if len(ns) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Printf(" %d: {\n", ld)
|
|
||||||
for _, key := range ns {
|
|
||||||
fmt.Printf(" hexEncPrivkey(\"%x\"),\n", crypto.FromECDSA(key))
|
|
||||||
}
|
|
||||||
fmt.Printf(" },\n")
|
|
||||||
}
|
|
||||||
fmt.Printf(" },\n")
|
|
||||||
fmt.Printf("}\n")
|
|
||||||
}
|
|
||||||
|
|
@ -1,787 +0,0 @@
|
||||||
// Copyright 2019 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"container/list"
|
|
||||||
"context"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
crand "crypto/rand"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/discover/v4wire"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/netutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Errors
|
|
||||||
var (
|
|
||||||
errExpired = errors.New("expired")
|
|
||||||
errUnsolicitedReply = errors.New("unsolicited reply")
|
|
||||||
errUnknownNode = errors.New("unknown node")
|
|
||||||
errTimeout = errors.New("RPC timeout")
|
|
||||||
errClockWarp = errors.New("reply deadline too far in the future")
|
|
||||||
errClosed = errors.New("socket closed")
|
|
||||||
errLowPort = errors.New("low port")
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
respTimeout = 500 * time.Millisecond
|
|
||||||
expiration = 20 * time.Second
|
|
||||||
bondExpiration = 24 * time.Hour
|
|
||||||
|
|
||||||
maxFindnodeFailures = 5 // nodes exceeding this limit are dropped
|
|
||||||
ntpFailureThreshold = 32 // Continuous timeouts after which to check NTP
|
|
||||||
ntpWarningCooldown = 10 * time.Minute // Minimum amount of time to pass before repeating NTP warning
|
|
||||||
driftThreshold = 10 * time.Second // Allowed clock drift before warning user
|
|
||||||
|
|
||||||
// Discovery packets are defined to be no larger than 1280 bytes.
|
|
||||||
// Packets larger than this size will be cut at the end and treated
|
|
||||||
// as invalid because their hash won't match.
|
|
||||||
maxPacketSize = 1280
|
|
||||||
)
|
|
||||||
|
|
||||||
// UDPv4 implements the v4 wire protocol.
|
|
||||||
type UDPv4 struct {
|
|
||||||
conn UDPConn
|
|
||||||
log log.Logger
|
|
||||||
netrestrict *netutil.Netlist
|
|
||||||
priv *ecdsa.PrivateKey
|
|
||||||
localNode *enode.LocalNode
|
|
||||||
db *enode.DB
|
|
||||||
tab *Table
|
|
||||||
closeOnce sync.Once
|
|
||||||
wg sync.WaitGroup
|
|
||||||
|
|
||||||
addReplyMatcher chan *replyMatcher
|
|
||||||
gotreply chan reply
|
|
||||||
closeCtx context.Context
|
|
||||||
cancelCloseCtx context.CancelFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
// replyMatcher represents a pending reply.
|
|
||||||
//
|
|
||||||
// Some implementations of the protocol wish to send more than one
|
|
||||||
// reply packet to findnode. In general, any neighbors packet cannot
|
|
||||||
// be matched up with a specific findnode packet.
|
|
||||||
//
|
|
||||||
// Our implementation handles this by storing a callback function for
|
|
||||||
// each pending reply. Incoming packets from a node are dispatched
|
|
||||||
// to all callback functions for that node.
|
|
||||||
type replyMatcher struct {
|
|
||||||
// these fields must match in the reply.
|
|
||||||
from enode.ID
|
|
||||||
ip net.IP
|
|
||||||
ptype byte
|
|
||||||
|
|
||||||
// time when the request must complete
|
|
||||||
deadline time.Time
|
|
||||||
|
|
||||||
// callback is called when a matching reply arrives. If it returns matched == true, the
|
|
||||||
// reply was acceptable. The second return value indicates whether the callback should
|
|
||||||
// be removed from the pending reply queue. If it returns false, the reply is considered
|
|
||||||
// incomplete and the callback will be invoked again for the next matching reply.
|
|
||||||
callback replyMatchFunc
|
|
||||||
|
|
||||||
// errc receives nil when the callback indicates completion or an
|
|
||||||
// error if no further reply is received within the timeout.
|
|
||||||
errc chan error
|
|
||||||
|
|
||||||
// reply contains the most recent reply. This field is safe for reading after errc has
|
|
||||||
// received a value.
|
|
||||||
reply v4wire.Packet
|
|
||||||
}
|
|
||||||
|
|
||||||
type replyMatchFunc func(v4wire.Packet) (matched bool, requestDone bool)
|
|
||||||
|
|
||||||
// reply is a reply packet from a certain node.
|
|
||||||
type reply struct {
|
|
||||||
from enode.ID
|
|
||||||
ip net.IP
|
|
||||||
data v4wire.Packet
|
|
||||||
// loop indicates whether there was
|
|
||||||
// a matching request by sending on this channel.
|
|
||||||
matched chan<- bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListenV4(c UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv4, error) {
|
|
||||||
cfg = cfg.withDefaults()
|
|
||||||
closeCtx, cancel := context.WithCancel(context.Background())
|
|
||||||
t := &UDPv4{
|
|
||||||
conn: newMeteredConn(c),
|
|
||||||
priv: cfg.PrivateKey,
|
|
||||||
netrestrict: cfg.NetRestrict,
|
|
||||||
localNode: ln,
|
|
||||||
db: ln.Database(),
|
|
||||||
gotreply: make(chan reply),
|
|
||||||
addReplyMatcher: make(chan *replyMatcher),
|
|
||||||
closeCtx: closeCtx,
|
|
||||||
cancelCloseCtx: cancel,
|
|
||||||
log: cfg.Log,
|
|
||||||
}
|
|
||||||
|
|
||||||
tab, err := newMeteredTable(t, ln.Database(), cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
t.tab = tab
|
|
||||||
go tab.loop()
|
|
||||||
|
|
||||||
t.wg.Add(2)
|
|
||||||
go t.loop()
|
|
||||||
go t.readLoop(cfg.Unhandled)
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Self returns the local node.
|
|
||||||
func (t *UDPv4) Self() *enode.Node {
|
|
||||||
return t.localNode.Node()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close shuts down the socket and aborts any running queries.
|
|
||||||
func (t *UDPv4) Close() {
|
|
||||||
t.closeOnce.Do(func() {
|
|
||||||
t.cancelCloseCtx()
|
|
||||||
t.conn.Close()
|
|
||||||
t.wg.Wait()
|
|
||||||
t.tab.close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve searches for a specific node with the given ID and tries to get the most recent
|
|
||||||
// version of the node record for it. It returns n if the node could not be resolved.
|
|
||||||
func (t *UDPv4) Resolve(n *enode.Node) *enode.Node {
|
|
||||||
// Try asking directly. This works if the node is still responding on the endpoint we have.
|
|
||||||
if rn, err := t.RequestENR(n); err == nil {
|
|
||||||
return rn
|
|
||||||
}
|
|
||||||
// Check table for the ID, we might have a newer version there.
|
|
||||||
if intable := t.tab.getNode(n.ID()); intable != nil && intable.Seq() > n.Seq() {
|
|
||||||
n = intable
|
|
||||||
if rn, err := t.RequestENR(n); err == nil {
|
|
||||||
return rn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Otherwise perform a network lookup.
|
|
||||||
var key enode.Secp256k1
|
|
||||||
if n.Load(&key) != nil {
|
|
||||||
return n // no secp256k1 key
|
|
||||||
}
|
|
||||||
result := t.LookupPubkey((*ecdsa.PublicKey)(&key))
|
|
||||||
for _, rn := range result {
|
|
||||||
if rn.ID() == n.ID() {
|
|
||||||
if rn, err := t.RequestENR(rn); err == nil {
|
|
||||||
return rn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPv4) ourEndpoint() v4wire.Endpoint {
|
|
||||||
n := t.Self()
|
|
||||||
a := &net.UDPAddr{IP: n.IP(), Port: n.UDP()}
|
|
||||||
return v4wire.NewEndpoint(a, uint16(n.TCP()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping sends a ping message to the given node.
|
|
||||||
func (t *UDPv4) Ping(n *enode.Node) error {
|
|
||||||
_, err := t.ping(n)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ping sends a ping message to the given node and waits for a reply.
|
|
||||||
func (t *UDPv4) ping(n *enode.Node) (seq uint64, err error) {
|
|
||||||
rm := t.sendPing(n.ID(), &net.UDPAddr{IP: n.IP(), Port: n.UDP()}, nil)
|
|
||||||
if err = <-rm.errc; err == nil {
|
|
||||||
seq = rm.reply.(*v4wire.Pong).ENRSeq
|
|
||||||
}
|
|
||||||
return seq, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendPing sends a ping message to the given node and invokes the callback
|
|
||||||
// when the reply arrives.
|
|
||||||
func (t *UDPv4) sendPing(toid enode.ID, toaddr *net.UDPAddr, callback func()) *replyMatcher {
|
|
||||||
req := t.makePing(toaddr)
|
|
||||||
packet, hash, err := v4wire.Encode(t.priv, req)
|
|
||||||
if err != nil {
|
|
||||||
errc := make(chan error, 1)
|
|
||||||
errc <- err
|
|
||||||
return &replyMatcher{errc: errc}
|
|
||||||
}
|
|
||||||
// Add a matcher for the reply to the pending reply queue. Pongs are matched if they
|
|
||||||
// reference the ping we're about to send.
|
|
||||||
rm := t.pending(toid, toaddr.IP, v4wire.PongPacket, func(p v4wire.Packet) (matched bool, requestDone bool) {
|
|
||||||
matched = bytes.Equal(p.(*v4wire.Pong).ReplyTok, hash)
|
|
||||||
if matched && callback != nil {
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
return matched, matched
|
|
||||||
})
|
|
||||||
// Send the packet.
|
|
||||||
t.localNode.UDPContact(toaddr)
|
|
||||||
t.write(toaddr, toid, req.Name(), packet)
|
|
||||||
return rm
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPv4) makePing(toaddr *net.UDPAddr) *v4wire.Ping {
|
|
||||||
return &v4wire.Ping{
|
|
||||||
Version: 4,
|
|
||||||
From: t.ourEndpoint(),
|
|
||||||
To: v4wire.NewEndpoint(toaddr, 0),
|
|
||||||
Expiration: uint64(time.Now().Add(expiration).Unix()),
|
|
||||||
ENRSeq: t.localNode.Node().Seq(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookupPubkey finds the closest nodes to the given public key.
|
|
||||||
func (t *UDPv4) LookupPubkey(key *ecdsa.PublicKey) []*enode.Node {
|
|
||||||
if t.tab.len() == 0 {
|
|
||||||
// All nodes were dropped, refresh. The very first query will hit this
|
|
||||||
// case and run the bootstrapping logic.
|
|
||||||
<-t.tab.refresh()
|
|
||||||
}
|
|
||||||
return t.newLookup(t.closeCtx, encodePubkey(key)).run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// RandomNodes is an iterator yielding nodes from a random walk of the DHT.
|
|
||||||
func (t *UDPv4) RandomNodes() enode.Iterator {
|
|
||||||
return newLookupIterator(t.closeCtx, t.newRandomLookup)
|
|
||||||
}
|
|
||||||
|
|
||||||
// lookupRandom implements transport.
|
|
||||||
func (t *UDPv4) lookupRandom() []*enode.Node {
|
|
||||||
return t.newRandomLookup(t.closeCtx).run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// lookupSelf implements transport.
|
|
||||||
func (t *UDPv4) lookupSelf() []*enode.Node {
|
|
||||||
return t.newLookup(t.closeCtx, encodePubkey(&t.priv.PublicKey)).run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPv4) newRandomLookup(ctx context.Context) *lookup {
|
|
||||||
var target encPubkey
|
|
||||||
crand.Read(target[:])
|
|
||||||
return t.newLookup(ctx, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
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) {
|
|
||||||
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)
|
|
||||||
nreceived := 0
|
|
||||||
rm := t.pending(toid, toaddr.IP, v4wire.NeighborsPacket, func(r v4wire.Packet) (matched bool, requestDone bool) {
|
|
||||||
reply := r.(*v4wire.Neighbors)
|
|
||||||
for _, rn := range reply.Nodes {
|
|
||||||
nreceived++
|
|
||||||
n, err := t.nodeFromRPC(toaddr, rn)
|
|
||||||
if err != nil {
|
|
||||||
t.log.Trace("Invalid neighbor node received", "ip", rn.IP, "addr", toaddr, "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
nodes = append(nodes, n)
|
|
||||||
}
|
|
||||||
return true, nreceived >= bucketSize
|
|
||||||
})
|
|
||||||
t.send(toaddr, toid, &v4wire.Findnode{
|
|
||||||
Target: target,
|
|
||||||
Expiration: uint64(time.Now().Add(expiration).Unix()),
|
|
||||||
})
|
|
||||||
// Ensure that callers don't see a timeout if the node actually responded. Since
|
|
||||||
// findnode can receive more than one neighbors response, the reply matcher will be
|
|
||||||
// active until the remote node sends enough nodes. If the remote end doesn't have
|
|
||||||
// enough nodes the reply matcher will time out waiting for the second reply, but
|
|
||||||
// there's no need for an error in that case.
|
|
||||||
err := <-rm.errc
|
|
||||||
if errors.Is(err, errTimeout) && rm.reply != nil {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
return nodes, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestENR sends ENRRequest to the given node and waits for a response.
|
|
||||||
func (t *UDPv4) RequestENR(n *enode.Node) (*enode.Node, error) {
|
|
||||||
addr := &net.UDPAddr{IP: n.IP(), Port: n.UDP()}
|
|
||||||
t.ensureBond(n.ID(), addr)
|
|
||||||
|
|
||||||
req := &v4wire.ENRRequest{
|
|
||||||
Expiration: uint64(time.Now().Add(expiration).Unix()),
|
|
||||||
}
|
|
||||||
packet, hash, err := v4wire.Encode(t.priv, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a matcher for the reply to the pending reply queue. Responses are matched if
|
|
||||||
// they reference the request we're about to send.
|
|
||||||
rm := t.pending(n.ID(), addr.IP, v4wire.ENRResponsePacket, func(r v4wire.Packet) (matched bool, requestDone bool) {
|
|
||||||
matched = bytes.Equal(r.(*v4wire.ENRResponse).ReplyTok, hash)
|
|
||||||
return matched, matched
|
|
||||||
})
|
|
||||||
// Send the packet and wait for the reply.
|
|
||||||
t.write(addr, n.ID(), req.Name(), packet)
|
|
||||||
if err := <-rm.errc; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Verify the response record.
|
|
||||||
respN, err := enode.New(enode.ValidSchemes, &rm.reply.(*v4wire.ENRResponse).Record)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if respN.ID() != n.ID() {
|
|
||||||
return nil, fmt.Errorf("invalid ID in response record")
|
|
||||||
}
|
|
||||||
if respN.Seq() < n.Seq() {
|
|
||||||
return n, nil // response record is older
|
|
||||||
}
|
|
||||||
if err := netutil.CheckRelayIP(addr.IP, respN.IP()); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid IP in response record: %v", err)
|
|
||||||
}
|
|
||||||
return respN, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// pending adds a reply matcher to the pending reply queue.
|
|
||||||
// see the documentation of type replyMatcher for a detailed explanation.
|
|
||||||
func (t *UDPv4) pending(id enode.ID, ip net.IP, ptype byte, callback replyMatchFunc) *replyMatcher {
|
|
||||||
ch := make(chan error, 1)
|
|
||||||
p := &replyMatcher{from: id, ip: ip, ptype: ptype, callback: callback, errc: ch}
|
|
||||||
select {
|
|
||||||
case t.addReplyMatcher <- p:
|
|
||||||
// loop will handle it
|
|
||||||
case <-t.closeCtx.Done():
|
|
||||||
ch <- errClosed
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleReply dispatches a reply packet, invoking reply matchers. It returns
|
|
||||||
// whether any matcher considered the packet acceptable.
|
|
||||||
func (t *UDPv4) handleReply(from enode.ID, fromIP net.IP, req v4wire.Packet) bool {
|
|
||||||
matched := make(chan bool, 1)
|
|
||||||
select {
|
|
||||||
case t.gotreply <- reply{from, fromIP, req, matched}:
|
|
||||||
// loop will handle it
|
|
||||||
return <-matched
|
|
||||||
case <-t.closeCtx.Done():
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// loop runs in its own goroutine. it keeps track of
|
|
||||||
// the refresh timer and the pending reply queue.
|
|
||||||
func (t *UDPv4) loop() {
|
|
||||||
defer t.wg.Done()
|
|
||||||
|
|
||||||
var (
|
|
||||||
plist = list.New()
|
|
||||||
timeout = time.NewTimer(0)
|
|
||||||
nextTimeout *replyMatcher // head of plist when timeout was last reset
|
|
||||||
contTimeouts = 0 // number of continuous timeouts to do NTP checks
|
|
||||||
ntpWarnTime = time.Unix(0, 0)
|
|
||||||
)
|
|
||||||
<-timeout.C // ignore first timeout
|
|
||||||
defer timeout.Stop()
|
|
||||||
|
|
||||||
resetTimeout := func() {
|
|
||||||
if plist.Front() == nil || nextTimeout == plist.Front().Value {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Start the timer so it fires when the next pending reply has expired.
|
|
||||||
now := time.Now()
|
|
||||||
for el := plist.Front(); el != nil; el = el.Next() {
|
|
||||||
nextTimeout = el.Value.(*replyMatcher)
|
|
||||||
if dist := nextTimeout.deadline.Sub(now); dist < 2*respTimeout {
|
|
||||||
timeout.Reset(dist)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Remove pending replies whose deadline is too far in the
|
|
||||||
// future. These can occur if the system clock jumped
|
|
||||||
// backwards after the deadline was assigned.
|
|
||||||
nextTimeout.errc <- errClockWarp
|
|
||||||
plist.Remove(el)
|
|
||||||
}
|
|
||||||
nextTimeout = nil
|
|
||||||
timeout.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
resetTimeout()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-t.closeCtx.Done():
|
|
||||||
for el := plist.Front(); el != nil; el = el.Next() {
|
|
||||||
el.Value.(*replyMatcher).errc <- errClosed
|
|
||||||
}
|
|
||||||
return
|
|
||||||
|
|
||||||
case p := <-t.addReplyMatcher:
|
|
||||||
p.deadline = time.Now().Add(respTimeout)
|
|
||||||
plist.PushBack(p)
|
|
||||||
|
|
||||||
case r := <-t.gotreply:
|
|
||||||
var matched bool // whether any replyMatcher considered the reply acceptable.
|
|
||||||
for el := plist.Front(); el != nil; el = el.Next() {
|
|
||||||
p := el.Value.(*replyMatcher)
|
|
||||||
if p.from == r.from && p.ptype == r.data.Kind() && p.ip.Equal(r.ip) {
|
|
||||||
ok, requestDone := p.callback(r.data)
|
|
||||||
matched = matched || ok
|
|
||||||
p.reply = r.data
|
|
||||||
// Remove the matcher if callback indicates that all replies have been received.
|
|
||||||
if requestDone {
|
|
||||||
p.errc <- nil
|
|
||||||
plist.Remove(el)
|
|
||||||
}
|
|
||||||
// Reset the continuous timeout counter (time drift detection)
|
|
||||||
contTimeouts = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r.matched <- matched
|
|
||||||
|
|
||||||
case now := <-timeout.C:
|
|
||||||
nextTimeout = nil
|
|
||||||
|
|
||||||
// Notify and remove callbacks whose deadline is in the past.
|
|
||||||
for el := plist.Front(); el != nil; el = el.Next() {
|
|
||||||
p := el.Value.(*replyMatcher)
|
|
||||||
if now.After(p.deadline) || now.Equal(p.deadline) {
|
|
||||||
p.errc <- errTimeout
|
|
||||||
plist.Remove(el)
|
|
||||||
contTimeouts++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If we've accumulated too many timeouts, do an NTP time sync check
|
|
||||||
if contTimeouts > ntpFailureThreshold {
|
|
||||||
if time.Since(ntpWarnTime) >= ntpWarningCooldown {
|
|
||||||
ntpWarnTime = time.Now()
|
|
||||||
go checkClockDrift()
|
|
||||||
}
|
|
||||||
contTimeouts = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPv4) send(toaddr *net.UDPAddr, toid enode.ID, req v4wire.Packet) ([]byte, error) {
|
|
||||||
packet, hash, err := v4wire.Encode(t.priv, req)
|
|
||||||
if err != nil {
|
|
||||||
return hash, err
|
|
||||||
}
|
|
||||||
return hash, t.write(toaddr, toid, req.Name(), packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPv4) write(toaddr *net.UDPAddr, toid enode.ID, what string, packet []byte) error {
|
|
||||||
_, err := t.conn.WriteToUDP(packet, toaddr)
|
|
||||||
t.log.Trace(">> "+what, "id", toid, "addr", toaddr, "err", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// readLoop runs in its own goroutine. it handles incoming UDP packets.
|
|
||||||
func (t *UDPv4) readLoop(unhandled chan<- ReadPacket) {
|
|
||||||
defer t.wg.Done()
|
|
||||||
if unhandled != nil {
|
|
||||||
defer close(unhandled)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, maxPacketSize)
|
|
||||||
for {
|
|
||||||
nbytes, from, err := t.conn.ReadFromUDP(buf)
|
|
||||||
if netutil.IsTemporaryError(err) {
|
|
||||||
// Ignore temporary read errors.
|
|
||||||
t.log.Debug("Temporary UDP read error", "err", err)
|
|
||||||
continue
|
|
||||||
} else if err != nil {
|
|
||||||
// Shut down the loop for permanent errors.
|
|
||||||
if !errors.Is(err, io.EOF) {
|
|
||||||
t.log.Debug("UDP read error", "err", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if t.handlePacket(from, buf[:nbytes]) != nil && unhandled != nil {
|
|
||||||
select {
|
|
||||||
case unhandled <- ReadPacket{buf[:nbytes], from}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPv4) handlePacket(from *net.UDPAddr, buf []byte) error {
|
|
||||||
rawpacket, fromKey, hash, err := v4wire.Decode(buf)
|
|
||||||
if err != nil {
|
|
||||||
t.log.Debug("Bad discv4 packet", "addr", from, "err", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
packet := t.wrapPacket(rawpacket)
|
|
||||||
fromID := fromKey.ID()
|
|
||||||
if err == nil && packet.preverify != nil {
|
|
||||||
err = packet.preverify(packet, from, fromID, fromKey)
|
|
||||||
}
|
|
||||||
t.log.Trace("<< "+packet.Name(), "id", fromID, "addr", from, "err", err)
|
|
||||||
if err == nil && packet.handle != nil {
|
|
||||||
packet.handle(packet, from, fromID, hash)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkBond checks if the given node has a recent enough endpoint proof.
|
|
||||||
func (t *UDPv4) checkBond(id enode.ID, ip net.IP) bool {
|
|
||||||
return time.Since(t.db.LastPongReceived(id, ip)) < bondExpiration
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureBond solicits a ping from a node if we haven't seen a ping from it for a while.
|
|
||||||
// This ensures there is a valid endpoint proof on the remote end.
|
|
||||||
func (t *UDPv4) ensureBond(toid enode.ID, toaddr *net.UDPAddr) {
|
|
||||||
tooOld := time.Since(t.db.LastPingReceived(toid, toaddr.IP)) > bondExpiration
|
|
||||||
if tooOld || t.db.FindFails(toid, toaddr.IP) > maxFindnodeFailures {
|
|
||||||
rm := t.sendPing(toid, toaddr, nil)
|
|
||||||
<-rm.errc
|
|
||||||
// Wait for them to ping back and process our pong.
|
|
||||||
time.Sleep(respTimeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPv4) nodeFromRPC(sender *net.UDPAddr, rn v4wire.Node) (*node, error) {
|
|
||||||
if rn.UDP <= 1024 {
|
|
||||||
return nil, errLowPort
|
|
||||||
}
|
|
||||||
if err := netutil.CheckRelayIP(sender.IP, rn.IP); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if t.netrestrict != nil && !t.netrestrict.Contains(rn.IP) {
|
|
||||||
return nil, errors.New("not contained in netrestrict list")
|
|
||||||
}
|
|
||||||
key, err := v4wire.DecodePubkey(crypto.S256(), rn.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
n := wrapNode(enode.NewV4(key, rn.IP, int(rn.TCP), int(rn.UDP)))
|
|
||||||
err = n.ValidateComplete()
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func nodeToRPC(n *node) v4wire.Node {
|
|
||||||
var key ecdsa.PublicKey
|
|
||||||
var ekey v4wire.Pubkey
|
|
||||||
if err := n.Load((*enode.Secp256k1)(&key)); err == nil {
|
|
||||||
ekey = v4wire.EncodePubkey(&key)
|
|
||||||
}
|
|
||||||
return v4wire.Node{ID: ekey, IP: n.IP(), UDP: uint16(n.UDP()), TCP: uint16(n.TCP())}
|
|
||||||
}
|
|
||||||
|
|
||||||
// wrapPacket returns the handler functions applicable to a packet.
|
|
||||||
func (t *UDPv4) wrapPacket(p v4wire.Packet) *packetHandlerV4 {
|
|
||||||
var h packetHandlerV4
|
|
||||||
h.Packet = p
|
|
||||||
switch p.(type) {
|
|
||||||
case *v4wire.Ping:
|
|
||||||
h.preverify = t.verifyPing
|
|
||||||
h.handle = t.handlePing
|
|
||||||
case *v4wire.Pong:
|
|
||||||
h.preverify = t.verifyPong
|
|
||||||
case *v4wire.Findnode:
|
|
||||||
h.preverify = t.verifyFindnode
|
|
||||||
h.handle = t.handleFindnode
|
|
||||||
case *v4wire.Neighbors:
|
|
||||||
h.preverify = t.verifyNeighbors
|
|
||||||
case *v4wire.ENRRequest:
|
|
||||||
h.preverify = t.verifyENRRequest
|
|
||||||
h.handle = t.handleENRRequest
|
|
||||||
case *v4wire.ENRResponse:
|
|
||||||
h.preverify = t.verifyENRResponse
|
|
||||||
}
|
|
||||||
return &h
|
|
||||||
}
|
|
||||||
|
|
||||||
// packetHandlerV4 wraps a packet with handler functions.
|
|
||||||
type packetHandlerV4 struct {
|
|
||||||
v4wire.Packet
|
|
||||||
senderKey *ecdsa.PublicKey // used for ping
|
|
||||||
|
|
||||||
// preverify checks whether the packet is valid and should be handled at all.
|
|
||||||
preverify func(p *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error
|
|
||||||
// handle handles the packet.
|
|
||||||
handle func(req *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, mac []byte)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PING/v4
|
|
||||||
|
|
||||||
func (t *UDPv4) verifyPing(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error {
|
|
||||||
req := h.Packet.(*v4wire.Ping)
|
|
||||||
|
|
||||||
if v4wire.Expired(req.Expiration) {
|
|
||||||
return errExpired
|
|
||||||
}
|
|
||||||
senderKey, err := v4wire.DecodePubkey(crypto.S256(), fromKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
h.senderKey = senderKey
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPv4) handlePing(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, mac []byte) {
|
|
||||||
req := h.Packet.(*v4wire.Ping)
|
|
||||||
|
|
||||||
// Reply.
|
|
||||||
t.send(from, fromID, &v4wire.Pong{
|
|
||||||
To: v4wire.NewEndpoint(from, req.From.TCP),
|
|
||||||
ReplyTok: mac,
|
|
||||||
Expiration: uint64(time.Now().Add(expiration).Unix()),
|
|
||||||
ENRSeq: t.localNode.Node().Seq(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
if time.Since(t.db.LastPongReceived(n.ID(), from.IP)) > bondExpiration {
|
|
||||||
t.sendPing(fromID, from, func() {
|
|
||||||
t.tab.addVerifiedNode(n)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
t.tab.addVerifiedNode(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)})
|
|
||||||
}
|
|
||||||
|
|
||||||
// PONG/v4
|
|
||||||
|
|
||||||
func (t *UDPv4) verifyPong(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error {
|
|
||||||
req := h.Packet.(*v4wire.Pong)
|
|
||||||
|
|
||||||
if v4wire.Expired(req.Expiration) {
|
|
||||||
return errExpired
|
|
||||||
}
|
|
||||||
if !t.handleReply(fromID, from.IP, req) {
|
|
||||||
return errUnsolicitedReply
|
|
||||||
}
|
|
||||||
t.localNode.UDPEndpointStatement(from, &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)})
|
|
||||||
t.db.UpdateLastPongReceived(fromID, from.IP, time.Now())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FINDNODE/v4
|
|
||||||
|
|
||||||
func (t *UDPv4) verifyFindnode(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error {
|
|
||||||
req := h.Packet.(*v4wire.Findnode)
|
|
||||||
|
|
||||||
if v4wire.Expired(req.Expiration) {
|
|
||||||
return errExpired
|
|
||||||
}
|
|
||||||
if !t.checkBond(fromID, from.IP) {
|
|
||||||
// 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
|
|
||||||
// DDOS attack. A malicious actor would send a findnode request with the IP address
|
|
||||||
// and UDP port of the target as the source address. The recipient of the findnode
|
|
||||||
// packet would then send a neighbors packet (which is a much bigger packet than
|
|
||||||
// findnode) to the victim.
|
|
||||||
return errUnknownNode
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPv4) handleFindnode(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, mac []byte) {
|
|
||||||
req := h.Packet.(*v4wire.Findnode)
|
|
||||||
|
|
||||||
// Determine closest nodes.
|
|
||||||
target := enode.ID(crypto.Keccak256Hash(req.Target[:]))
|
|
||||||
closest := t.tab.findnodeByID(target, bucketSize, true).entries
|
|
||||||
|
|
||||||
// Send neighbors in chunks with at most maxNeighbors per packet
|
|
||||||
// to stay below the packet size limit.
|
|
||||||
p := v4wire.Neighbors{Expiration: uint64(time.Now().Add(expiration).Unix())}
|
|
||||||
var sent bool
|
|
||||||
for _, n := range closest {
|
|
||||||
if netutil.CheckRelayIP(from.IP, n.IP()) == nil {
|
|
||||||
p.Nodes = append(p.Nodes, nodeToRPC(n))
|
|
||||||
}
|
|
||||||
if len(p.Nodes) == v4wire.MaxNeighbors {
|
|
||||||
t.send(from, fromID, &p)
|
|
||||||
p.Nodes = p.Nodes[:0]
|
|
||||||
sent = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(p.Nodes) > 0 || !sent {
|
|
||||||
t.send(from, fromID, &p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NEIGHBORS/v4
|
|
||||||
|
|
||||||
func (t *UDPv4) verifyNeighbors(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error {
|
|
||||||
req := h.Packet.(*v4wire.Neighbors)
|
|
||||||
|
|
||||||
if v4wire.Expired(req.Expiration) {
|
|
||||||
return errExpired
|
|
||||||
}
|
|
||||||
if !t.handleReply(fromID, from.IP, h.Packet) {
|
|
||||||
return errUnsolicitedReply
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ENRREQUEST/v4
|
|
||||||
|
|
||||||
func (t *UDPv4) verifyENRRequest(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error {
|
|
||||||
req := h.Packet.(*v4wire.ENRRequest)
|
|
||||||
|
|
||||||
if v4wire.Expired(req.Expiration) {
|
|
||||||
return errExpired
|
|
||||||
}
|
|
||||||
if !t.checkBond(fromID, from.IP) {
|
|
||||||
return errUnknownNode
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPv4) handleENRRequest(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, mac []byte) {
|
|
||||||
t.send(from, fromID, &v4wire.ENRResponse{
|
|
||||||
ReplyTok: mac,
|
|
||||||
Record: *t.localNode.Node().Record(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ENRRESPONSE/v4
|
|
||||||
|
|
||||||
func (t *UDPv4) verifyENRResponse(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, fromKey v4wire.Pubkey) error {
|
|
||||||
if !t.handleReply(fromID, from.IP, h.Packet) {
|
|
||||||
return errUnsolicitedReply
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,661 +0,0 @@
|
||||||
// Copyright 2015 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
crand "crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math/rand"
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/internal/testlog"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/discover/v4wire"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
)
|
|
||||||
|
|
||||||
// shared test variables
|
|
||||||
var (
|
|
||||||
futureExp = uint64(time.Now().Add(10 * time.Hour).Unix())
|
|
||||||
testTarget = v4wire.Pubkey{0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1}
|
|
||||||
testRemote = v4wire.Endpoint{IP: net.ParseIP("1.1.1.1").To4(), UDP: 1, TCP: 2}
|
|
||||||
testLocalAnnounced = v4wire.Endpoint{IP: net.ParseIP("2.2.2.2").To4(), UDP: 3, TCP: 4}
|
|
||||||
testLocal = v4wire.Endpoint{IP: net.ParseIP("3.3.3.3").To4(), UDP: 5, TCP: 6}
|
|
||||||
)
|
|
||||||
|
|
||||||
type udpTest struct {
|
|
||||||
t *testing.T
|
|
||||||
pipe *dgramPipe
|
|
||||||
table *Table
|
|
||||||
db *enode.DB
|
|
||||||
udp *UDPv4
|
|
||||||
sent [][]byte
|
|
||||||
localkey, remotekey *ecdsa.PrivateKey
|
|
||||||
remoteaddr *net.UDPAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUDPTest(t *testing.T) *udpTest {
|
|
||||||
test := &udpTest{
|
|
||||||
t: t,
|
|
||||||
pipe: newpipe(),
|
|
||||||
localkey: newkey(),
|
|
||||||
remotekey: newkey(),
|
|
||||||
remoteaddr: &net.UDPAddr{IP: net.IP{10, 0, 1, 99}, Port: 30303},
|
|
||||||
}
|
|
||||||
|
|
||||||
test.db, _ = enode.OpenDB("")
|
|
||||||
ln := enode.NewLocalNode(test.db, test.localkey)
|
|
||||||
test.udp, _ = ListenV4(test.pipe, ln, Config{
|
|
||||||
PrivateKey: test.localkey,
|
|
||||||
Log: testlog.Logger(t, log.LvlTrace),
|
|
||||||
})
|
|
||||||
test.table = test.udp.tab
|
|
||||||
// Wait for initial refresh so the table doesn't send unexpected findnode.
|
|
||||||
<-test.table.initDone
|
|
||||||
return test
|
|
||||||
}
|
|
||||||
|
|
||||||
func (test *udpTest) close() {
|
|
||||||
test.udp.Close()
|
|
||||||
test.db.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// handles a packet as if it had been sent to the transport.
|
|
||||||
func (test *udpTest) packetIn(wantError error, data v4wire.Packet) {
|
|
||||||
test.t.Helper()
|
|
||||||
|
|
||||||
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, data v4wire.Packet) {
|
|
||||||
test.t.Helper()
|
|
||||||
|
|
||||||
enc, _, err := v4wire.Encode(key, data)
|
|
||||||
if err != nil {
|
|
||||||
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 {
|
|
||||||
test.t.Errorf("error mismatch: got %q, want %q", err, wantError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// waits for a packet to be sent by the transport.
|
|
||||||
// validate should have type func(X, *net.UDPAddr, []byte), where X is a packet type.
|
|
||||||
func (test *udpTest) waitPacketOut(validate interface{}) (closed bool) {
|
|
||||||
test.t.Helper()
|
|
||||||
|
|
||||||
dgram, err := test.pipe.receive()
|
|
||||||
if err == errClosed {
|
|
||||||
return true
|
|
||||||
} else if err != nil {
|
|
||||||
test.t.Error("packet receive error:", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
p, _, hash, err := v4wire.Decode(dgram.data)
|
|
||||||
if err != nil {
|
|
||||||
test.t.Errorf("sent packet decode error: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
fn := reflect.ValueOf(validate)
|
|
||||||
exptype := fn.Type().In(0)
|
|
||||||
if !reflect.TypeOf(p).AssignableTo(exptype) {
|
|
||||||
test.t.Errorf("sent packet type mismatch, got: %v, want: %v", reflect.TypeOf(p), exptype)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
fn.Call([]reflect.Value{reflect.ValueOf(p), reflect.ValueOf(&dgram.to), reflect.ValueOf(hash)})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUDPv4_packetErrors(t *testing.T) {
|
|
||||||
test := newUDPTest(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
test.packetIn(errExpired, &v4wire.Ping{From: testRemote, To: testLocalAnnounced, Version: 4})
|
|
||||||
test.packetIn(errUnsolicitedReply, &v4wire.Pong{ReplyTok: []byte{}, Expiration: futureExp})
|
|
||||||
test.packetIn(errUnknownNode, &v4wire.Findnode{Expiration: futureExp})
|
|
||||||
test.packetIn(errUnsolicitedReply, &v4wire.Neighbors{Expiration: futureExp})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUDPv4_pingTimeout(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPTest(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
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 {
|
|
||||||
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 TestUDPv4_responseTimeouts(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPTest(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
randomDuration := func(max time.Duration) time.Duration {
|
|
||||||
return time.Duration(rand.Int63n(int64(max)))
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
nReqs = 200
|
|
||||||
nTimeouts = 0 // number of requests with ptype > 128
|
|
||||||
nilErr = make(chan error, nReqs) // for requests that get a reply
|
|
||||||
timeoutErr = make(chan error, nReqs) // for requests that time out
|
|
||||||
)
|
|
||||||
for i := 0; i < nReqs; i++ {
|
|
||||||
// Create a matcher for a random request in udp.loop. Requests
|
|
||||||
// with ptype <= 128 will not get a reply and should time out.
|
|
||||||
// For all other requests, a reply is scheduled to arrive
|
|
||||||
// within the timeout window.
|
|
||||||
p := &replyMatcher{
|
|
||||||
ptype: byte(rand.Intn(255)),
|
|
||||||
callback: func(v4wire.Packet) (bool, bool) { return true, true },
|
|
||||||
}
|
|
||||||
binary.BigEndian.PutUint64(p.from[:], uint64(i))
|
|
||||||
if p.ptype <= 128 {
|
|
||||||
p.errc = timeoutErr
|
|
||||||
test.udp.addReplyMatcher <- p
|
|
||||||
nTimeouts++
|
|
||||||
} else {
|
|
||||||
p.errc = nilErr
|
|
||||||
test.udp.addReplyMatcher <- p
|
|
||||||
time.AfterFunc(randomDuration(60*time.Millisecond), func() {
|
|
||||||
if !test.udp.handleReply(p.from, p.ip, testPacket(p.ptype)) {
|
|
||||||
t.Logf("not matched: %v", p)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
time.Sleep(randomDuration(30 * time.Millisecond))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that all timeouts were delivered and that the rest got nil errors.
|
|
||||||
// The replies must be delivered.
|
|
||||||
var (
|
|
||||||
recvDeadline = time.After(20 * time.Second)
|
|
||||||
nTimeoutsRecv, nNil = 0, 0
|
|
||||||
)
|
|
||||||
for i := 0; i < nReqs; i++ {
|
|
||||||
select {
|
|
||||||
case err := <-timeoutErr:
|
|
||||||
if err != errTimeout {
|
|
||||||
t.Fatalf("got non-timeout error on timeoutErr %d: %v", i, err)
|
|
||||||
}
|
|
||||||
nTimeoutsRecv++
|
|
||||||
case err := <-nilErr:
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("got non-nil error on nilErr %d: %v", i, err)
|
|
||||||
}
|
|
||||||
nNil++
|
|
||||||
case <-recvDeadline:
|
|
||||||
t.Fatalf("exceeded recv deadline")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if nTimeoutsRecv != nTimeouts {
|
|
||||||
t.Errorf("wrong number of timeout errors received: got %d, want %d", nTimeoutsRecv, nTimeouts)
|
|
||||||
}
|
|
||||||
if nNil != nReqs-nTimeouts {
|
|
||||||
t.Errorf("wrong number of successful replies: got %d, want %d", nNil, nReqs-nTimeouts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUDPv4_findnodeTimeout(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPTest(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
toaddr := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222}
|
|
||||||
toid := enode.ID{1, 2, 3, 4}
|
|
||||||
target := v4wire.Pubkey{4, 5, 6, 7}
|
|
||||||
result, err := test.udp.findnode(toid, toaddr, target)
|
|
||||||
if err != errTimeout {
|
|
||||||
t.Error("expected timeout error, got", err)
|
|
||||||
}
|
|
||||||
if len(result) > 0 {
|
|
||||||
t.Error("expected empty result, got", result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUDPv4_findnode(t *testing.T) {
|
|
||||||
test := newUDPTest(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
// put a few nodes into the table. their exact
|
|
||||||
// distribution shouldn't matter much, although we need to
|
|
||||||
// take care not to overflow any bucket.
|
|
||||||
nodes := &nodesByDistance{target: testTarget.ID()}
|
|
||||||
live := make(map[enode.ID]bool)
|
|
||||||
numCandidates := 2 * bucketSize
|
|
||||||
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))
|
|
||||||
// Ensure half of table content isn't verified live yet.
|
|
||||||
if i > numCandidates/2 {
|
|
||||||
n.livenessChecks = 1
|
|
||||||
live[n.ID()] = true
|
|
||||||
}
|
|
||||||
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())
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
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), bucketSize)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for i, n := range p.Nodes {
|
|
||||||
if n.ID.ID() != want[i].ID() {
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Receive replies.
|
|
||||||
want := expected.entries
|
|
||||||
if len(want) > v4wire.MaxNeighbors {
|
|
||||||
waitNeighbors(want[:v4wire.MaxNeighbors])
|
|
||||||
want = want[v4wire.MaxNeighbors:]
|
|
||||||
}
|
|
||||||
waitNeighbors(want)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUDPv4_findnodeMultiReply(t *testing.T) {
|
|
||||||
test := newUDPTest(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
rid := enode.PubkeyToIDV4(&test.remotekey.PublicKey)
|
|
||||||
test.table.db.UpdateLastPingReceived(rid, test.remoteaddr.IP, time.Now())
|
|
||||||
|
|
||||||
// queue a pending findnode request
|
|
||||||
resultc, errc := make(chan []*node, 1), make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
rid := encodePubkey(&test.remotekey.PublicKey).id()
|
|
||||||
ns, err := test.udp.findnode(rid, test.remoteaddr, testTarget)
|
|
||||||
if err != nil && len(ns) == 0 {
|
|
||||||
errc <- err
|
|
||||||
} else {
|
|
||||||
resultc <- ns
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// wait for the findnode to be sent.
|
|
||||||
// after it is sent, the transport is waiting for a reply
|
|
||||||
test.waitPacketOut(func(p *v4wire.Findnode, to *net.UDPAddr, hash []byte) {
|
|
||||||
if p.Target != testTarget {
|
|
||||||
t.Errorf("wrong target: got %v, want %v", p.Target, testTarget)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 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")),
|
|
||||||
}
|
|
||||||
rpclist := make([]v4wire.Node, len(list))
|
|
||||||
for i := range list {
|
|
||||||
rpclist[i] = nodeToRPC(list[i])
|
|
||||||
}
|
|
||||||
test.packetIn(nil, &v4wire.Neighbors{Expiration: futureExp, Nodes: rpclist[:2]})
|
|
||||||
test.packetIn(nil, &v4wire.Neighbors{Expiration: futureExp, Nodes: rpclist[2:]})
|
|
||||||
|
|
||||||
// check that the sent neighbors are all returned by findnode
|
|
||||||
select {
|
|
||||||
case result := <-resultc:
|
|
||||||
want := append(list[:2], list[3:]...)
|
|
||||||
if !reflect.DeepEqual(result, want) {
|
|
||||||
t.Errorf("neighbors mismatch:\n got: %v\n want: %v", result, want)
|
|
||||||
}
|
|
||||||
case err := <-errc:
|
|
||||||
t.Errorf("findnode error: %v", err)
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
t.Error("findnode did not return within 5 seconds")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that reply matching of pong verifies the ping hash.
|
|
||||||
func TestUDPv4_pingMatch(t *testing.T) {
|
|
||||||
test := newUDPTest(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
randToken := make([]byte, 32)
|
|
||||||
crand.Read(randToken)
|
|
||||||
|
|
||||||
test.packetIn(nil, &v4wire.Ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
|
|
||||||
test.waitPacketOut(func(*v4wire.Pong, *net.UDPAddr, []byte) {})
|
|
||||||
test.waitPacketOut(func(*v4wire.Ping, *net.UDPAddr, []byte) {})
|
|
||||||
test.packetIn(errUnsolicitedReply, &v4wire.Pong{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, &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.packetInFrom(errUnsolicitedReply, test.remotekey, wrongAddr, &v4wire.Pong{
|
|
||||||
ReplyTok: hash,
|
|
||||||
To: testLocalAnnounced,
|
|
||||||
Expiration: futureExp,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUDPv4_successfulPing(t *testing.T) {
|
|
||||||
test := newUDPTest(t)
|
|
||||||
added := make(chan *node, 1)
|
|
||||||
test.table.nodeAddedHook = func(b *bucket, n *node) { added <- n }
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
// The remote side sends a ping packet to initiate the exchange.
|
|
||||||
go test.packetIn(nil, &v4wire.Ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp})
|
|
||||||
|
|
||||||
// The ping is replied to.
|
|
||||||
test.waitPacketOut(func(p *v4wire.Pong, to *net.UDPAddr, hash []byte) {
|
|
||||||
pinghash := test.sent[0][:32]
|
|
||||||
if !bytes.Equal(p.ReplyTok, pinghash) {
|
|
||||||
t.Errorf("got pong.ReplyTok %x, want %x", p.ReplyTok, pinghash)
|
|
||||||
}
|
|
||||||
wantTo := v4wire.Endpoint{
|
|
||||||
// The mirrored UDP address is the UDP packet sender
|
|
||||||
IP: test.remoteaddr.IP, UDP: uint16(test.remoteaddr.Port),
|
|
||||||
// The mirrored TCP port is the one from the ping packet
|
|
||||||
TCP: testRemote.TCP,
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(p.To, wantTo) {
|
|
||||||
t.Errorf("got pong.To %v, want %v", p.To, wantTo)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Remote is unknown, the table pings back.
|
|
||||||
test.waitPacketOut(func(p *v4wire.Ping, 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())
|
|
||||||
}
|
|
||||||
wantTo := v4wire.Endpoint{
|
|
||||||
// The mirrored UDP address is the UDP packet sender.
|
|
||||||
IP: test.remoteaddr.IP,
|
|
||||||
UDP: uint16(test.remoteaddr.Port),
|
|
||||||
TCP: 0,
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(p.To, wantTo) {
|
|
||||||
t.Errorf("got ping.To %v, want %v", p.To, wantTo)
|
|
||||||
}
|
|
||||||
test.packetIn(nil, &v4wire.Pong{ReplyTok: hash, Expiration: futureExp})
|
|
||||||
})
|
|
||||||
|
|
||||||
// The node should be added to the table shortly after getting the
|
|
||||||
// pong packet.
|
|
||||||
select {
|
|
||||||
case n := <-added:
|
|
||||||
rid := encodePubkey(&test.remotekey.PublicKey).id()
|
|
||||||
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.UDP() != 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) {
|
|
||||||
t.Errorf("node has wrong TCP port: got %v, want: %v", n.TCP(), testRemote.TCP)
|
|
||||||
}
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
t.Errorf("node was not added within 2 seconds")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, &v4wire.ENRRequest{Expiration: futureExp})
|
|
||||||
|
|
||||||
// Perform endpoint proof and check for sequence number in packet tail.
|
|
||||||
test.packetIn(nil, &v4wire.Ping{Expiration: futureExp})
|
|
||||||
test.waitPacketOut(func(p *v4wire.Pong, addr *net.UDPAddr, hash []byte) {
|
|
||||||
if p.ENRSeq != wantNode.Seq() {
|
|
||||||
t.Errorf("wrong sequence number in pong: %d, want %d", p.ENRSeq, wantNode.Seq())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
test.waitPacketOut(func(p *v4wire.Ping, addr *net.UDPAddr, hash []byte) {
|
|
||||||
if p.ENRSeq != wantNode.Seq() {
|
|
||||||
t.Errorf("wrong sequence number in ping: %d, want %d", p.ENRSeq, wantNode.Seq())
|
|
||||||
}
|
|
||||||
test.packetIn(nil, &v4wire.Pong{Expiration: futureExp, ReplyTok: hash})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Request should work now.
|
|
||||||
test.packetIn(nil, &v4wire.ENRRequest{Expiration: futureExp})
|
|
||||||
test.waitPacketOut(func(p *v4wire.ENRResponse, 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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test verifies that a small network of nodes can boot up into a healthy state.
|
|
||||||
func TestUDPv4_smallNetConvergence(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Start the network.
|
|
||||||
nodes := make([]*UDPv4, 4)
|
|
||||||
for i := range nodes {
|
|
||||||
var cfg Config
|
|
||||||
if i > 0 {
|
|
||||||
bn := nodes[0].Self()
|
|
||||||
cfg.Bootnodes = []*enode.Node{bn}
|
|
||||||
}
|
|
||||||
nodes[i] = startLocalhostV4(t, cfg)
|
|
||||||
defer nodes[i].Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run through the iterator on all nodes until
|
|
||||||
// they have all found each other.
|
|
||||||
status := make(chan error, len(nodes))
|
|
||||||
for i := range nodes {
|
|
||||||
node := nodes[i]
|
|
||||||
go func() {
|
|
||||||
found := make(map[enode.ID]bool, len(nodes))
|
|
||||||
it := node.RandomNodes()
|
|
||||||
for it.Next() {
|
|
||||||
found[it.Node().ID()] = true
|
|
||||||
if len(found) == len(nodes) {
|
|
||||||
status <- nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
status <- fmt.Errorf("node %s didn't find all nodes", node.Self().ID().TerminalString())
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all status reports.
|
|
||||||
timeout := time.NewTimer(30 * time.Second)
|
|
||||||
defer timeout.Stop()
|
|
||||||
for received := 0; received < len(nodes); {
|
|
||||||
select {
|
|
||||||
case <-timeout.C:
|
|
||||||
for _, node := range nodes {
|
|
||||||
node.Close()
|
|
||||||
}
|
|
||||||
case err := <-status:
|
|
||||||
received++
|
|
||||||
if err != nil {
|
|
||||||
t.Error("ERROR:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startLocalhostV4(t *testing.T, cfg Config) *UDPv4 {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
cfg.PrivateKey = newkey()
|
|
||||||
db, _ := enode.OpenDB("")
|
|
||||||
ln := enode.NewLocalNode(db, cfg.PrivateKey)
|
|
||||||
|
|
||||||
// Prefix logs with node ID.
|
|
||||||
lprefix := fmt.Sprintf("(%s)", ln.ID().TerminalString())
|
|
||||||
cfg.Log = testlog.Logger(t, log.LevelTrace).With("node-id", lprefix)
|
|
||||||
|
|
||||||
// Listen.
|
|
||||||
socket, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP{127, 0, 0, 1}})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
realaddr := socket.LocalAddr().(*net.UDPAddr)
|
|
||||||
ln.SetStaticIP(realaddr.IP)
|
|
||||||
ln.SetFallbackUDP(realaddr.Port)
|
|
||||||
udp, err := ListenV4(socket, ln, cfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return udp
|
|
||||||
}
|
|
||||||
|
|
||||||
// dgramPipe is a fake UDP socket. It queues all sent datagrams.
|
|
||||||
type dgramPipe struct {
|
|
||||||
mu *sync.Mutex
|
|
||||||
cond *sync.Cond
|
|
||||||
closing chan struct{}
|
|
||||||
closed bool
|
|
||||||
queue []dgram
|
|
||||||
}
|
|
||||||
|
|
||||||
type dgram struct {
|
|
||||||
to net.UDPAddr
|
|
||||||
data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func newpipe() *dgramPipe {
|
|
||||||
mu := new(sync.Mutex)
|
|
||||||
return &dgramPipe{
|
|
||||||
closing: make(chan struct{}),
|
|
||||||
cond: &sync.Cond{L: mu},
|
|
||||||
mu: mu,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteToUDP queues a datagram.
|
|
||||||
func (c *dgramPipe) WriteToUDP(b []byte, to *net.UDPAddr) (n int, err error) {
|
|
||||||
msg := make([]byte, len(b))
|
|
||||||
copy(msg, b)
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
if c.closed {
|
|
||||||
return 0, errors.New("closed")
|
|
||||||
}
|
|
||||||
c.queue = append(c.queue, dgram{*to, b})
|
|
||||||
c.cond.Signal()
|
|
||||||
return len(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadFromUDP just hangs until the pipe is closed.
|
|
||||||
func (c *dgramPipe) ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) {
|
|
||||||
<-c.closing
|
|
||||||
return 0, nil, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *dgramPipe) Close() error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
if !c.closed {
|
|
||||||
close(c.closing)
|
|
||||||
c.closed = true
|
|
||||||
}
|
|
||||||
c.cond.Broadcast()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *dgramPipe) LocalAddr() net.Addr {
|
|
||||||
return &net.UDPAddr{IP: testLocal.IP, Port: int(testLocal.UDP)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *dgramPipe) receive() (dgram, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
var timedOut bool
|
|
||||||
timer := time.AfterFunc(3*time.Second, func() {
|
|
||||||
c.mu.Lock()
|
|
||||||
timedOut = true
|
|
||||||
c.mu.Unlock()
|
|
||||||
c.cond.Broadcast()
|
|
||||||
})
|
|
||||||
defer timer.Stop()
|
|
||||||
|
|
||||||
for len(c.queue) == 0 && !c.closed && !timedOut {
|
|
||||||
c.cond.Wait()
|
|
||||||
}
|
|
||||||
if c.closed {
|
|
||||||
return dgram{}, errClosed
|
|
||||||
}
|
|
||||||
if timedOut {
|
|
||||||
return dgram{}, errTimeout
|
|
||||||
}
|
|
||||||
p := c.queue[0]
|
|
||||||
copy(c.queue, c.queue[1:])
|
|
||||||
c.queue = c.queue[:len(c.queue)-1]
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,296 +0,0 @@
|
||||||
// Copyright 2020 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Package v4wire implements the Discovery v4 Wire Protocol.
|
|
||||||
package v4wire
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RPC packet types
|
|
||||||
const (
|
|
||||||
PingPacket = iota + 1 // zero is 'reserved'
|
|
||||||
PongPacket
|
|
||||||
FindnodePacket
|
|
||||||
NeighborsPacket
|
|
||||||
ENRRequestPacket
|
|
||||||
ENRResponsePacket
|
|
||||||
)
|
|
||||||
|
|
||||||
// RPC request structures
|
|
||||||
type (
|
|
||||||
Ping struct {
|
|
||||||
Version uint
|
|
||||||
From, To Endpoint
|
|
||||||
Expiration uint64
|
|
||||||
ENRSeq uint64 `rlp:"optional"` // Sequence number of local record, added by EIP-868.
|
|
||||||
|
|
||||||
// Ignore additional fields (for forward compatibility).
|
|
||||||
Rest []rlp.RawValue `rlp:"tail"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pong is the reply to ping.
|
|
||||||
Pong struct {
|
|
||||||
// This field should mirror the UDP envelope address
|
|
||||||
// of the ping packet, which provides a way to discover the
|
|
||||||
// external address (after NAT).
|
|
||||||
To Endpoint
|
|
||||||
ReplyTok []byte // This contains the hash of the ping packet.
|
|
||||||
Expiration uint64 // Absolute timestamp at which the packet becomes invalid.
|
|
||||||
ENRSeq uint64 `rlp:"optional"` // Sequence number of local record, added by EIP-868.
|
|
||||||
|
|
||||||
// Ignore additional fields (for forward compatibility).
|
|
||||||
Rest []rlp.RawValue `rlp:"tail"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Findnode is a query for nodes close to the given target.
|
|
||||||
Findnode struct {
|
|
||||||
Target Pubkey
|
|
||||||
Expiration uint64
|
|
||||||
// Ignore additional fields (for forward compatibility).
|
|
||||||
Rest []rlp.RawValue `rlp:"tail"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Neighbors is the reply to findnode.
|
|
||||||
Neighbors struct {
|
|
||||||
Nodes []Node
|
|
||||||
Expiration uint64
|
|
||||||
// Ignore additional fields (for forward compatibility).
|
|
||||||
Rest []rlp.RawValue `rlp:"tail"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ENRRequest queries for the remote node's record.
|
|
||||||
ENRRequest struct {
|
|
||||||
Expiration uint64
|
|
||||||
// Ignore additional fields (for forward compatibility).
|
|
||||||
Rest []rlp.RawValue `rlp:"tail"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ENRResponse is the reply to ENRRequest.
|
|
||||||
ENRResponse struct {
|
|
||||||
ReplyTok []byte // Hash of the ENRRequest packet.
|
|
||||||
Record enr.Record
|
|
||||||
// Ignore additional fields (for forward compatibility).
|
|
||||||
Rest []rlp.RawValue `rlp:"tail"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// MaxNeighbors is the maximum number of neighbor nodes in a Neighbors packet.
|
|
||||||
const MaxNeighbors = 12
|
|
||||||
|
|
||||||
// This code computes the MaxNeighbors constant value.
|
|
||||||
|
|
||||||
// func init() {
|
|
||||||
// var maxNeighbors int
|
|
||||||
// p := Neighbors{Expiration: ^uint64(0)}
|
|
||||||
// maxSizeNode := Node{IP: make(net.IP, 16), UDP: ^uint16(0), TCP: ^uint16(0)}
|
|
||||||
// for n := 0; ; n++ {
|
|
||||||
// p.Nodes = append(p.Nodes, maxSizeNode)
|
|
||||||
// size, _, err := rlp.EncodeToReader(p)
|
|
||||||
// if err != nil {
|
|
||||||
// // If this ever happens, it will be caught by the unit tests.
|
|
||||||
// panic("cannot encode: " + err.Error())
|
|
||||||
// }
|
|
||||||
// if headSize+size+1 >= 1280 {
|
|
||||||
// maxNeighbors = n
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// fmt.Println("maxNeighbors", maxNeighbors)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Pubkey represents an encoded 64-byte secp256k1 public key.
|
|
||||||
type Pubkey [64]byte
|
|
||||||
|
|
||||||
// ID returns the node ID corresponding to the public key.
|
|
||||||
func (e Pubkey) ID() enode.ID {
|
|
||||||
return enode.ID(crypto.Keccak256Hash(e[:]))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node represents information about a node.
|
|
||||||
type Node struct {
|
|
||||||
IP net.IP // len 4 for IPv4 or 16 for IPv6
|
|
||||||
UDP uint16 // for discovery protocol
|
|
||||||
TCP uint16 // for RLPx protocol
|
|
||||||
ID Pubkey
|
|
||||||
}
|
|
||||||
|
|
||||||
// Endpoint represents a network endpoint.
|
|
||||||
type Endpoint struct {
|
|
||||||
IP net.IP // len 4 for IPv4 or 16 for IPv6
|
|
||||||
UDP uint16 // for discovery protocol
|
|
||||||
TCP uint16 // for RLPx protocol
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEndpoint creates an endpoint.
|
|
||||||
func NewEndpoint(addr *net.UDPAddr, tcpPort uint16) Endpoint {
|
|
||||||
ip := net.IP{}
|
|
||||||
if ip4 := addr.IP.To4(); ip4 != nil {
|
|
||||||
ip = ip4
|
|
||||||
} else if ip6 := addr.IP.To16(); ip6 != nil {
|
|
||||||
ip = ip6
|
|
||||||
}
|
|
||||||
return Endpoint{IP: ip, UDP: uint16(addr.Port), TCP: tcpPort}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Packet interface {
|
|
||||||
// Name is the name of the package, for logging purposes.
|
|
||||||
Name() string
|
|
||||||
// Kind is the packet type, for logging purposes.
|
|
||||||
Kind() byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (req *Ping) Name() string { return "PING/v4" }
|
|
||||||
func (req *Ping) Kind() byte { return PingPacket }
|
|
||||||
|
|
||||||
func (req *Pong) Name() string { return "PONG/v4" }
|
|
||||||
func (req *Pong) Kind() byte { return PongPacket }
|
|
||||||
|
|
||||||
func (req *Findnode) Name() string { return "FINDNODE/v4" }
|
|
||||||
func (req *Findnode) Kind() byte { return FindnodePacket }
|
|
||||||
|
|
||||||
func (req *Neighbors) Name() string { return "NEIGHBORS/v4" }
|
|
||||||
func (req *Neighbors) Kind() byte { return NeighborsPacket }
|
|
||||||
|
|
||||||
func (req *ENRRequest) Name() string { return "ENRREQUEST/v4" }
|
|
||||||
func (req *ENRRequest) Kind() byte { return ENRRequestPacket }
|
|
||||||
|
|
||||||
func (req *ENRResponse) Name() string { return "ENRRESPONSE/v4" }
|
|
||||||
func (req *ENRResponse) Kind() byte { return ENRResponsePacket }
|
|
||||||
|
|
||||||
// Expired checks whether the given UNIX time stamp is in the past.
|
|
||||||
func Expired(ts uint64) bool {
|
|
||||||
return time.Unix(int64(ts), 0).Before(time.Now())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encoder/decoder.
|
|
||||||
|
|
||||||
const (
|
|
||||||
macSize = 32
|
|
||||||
sigSize = crypto.SignatureLength
|
|
||||||
headSize = macSize + sigSize // space of packet frame data
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrPacketTooSmall = errors.New("too small")
|
|
||||||
ErrBadHash = errors.New("bad hash")
|
|
||||||
ErrBadPoint = errors.New("invalid curve point")
|
|
||||||
)
|
|
||||||
|
|
||||||
var headSpace = make([]byte, headSize)
|
|
||||||
|
|
||||||
// Decode reads a discovery v4 packet.
|
|
||||||
func Decode(input []byte) (Packet, Pubkey, []byte, error) {
|
|
||||||
if len(input) < headSize+1 {
|
|
||||||
return nil, Pubkey{}, nil, ErrPacketTooSmall
|
|
||||||
}
|
|
||||||
hash, sig, sigdata := input[:macSize], input[macSize:headSize], input[headSize:]
|
|
||||||
shouldhash := crypto.Keccak256(input[macSize:])
|
|
||||||
if !bytes.Equal(hash, shouldhash) {
|
|
||||||
return nil, Pubkey{}, nil, ErrBadHash
|
|
||||||
}
|
|
||||||
fromKey, err := recoverNodeKey(crypto.Keccak256(input[headSize:]), sig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fromKey, hash, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var req Packet
|
|
||||||
switch ptype := sigdata[0]; ptype {
|
|
||||||
case PingPacket:
|
|
||||||
req = new(Ping)
|
|
||||||
case PongPacket:
|
|
||||||
req = new(Pong)
|
|
||||||
case FindnodePacket:
|
|
||||||
req = new(Findnode)
|
|
||||||
case NeighborsPacket:
|
|
||||||
req = new(Neighbors)
|
|
||||||
case ENRRequestPacket:
|
|
||||||
req = new(ENRRequest)
|
|
||||||
case ENRResponsePacket:
|
|
||||||
req = new(ENRResponse)
|
|
||||||
default:
|
|
||||||
return nil, fromKey, hash, fmt.Errorf("unknown type: %d", ptype)
|
|
||||||
}
|
|
||||||
// Here we use NewStream to allow for additional data after the first
|
|
||||||
// RLP object (forward-compatibility).
|
|
||||||
s := rlp.NewStream(bytes.NewReader(sigdata[1:]), 0)
|
|
||||||
err = s.Decode(req)
|
|
||||||
return req, fromKey, hash, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode encodes a discovery packet.
|
|
||||||
func Encode(priv *ecdsa.PrivateKey, req Packet) (packet, hash []byte, err error) {
|
|
||||||
b := new(bytes.Buffer)
|
|
||||||
b.Write(headSpace)
|
|
||||||
b.WriteByte(req.Kind())
|
|
||||||
if err := rlp.Encode(b, req); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
packet = b.Bytes()
|
|
||||||
sig, err := crypto.Sign(crypto.Keccak256(packet[headSize:]), priv)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
copy(packet[macSize:], sig)
|
|
||||||
// Add the hash to the front. Note: this doesn't protect the packet in any way.
|
|
||||||
hash = crypto.Keccak256(packet[macSize:])
|
|
||||||
copy(packet, hash)
|
|
||||||
return packet, hash, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// recoverNodeKey computes the public key used to sign the given hash from the signature.
|
|
||||||
func recoverNodeKey(hash, sig []byte) (key Pubkey, err error) {
|
|
||||||
pubkey, err := crypto.Ecrecover(hash, sig)
|
|
||||||
if err != nil {
|
|
||||||
return key, err
|
|
||||||
}
|
|
||||||
copy(key[:], pubkey[1:])
|
|
||||||
return key, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodePubkey encodes a secp256k1 public key.
|
|
||||||
func EncodePubkey(key *ecdsa.PublicKey) Pubkey {
|
|
||||||
var e Pubkey
|
|
||||||
math.ReadBits(key.X, e[:len(e)/2])
|
|
||||||
math.ReadBits(key.Y, e[len(e)/2:])
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodePubkey reads an encoded secp256k1 public key.
|
|
||||||
func DecodePubkey(curve elliptic.Curve, e Pubkey) (*ecdsa.PublicKey, error) {
|
|
||||||
p := &ecdsa.PublicKey{Curve: curve, X: new(big.Int), Y: new(big.Int)}
|
|
||||||
half := len(e) / 2
|
|
||||||
p.X.SetBytes(e[:half])
|
|
||||||
p.Y.SetBytes(e[half:])
|
|
||||||
if !p.Curve.IsOnCurve(p.X, p.Y) {
|
|
||||||
return nil, ErrBadPoint
|
|
||||||
}
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
// Copyright 2020 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package v4wire
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EIP-8 test vectors.
|
|
||||||
var testPackets = []struct {
|
|
||||||
input string
|
|
||||||
wantPacket interface{}
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
input: "71dbda3a79554728d4f94411e42ee1f8b0d561c10e1e5f5893367948c6a7d70bb87b235fa28a77070271b6c164a2dce8c7e13a5739b53b5e96f2e5acb0e458a02902f5965d55ecbeb2ebb6cabb8b2b232896a36b737666c55265ad0a68412f250001ea04cb847f000001820cfa8215a8d790000000000000000000000000000000018208ae820d058443b9a355",
|
|
||||||
wantPacket: &Ping{
|
|
||||||
Version: 4,
|
|
||||||
From: Endpoint{net.ParseIP("127.0.0.1").To4(), 3322, 5544},
|
|
||||||
To: Endpoint{net.ParseIP("::1"), 2222, 3333},
|
|
||||||
Expiration: 1136239445,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "e9614ccfd9fc3e74360018522d30e1419a143407ffcce748de3e22116b7e8dc92ff74788c0b6663aaa3d67d641936511c8f8d6ad8698b820a7cf9e1be7155e9a241f556658c55428ec0563514365799a4be2be5a685a80971ddcfa80cb422cdd0101ec04cb847f000001820cfa8215a8d790000000000000000000000000000000018208ae820d058443b9a3550102",
|
|
||||||
wantPacket: &Ping{
|
|
||||||
Version: 4,
|
|
||||||
From: Endpoint{net.ParseIP("127.0.0.1").To4(), 3322, 5544},
|
|
||||||
To: Endpoint{net.ParseIP("::1"), 2222, 3333},
|
|
||||||
Expiration: 1136239445,
|
|
||||||
ENRSeq: 1,
|
|
||||||
Rest: []rlp.RawValue{{0x02}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "c7c44041b9f7c7e41934417ebac9a8e1a4c6298f74553f2fcfdcae6ed6fe53163eb3d2b52e39fe91831b8a927bf4fc222c3902202027e5e9eb812195f95d20061ef5cd31d502e47ecb61183f74a504fe04c51e73df81f25c4d506b26db4517490103f84eb840ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31387574077f301b421bc84df7266c44e9e6d569fc56be00812904767bf5ccd1fc7f8443b9a35582999983999999280dc62cc8255c73471e0a61da0c89acdc0e035e260add7fc0c04ad9ebf3919644c91cb247affc82b69bd2ca235c71eab8e49737c937a2c396",
|
|
||||||
wantPacket: &Findnode{
|
|
||||||
Target: hexPubkey("ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31387574077f301b421bc84df7266c44e9e6d569fc56be00812904767bf5ccd1fc7f"),
|
|
||||||
Expiration: 1136239445,
|
|
||||||
Rest: []rlp.RawValue{{0x82, 0x99, 0x99}, {0x83, 0x99, 0x99, 0x99}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "c679fc8fe0b8b12f06577f2e802d34f6fa257e6137a995f6f4cbfc9ee50ed3710faf6e66f932c4c8d81d64343f429651328758b47d3dbc02c4042f0fff6946a50f4a49037a72bb550f3a7872363a83e1b9ee6469856c24eb4ef80b7535bcf99c0004f9015bf90150f84d846321163782115c82115db8403155e1427f85f10a5c9a7755877748041af1bcd8d474ec065eb33df57a97babf54bfd2103575fa829115d224c523596b401065a97f74010610fce76382c0bf32f84984010203040101b840312c55512422cf9b8a4097e9a6ad79402e87a15ae909a4bfefa22398f03d20951933beea1e4dfa6f968212385e829f04c2d314fc2d4e255e0d3bc08792b069dbf8599020010db83c4d001500000000abcdef12820d05820d05b84038643200b172dcfef857492156971f0e6aa2c538d8b74010f8e140811d53b98c765dd2d96126051913f44582e8c199ad7c6d6819e9a56483f637feaac9448aacf8599020010db885a308d313198a2e037073488203e78203e8b8408dcab8618c3253b558d459da53bd8fa68935a719aff8b811197101a4b2b47dd2d47295286fc00cc081bb542d760717d1bdd6bec2c37cd72eca367d6dd3b9df738443b9a355010203b525a138aa34383fec3d2719a0",
|
|
||||||
wantPacket: &Neighbors{
|
|
||||||
Nodes: []Node{
|
|
||||||
{
|
|
||||||
ID: hexPubkey("3155e1427f85f10a5c9a7755877748041af1bcd8d474ec065eb33df57a97babf54bfd2103575fa829115d224c523596b401065a97f74010610fce76382c0bf32"),
|
|
||||||
IP: net.ParseIP("99.33.22.55").To4(),
|
|
||||||
UDP: 4444,
|
|
||||||
TCP: 4445,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: hexPubkey("312c55512422cf9b8a4097e9a6ad79402e87a15ae909a4bfefa22398f03d20951933beea1e4dfa6f968212385e829f04c2d314fc2d4e255e0d3bc08792b069db"),
|
|
||||||
IP: net.ParseIP("1.2.3.4").To4(),
|
|
||||||
UDP: 1,
|
|
||||||
TCP: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: hexPubkey("38643200b172dcfef857492156971f0e6aa2c538d8b74010f8e140811d53b98c765dd2d96126051913f44582e8c199ad7c6d6819e9a56483f637feaac9448aac"),
|
|
||||||
IP: net.ParseIP("2001:db8:3c4d:15::abcd:ef12"),
|
|
||||||
UDP: 3333,
|
|
||||||
TCP: 3333,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: hexPubkey("8dcab8618c3253b558d459da53bd8fa68935a719aff8b811197101a4b2b47dd2d47295286fc00cc081bb542d760717d1bdd6bec2c37cd72eca367d6dd3b9df73"),
|
|
||||||
IP: net.ParseIP("2001:db8:85a3:8d3:1319:8a2e:370:7348"),
|
|
||||||
UDP: 999,
|
|
||||||
TCP: 1000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Expiration: 1136239445,
|
|
||||||
Rest: []rlp.RawValue{{0x01}, {0x02}, {0x03}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that the decoder accepts packets according to EIP-8.
|
|
||||||
func TestForwardCompatibility(t *testing.T) {
|
|
||||||
testkey, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
|
||||||
wantNodeKey := EncodePubkey(&testkey.PublicKey)
|
|
||||||
|
|
||||||
for _, test := range testPackets {
|
|
||||||
input, err := hex.DecodeString(test.input)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("invalid hex: %s", test.input)
|
|
||||||
}
|
|
||||||
packet, nodekey, _, err := Decode(input)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("did not accept packet %s\n%v", test.input, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(packet, test.wantPacket) {
|
|
||||||
t.Errorf("got %s\nwant %s", spew.Sdump(packet), spew.Sdump(test.wantPacket))
|
|
||||||
}
|
|
||||||
if nodekey != wantNodeKey {
|
|
||||||
t.Errorf("got id %v\nwant id %v", nodekey, wantNodeKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func hexPubkey(h string) (ret Pubkey) {
|
|
||||||
b, err := hex.DecodeString(h)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if len(b) != len(ret) {
|
|
||||||
panic("invalid length")
|
|
||||||
}
|
|
||||||
copy(ret[:], b)
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
// Copyright 2023 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/discover/v5wire"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This is a limit for the number of concurrent talk requests.
|
|
||||||
const maxActiveTalkRequests = 1024
|
|
||||||
|
|
||||||
// This is the timeout for acquiring a handler execution slot for a talk request.
|
|
||||||
// The timeout should be short enough to fit within the request timeout.
|
|
||||||
const talkHandlerLaunchTimeout = 400 * time.Millisecond
|
|
||||||
|
|
||||||
// TalkRequestHandler callback processes a talk request and returns a response.
|
|
||||||
//
|
|
||||||
// Note that talk handlers are expected to come up with a response very quickly, within at
|
|
||||||
// most 200ms or so. If the handler takes longer than that, the remote end may time out
|
|
||||||
// and wont receive the response.
|
|
||||||
type TalkRequestHandler func(enode.ID, *net.UDPAddr, []byte) []byte
|
|
||||||
|
|
||||||
type talkSystem struct {
|
|
||||||
transport *UDPv5
|
|
||||||
|
|
||||||
mutex sync.Mutex
|
|
||||||
handlers map[string]TalkRequestHandler
|
|
||||||
slots chan struct{}
|
|
||||||
lastLog time.Time
|
|
||||||
dropCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTalkSystem(transport *UDPv5) *talkSystem {
|
|
||||||
t := &talkSystem{
|
|
||||||
transport: transport,
|
|
||||||
handlers: make(map[string]TalkRequestHandler),
|
|
||||||
slots: make(chan struct{}, maxActiveTalkRequests),
|
|
||||||
}
|
|
||||||
for i := 0; i < cap(t.slots); i++ {
|
|
||||||
t.slots <- struct{}{}
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
// register adds a protocol handler.
|
|
||||||
func (t *talkSystem) register(protocol string, handler TalkRequestHandler) {
|
|
||||||
t.mutex.Lock()
|
|
||||||
t.handlers[protocol] = handler
|
|
||||||
t.mutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleRequest handles a talk request.
|
|
||||||
func (t *talkSystem) handleRequest(id enode.ID, addr *net.UDPAddr, req *v5wire.TalkRequest) {
|
|
||||||
t.mutex.Lock()
|
|
||||||
handler, ok := t.handlers[req.Protocol]
|
|
||||||
t.mutex.Unlock()
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
resp := &v5wire.TalkResponse{ReqID: req.ReqID}
|
|
||||||
t.transport.sendResponse(id, addr, resp)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for a slot to become available, then run the handler.
|
|
||||||
timeout := time.NewTimer(talkHandlerLaunchTimeout)
|
|
||||||
defer timeout.Stop()
|
|
||||||
select {
|
|
||||||
case <-t.slots:
|
|
||||||
go func() {
|
|
||||||
defer func() { t.slots <- struct{}{} }()
|
|
||||||
respMessage := handler(id, addr, req.Message)
|
|
||||||
resp := &v5wire.TalkResponse{ReqID: req.ReqID, Message: respMessage}
|
|
||||||
t.transport.sendFromAnotherThread(id, addr, resp)
|
|
||||||
}()
|
|
||||||
case <-timeout.C:
|
|
||||||
// Couldn't get it in time, drop the request.
|
|
||||||
if time.Since(t.lastLog) > 5*time.Second {
|
|
||||||
log.Warn("Dropping TALKREQ due to overload", "ndrop", t.dropCount)
|
|
||||||
t.lastLog = time.Now()
|
|
||||||
t.dropCount++
|
|
||||||
}
|
|
||||||
case <-t.transport.closeCtx.Done():
|
|
||||||
// Transport closed, drop the request.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait blocks until all active requests have finished, and prevents new request
|
|
||||||
// handlers from being launched.
|
|
||||||
func (t *talkSystem) wait() {
|
|
||||||
for i := 0; i < cap(t.slots); i++ {
|
|
||||||
<-t.slots
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,910 +0,0 @@
|
||||||
// Copyright 2020 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
crand "crypto/rand"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/discover/v5wire"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/netutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
lookupRequestLimit = 3 // max requests against a single node during lookup
|
|
||||||
findnodeResultLimit = 16 // applies in FINDNODE handler
|
|
||||||
totalNodesResponseLimit = 5 // applies in waitForNodes
|
|
||||||
|
|
||||||
respTimeoutV5 = 700 * time.Millisecond
|
|
||||||
)
|
|
||||||
|
|
||||||
// codecV5 is implemented by v5wire.Codec (and testCodec).
|
|
||||||
//
|
|
||||||
// The UDPv5 transport is split into two objects: the codec object deals with
|
|
||||||
// encoding/decoding and with the handshake; the UDPv5 object handles higher-level concerns.
|
|
||||||
type codecV5 interface {
|
|
||||||
// Encode encodes a packet.
|
|
||||||
Encode(enode.ID, string, v5wire.Packet, *v5wire.Whoareyou) ([]byte, v5wire.Nonce, error)
|
|
||||||
|
|
||||||
// Decode decodes a packet. It returns a *v5wire.Unknown packet if decryption fails.
|
|
||||||
// The *enode.Node return value is non-nil when the input contains a handshake response.
|
|
||||||
Decode([]byte, string) (enode.ID, *enode.Node, v5wire.Packet, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UDPv5 is the implementation of protocol version 5.
|
|
||||||
type UDPv5 struct {
|
|
||||||
// static fields
|
|
||||||
conn UDPConn
|
|
||||||
tab *Table
|
|
||||||
netrestrict *netutil.Netlist
|
|
||||||
priv *ecdsa.PrivateKey
|
|
||||||
localNode *enode.LocalNode
|
|
||||||
db *enode.DB
|
|
||||||
log log.Logger
|
|
||||||
clock mclock.Clock
|
|
||||||
validSchemes enr.IdentityScheme
|
|
||||||
|
|
||||||
// misc buffers used during message handling
|
|
||||||
logcontext []interface{}
|
|
||||||
|
|
||||||
// talkreq handler registry
|
|
||||||
talk *talkSystem
|
|
||||||
|
|
||||||
// channels into dispatch
|
|
||||||
packetInCh chan ReadPacket
|
|
||||||
readNextCh chan struct{}
|
|
||||||
callCh chan *callV5
|
|
||||||
callDoneCh chan *callV5
|
|
||||||
respTimeoutCh chan *callTimeout
|
|
||||||
sendCh chan sendRequest
|
|
||||||
unhandled chan<- ReadPacket
|
|
||||||
|
|
||||||
// state of dispatch
|
|
||||||
codec codecV5
|
|
||||||
activeCallByNode map[enode.ID]*callV5
|
|
||||||
activeCallByAuth map[v5wire.Nonce]*callV5
|
|
||||||
callQueue map[enode.ID][]*callV5
|
|
||||||
|
|
||||||
// shutdown stuff
|
|
||||||
closeOnce sync.Once
|
|
||||||
closeCtx context.Context
|
|
||||||
cancelCloseCtx context.CancelFunc
|
|
||||||
wg sync.WaitGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
type sendRequest struct {
|
|
||||||
destID enode.ID
|
|
||||||
destAddr *net.UDPAddr
|
|
||||||
msg v5wire.Packet
|
|
||||||
}
|
|
||||||
|
|
||||||
// callV5 represents a remote procedure call against another node.
|
|
||||||
type callV5 struct {
|
|
||||||
id enode.ID
|
|
||||||
addr *net.UDPAddr
|
|
||||||
node *enode.Node // This is required to perform handshakes.
|
|
||||||
|
|
||||||
packet v5wire.Packet
|
|
||||||
responseType byte // expected packet type of response
|
|
||||||
reqid []byte
|
|
||||||
ch chan v5wire.Packet // responses sent here
|
|
||||||
err chan error // errors sent here
|
|
||||||
|
|
||||||
// Valid for active calls only:
|
|
||||||
nonce v5wire.Nonce // nonce of request packet
|
|
||||||
handshakeCount int // # times we attempted handshake for this call
|
|
||||||
challenge *v5wire.Whoareyou // last sent handshake challenge
|
|
||||||
timeout mclock.Timer
|
|
||||||
}
|
|
||||||
|
|
||||||
// callTimeout is the response timeout event of a call.
|
|
||||||
type callTimeout struct {
|
|
||||||
c *callV5
|
|
||||||
timer mclock.Timer
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenV5 listens on the given connection.
|
|
||||||
func ListenV5(conn UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv5, error) {
|
|
||||||
t, err := newUDPv5(conn, ln, cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
go t.tab.loop()
|
|
||||||
t.wg.Add(2)
|
|
||||||
go t.readLoop()
|
|
||||||
go t.dispatch()
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// newUDPv5 creates a UDPv5 transport, but doesn't start any goroutines.
|
|
||||||
func newUDPv5(conn UDPConn, ln *enode.LocalNode, cfg Config) (*UDPv5, error) {
|
|
||||||
closeCtx, cancelCloseCtx := context.WithCancel(context.Background())
|
|
||||||
cfg = cfg.withDefaults()
|
|
||||||
t := &UDPv5{
|
|
||||||
// static fields
|
|
||||||
conn: newMeteredConn(conn),
|
|
||||||
localNode: ln,
|
|
||||||
db: ln.Database(),
|
|
||||||
netrestrict: cfg.NetRestrict,
|
|
||||||
priv: cfg.PrivateKey,
|
|
||||||
log: cfg.Log,
|
|
||||||
validSchemes: cfg.ValidSchemes,
|
|
||||||
clock: cfg.Clock,
|
|
||||||
// channels into dispatch
|
|
||||||
packetInCh: make(chan ReadPacket, 1),
|
|
||||||
readNextCh: make(chan struct{}, 1),
|
|
||||||
callCh: make(chan *callV5),
|
|
||||||
callDoneCh: make(chan *callV5),
|
|
||||||
sendCh: make(chan sendRequest),
|
|
||||||
respTimeoutCh: make(chan *callTimeout),
|
|
||||||
unhandled: cfg.Unhandled,
|
|
||||||
// state of dispatch
|
|
||||||
codec: v5wire.NewCodec(ln, cfg.PrivateKey, cfg.Clock, cfg.V5ProtocolID),
|
|
||||||
activeCallByNode: make(map[enode.ID]*callV5),
|
|
||||||
activeCallByAuth: make(map[v5wire.Nonce]*callV5),
|
|
||||||
callQueue: make(map[enode.ID][]*callV5),
|
|
||||||
// shutdown
|
|
||||||
closeCtx: closeCtx,
|
|
||||||
cancelCloseCtx: cancelCloseCtx,
|
|
||||||
}
|
|
||||||
t.talk = newTalkSystem(t)
|
|
||||||
tab, err := newMeteredTable(t, t.db, cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
t.tab = tab
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Self returns the local node record.
|
|
||||||
func (t *UDPv5) Self() *enode.Node {
|
|
||||||
return t.localNode.Node()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close shuts down packet processing.
|
|
||||||
func (t *UDPv5) Close() {
|
|
||||||
t.closeOnce.Do(func() {
|
|
||||||
t.cancelCloseCtx()
|
|
||||||
t.conn.Close()
|
|
||||||
t.talk.wait()
|
|
||||||
t.wg.Wait()
|
|
||||||
t.tab.close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping sends a ping message to the given node.
|
|
||||||
func (t *UDPv5) Ping(n *enode.Node) error {
|
|
||||||
_, err := t.ping(n)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve searches for a specific node with the given ID and tries to get the most recent
|
|
||||||
// version of the node record for it. It returns n if the node could not be resolved.
|
|
||||||
func (t *UDPv5) Resolve(n *enode.Node) *enode.Node {
|
|
||||||
if intable := t.tab.getNode(n.ID()); intable != nil && intable.Seq() > n.Seq() {
|
|
||||||
n = intable
|
|
||||||
}
|
|
||||||
// Try asking directly. This works if the node is still responding on the endpoint we have.
|
|
||||||
if resp, err := t.RequestENR(n); err == nil {
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
// Otherwise do a network lookup.
|
|
||||||
result := t.Lookup(n.ID())
|
|
||||||
for _, rn := range result {
|
|
||||||
if rn.ID() == n.ID() && rn.Seq() > n.Seq() {
|
|
||||||
return rn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllNodes returns all the nodes stored in the local table.
|
|
||||||
func (t *UDPv5) AllNodes() []*enode.Node {
|
|
||||||
t.tab.mutex.Lock()
|
|
||||||
defer t.tab.mutex.Unlock()
|
|
||||||
nodes := make([]*enode.Node, 0)
|
|
||||||
|
|
||||||
for _, b := range &t.tab.buckets {
|
|
||||||
for _, n := range b.entries {
|
|
||||||
nodes = append(nodes, unwrapNode(n))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
// LocalNode returns the current local node running the
|
|
||||||
// protocol.
|
|
||||||
func (t *UDPv5) LocalNode() *enode.LocalNode {
|
|
||||||
return t.localNode
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterTalkHandler adds a handler for 'talk requests'. The handler function is called
|
|
||||||
// whenever a request for the given protocol is received and should return the response
|
|
||||||
// data or nil.
|
|
||||||
func (t *UDPv5) RegisterTalkHandler(protocol string, handler TalkRequestHandler) {
|
|
||||||
t.talk.register(protocol, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TalkRequest sends a talk request to a node and waits for a response.
|
|
||||||
func (t *UDPv5) TalkRequest(n *enode.Node, protocol string, request []byte) ([]byte, error) {
|
|
||||||
req := &v5wire.TalkRequest{Protocol: protocol, Message: request}
|
|
||||||
resp := t.callToNode(n, v5wire.TalkResponseMsg, req)
|
|
||||||
defer t.callDone(resp)
|
|
||||||
select {
|
|
||||||
case respMsg := <-resp.ch:
|
|
||||||
return respMsg.(*v5wire.TalkResponse).Message, nil
|
|
||||||
case err := <-resp.err:
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TalkRequestToID sends a talk request to a node and waits for a response.
|
|
||||||
func (t *UDPv5) TalkRequestToID(id enode.ID, addr *net.UDPAddr, protocol string, request []byte) ([]byte, error) {
|
|
||||||
req := &v5wire.TalkRequest{Protocol: protocol, Message: request}
|
|
||||||
resp := t.callToID(id, addr, v5wire.TalkResponseMsg, req)
|
|
||||||
defer t.callDone(resp)
|
|
||||||
select {
|
|
||||||
case respMsg := <-resp.ch:
|
|
||||||
return respMsg.(*v5wire.TalkResponse).Message, nil
|
|
||||||
case err := <-resp.err:
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RandomNodes returns an iterator that finds random nodes in the DHT.
|
|
||||||
func (t *UDPv5) RandomNodes() enode.Iterator {
|
|
||||||
if t.tab.len() == 0 {
|
|
||||||
// All nodes were dropped, refresh. The very first query will hit this
|
|
||||||
// case and run the bootstrapping logic.
|
|
||||||
<-t.tab.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
return newLookupIterator(t.closeCtx, t.newRandomLookup)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lookup performs a recursive lookup for the given target.
|
|
||||||
// It returns the closest nodes to target.
|
|
||||||
func (t *UDPv5) Lookup(target enode.ID) []*enode.Node {
|
|
||||||
return t.newLookup(t.closeCtx, target).run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// lookupRandom looks up a random target.
|
|
||||||
// This is needed to satisfy the transport interface.
|
|
||||||
func (t *UDPv5) lookupRandom() []*enode.Node {
|
|
||||||
return t.newRandomLookup(t.closeCtx).run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// lookupSelf looks up our own node ID.
|
|
||||||
// This is needed to satisfy the transport interface.
|
|
||||||
func (t *UDPv5) lookupSelf() []*enode.Node {
|
|
||||||
return t.newLookup(t.closeCtx, t.Self().ID()).run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPv5) newRandomLookup(ctx context.Context) *lookup {
|
|
||||||
var target enode.ID
|
|
||||||
crand.Read(target[:])
|
|
||||||
return t.newLookup(ctx, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPv5) newLookup(ctx context.Context, target enode.ID) *lookup {
|
|
||||||
return newLookup(ctx, t.tab, target, func(n *node) ([]*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) {
|
|
||||||
var (
|
|
||||||
dists = lookupDistances(target, destNode.ID())
|
|
||||||
nodes = nodesByDistance{target: target}
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
var r []*enode.Node
|
|
||||||
r, err = t.findnode(unwrapNode(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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nodes.entries, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// lookupDistances computes the distance parameter for FINDNODE calls to dest.
|
|
||||||
// It chooses distances adjacent to logdist(target, dest), e.g. for a target
|
|
||||||
// with logdist(target, dest) = 255 the result is [255, 256, 254].
|
|
||||||
func lookupDistances(target, dest enode.ID) (dists []uint) {
|
|
||||||
td := enode.LogDist(target, dest)
|
|
||||||
dists = append(dists, uint(td))
|
|
||||||
for i := 1; len(dists) < lookupRequestLimit; i++ {
|
|
||||||
if td+i <= 256 {
|
|
||||||
dists = append(dists, uint(td+i))
|
|
||||||
}
|
|
||||||
if td-i > 0 {
|
|
||||||
dists = append(dists, uint(td-i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dists
|
|
||||||
}
|
|
||||||
|
|
||||||
// ping calls PING on a node and waits for a PONG response.
|
|
||||||
func (t *UDPv5) ping(n *enode.Node) (uint64, error) {
|
|
||||||
req := &v5wire.Ping{ENRSeq: t.localNode.Node().Seq()}
|
|
||||||
resp := t.callToNode(n, v5wire.PongMsg, req)
|
|
||||||
defer t.callDone(resp)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case pong := <-resp.ch:
|
|
||||||
return pong.(*v5wire.Pong).ENRSeq, nil
|
|
||||||
case err := <-resp.err:
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestENR requests n's record.
|
|
||||||
func (t *UDPv5) RequestENR(n *enode.Node) (*enode.Node, error) {
|
|
||||||
nodes, err := t.findnode(n, []uint{0})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(nodes) != 1 {
|
|
||||||
return nil, fmt.Errorf("%d nodes in response for distance zero", len(nodes))
|
|
||||||
}
|
|
||||||
return nodes[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// findnode calls FINDNODE on a node and waits for responses.
|
|
||||||
func (t *UDPv5) findnode(n *enode.Node, distances []uint) ([]*enode.Node, error) {
|
|
||||||
resp := t.callToNode(n, v5wire.NodesMsg, &v5wire.Findnode{Distances: distances})
|
|
||||||
return t.waitForNodes(resp, distances)
|
|
||||||
}
|
|
||||||
|
|
||||||
// waitForNodes waits for NODES responses to the given call.
|
|
||||||
func (t *UDPv5) waitForNodes(c *callV5, distances []uint) ([]*enode.Node, error) {
|
|
||||||
defer t.callDone(c)
|
|
||||||
|
|
||||||
var (
|
|
||||||
nodes []*enode.Node
|
|
||||||
seen = make(map[enode.ID]struct{})
|
|
||||||
received, total = 0, -1
|
|
||||||
)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case responseP := <-c.ch:
|
|
||||||
response := responseP.(*v5wire.Nodes)
|
|
||||||
for _, record := range response.Nodes {
|
|
||||||
node, err := t.verifyResponseNode(c, record, distances, seen)
|
|
||||||
if err != nil {
|
|
||||||
t.log.Debug("Invalid record in "+response.Name(), "id", c.node.ID(), "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
nodes = append(nodes, node)
|
|
||||||
}
|
|
||||||
if total == -1 {
|
|
||||||
total = min(int(response.RespCount), totalNodesResponseLimit)
|
|
||||||
}
|
|
||||||
if received++; received == total {
|
|
||||||
return nodes, nil
|
|
||||||
}
|
|
||||||
case err := <-c.err:
|
|
||||||
return nodes, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// verifyResponseNode checks validity of a record in a NODES response.
|
|
||||||
func (t *UDPv5) verifyResponseNode(c *callV5, r *enr.Record, distances []uint, seen map[enode.ID]struct{}) (*enode.Node, error) {
|
|
||||||
node, err := enode.New(t.validSchemes, r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := netutil.CheckRelayIP(c.addr.IP, node.IP()); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if t.netrestrict != nil && !t.netrestrict.Contains(node.IP()) {
|
|
||||||
return nil, errors.New("not contained in netrestrict list")
|
|
||||||
}
|
|
||||||
if node.UDP() <= 1024 {
|
|
||||||
return nil, errLowPort
|
|
||||||
}
|
|
||||||
if distances != nil {
|
|
||||||
nd := enode.LogDist(c.id, node.ID())
|
|
||||||
if !containsUint(uint(nd), distances) {
|
|
||||||
return nil, errors.New("does not match any requested distance")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, ok := seen[node.ID()]; ok {
|
|
||||||
return nil, fmt.Errorf("duplicate record")
|
|
||||||
}
|
|
||||||
seen[node.ID()] = struct{}{}
|
|
||||||
return node, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func containsUint(x uint, xs []uint) bool {
|
|
||||||
for _, v := range xs {
|
|
||||||
if x == v {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// callToNode sends the given call and sets up a handler for response packets (of message
|
|
||||||
// type responseType). Responses are dispatched to the call's response channel.
|
|
||||||
func (t *UDPv5) callToNode(n *enode.Node, responseType byte, req v5wire.Packet) *callV5 {
|
|
||||||
addr := &net.UDPAddr{IP: n.IP(), Port: n.UDP()}
|
|
||||||
c := &callV5{id: n.ID(), addr: addr, node: n}
|
|
||||||
t.initCall(c, responseType, req)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// callToID is like callToNode, but for cases where the node record is not available.
|
|
||||||
func (t *UDPv5) callToID(id enode.ID, addr *net.UDPAddr, responseType byte, req v5wire.Packet) *callV5 {
|
|
||||||
c := &callV5{id: id, addr: addr}
|
|
||||||
t.initCall(c, responseType, req)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPv5) initCall(c *callV5, responseType byte, packet v5wire.Packet) {
|
|
||||||
c.packet = packet
|
|
||||||
c.responseType = responseType
|
|
||||||
c.reqid = make([]byte, 8)
|
|
||||||
c.ch = make(chan v5wire.Packet, 1)
|
|
||||||
c.err = make(chan error, 1)
|
|
||||||
// Assign request ID.
|
|
||||||
crand.Read(c.reqid)
|
|
||||||
packet.SetRequestID(c.reqid)
|
|
||||||
// Send call to dispatch.
|
|
||||||
select {
|
|
||||||
case t.callCh <- c:
|
|
||||||
case <-t.closeCtx.Done():
|
|
||||||
c.err <- errClosed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// callDone tells dispatch that the active call is done.
|
|
||||||
func (t *UDPv5) callDone(c *callV5) {
|
|
||||||
// This needs a loop because further responses may be incoming until the
|
|
||||||
// send to callDoneCh has completed. Such responses need to be discarded
|
|
||||||
// in order to avoid blocking the dispatch loop.
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-c.ch:
|
|
||||||
// late response, discard.
|
|
||||||
case <-c.err:
|
|
||||||
// late error, discard.
|
|
||||||
case t.callDoneCh <- c:
|
|
||||||
return
|
|
||||||
case <-t.closeCtx.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// dispatch runs in its own goroutine, handles incoming packets and deals with calls.
|
|
||||||
//
|
|
||||||
// For any destination node there is at most one 'active call', stored in the t.activeCall*
|
|
||||||
// maps. A call is made active when it is sent. The active call can be answered by a
|
|
||||||
// matching response, in which case c.ch receives the response; or by timing out, in which case
|
|
||||||
// c.err receives the error. When the function that created the call signals the active
|
|
||||||
// call is done through callDone, the next call from the call queue is started.
|
|
||||||
//
|
|
||||||
// Calls may also be answered by a WHOAREYOU packet referencing the call packet's authTag.
|
|
||||||
// When that happens the call is simply re-sent to complete the handshake. We allow one
|
|
||||||
// handshake attempt per call.
|
|
||||||
func (t *UDPv5) dispatch() {
|
|
||||||
defer t.wg.Done()
|
|
||||||
|
|
||||||
// Arm first read.
|
|
||||||
t.readNextCh <- struct{}{}
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case c := <-t.callCh:
|
|
||||||
t.callQueue[c.id] = append(t.callQueue[c.id], c)
|
|
||||||
t.sendNextCall(c.id)
|
|
||||||
|
|
||||||
case ct := <-t.respTimeoutCh:
|
|
||||||
active := t.activeCallByNode[ct.c.id]
|
|
||||||
if ct.c == active && ct.timer == active.timeout {
|
|
||||||
ct.c.err <- errTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
case c := <-t.callDoneCh:
|
|
||||||
active := t.activeCallByNode[c.id]
|
|
||||||
if active != c {
|
|
||||||
panic("BUG: callDone for inactive call")
|
|
||||||
}
|
|
||||||
c.timeout.Stop()
|
|
||||||
delete(t.activeCallByAuth, c.nonce)
|
|
||||||
delete(t.activeCallByNode, c.id)
|
|
||||||
t.sendNextCall(c.id)
|
|
||||||
|
|
||||||
case r := <-t.sendCh:
|
|
||||||
t.send(r.destID, r.destAddr, r.msg, nil)
|
|
||||||
|
|
||||||
case p := <-t.packetInCh:
|
|
||||||
t.handlePacket(p.Data, p.Addr)
|
|
||||||
// Arm next read.
|
|
||||||
t.readNextCh <- struct{}{}
|
|
||||||
|
|
||||||
case <-t.closeCtx.Done():
|
|
||||||
close(t.readNextCh)
|
|
||||||
for id, queue := range t.callQueue {
|
|
||||||
for _, c := range queue {
|
|
||||||
c.err <- errClosed
|
|
||||||
}
|
|
||||||
delete(t.callQueue, id)
|
|
||||||
}
|
|
||||||
for id, c := range t.activeCallByNode {
|
|
||||||
c.err <- errClosed
|
|
||||||
delete(t.activeCallByNode, id)
|
|
||||||
delete(t.activeCallByAuth, c.nonce)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// startResponseTimeout sets the response timer for a call.
|
|
||||||
func (t *UDPv5) startResponseTimeout(c *callV5) {
|
|
||||||
if c.timeout != nil {
|
|
||||||
c.timeout.Stop()
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
timer mclock.Timer
|
|
||||||
done = make(chan struct{})
|
|
||||||
)
|
|
||||||
timer = t.clock.AfterFunc(respTimeoutV5, func() {
|
|
||||||
<-done
|
|
||||||
select {
|
|
||||||
case t.respTimeoutCh <- &callTimeout{c, timer}:
|
|
||||||
case <-t.closeCtx.Done():
|
|
||||||
}
|
|
||||||
})
|
|
||||||
c.timeout = timer
|
|
||||||
close(done)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendNextCall sends the next call in the call queue if there is no active call.
|
|
||||||
func (t *UDPv5) sendNextCall(id enode.ID) {
|
|
||||||
queue := t.callQueue[id]
|
|
||||||
if len(queue) == 0 || t.activeCallByNode[id] != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.activeCallByNode[id] = queue[0]
|
|
||||||
t.sendCall(t.activeCallByNode[id])
|
|
||||||
if len(queue) == 1 {
|
|
||||||
delete(t.callQueue, id)
|
|
||||||
} else {
|
|
||||||
copy(queue, queue[1:])
|
|
||||||
t.callQueue[id] = queue[:len(queue)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendCall encodes and sends a request packet to the call's recipient node.
|
|
||||||
// This performs a handshake if needed.
|
|
||||||
func (t *UDPv5) sendCall(c *callV5) {
|
|
||||||
// The call might have a nonce from a previous handshake attempt. Remove the entry for
|
|
||||||
// the old nonce because we're about to generate a new nonce for this call.
|
|
||||||
if c.nonce != (v5wire.Nonce{}) {
|
|
||||||
delete(t.activeCallByAuth, c.nonce)
|
|
||||||
}
|
|
||||||
|
|
||||||
newNonce, _ := t.send(c.id, c.addr, c.packet, c.challenge)
|
|
||||||
c.nonce = newNonce
|
|
||||||
t.activeCallByAuth[newNonce] = c
|
|
||||||
t.startResponseTimeout(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendResponse sends a response packet to the given node.
|
|
||||||
// This doesn't trigger a handshake even if no keys are available.
|
|
||||||
func (t *UDPv5) sendResponse(toID enode.ID, toAddr *net.UDPAddr, packet v5wire.Packet) error {
|
|
||||||
_, err := t.send(toID, toAddr, packet, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *UDPv5) sendFromAnotherThread(toID enode.ID, toAddr *net.UDPAddr, packet v5wire.Packet) {
|
|
||||||
select {
|
|
||||||
case t.sendCh <- sendRequest{toID, toAddr, packet}:
|
|
||||||
case <-t.closeCtx.Done():
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// send sends a packet to the given node.
|
|
||||||
func (t *UDPv5) send(toID enode.ID, toAddr *net.UDPAddr, packet v5wire.Packet, c *v5wire.Whoareyou) (v5wire.Nonce, error) {
|
|
||||||
addr := toAddr.String()
|
|
||||||
t.logcontext = append(t.logcontext[:0], "id", toID, "addr", addr)
|
|
||||||
t.logcontext = packet.AppendLogInfo(t.logcontext)
|
|
||||||
|
|
||||||
enc, nonce, err := t.codec.Encode(toID, addr, packet, c)
|
|
||||||
if err != nil {
|
|
||||||
t.logcontext = append(t.logcontext, "err", err)
|
|
||||||
t.log.Warn(">> "+packet.Name(), t.logcontext...)
|
|
||||||
return nonce, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = t.conn.WriteToUDP(enc, toAddr)
|
|
||||||
t.log.Trace(">> "+packet.Name(), t.logcontext...)
|
|
||||||
return nonce, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// readLoop runs in its own goroutine and reads packets from the network.
|
|
||||||
func (t *UDPv5) readLoop() {
|
|
||||||
defer t.wg.Done()
|
|
||||||
|
|
||||||
buf := make([]byte, maxPacketSize)
|
|
||||||
for range t.readNextCh {
|
|
||||||
nbytes, from, err := t.conn.ReadFromUDP(buf)
|
|
||||||
if netutil.IsTemporaryError(err) {
|
|
||||||
// Ignore temporary read errors.
|
|
||||||
t.log.Debug("Temporary UDP read error", "err", err)
|
|
||||||
continue
|
|
||||||
} else if err != nil {
|
|
||||||
// Shut down the loop for permanent errors.
|
|
||||||
if !errors.Is(err, io.EOF) {
|
|
||||||
t.log.Debug("UDP read error", "err", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.dispatchReadPacket(from, buf[:nbytes])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// dispatchReadPacket sends a packet into the dispatch loop.
|
|
||||||
func (t *UDPv5) dispatchReadPacket(from *net.UDPAddr, content []byte) bool {
|
|
||||||
select {
|
|
||||||
case t.packetInCh <- ReadPacket{content, from}:
|
|
||||||
return true
|
|
||||||
case <-t.closeCtx.Done():
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handlePacket decodes and processes an incoming packet from the network.
|
|
||||||
func (t *UDPv5) handlePacket(rawpacket []byte, fromAddr *net.UDPAddr) error {
|
|
||||||
addr := fromAddr.String()
|
|
||||||
fromID, fromNode, packet, err := t.codec.Decode(rawpacket, addr)
|
|
||||||
if err != nil {
|
|
||||||
if t.unhandled != nil && v5wire.IsInvalidHeader(err) {
|
|
||||||
// The packet seems unrelated to discv5, send it to the next protocol.
|
|
||||||
// t.log.Trace("Unhandled discv5 packet", "id", fromID, "addr", addr, "err", err)
|
|
||||||
up := ReadPacket{Data: make([]byte, len(rawpacket)), Addr: fromAddr}
|
|
||||||
copy(up.Data, rawpacket)
|
|
||||||
t.unhandled <- up
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
t.log.Debug("Bad discv5 packet", "id", fromID, "addr", addr, "err", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if fromNode != nil {
|
|
||||||
// Handshake succeeded, add to table.
|
|
||||||
t.tab.addSeenNode(wrapNode(fromNode))
|
|
||||||
}
|
|
||||||
if packet.Kind() != v5wire.WhoareyouPacket {
|
|
||||||
// WHOAREYOU logged separately to report errors.
|
|
||||||
t.logcontext = append(t.logcontext[:0], "id", fromID, "addr", addr)
|
|
||||||
t.logcontext = packet.AppendLogInfo(t.logcontext)
|
|
||||||
t.log.Trace("<< "+packet.Name(), t.logcontext...)
|
|
||||||
}
|
|
||||||
t.handle(packet, fromID, fromAddr)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleCallResponse dispatches a response packet to the call waiting for it.
|
|
||||||
func (t *UDPv5) handleCallResponse(fromID enode.ID, fromAddr *net.UDPAddr, p v5wire.Packet) bool {
|
|
||||||
ac := t.activeCallByNode[fromID]
|
|
||||||
if ac == nil || !bytes.Equal(p.RequestID(), ac.reqid) {
|
|
||||||
t.log.Debug(fmt.Sprintf("Unsolicited/late %s response", p.Name()), "id", fromID, "addr", fromAddr)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !fromAddr.IP.Equal(ac.addr.IP) || fromAddr.Port != ac.addr.Port {
|
|
||||||
t.log.Debug(fmt.Sprintf("%s from wrong endpoint", p.Name()), "id", fromID, "addr", fromAddr)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if p.Kind() != ac.responseType {
|
|
||||||
t.log.Debug(fmt.Sprintf("Wrong discv5 response type %s", p.Name()), "id", fromID, "addr", fromAddr)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
t.startResponseTimeout(ac)
|
|
||||||
ac.ch <- p
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// getNode looks for a node record in table and database.
|
|
||||||
func (t *UDPv5) getNode(id enode.ID) *enode.Node {
|
|
||||||
if n := t.tab.getNode(id); n != nil {
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
if n := t.localNode.Database().Node(id); n != nil {
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle processes incoming packets according to their message type.
|
|
||||||
func (t *UDPv5) handle(p v5wire.Packet, fromID enode.ID, fromAddr *net.UDPAddr) {
|
|
||||||
switch p := p.(type) {
|
|
||||||
case *v5wire.Unknown:
|
|
||||||
t.handleUnknown(p, fromID, fromAddr)
|
|
||||||
case *v5wire.Whoareyou:
|
|
||||||
t.handleWhoareyou(p, fromID, fromAddr)
|
|
||||||
case *v5wire.Ping:
|
|
||||||
t.handlePing(p, fromID, fromAddr)
|
|
||||||
case *v5wire.Pong:
|
|
||||||
if t.handleCallResponse(fromID, fromAddr, p) {
|
|
||||||
t.localNode.UDPEndpointStatement(fromAddr, &net.UDPAddr{IP: p.ToIP, Port: int(p.ToPort)})
|
|
||||||
}
|
|
||||||
case *v5wire.Findnode:
|
|
||||||
t.handleFindnode(p, fromID, fromAddr)
|
|
||||||
case *v5wire.Nodes:
|
|
||||||
t.handleCallResponse(fromID, fromAddr, p)
|
|
||||||
case *v5wire.TalkRequest:
|
|
||||||
t.talk.handleRequest(fromID, fromAddr, p)
|
|
||||||
case *v5wire.TalkResponse:
|
|
||||||
t.handleCallResponse(fromID, fromAddr, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleUnknown initiates a handshake by responding with WHOAREYOU.
|
|
||||||
func (t *UDPv5) handleUnknown(p *v5wire.Unknown, fromID enode.ID, fromAddr *net.UDPAddr) {
|
|
||||||
challenge := &v5wire.Whoareyou{Nonce: p.Nonce}
|
|
||||||
crand.Read(challenge.IDNonce[:])
|
|
||||||
if n := t.getNode(fromID); n != nil {
|
|
||||||
challenge.Node = n
|
|
||||||
challenge.RecordSeq = n.Seq()
|
|
||||||
}
|
|
||||||
t.sendResponse(fromID, fromAddr, challenge)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
errChallengeNoCall = errors.New("no matching call")
|
|
||||||
errChallengeTwice = errors.New("second handshake")
|
|
||||||
)
|
|
||||||
|
|
||||||
// handleWhoareyou resends the active call as a handshake packet.
|
|
||||||
func (t *UDPv5) handleWhoareyou(p *v5wire.Whoareyou, fromID enode.ID, fromAddr *net.UDPAddr) {
|
|
||||||
c, err := t.matchWithCall(fromID, p.Nonce)
|
|
||||||
if err != nil {
|
|
||||||
t.log.Debug("Invalid "+p.Name(), "addr", fromAddr, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.node == nil {
|
|
||||||
// Can't perform handshake because we don't have the ENR.
|
|
||||||
t.log.Debug("Can't handle "+p.Name(), "addr", fromAddr, "err", "call has no ENR")
|
|
||||||
c.err <- errors.New("remote wants handshake, but call has no ENR")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Resend the call that was answered by WHOAREYOU.
|
|
||||||
t.log.Trace("<< "+p.Name(), "id", c.node.ID(), "addr", fromAddr)
|
|
||||||
c.handshakeCount++
|
|
||||||
c.challenge = p
|
|
||||||
p.Node = c.node
|
|
||||||
t.sendCall(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchWithCall checks whether a handshake attempt matches the active call.
|
|
||||||
func (t *UDPv5) matchWithCall(fromID enode.ID, nonce v5wire.Nonce) (*callV5, error) {
|
|
||||||
c := t.activeCallByAuth[nonce]
|
|
||||||
if c == nil {
|
|
||||||
return nil, errChallengeNoCall
|
|
||||||
}
|
|
||||||
if c.handshakeCount > 0 {
|
|
||||||
return nil, errChallengeTwice
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handlePing sends a PONG response.
|
|
||||||
func (t *UDPv5) handlePing(p *v5wire.Ping, fromID enode.ID, fromAddr *net.UDPAddr) {
|
|
||||||
remoteIP := fromAddr.IP
|
|
||||||
// Handle IPv4 mapped IPv6 addresses in the
|
|
||||||
// event the local node is binded to an
|
|
||||||
// ipv6 interface.
|
|
||||||
if remoteIP.To4() != nil {
|
|
||||||
remoteIP = remoteIP.To4()
|
|
||||||
}
|
|
||||||
t.sendResponse(fromID, fromAddr, &v5wire.Pong{
|
|
||||||
ReqID: p.ReqID,
|
|
||||||
ToIP: remoteIP,
|
|
||||||
ToPort: uint16(fromAddr.Port),
|
|
||||||
ENRSeq: t.localNode.Node().Seq(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleFindnode returns nodes to the requester.
|
|
||||||
func (t *UDPv5) handleFindnode(p *v5wire.Findnode, fromID enode.ID, fromAddr *net.UDPAddr) {
|
|
||||||
nodes := t.collectTableNodes(fromAddr.IP, p.Distances, findnodeResultLimit)
|
|
||||||
for _, resp := range packNodes(p.ReqID, nodes) {
|
|
||||||
t.sendResponse(fromID, fromAddr, resp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// collectTableNodes creates a FINDNODE result set for the given distances.
|
|
||||||
func (t *UDPv5) collectTableNodes(rip net.IP, distances []uint, limit int) []*enode.Node {
|
|
||||||
var bn []*enode.Node
|
|
||||||
var nodes []*enode.Node
|
|
||||||
var processed = make(map[uint]struct{})
|
|
||||||
for _, dist := range distances {
|
|
||||||
// Reject duplicate / invalid distances.
|
|
||||||
_, seen := processed[dist]
|
|
||||||
if seen || dist > 256 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
processed[dist] = struct{}{}
|
|
||||||
|
|
||||||
for _, n := range t.tab.appendLiveNodes(dist, bn[:0]) {
|
|
||||||
// Apply some pre-checks to avoid sending invalid nodes.
|
|
||||||
// Note liveness is checked by appendLiveNodes.
|
|
||||||
if netutil.CheckRelayIP(rip, n.IP()) != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
nodes = append(nodes, n)
|
|
||||||
if len(nodes) >= limit {
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
// packNodes creates NODES response packets for the given node list.
|
|
||||||
func packNodes(reqid []byte, nodes []*enode.Node) []*v5wire.Nodes {
|
|
||||||
if len(nodes) == 0 {
|
|
||||||
return []*v5wire.Nodes{{ReqID: reqid, RespCount: 1}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This limit represents the available space for nodes in output packets. Maximum
|
|
||||||
// packet size is 1280, and out of this ~80 bytes will be taken up by the packet
|
|
||||||
// frame. So limiting to 1000 bytes here leaves 200 bytes for other fields of the
|
|
||||||
// NODES message, which is a lot.
|
|
||||||
const sizeLimit = 1000
|
|
||||||
|
|
||||||
var resp []*v5wire.Nodes
|
|
||||||
for len(nodes) > 0 {
|
|
||||||
p := &v5wire.Nodes{ReqID: reqid}
|
|
||||||
size := uint64(0)
|
|
||||||
for len(nodes) > 0 {
|
|
||||||
r := nodes[0].Record()
|
|
||||||
if size += r.Size(); size > sizeLimit {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
p.Nodes = append(p.Nodes, r)
|
|
||||||
nodes = nodes[1:]
|
|
||||||
}
|
|
||||||
resp = append(resp, p)
|
|
||||||
}
|
|
||||||
for _, msg := range resp {
|
|
||||||
msg.RespCount = uint8(len(resp))
|
|
||||||
}
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
@ -1,859 +0,0 @@
|
||||||
// Copyright 2020 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package discover
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/internal/testlog"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/discover/v5wire"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Real sockets, real crypto: this test checks end-to-end connectivity for UDPv5.
|
|
||||||
func TestUDPv5_lookupE2E(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const N = 5
|
|
||||||
var nodes []*UDPv5
|
|
||||||
for i := 0; i < N; i++ {
|
|
||||||
var cfg Config
|
|
||||||
if len(nodes) > 0 {
|
|
||||||
bn := nodes[0].Self()
|
|
||||||
cfg.Bootnodes = []*enode.Node{bn}
|
|
||||||
}
|
|
||||||
node := startLocalhostV5(t, cfg)
|
|
||||||
nodes = append(nodes, node)
|
|
||||||
defer node.Close()
|
|
||||||
}
|
|
||||||
last := nodes[N-1]
|
|
||||||
target := nodes[rand.Intn(N-2)].Self()
|
|
||||||
|
|
||||||
// It is expected that all nodes can be found.
|
|
||||||
expectedResult := make([]*enode.Node, len(nodes))
|
|
||||||
for i := range nodes {
|
|
||||||
expectedResult[i] = nodes[i].Self()
|
|
||||||
}
|
|
||||||
slices.SortFunc(expectedResult, func(a, b *enode.Node) int {
|
|
||||||
return enode.DistCmp(target.ID(), a.ID(), b.ID())
|
|
||||||
})
|
|
||||||
|
|
||||||
// Do the lookup.
|
|
||||||
results := last.Lookup(target.ID())
|
|
||||||
if err := checkNodesEqual(results, expectedResult); err != nil {
|
|
||||||
t.Fatalf("lookup returned wrong results: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startLocalhostV5(t *testing.T, cfg Config) *UDPv5 {
|
|
||||||
cfg.PrivateKey = newkey()
|
|
||||||
db, _ := enode.OpenDB("")
|
|
||||||
ln := enode.NewLocalNode(db, cfg.PrivateKey)
|
|
||||||
|
|
||||||
// Prefix logs with node ID.
|
|
||||||
lprefix := fmt.Sprintf("(%s)", ln.ID().TerminalString())
|
|
||||||
cfg.Log = testlog.Logger(t, log.LevelTrace).With("node-id", lprefix)
|
|
||||||
|
|
||||||
// Listen.
|
|
||||||
socket, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP{127, 0, 0, 1}})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
realaddr := socket.LocalAddr().(*net.UDPAddr)
|
|
||||||
ln.SetStaticIP(realaddr.IP)
|
|
||||||
ln.Set(enr.UDP(realaddr.Port))
|
|
||||||
udp, err := ListenV5(socket, ln, cfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return udp
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that incoming PING calls are handled correctly.
|
|
||||||
func TestUDPv5_pingHandling(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPV5Test(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
test.packetIn(&v5wire.Ping{ReqID: []byte("foo")})
|
|
||||||
test.waitPacketOut(func(p *v5wire.Pong, addr *net.UDPAddr, _ v5wire.Nonce) {
|
|
||||||
if !bytes.Equal(p.ReqID, []byte("foo")) {
|
|
||||||
t.Error("wrong request ID in response:", p.ReqID)
|
|
||||||
}
|
|
||||||
if p.ENRSeq != test.table.self().Seq() {
|
|
||||||
t.Error("wrong ENR sequence number in response:", p.ENRSeq)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that incoming 'unknown' packets trigger the handshake.
|
|
||||||
func TestUDPv5_unknownPacket(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPV5Test(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
nonce := v5wire.Nonce{1, 2, 3}
|
|
||||||
check := func(p *v5wire.Whoareyou, wantSeq uint64) {
|
|
||||||
t.Helper()
|
|
||||||
if p.Nonce != nonce {
|
|
||||||
t.Error("wrong nonce in WHOAREYOU:", p.Nonce, nonce)
|
|
||||||
}
|
|
||||||
if p.IDNonce == ([16]byte{}) {
|
|
||||||
t.Error("all zero ID nonce")
|
|
||||||
}
|
|
||||||
if p.RecordSeq != wantSeq {
|
|
||||||
t.Errorf("wrong record seq %d in WHOAREYOU, want %d", p.RecordSeq, wantSeq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unknown packet from unknown node.
|
|
||||||
test.packetIn(&v5wire.Unknown{Nonce: nonce})
|
|
||||||
test.waitPacketOut(func(p *v5wire.Whoareyou, addr *net.UDPAddr, _ v5wire.Nonce) {
|
|
||||||
check(p, 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Make node known.
|
|
||||||
n := test.getNode(test.remotekey, test.remoteaddr).Node()
|
|
||||||
test.table.addSeenNode(wrapNode(n))
|
|
||||||
|
|
||||||
test.packetIn(&v5wire.Unknown{Nonce: nonce})
|
|
||||||
test.waitPacketOut(func(p *v5wire.Whoareyou, addr *net.UDPAddr, _ v5wire.Nonce) {
|
|
||||||
check(p, n.Seq())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that incoming FINDNODE calls are handled correctly.
|
|
||||||
func TestUDPv5_findnodeHandling(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPV5Test(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
// Create test nodes and insert them into the table.
|
|
||||||
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)
|
|
||||||
|
|
||||||
// Requesting with distance zero should return the node's own record.
|
|
||||||
test.packetIn(&v5wire.Findnode{ReqID: []byte{0}, Distances: []uint{0}})
|
|
||||||
test.expectNodes([]byte{0}, 1, []*enode.Node{test.udp.Self()})
|
|
||||||
|
|
||||||
// Requesting with distance > 256 shouldn't crash.
|
|
||||||
test.packetIn(&v5wire.Findnode{ReqID: []byte{1}, Distances: []uint{4234098}})
|
|
||||||
test.expectNodes([]byte{1}, 1, nil)
|
|
||||||
|
|
||||||
// Requesting with empty distance list shouldn't crash either.
|
|
||||||
test.packetIn(&v5wire.Findnode{ReqID: []byte{2}, Distances: []uint{}})
|
|
||||||
test.expectNodes([]byte{2}, 1, nil)
|
|
||||||
|
|
||||||
// This request gets no nodes because the corresponding bucket is empty.
|
|
||||||
test.packetIn(&v5wire.Findnode{ReqID: []byte{3}, Distances: []uint{254}})
|
|
||||||
test.expectNodes([]byte{3}, 1, nil)
|
|
||||||
|
|
||||||
// This request gets all the distance-253 nodes.
|
|
||||||
test.packetIn(&v5wire.Findnode{ReqID: []byte{4}, Distances: []uint{253}})
|
|
||||||
test.expectNodes([]byte{4}, 2, nodes253)
|
|
||||||
|
|
||||||
// This request gets all the distance-249 nodes and some more at 248 because
|
|
||||||
// the bucket at 249 is not full.
|
|
||||||
test.packetIn(&v5wire.Findnode{ReqID: []byte{5}, Distances: []uint{249, 248}})
|
|
||||||
var nodes []*enode.Node
|
|
||||||
nodes = append(nodes, nodes249...)
|
|
||||||
nodes = append(nodes, nodes248[:10]...)
|
|
||||||
test.expectNodes([]byte{5}, 1, nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (test *udpV5Test) expectNodes(wantReqID []byte, wantTotal uint8, wantNodes []*enode.Node) {
|
|
||||||
nodeSet := make(map[enode.ID]*enr.Record, len(wantNodes))
|
|
||||||
for _, n := range wantNodes {
|
|
||||||
nodeSet[n.ID()] = n.Record()
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
test.waitPacketOut(func(p *v5wire.Nodes, addr *net.UDPAddr, _ v5wire.Nonce) {
|
|
||||||
if !bytes.Equal(p.ReqID, wantReqID) {
|
|
||||||
test.t.Fatalf("wrong request ID %v in response, want %v", p.ReqID, wantReqID)
|
|
||||||
}
|
|
||||||
if p.RespCount != wantTotal {
|
|
||||||
test.t.Fatalf("wrong total response count %d, want %d", p.RespCount, wantTotal)
|
|
||||||
}
|
|
||||||
for _, record := range p.Nodes {
|
|
||||||
n, _ := enode.New(enode.ValidSchemesForTesting, record)
|
|
||||||
want := nodeSet[n.ID()]
|
|
||||||
if want == nil {
|
|
||||||
test.t.Fatalf("unexpected node in response: %v", n)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(record, want) {
|
|
||||||
test.t.Fatalf("wrong record in response: %v", n)
|
|
||||||
}
|
|
||||||
delete(nodeSet, n.ID())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if len(nodeSet) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that outgoing PING calls work.
|
|
||||||
func TestUDPv5_pingCall(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPV5Test(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
remote := test.getNode(test.remotekey, test.remoteaddr).Node()
|
|
||||||
done := make(chan error, 1)
|
|
||||||
|
|
||||||
// This ping times out.
|
|
||||||
go func() {
|
|
||||||
_, err := test.udp.ping(remote)
|
|
||||||
done <- err
|
|
||||||
}()
|
|
||||||
test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) {})
|
|
||||||
if err := <-done; err != errTimeout {
|
|
||||||
t.Fatalf("want errTimeout, got %q", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This ping works.
|
|
||||||
go func() {
|
|
||||||
_, err := test.udp.ping(remote)
|
|
||||||
done <- err
|
|
||||||
}()
|
|
||||||
test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) {
|
|
||||||
test.packetInFrom(test.remotekey, test.remoteaddr, &v5wire.Pong{ReqID: p.ReqID})
|
|
||||||
})
|
|
||||||
if err := <-done; err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This ping gets a reply from the wrong endpoint.
|
|
||||||
go func() {
|
|
||||||
_, err := test.udp.ping(remote)
|
|
||||||
done <- err
|
|
||||||
}()
|
|
||||||
test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) {
|
|
||||||
wrongAddr := &net.UDPAddr{IP: net.IP{33, 44, 55, 22}, Port: 10101}
|
|
||||||
test.packetInFrom(test.remotekey, wrongAddr, &v5wire.Pong{ReqID: p.ReqID})
|
|
||||||
})
|
|
||||||
if err := <-done; err != errTimeout {
|
|
||||||
t.Fatalf("want errTimeout for reply from wrong IP, got %q", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that outgoing FINDNODE calls work and multiple NODES
|
|
||||||
// replies are aggregated.
|
|
||||||
func TestUDPv5_findnodeCall(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPV5Test(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
// Launch the request:
|
|
||||||
var (
|
|
||||||
distances = []uint{230}
|
|
||||||
remote = test.getNode(test.remotekey, test.remoteaddr).Node()
|
|
||||||
nodes = nodesAtDistance(remote.ID(), int(distances[0]), 8)
|
|
||||||
done = make(chan error, 1)
|
|
||||||
response []*enode.Node
|
|
||||||
)
|
|
||||||
go func() {
|
|
||||||
var err error
|
|
||||||
response, err = test.udp.findnode(remote, distances)
|
|
||||||
done <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Serve the responses:
|
|
||||||
test.waitPacketOut(func(p *v5wire.Findnode, addr *net.UDPAddr, _ v5wire.Nonce) {
|
|
||||||
if !reflect.DeepEqual(p.Distances, distances) {
|
|
||||||
t.Fatalf("wrong distances in request: %v", p.Distances)
|
|
||||||
}
|
|
||||||
test.packetIn(&v5wire.Nodes{
|
|
||||||
ReqID: p.ReqID,
|
|
||||||
RespCount: 2,
|
|
||||||
Nodes: nodesToRecords(nodes[:4]),
|
|
||||||
})
|
|
||||||
test.packetIn(&v5wire.Nodes{
|
|
||||||
ReqID: p.ReqID,
|
|
||||||
RespCount: 2,
|
|
||||||
Nodes: nodesToRecords(nodes[4:]),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check results:
|
|
||||||
if err := <-done; err != nil {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(response, nodes) {
|
|
||||||
t.Fatalf("wrong nodes in response")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: check invalid IPs
|
|
||||||
// TODO: check invalid/unsigned record
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that pending calls are re-sent when a handshake happens.
|
|
||||||
func TestUDPv5_callResend(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPV5Test(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
remote := test.getNode(test.remotekey, test.remoteaddr).Node()
|
|
||||||
done := make(chan error, 2)
|
|
||||||
go func() {
|
|
||||||
_, err := test.udp.ping(remote)
|
|
||||||
done <- err
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
_, err := test.udp.ping(remote)
|
|
||||||
done <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Ping answered by WHOAREYOU.
|
|
||||||
test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, nonce v5wire.Nonce) {
|
|
||||||
test.packetIn(&v5wire.Whoareyou{Nonce: nonce})
|
|
||||||
})
|
|
||||||
// Ping should be re-sent.
|
|
||||||
test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) {
|
|
||||||
test.packetIn(&v5wire.Pong{ReqID: p.ReqID})
|
|
||||||
})
|
|
||||||
// Answer the other ping.
|
|
||||||
test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, _ v5wire.Nonce) {
|
|
||||||
test.packetIn(&v5wire.Pong{ReqID: p.ReqID})
|
|
||||||
})
|
|
||||||
if err := <-done; err != nil {
|
|
||||||
t.Fatalf("unexpected ping error: %v", err)
|
|
||||||
}
|
|
||||||
if err := <-done; err != nil {
|
|
||||||
t.Fatalf("unexpected ping error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test ensures we don't allow multiple rounds of WHOAREYOU for a single call.
|
|
||||||
func TestUDPv5_multipleHandshakeRounds(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPV5Test(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
remote := test.getNode(test.remotekey, test.remoteaddr).Node()
|
|
||||||
done := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
_, err := test.udp.ping(remote)
|
|
||||||
done <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Ping answered by WHOAREYOU.
|
|
||||||
test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, nonce v5wire.Nonce) {
|
|
||||||
test.packetIn(&v5wire.Whoareyou{Nonce: nonce})
|
|
||||||
})
|
|
||||||
// Ping answered by WHOAREYOU again.
|
|
||||||
test.waitPacketOut(func(p *v5wire.Ping, addr *net.UDPAddr, nonce v5wire.Nonce) {
|
|
||||||
test.packetIn(&v5wire.Whoareyou{Nonce: nonce})
|
|
||||||
})
|
|
||||||
if err := <-done; err != errTimeout {
|
|
||||||
t.Fatalf("unexpected ping error: %q", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that calls with n replies may take up to n * respTimeout.
|
|
||||||
func TestUDPv5_callTimeoutReset(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPV5Test(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
// Launch the request:
|
|
||||||
var (
|
|
||||||
distance = uint(230)
|
|
||||||
remote = test.getNode(test.remotekey, test.remoteaddr).Node()
|
|
||||||
nodes = nodesAtDistance(remote.ID(), int(distance), 8)
|
|
||||||
done = make(chan error, 1)
|
|
||||||
)
|
|
||||||
go func() {
|
|
||||||
_, err := test.udp.findnode(remote, []uint{distance})
|
|
||||||
done <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Serve two responses, slowly.
|
|
||||||
test.waitPacketOut(func(p *v5wire.Findnode, addr *net.UDPAddr, _ v5wire.Nonce) {
|
|
||||||
time.Sleep(respTimeout - 50*time.Millisecond)
|
|
||||||
test.packetIn(&v5wire.Nodes{
|
|
||||||
ReqID: p.ReqID,
|
|
||||||
RespCount: 2,
|
|
||||||
Nodes: nodesToRecords(nodes[:4]),
|
|
||||||
})
|
|
||||||
|
|
||||||
time.Sleep(respTimeout - 50*time.Millisecond)
|
|
||||||
test.packetIn(&v5wire.Nodes{
|
|
||||||
ReqID: p.ReqID,
|
|
||||||
RespCount: 2,
|
|
||||||
Nodes: nodesToRecords(nodes[4:]),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if err := <-done; err != nil {
|
|
||||||
t.Fatalf("unexpected error: %q", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that TALKREQ calls the registered handler function.
|
|
||||||
func TestUDPv5_talkHandling(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPV5Test(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
var recvMessage []byte
|
|
||||||
test.udp.RegisterTalkHandler("test", func(id enode.ID, addr *net.UDPAddr, message []byte) []byte {
|
|
||||||
recvMessage = message
|
|
||||||
return []byte("test response")
|
|
||||||
})
|
|
||||||
|
|
||||||
// Successful case:
|
|
||||||
test.packetIn(&v5wire.TalkRequest{
|
|
||||||
ReqID: []byte("foo"),
|
|
||||||
Protocol: "test",
|
|
||||||
Message: []byte("test request"),
|
|
||||||
})
|
|
||||||
test.waitPacketOut(func(p *v5wire.TalkResponse, addr *net.UDPAddr, _ v5wire.Nonce) {
|
|
||||||
if !bytes.Equal(p.ReqID, []byte("foo")) {
|
|
||||||
t.Error("wrong request ID in response:", p.ReqID)
|
|
||||||
}
|
|
||||||
if string(p.Message) != "test response" {
|
|
||||||
t.Errorf("wrong talk response message: %q", p.Message)
|
|
||||||
}
|
|
||||||
if string(recvMessage) != "test request" {
|
|
||||||
t.Errorf("wrong message received in handler: %q", recvMessage)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check that empty response is returned for unregistered protocols.
|
|
||||||
recvMessage = nil
|
|
||||||
test.packetIn(&v5wire.TalkRequest{
|
|
||||||
ReqID: []byte("2"),
|
|
||||||
Protocol: "wrong",
|
|
||||||
Message: []byte("test request"),
|
|
||||||
})
|
|
||||||
test.waitPacketOut(func(p *v5wire.TalkResponse, addr *net.UDPAddr, _ v5wire.Nonce) {
|
|
||||||
if !bytes.Equal(p.ReqID, []byte("2")) {
|
|
||||||
t.Error("wrong request ID in response:", p.ReqID)
|
|
||||||
}
|
|
||||||
if string(p.Message) != "" {
|
|
||||||
t.Errorf("wrong talk response message: %q", p.Message)
|
|
||||||
}
|
|
||||||
if recvMessage != nil {
|
|
||||||
t.Errorf("handler was called for wrong protocol: %q", recvMessage)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that outgoing TALKREQ calls work.
|
|
||||||
func TestUDPv5_talkRequest(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPV5Test(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
remote := test.getNode(test.remotekey, test.remoteaddr).Node()
|
|
||||||
done := make(chan error, 1)
|
|
||||||
|
|
||||||
// This request times out.
|
|
||||||
go func() {
|
|
||||||
_, err := test.udp.TalkRequest(remote, "test", []byte("test request"))
|
|
||||||
done <- err
|
|
||||||
}()
|
|
||||||
test.waitPacketOut(func(p *v5wire.TalkRequest, addr *net.UDPAddr, _ v5wire.Nonce) {})
|
|
||||||
if err := <-done; err != errTimeout {
|
|
||||||
t.Fatalf("want errTimeout, got %q", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This request works.
|
|
||||||
go func() {
|
|
||||||
_, err := test.udp.TalkRequest(remote, "test", []byte("test request"))
|
|
||||||
done <- err
|
|
||||||
}()
|
|
||||||
test.waitPacketOut(func(p *v5wire.TalkRequest, addr *net.UDPAddr, _ v5wire.Nonce) {
|
|
||||||
if p.Protocol != "test" {
|
|
||||||
t.Errorf("wrong protocol ID in talk request: %q", p.Protocol)
|
|
||||||
}
|
|
||||||
if string(p.Message) != "test request" {
|
|
||||||
t.Errorf("wrong message talk request: %q", p.Message)
|
|
||||||
}
|
|
||||||
test.packetInFrom(test.remotekey, test.remoteaddr, &v5wire.TalkResponse{
|
|
||||||
ReqID: p.ReqID,
|
|
||||||
Message: []byte("test response"),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if err := <-done; err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check requesting without ENR.
|
|
||||||
go func() {
|
|
||||||
_, err := test.udp.TalkRequestToID(remote.ID(), test.remoteaddr, "test", []byte("test request 2"))
|
|
||||||
done <- err
|
|
||||||
}()
|
|
||||||
test.waitPacketOut(func(p *v5wire.TalkRequest, addr *net.UDPAddr, _ v5wire.Nonce) {
|
|
||||||
if p.Protocol != "test" {
|
|
||||||
t.Errorf("wrong protocol ID in talk request: %q", p.Protocol)
|
|
||||||
}
|
|
||||||
if string(p.Message) != "test request 2" {
|
|
||||||
t.Errorf("wrong message talk request: %q", p.Message)
|
|
||||||
}
|
|
||||||
test.packetInFrom(test.remotekey, test.remoteaddr, &v5wire.TalkResponse{
|
|
||||||
ReqID: p.ReqID,
|
|
||||||
Message: []byte("test response 2"),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if err := <-done; err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that lookupDistances works.
|
|
||||||
func TestUDPv5_lookupDistances(t *testing.T) {
|
|
||||||
test := newUDPV5Test(t)
|
|
||||||
lnID := test.table.self().ID()
|
|
||||||
|
|
||||||
t.Run("target distance of 1", func(t *testing.T) {
|
|
||||||
node := nodeAtDistance(lnID, 1, intIP(0))
|
|
||||||
dists := lookupDistances(lnID, node.ID())
|
|
||||||
require.Equal(t, []uint{1, 2, 3}, dists)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("target distance of 2", func(t *testing.T) {
|
|
||||||
node := nodeAtDistance(lnID, 2, intIP(0))
|
|
||||||
dists := lookupDistances(lnID, node.ID())
|
|
||||||
require.Equal(t, []uint{2, 3, 1}, dists)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("target distance of 128", func(t *testing.T) {
|
|
||||||
node := nodeAtDistance(lnID, 128, intIP(0))
|
|
||||||
dists := lookupDistances(lnID, node.ID())
|
|
||||||
require.Equal(t, []uint{128, 129, 127}, dists)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("target distance of 255", func(t *testing.T) {
|
|
||||||
node := nodeAtDistance(lnID, 255, intIP(0))
|
|
||||||
dists := lookupDistances(lnID, node.ID())
|
|
||||||
require.Equal(t, []uint{255, 256, 254}, dists)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("target distance of 256", func(t *testing.T) {
|
|
||||||
node := nodeAtDistance(lnID, 256, intIP(0))
|
|
||||||
dists := lookupDistances(lnID, node.ID())
|
|
||||||
require.Equal(t, []uint{256, 255, 254}, dists)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that lookup works.
|
|
||||||
func TestUDPv5_lookup(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPV5Test(t)
|
|
||||||
|
|
||||||
// Lookup on empty table returns no nodes.
|
|
||||||
if results := test.udp.Lookup(lookupTestnet.target.id()); len(results) > 0 {
|
|
||||||
t.Fatalf("lookup on empty table returned %d results: %#v", len(results), results)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the tester knows all nodes in lookupTestnet by IP.
|
|
||||||
for d, nn := range lookupTestnet.dists {
|
|
||||||
for i, key := range nn {
|
|
||||||
n := lookupTestnet.node(d, i)
|
|
||||||
test.getNode(key, &net.UDPAddr{IP: n.IP(), Port: n.UDP()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed table with initial node.
|
|
||||||
initialNode := lookupTestnet.node(256, 0)
|
|
||||||
fillTable(test.table, []*node{wrapNode(initialNode)}, true)
|
|
||||||
|
|
||||||
// Start the lookup.
|
|
||||||
resultC := make(chan []*enode.Node, 1)
|
|
||||||
go func() {
|
|
||||||
resultC <- test.udp.Lookup(lookupTestnet.target.id())
|
|
||||||
test.close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
recipient, key := lookupTestnet.nodeByAddr(to)
|
|
||||||
switch p := p.(type) {
|
|
||||||
case *v5wire.Ping:
|
|
||||||
test.packetInFrom(key, to, &v5wire.Pong{ReqID: p.ReqID})
|
|
||||||
case *v5wire.Findnode:
|
|
||||||
if asked[recipient.ID()] {
|
|
||||||
t.Error("Asked node", recipient.ID(), "twice")
|
|
||||||
}
|
|
||||||
asked[recipient.ID()] = true
|
|
||||||
nodes := lookupTestnet.neighborsAtDistances(recipient, p.Distances, 16)
|
|
||||||
t.Logf("Got FINDNODE for %v, returning %d nodes", p.Distances, len(nodes))
|
|
||||||
for _, resp := range packNodes(p.ReqID, nodes) {
|
|
||||||
test.packetInFrom(key, to, resp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify result nodes.
|
|
||||||
results := <-resultC
|
|
||||||
checkLookupResults(t, lookupTestnet, results)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks the local node can be utilised to set key-values.
|
|
||||||
func TestUDPv5_LocalNode(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
var cfg Config
|
|
||||||
node := startLocalhostV5(t, cfg)
|
|
||||||
defer node.Close()
|
|
||||||
localNd := node.LocalNode()
|
|
||||||
|
|
||||||
// set value in node's local record
|
|
||||||
testVal := [4]byte{'A', 'B', 'C', 'D'}
|
|
||||||
localNd.Set(enr.WithEntry("testing", &testVal))
|
|
||||||
|
|
||||||
// retrieve the value from self to make sure it matches.
|
|
||||||
outputVal := [4]byte{}
|
|
||||||
if err := node.Self().Load(enr.WithEntry("testing", &outputVal)); err != nil {
|
|
||||||
t.Errorf("Could not load value from record: %v", err)
|
|
||||||
}
|
|
||||||
if testVal != outputVal {
|
|
||||||
t.Errorf("Wanted %#x to be retrieved from the record but instead got %#x", testVal, outputVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUDPv5_PingWithIPV4MappedAddress(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
test := newUDPV5Test(t)
|
|
||||||
defer test.close()
|
|
||||||
|
|
||||||
rawIP := net.IPv4(0xFF, 0x12, 0x33, 0xE5)
|
|
||||||
test.remoteaddr = &net.UDPAddr{
|
|
||||||
IP: rawIP.To16(),
|
|
||||||
Port: 0,
|
|
||||||
}
|
|
||||||
remote := test.getNode(test.remotekey, test.remoteaddr).Node()
|
|
||||||
done := make(chan struct{}, 1)
|
|
||||||
|
|
||||||
// This handler will truncate the ipv4-mapped in ipv6 address.
|
|
||||||
go func() {
|
|
||||||
test.udp.handlePing(&v5wire.Ping{ENRSeq: 1}, remote.ID(), test.remoteaddr)
|
|
||||||
done <- struct{}{}
|
|
||||||
}()
|
|
||||||
test.waitPacketOut(func(p *v5wire.Pong, addr *net.UDPAddr, _ v5wire.Nonce) {
|
|
||||||
if len(p.ToIP) == net.IPv6len {
|
|
||||||
t.Error("Received untruncated ip address")
|
|
||||||
}
|
|
||||||
if len(p.ToIP) != net.IPv4len {
|
|
||||||
t.Errorf("Received ip address with incorrect length: %d", len(p.ToIP))
|
|
||||||
}
|
|
||||||
if !p.ToIP.Equal(rawIP) {
|
|
||||||
t.Errorf("Received incorrect ip address: wanted %s but received %s", rawIP.String(), p.ToIP.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
<-done
|
|
||||||
}
|
|
||||||
|
|
||||||
// udpV5Test is the framework for all tests above.
|
|
||||||
// It runs the UDPv5 transport on a virtual socket and allows testing outgoing packets.
|
|
||||||
type udpV5Test struct {
|
|
||||||
t *testing.T
|
|
||||||
pipe *dgramPipe
|
|
||||||
table *Table
|
|
||||||
db *enode.DB
|
|
||||||
udp *UDPv5
|
|
||||||
localkey, remotekey *ecdsa.PrivateKey
|
|
||||||
remoteaddr *net.UDPAddr
|
|
||||||
nodesByID map[enode.ID]*enode.LocalNode
|
|
||||||
nodesByIP map[string]*enode.LocalNode
|
|
||||||
}
|
|
||||||
|
|
||||||
// testCodec is the packet encoding used by protocol tests. This codec does not perform encryption.
|
|
||||||
type testCodec struct {
|
|
||||||
test *udpV5Test
|
|
||||||
id enode.ID
|
|
||||||
ctr uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
type testCodecFrame struct {
|
|
||||||
NodeID enode.ID
|
|
||||||
AuthTag v5wire.Nonce
|
|
||||||
Ptype byte
|
|
||||||
Packet rlp.RawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCodec) Encode(toID enode.ID, addr string, p v5wire.Packet, _ *v5wire.Whoareyou) ([]byte, v5wire.Nonce, error) {
|
|
||||||
c.ctr++
|
|
||||||
var authTag v5wire.Nonce
|
|
||||||
binary.BigEndian.PutUint64(authTag[:], c.ctr)
|
|
||||||
|
|
||||||
penc, _ := rlp.EncodeToBytes(p)
|
|
||||||
frame, err := rlp.EncodeToBytes(testCodecFrame{c.id, authTag, p.Kind(), penc})
|
|
||||||
return frame, authTag, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCodec) Decode(input []byte, addr string) (enode.ID, *enode.Node, v5wire.Packet, error) {
|
|
||||||
frame, p, err := c.decodeFrame(input)
|
|
||||||
if err != nil {
|
|
||||||
return enode.ID{}, nil, nil, err
|
|
||||||
}
|
|
||||||
return frame.NodeID, nil, p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testCodec) decodeFrame(input []byte) (frame testCodecFrame, p v5wire.Packet, err error) {
|
|
||||||
if err = rlp.DecodeBytes(input, &frame); err != nil {
|
|
||||||
return frame, nil, fmt.Errorf("invalid frame: %v", err)
|
|
||||||
}
|
|
||||||
switch frame.Ptype {
|
|
||||||
case v5wire.UnknownPacket:
|
|
||||||
dec := new(v5wire.Unknown)
|
|
||||||
err = rlp.DecodeBytes(frame.Packet, &dec)
|
|
||||||
p = dec
|
|
||||||
case v5wire.WhoareyouPacket:
|
|
||||||
dec := new(v5wire.Whoareyou)
|
|
||||||
err = rlp.DecodeBytes(frame.Packet, &dec)
|
|
||||||
p = dec
|
|
||||||
default:
|
|
||||||
p, err = v5wire.DecodeMessage(frame.Ptype, frame.Packet)
|
|
||||||
}
|
|
||||||
return frame, p, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUDPV5Test(t *testing.T) *udpV5Test {
|
|
||||||
test := &udpV5Test{
|
|
||||||
t: t,
|
|
||||||
pipe: newpipe(),
|
|
||||||
localkey: newkey(),
|
|
||||||
remotekey: newkey(),
|
|
||||||
remoteaddr: &net.UDPAddr{IP: net.IP{10, 0, 1, 99}, Port: 30303},
|
|
||||||
nodesByID: make(map[enode.ID]*enode.LocalNode),
|
|
||||||
nodesByIP: make(map[string]*enode.LocalNode),
|
|
||||||
}
|
|
||||||
test.db, _ = enode.OpenDB("")
|
|
||||||
ln := enode.NewLocalNode(test.db, test.localkey)
|
|
||||||
ln.SetStaticIP(net.IP{10, 0, 0, 1})
|
|
||||||
ln.Set(enr.UDP(30303))
|
|
||||||
test.udp, _ = ListenV5(test.pipe, ln, Config{
|
|
||||||
PrivateKey: test.localkey,
|
|
||||||
Log: testlog.Logger(t, log.LvlTrace),
|
|
||||||
ValidSchemes: enode.ValidSchemesForTesting,
|
|
||||||
})
|
|
||||||
test.udp.codec = &testCodec{test: test, id: ln.ID()}
|
|
||||||
test.table = test.udp.tab
|
|
||||||
test.nodesByID[ln.ID()] = ln
|
|
||||||
// Wait for initial refresh so the table doesn't send unexpected findnode.
|
|
||||||
<-test.table.initDone
|
|
||||||
return test
|
|
||||||
}
|
|
||||||
|
|
||||||
// handles a packet as if it had been sent to the transport.
|
|
||||||
func (test *udpV5Test) packetIn(packet v5wire.Packet) {
|
|
||||||
test.t.Helper()
|
|
||||||
test.packetInFrom(test.remotekey, test.remoteaddr, 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) {
|
|
||||||
test.t.Helper()
|
|
||||||
|
|
||||||
ln := test.getNode(key, addr)
|
|
||||||
codec := &testCodec{test: test, id: ln.ID()}
|
|
||||||
enc, _, err := codec.Encode(test.udp.Self().ID(), addr.String(), packet, nil)
|
|
||||||
if err != nil {
|
|
||||||
test.t.Errorf("%s encode error: %v", packet.Name(), err)
|
|
||||||
}
|
|
||||||
if test.udp.dispatchReadPacket(addr, enc) {
|
|
||||||
<-test.udp.readNextCh // unblock UDPv5.dispatch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getNode ensures the test knows about a node at the given endpoint.
|
|
||||||
func (test *udpV5Test) getNode(key *ecdsa.PrivateKey, addr *net.UDPAddr) *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))
|
|
||||||
test.nodesByID[id] = ln
|
|
||||||
}
|
|
||||||
test.nodesByIP[string(addr.IP)] = ln
|
|
||||||
return ln
|
|
||||||
}
|
|
||||||
|
|
||||||
// waitPacketOut waits for the next output packet and handles it using the given 'validate'
|
|
||||||
// function. The function must be of type func (X, *net.UDPAddr, v5wire.Nonce) where X is
|
|
||||||
// assignable to packetV5.
|
|
||||||
func (test *udpV5Test) waitPacketOut(validate interface{}) (closed bool) {
|
|
||||||
test.t.Helper()
|
|
||||||
|
|
||||||
fn := reflect.ValueOf(validate)
|
|
||||||
exptype := fn.Type().In(0)
|
|
||||||
|
|
||||||
dgram, err := test.pipe.receive()
|
|
||||||
if err == errClosed {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if err == errTimeout {
|
|
||||||
test.t.Fatalf("timed out waiting for %v", exptype)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ln := test.nodesByIP[string(dgram.to.IP)]
|
|
||||||
if ln == nil {
|
|
||||||
test.t.Fatalf("attempt to send to non-existing node %v", &dgram.to)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
codec := &testCodec{test: test, id: ln.ID()}
|
|
||||||
frame, p, err := codec.decodeFrame(dgram.data)
|
|
||||||
if err != nil {
|
|
||||||
test.t.Errorf("sent packet decode error: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !reflect.TypeOf(p).AssignableTo(exptype) {
|
|
||||||
test.t.Errorf("sent packet type mismatch, got: %v, want: %v", reflect.TypeOf(p), exptype)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
fn.Call([]reflect.Value{reflect.ValueOf(p), reflect.ValueOf(&dgram.to), reflect.ValueOf(frame.AuthTag)})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (test *udpV5Test) close() {
|
|
||||||
test.t.Helper()
|
|
||||||
|
|
||||||
test.udp.Close()
|
|
||||||
test.db.Close()
|
|
||||||
for id, n := range test.nodesByID {
|
|
||||||
if id != test.udp.Self().ID() {
|
|
||||||
n.Database().Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(test.pipe.queue) != 0 {
|
|
||||||
test.t.Fatalf("%d unmatched UDP packets in queue", len(test.pipe.queue))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
// Copyright 2020 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package v5wire
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"hash"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"golang.org/x/crypto/hkdf"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Encryption/authentication parameters.
|
|
||||||
aesKeySize = 16
|
|
||||||
gcmNonceSize = 12
|
|
||||||
)
|
|
||||||
|
|
||||||
// Nonce represents a nonce used for AES/GCM.
|
|
||||||
type Nonce [gcmNonceSize]byte
|
|
||||||
|
|
||||||
// EncodePubkey encodes a public key.
|
|
||||||
func EncodePubkey(key *ecdsa.PublicKey) []byte {
|
|
||||||
switch key.Curve {
|
|
||||||
case crypto.S256():
|
|
||||||
return crypto.CompressPubkey(key)
|
|
||||||
default:
|
|
||||||
panic("unsupported curve " + key.Curve.Params().Name + " in EncodePubkey")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodePubkey decodes a public key in compressed format.
|
|
||||||
func DecodePubkey(curve elliptic.Curve, e []byte) (*ecdsa.PublicKey, error) {
|
|
||||||
switch curve {
|
|
||||||
case crypto.S256():
|
|
||||||
if len(e) != 33 {
|
|
||||||
return nil, errors.New("wrong size public key data")
|
|
||||||
}
|
|
||||||
return crypto.DecompressPubkey(e)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported curve %s in DecodePubkey", curve.Params().Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// idNonceHash computes the ID signature hash used in the handshake.
|
|
||||||
func idNonceHash(h hash.Hash, challenge, ephkey []byte, destID enode.ID) []byte {
|
|
||||||
h.Reset()
|
|
||||||
h.Write([]byte("discovery v5 identity proof"))
|
|
||||||
h.Write(challenge)
|
|
||||||
h.Write(ephkey)
|
|
||||||
h.Write(destID[:])
|
|
||||||
return h.Sum(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeIDSignature creates the ID nonce signature.
|
|
||||||
func makeIDSignature(hash hash.Hash, key *ecdsa.PrivateKey, challenge, ephkey []byte, destID enode.ID) ([]byte, error) {
|
|
||||||
input := idNonceHash(hash, challenge, ephkey, destID)
|
|
||||||
switch key.Curve {
|
|
||||||
case crypto.S256():
|
|
||||||
idsig, err := crypto.Sign(input, key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return idsig[:len(idsig)-1], nil // remove recovery ID
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported curve %s", key.Curve.Params().Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// s256raw is an unparsed secp256k1 public key ENR entry.
|
|
||||||
type s256raw []byte
|
|
||||||
|
|
||||||
func (s256raw) ENRKey() string { return "secp256k1" }
|
|
||||||
|
|
||||||
// verifyIDSignature checks that signature over idnonce was made by the given node.
|
|
||||||
func verifyIDSignature(hash hash.Hash, sig []byte, n *enode.Node, challenge, ephkey []byte, destID enode.ID) error {
|
|
||||||
switch idscheme := n.Record().IdentityScheme(); idscheme {
|
|
||||||
case "v4":
|
|
||||||
var pubkey s256raw
|
|
||||||
if n.Load(&pubkey) != nil {
|
|
||||||
return errors.New("no secp256k1 public key in record")
|
|
||||||
}
|
|
||||||
input := idNonceHash(hash, challenge, ephkey, destID)
|
|
||||||
if !crypto.VerifySignature(pubkey, input, sig) {
|
|
||||||
return errInvalidNonceSig
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("can't verify ID nonce signature against scheme %q", idscheme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type hashFn func() hash.Hash
|
|
||||||
|
|
||||||
// deriveKeys creates the session keys.
|
|
||||||
func deriveKeys(hash hashFn, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, n1, n2 enode.ID, challenge []byte) *session {
|
|
||||||
const text = "discovery v5 key agreement"
|
|
||||||
var info = make([]byte, 0, len(text)+len(n1)+len(n2))
|
|
||||||
info = append(info, text...)
|
|
||||||
info = append(info, n1[:]...)
|
|
||||||
info = append(info, n2[:]...)
|
|
||||||
|
|
||||||
eph := ecdh(priv, pub)
|
|
||||||
if eph == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
kdf := hkdf.New(hash, eph, challenge, info)
|
|
||||||
sec := session{writeKey: make([]byte, aesKeySize), readKey: make([]byte, aesKeySize)}
|
|
||||||
kdf.Read(sec.writeKey)
|
|
||||||
kdf.Read(sec.readKey)
|
|
||||||
for i := range eph {
|
|
||||||
eph[i] = 0
|
|
||||||
}
|
|
||||||
return &sec
|
|
||||||
}
|
|
||||||
|
|
||||||
// ecdh creates a shared secret.
|
|
||||||
func ecdh(privkey *ecdsa.PrivateKey, pubkey *ecdsa.PublicKey) []byte {
|
|
||||||
secX, secY := pubkey.ScalarMult(pubkey.X, pubkey.Y, privkey.D.Bytes())
|
|
||||||
if secX == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
sec := make([]byte, 33)
|
|
||||||
sec[0] = 0x02 | byte(secY.Bit(0))
|
|
||||||
math.ReadBits(secX, sec[1:])
|
|
||||||
return sec
|
|
||||||
}
|
|
||||||
|
|
||||||
// encryptGCM encrypts pt using AES-GCM with the given key and nonce. The ciphertext is
|
|
||||||
// appended to dest, which must not overlap with plaintext. The resulting ciphertext is 16
|
|
||||||
// bytes longer than plaintext because it contains an authentication tag.
|
|
||||||
func encryptGCM(dest, key, nonce, plaintext, authData []byte) ([]byte, error) {
|
|
||||||
block, err := aes.NewCipher(key)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("can't create block cipher: %v", err))
|
|
||||||
}
|
|
||||||
aesgcm, err := cipher.NewGCMWithNonceSize(block, gcmNonceSize)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("can't create GCM: %v", err))
|
|
||||||
}
|
|
||||||
return aesgcm.Seal(dest, nonce, plaintext, authData), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// decryptGCM decrypts ct using AES-GCM with the given key and nonce.
|
|
||||||
func decryptGCM(key, nonce, ct, authData []byte) ([]byte, error) {
|
|
||||||
block, err := aes.NewCipher(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't create block cipher: %v", err)
|
|
||||||
}
|
|
||||||
if len(nonce) != gcmNonceSize {
|
|
||||||
return nil, fmt.Errorf("invalid GCM nonce size: %d", len(nonce))
|
|
||||||
}
|
|
||||||
aesgcm, err := cipher.NewGCMWithNonceSize(block, gcmNonceSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't create GCM: %v", err)
|
|
||||||
}
|
|
||||||
pt := make([]byte, 0, len(ct))
|
|
||||||
return aesgcm.Open(pt, nonce, ct, authData)
|
|
||||||
}
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
// Copyright 2020 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package v5wire
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/sha256"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestVector_ECDH(t *testing.T) {
|
|
||||||
var (
|
|
||||||
staticKey = hexPrivkey("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736")
|
|
||||||
publicKey = hexPubkey(crypto.S256(), "0x039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231")
|
|
||||||
want = hexutil.MustDecode("0x033b11a2a1f214567e1537ce5e509ffd9b21373247f2a3ff6841f4976f53165e7e")
|
|
||||||
)
|
|
||||||
result := ecdh(staticKey, publicKey)
|
|
||||||
check(t, "shared-secret", result, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVector_KDF(t *testing.T) {
|
|
||||||
var (
|
|
||||||
ephKey = hexPrivkey("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736")
|
|
||||||
cdata = hexutil.MustDecode("0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000")
|
|
||||||
net = newHandshakeTest()
|
|
||||||
)
|
|
||||||
defer net.close()
|
|
||||||
|
|
||||||
destKey := &testKeyB.PublicKey
|
|
||||||
s := deriveKeys(sha256.New, ephKey, destKey, net.nodeA.id(), net.nodeB.id(), cdata)
|
|
||||||
t.Logf("ephemeral-key = %#x", ephKey.D)
|
|
||||||
t.Logf("dest-pubkey = %#x", EncodePubkey(destKey))
|
|
||||||
t.Logf("node-id-a = %#x", net.nodeA.id().Bytes())
|
|
||||||
t.Logf("node-id-b = %#x", net.nodeB.id().Bytes())
|
|
||||||
t.Logf("challenge-data = %#x", cdata)
|
|
||||||
check(t, "initiator-key", s.writeKey, hexutil.MustDecode("0xdccc82d81bd610f4f76d3ebe97a40571"))
|
|
||||||
check(t, "recipient-key", s.readKey, hexutil.MustDecode("0xac74bb8773749920b0d3a8881c173ec5"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVector_IDSignature(t *testing.T) {
|
|
||||||
var (
|
|
||||||
key = hexPrivkey("0xfb757dc581730490a1d7a00deea65e9b1936924caaea8f44d476014856b68736")
|
|
||||||
destID = enode.HexID("0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9")
|
|
||||||
ephkey = hexutil.MustDecode("0x039961e4c2356d61bedb83052c115d311acb3a96f5777296dcf297351130266231")
|
|
||||||
cdata = hexutil.MustDecode("0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000")
|
|
||||||
)
|
|
||||||
|
|
||||||
sig, err := makeIDSignature(sha256.New(), key, cdata, ephkey, destID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Logf("static-key = %#x", key.D)
|
|
||||||
t.Logf("challenge-data = %#x", cdata)
|
|
||||||
t.Logf("ephemeral-pubkey = %#x", ephkey)
|
|
||||||
t.Logf("node-id-B = %#x", destID.Bytes())
|
|
||||||
expected := "0x94852a1e2318c4e5e9d422c98eaf19d1d90d876b29cd06ca7cb7546d0fff7b484fe86c09a064fe72bdbef73ba8e9c34df0cd2b53e9d65528c2c7f336d5dfc6e6"
|
|
||||||
check(t, "id-signature", sig, hexutil.MustDecode(expected))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeriveKeys(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
n1 = enode.ID{1}
|
|
||||||
n2 = enode.ID{2}
|
|
||||||
cdata = []byte{1, 2, 3, 4}
|
|
||||||
)
|
|
||||||
sec1 := deriveKeys(sha256.New, testKeyA, &testKeyB.PublicKey, n1, n2, cdata)
|
|
||||||
sec2 := deriveKeys(sha256.New, testKeyB, &testKeyA.PublicKey, n1, n2, cdata)
|
|
||||||
if sec1 == nil || sec2 == nil {
|
|
||||||
t.Fatal("key agreement failed")
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(sec1, sec2) {
|
|
||||||
t.Fatalf("keys not equal:\n %+v\n %+v", sec1, sec2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func check(t *testing.T, what string, x, y []byte) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
if !bytes.Equal(x, y) {
|
|
||||||
t.Errorf("wrong %s: %#x != %#x", what, x, y)
|
|
||||||
} else {
|
|
||||||
t.Logf("%s = %#x", what, x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func hexPrivkey(input string) *ecdsa.PrivateKey {
|
|
||||||
key, err := crypto.HexToECDSA(strings.TrimPrefix(input, "0x"))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
func hexPubkey(curve elliptic.Curve, input string) *ecdsa.PublicKey {
|
|
||||||
key, err := DecodePubkey(curve, hexutil.MustDecode(input))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
@ -1,672 +0,0 @@
|
||||||
// Copyright 2020 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package v5wire
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
crand "crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"hash"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO concurrent WHOAREYOU tie-breaker
|
|
||||||
// TODO rehandshake after X packets
|
|
||||||
|
|
||||||
// Header represents a packet header.
|
|
||||||
type Header struct {
|
|
||||||
IV [sizeofMaskingIV]byte
|
|
||||||
StaticHeader
|
|
||||||
AuthData []byte
|
|
||||||
|
|
||||||
src enode.ID // used by decoder
|
|
||||||
}
|
|
||||||
|
|
||||||
// StaticHeader contains the static fields of a packet header.
|
|
||||||
type StaticHeader struct {
|
|
||||||
ProtocolID [6]byte
|
|
||||||
Version uint16
|
|
||||||
Flag byte
|
|
||||||
Nonce Nonce
|
|
||||||
AuthSize uint16
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authdata layouts.
|
|
||||||
type (
|
|
||||||
whoareyouAuthData struct {
|
|
||||||
IDNonce [16]byte // ID proof data
|
|
||||||
RecordSeq uint64 // highest known ENR sequence of requester
|
|
||||||
}
|
|
||||||
|
|
||||||
handshakeAuthData struct {
|
|
||||||
h struct {
|
|
||||||
SrcID enode.ID
|
|
||||||
SigSize byte // signature data
|
|
||||||
PubkeySize byte // offset of
|
|
||||||
}
|
|
||||||
// Trailing variable-size data.
|
|
||||||
signature, pubkey, record []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
messageAuthData struct {
|
|
||||||
SrcID enode.ID
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Packet header flag values.
|
|
||||||
const (
|
|
||||||
flagMessage = iota
|
|
||||||
flagWhoareyou
|
|
||||||
flagHandshake
|
|
||||||
)
|
|
||||||
|
|
||||||
// Protocol constants.
|
|
||||||
const (
|
|
||||||
version = 1
|
|
||||||
minVersion = 1
|
|
||||||
sizeofMaskingIV = 16
|
|
||||||
|
|
||||||
// The minimum size of any Discovery v5 packet is 63 bytes.
|
|
||||||
// Should reject packets smaller than minPacketSize.
|
|
||||||
minPacketSize = 63
|
|
||||||
|
|
||||||
maxPacketSize = 1280
|
|
||||||
|
|
||||||
minMessageSize = 48 // this refers to data after static headers
|
|
||||||
randomPacketMsgSize = 20
|
|
||||||
)
|
|
||||||
|
|
||||||
var DefaultProtocolID = [6]byte{'d', 'i', 's', 'c', 'v', '5'}
|
|
||||||
|
|
||||||
// Errors.
|
|
||||||
var (
|
|
||||||
errTooShort = errors.New("packet too short")
|
|
||||||
errInvalidHeader = errors.New("invalid packet header")
|
|
||||||
errInvalidFlag = errors.New("invalid flag value in header")
|
|
||||||
errMinVersion = errors.New("version of packet header below minimum")
|
|
||||||
errMsgTooShort = errors.New("message/handshake packet below minimum size")
|
|
||||||
errAuthSize = errors.New("declared auth size is beyond packet length")
|
|
||||||
errUnexpectedHandshake = errors.New("unexpected auth response, not in handshake")
|
|
||||||
errInvalidAuthKey = errors.New("invalid ephemeral pubkey")
|
|
||||||
errNoRecord = errors.New("expected ENR in handshake but none sent")
|
|
||||||
errInvalidNonceSig = errors.New("invalid ID nonce signature")
|
|
||||||
errMessageTooShort = errors.New("message contains no data")
|
|
||||||
errMessageDecrypt = errors.New("cannot decrypt message")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Public errors.
|
|
||||||
var (
|
|
||||||
// ErrInvalidReqID represents error when the ID is invalid.
|
|
||||||
ErrInvalidReqID = errors.New("request ID larger than 8 bytes")
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsInvalidHeader reports whether 'err' is related to an invalid packet header. When it
|
|
||||||
// returns false, it is pretty certain that the packet causing the error does not belong
|
|
||||||
// to discv5.
|
|
||||||
func IsInvalidHeader(err error) bool {
|
|
||||||
return err == errTooShort || err == errInvalidHeader || err == errMsgTooShort
|
|
||||||
}
|
|
||||||
|
|
||||||
// Packet sizes.
|
|
||||||
var (
|
|
||||||
sizeofStaticHeader = binary.Size(StaticHeader{})
|
|
||||||
sizeofWhoareyouAuthData = binary.Size(whoareyouAuthData{})
|
|
||||||
sizeofHandshakeAuthData = binary.Size(handshakeAuthData{}.h)
|
|
||||||
sizeofMessageAuthData = binary.Size(messageAuthData{})
|
|
||||||
sizeofStaticPacketData = sizeofMaskingIV + sizeofStaticHeader
|
|
||||||
)
|
|
||||||
|
|
||||||
// Codec encodes and decodes Discovery v5 packets.
|
|
||||||
// This type is not safe for concurrent use.
|
|
||||||
type Codec struct {
|
|
||||||
sha256 hash.Hash
|
|
||||||
localnode *enode.LocalNode
|
|
||||||
privkey *ecdsa.PrivateKey
|
|
||||||
sc *SessionCache
|
|
||||||
protocolID [6]byte
|
|
||||||
|
|
||||||
// encoder buffers
|
|
||||||
buf bytes.Buffer // whole packet
|
|
||||||
headbuf bytes.Buffer // packet header
|
|
||||||
msgbuf bytes.Buffer // message RLP plaintext
|
|
||||||
msgctbuf []byte // message data ciphertext
|
|
||||||
|
|
||||||
// decoder buffer
|
|
||||||
decbuf []byte
|
|
||||||
reader bytes.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCodec creates a wire codec.
|
|
||||||
func NewCodec(ln *enode.LocalNode, key *ecdsa.PrivateKey, clock mclock.Clock, protocolID *[6]byte) *Codec {
|
|
||||||
c := &Codec{
|
|
||||||
sha256: sha256.New(),
|
|
||||||
localnode: ln,
|
|
||||||
privkey: key,
|
|
||||||
sc: NewSessionCache(1024, clock),
|
|
||||||
protocolID: DefaultProtocolID,
|
|
||||||
decbuf: make([]byte, maxPacketSize),
|
|
||||||
}
|
|
||||||
if protocolID != nil {
|
|
||||||
c.protocolID = *protocolID
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode encodes a packet to a node. 'id' and 'addr' specify the destination node. The
|
|
||||||
// 'challenge' parameter should be the most recently received WHOAREYOU packet from that
|
|
||||||
// node.
|
|
||||||
func (c *Codec) Encode(id enode.ID, addr string, packet Packet, challenge *Whoareyou) ([]byte, Nonce, error) {
|
|
||||||
// Create the packet header.
|
|
||||||
var (
|
|
||||||
head Header
|
|
||||||
session *session
|
|
||||||
msgData []byte
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
switch {
|
|
||||||
case packet.Kind() == WhoareyouPacket:
|
|
||||||
head, err = c.encodeWhoareyou(id, packet.(*Whoareyou))
|
|
||||||
case challenge != nil:
|
|
||||||
// We have an unanswered challenge, send handshake.
|
|
||||||
head, session, err = c.encodeHandshakeHeader(id, addr, challenge)
|
|
||||||
default:
|
|
||||||
session = c.sc.session(id, addr)
|
|
||||||
if session != nil {
|
|
||||||
// There is a session, use it.
|
|
||||||
head, err = c.encodeMessageHeader(id, session)
|
|
||||||
} else {
|
|
||||||
// No keys, send random data to kick off the handshake.
|
|
||||||
head, msgData, err = c.encodeRandom(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, Nonce{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate masking IV.
|
|
||||||
if err := c.sc.maskingIVGen(head.IV[:]); err != nil {
|
|
||||||
return nil, Nonce{}, fmt.Errorf("can't generate masking IV: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode header data.
|
|
||||||
c.writeHeaders(&head)
|
|
||||||
|
|
||||||
// Store sent WHOAREYOU challenges.
|
|
||||||
if challenge, ok := packet.(*Whoareyou); ok {
|
|
||||||
challenge.ChallengeData = bytesCopy(&c.buf)
|
|
||||||
c.sc.storeSentHandshake(id, addr, challenge)
|
|
||||||
} else if msgData == nil {
|
|
||||||
headerData := c.buf.Bytes()
|
|
||||||
msgData, err = c.encryptMessage(session, packet, &head, headerData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, Nonce{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enc, err := c.EncodeRaw(id, head, msgData)
|
|
||||||
return enc, head.Nonce, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodeRaw encodes a packet with the given header.
|
|
||||||
func (c *Codec) EncodeRaw(id enode.ID, head Header, msgdata []byte) ([]byte, error) {
|
|
||||||
c.writeHeaders(&head)
|
|
||||||
|
|
||||||
// Apply masking.
|
|
||||||
masked := c.buf.Bytes()[sizeofMaskingIV:]
|
|
||||||
mask := head.mask(id)
|
|
||||||
mask.XORKeyStream(masked[:], masked[:])
|
|
||||||
|
|
||||||
// Write message data.
|
|
||||||
c.buf.Write(msgdata)
|
|
||||||
return c.buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Codec) writeHeaders(head *Header) {
|
|
||||||
c.buf.Reset()
|
|
||||||
c.buf.Write(head.IV[:])
|
|
||||||
binary.Write(&c.buf, binary.BigEndian, &head.StaticHeader)
|
|
||||||
c.buf.Write(head.AuthData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeHeader creates a packet header.
|
|
||||||
func (c *Codec) makeHeader(toID enode.ID, flag byte, authsizeExtra int) Header {
|
|
||||||
var authsize int
|
|
||||||
switch flag {
|
|
||||||
case flagMessage:
|
|
||||||
authsize = sizeofMessageAuthData
|
|
||||||
case flagWhoareyou:
|
|
||||||
authsize = sizeofWhoareyouAuthData
|
|
||||||
case flagHandshake:
|
|
||||||
authsize = sizeofHandshakeAuthData
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("BUG: invalid packet header flag %x", flag))
|
|
||||||
}
|
|
||||||
authsize += authsizeExtra
|
|
||||||
if authsize > int(^uint16(0)) {
|
|
||||||
panic(fmt.Errorf("BUG: auth size %d overflows uint16", authsize))
|
|
||||||
}
|
|
||||||
return Header{
|
|
||||||
StaticHeader: StaticHeader{
|
|
||||||
ProtocolID: c.protocolID,
|
|
||||||
Version: version,
|
|
||||||
Flag: flag,
|
|
||||||
AuthSize: uint16(authsize),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// encodeRandom encodes a packet with random content.
|
|
||||||
func (c *Codec) encodeRandom(toID enode.ID) (Header, []byte, error) {
|
|
||||||
head := c.makeHeader(toID, flagMessage, 0)
|
|
||||||
|
|
||||||
// Encode auth data.
|
|
||||||
auth := messageAuthData{SrcID: c.localnode.ID()}
|
|
||||||
if _, err := crand.Read(head.Nonce[:]); err != nil {
|
|
||||||
return head, nil, fmt.Errorf("can't get random data: %v", err)
|
|
||||||
}
|
|
||||||
c.headbuf.Reset()
|
|
||||||
binary.Write(&c.headbuf, binary.BigEndian, auth)
|
|
||||||
head.AuthData = c.headbuf.Bytes()
|
|
||||||
|
|
||||||
// Fill message ciphertext buffer with random bytes.
|
|
||||||
c.msgctbuf = append(c.msgctbuf[:0], make([]byte, randomPacketMsgSize)...)
|
|
||||||
crand.Read(c.msgctbuf)
|
|
||||||
return head, c.msgctbuf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// encodeWhoareyou encodes a WHOAREYOU packet.
|
|
||||||
func (c *Codec) encodeWhoareyou(toID enode.ID, packet *Whoareyou) (Header, error) {
|
|
||||||
// Sanity check node field to catch misbehaving callers.
|
|
||||||
if packet.RecordSeq > 0 && packet.Node == nil {
|
|
||||||
panic("BUG: missing node in whoareyou with non-zero seq")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create header.
|
|
||||||
head := c.makeHeader(toID, flagWhoareyou, 0)
|
|
||||||
head.AuthData = bytesCopy(&c.buf)
|
|
||||||
head.Nonce = packet.Nonce
|
|
||||||
|
|
||||||
// Encode auth data.
|
|
||||||
auth := &whoareyouAuthData{
|
|
||||||
IDNonce: packet.IDNonce,
|
|
||||||
RecordSeq: packet.RecordSeq,
|
|
||||||
}
|
|
||||||
c.headbuf.Reset()
|
|
||||||
binary.Write(&c.headbuf, binary.BigEndian, auth)
|
|
||||||
head.AuthData = c.headbuf.Bytes()
|
|
||||||
return head, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// encodeHandshakeHeader encodes the handshake message packet header.
|
|
||||||
func (c *Codec) encodeHandshakeHeader(toID enode.ID, addr string, challenge *Whoareyou) (Header, *session, error) {
|
|
||||||
// Ensure calling code sets challenge.node.
|
|
||||||
if challenge.Node == nil {
|
|
||||||
panic("BUG: missing challenge.Node in encode")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate new secrets.
|
|
||||||
auth, session, err := c.makeHandshakeAuth(toID, addr, challenge)
|
|
||||||
if err != nil {
|
|
||||||
return Header{}, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate nonce for message.
|
|
||||||
nonce, err := c.sc.nextNonce(session)
|
|
||||||
if err != nil {
|
|
||||||
return Header{}, nil, fmt.Errorf("can't generate nonce: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this should happen when the first authenticated message is received
|
|
||||||
c.sc.storeNewSession(toID, addr, session)
|
|
||||||
|
|
||||||
// Encode the auth header.
|
|
||||||
var (
|
|
||||||
authsizeExtra = len(auth.pubkey) + len(auth.signature) + len(auth.record)
|
|
||||||
head = c.makeHeader(toID, flagHandshake, authsizeExtra)
|
|
||||||
)
|
|
||||||
c.headbuf.Reset()
|
|
||||||
binary.Write(&c.headbuf, binary.BigEndian, &auth.h)
|
|
||||||
c.headbuf.Write(auth.signature)
|
|
||||||
c.headbuf.Write(auth.pubkey)
|
|
||||||
c.headbuf.Write(auth.record)
|
|
||||||
head.AuthData = c.headbuf.Bytes()
|
|
||||||
head.Nonce = nonce
|
|
||||||
return head, session, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeHandshakeAuth creates the auth header on a request packet following WHOAREYOU.
|
|
||||||
func (c *Codec) makeHandshakeAuth(toID enode.ID, addr string, challenge *Whoareyou) (*handshakeAuthData, *session, error) {
|
|
||||||
auth := new(handshakeAuthData)
|
|
||||||
auth.h.SrcID = c.localnode.ID()
|
|
||||||
|
|
||||||
// Create the ephemeral key. This needs to be first because the
|
|
||||||
// key is part of the ID nonce signature.
|
|
||||||
var remotePubkey = new(ecdsa.PublicKey)
|
|
||||||
if err := challenge.Node.Load((*enode.Secp256k1)(remotePubkey)); err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("can't find secp256k1 key for recipient")
|
|
||||||
}
|
|
||||||
ephkey, err := c.sc.ephemeralKeyGen()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("can't generate ephemeral key")
|
|
||||||
}
|
|
||||||
ephpubkey := EncodePubkey(&ephkey.PublicKey)
|
|
||||||
auth.pubkey = ephpubkey[:]
|
|
||||||
auth.h.PubkeySize = byte(len(auth.pubkey))
|
|
||||||
|
|
||||||
// Add ID nonce signature to response.
|
|
||||||
cdata := challenge.ChallengeData
|
|
||||||
idsig, err := makeIDSignature(c.sha256, c.privkey, cdata, ephpubkey[:], toID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("can't sign: %v", err)
|
|
||||||
}
|
|
||||||
auth.signature = idsig
|
|
||||||
auth.h.SigSize = byte(len(auth.signature))
|
|
||||||
|
|
||||||
// Add our record to response if it's newer than what remote side has.
|
|
||||||
ln := c.localnode.Node()
|
|
||||||
if challenge.RecordSeq < ln.Seq() {
|
|
||||||
auth.record, _ = rlp.EncodeToBytes(ln.Record())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create session keys.
|
|
||||||
sec := deriveKeys(sha256.New, ephkey, remotePubkey, c.localnode.ID(), challenge.Node.ID(), cdata)
|
|
||||||
if sec == nil {
|
|
||||||
return nil, nil, fmt.Errorf("key derivation failed")
|
|
||||||
}
|
|
||||||
return auth, sec, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// encodeMessageHeader encodes an encrypted message packet.
|
|
||||||
func (c *Codec) encodeMessageHeader(toID enode.ID, s *session) (Header, error) {
|
|
||||||
head := c.makeHeader(toID, flagMessage, 0)
|
|
||||||
|
|
||||||
// Create the header.
|
|
||||||
nonce, err := c.sc.nextNonce(s)
|
|
||||||
if err != nil {
|
|
||||||
return Header{}, fmt.Errorf("can't generate nonce: %v", err)
|
|
||||||
}
|
|
||||||
auth := messageAuthData{SrcID: c.localnode.ID()}
|
|
||||||
c.buf.Reset()
|
|
||||||
binary.Write(&c.buf, binary.BigEndian, &auth)
|
|
||||||
head.AuthData = bytesCopy(&c.buf)
|
|
||||||
head.Nonce = nonce
|
|
||||||
return head, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Codec) encryptMessage(s *session, p Packet, head *Header, headerData []byte) ([]byte, error) {
|
|
||||||
// Encode message plaintext.
|
|
||||||
c.msgbuf.Reset()
|
|
||||||
c.msgbuf.WriteByte(p.Kind())
|
|
||||||
if err := rlp.Encode(&c.msgbuf, p); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
messagePT := c.msgbuf.Bytes()
|
|
||||||
|
|
||||||
// Encrypt into message ciphertext buffer.
|
|
||||||
messageCT, err := encryptGCM(c.msgctbuf[:0], s.writeKey, head.Nonce[:], messagePT, headerData)
|
|
||||||
if err == nil {
|
|
||||||
c.msgctbuf = messageCT
|
|
||||||
}
|
|
||||||
return messageCT, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode decodes a discovery packet.
|
|
||||||
func (c *Codec) Decode(inputData []byte, addr string) (src enode.ID, n *enode.Node, p Packet, err error) {
|
|
||||||
if len(inputData) < minPacketSize {
|
|
||||||
return enode.ID{}, nil, nil, errTooShort
|
|
||||||
}
|
|
||||||
// Copy the packet to a tmp buffer to avoid modifying it.
|
|
||||||
c.decbuf = append(c.decbuf[:0], inputData...)
|
|
||||||
input := c.decbuf
|
|
||||||
// Unmask the static header.
|
|
||||||
var head Header
|
|
||||||
copy(head.IV[:], input[:sizeofMaskingIV])
|
|
||||||
mask := head.mask(c.localnode.ID())
|
|
||||||
staticHeader := input[sizeofMaskingIV:sizeofStaticPacketData]
|
|
||||||
mask.XORKeyStream(staticHeader, staticHeader)
|
|
||||||
|
|
||||||
// Decode and verify the static header.
|
|
||||||
c.reader.Reset(staticHeader)
|
|
||||||
binary.Read(&c.reader, binary.BigEndian, &head.StaticHeader)
|
|
||||||
remainingInput := len(input) - sizeofStaticPacketData
|
|
||||||
if err := head.checkValid(remainingInput, c.protocolID); err != nil {
|
|
||||||
return enode.ID{}, nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmask auth data.
|
|
||||||
authDataEnd := sizeofStaticPacketData + int(head.AuthSize)
|
|
||||||
authData := input[sizeofStaticPacketData:authDataEnd]
|
|
||||||
mask.XORKeyStream(authData, authData)
|
|
||||||
head.AuthData = authData
|
|
||||||
|
|
||||||
// Delete timed-out handshakes. This must happen before decoding to avoid
|
|
||||||
// processing the same handshake twice.
|
|
||||||
c.sc.handshakeGC()
|
|
||||||
|
|
||||||
// Decode auth part and message.
|
|
||||||
headerData := input[:authDataEnd]
|
|
||||||
msgData := input[authDataEnd:]
|
|
||||||
switch head.Flag {
|
|
||||||
case flagWhoareyou:
|
|
||||||
p, err = c.decodeWhoareyou(&head, headerData)
|
|
||||||
case flagHandshake:
|
|
||||||
n, p, err = c.decodeHandshakeMessage(addr, &head, headerData, msgData)
|
|
||||||
case flagMessage:
|
|
||||||
p, err = c.decodeMessage(addr, &head, headerData, msgData)
|
|
||||||
default:
|
|
||||||
err = errInvalidFlag
|
|
||||||
}
|
|
||||||
return head.src, n, p, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// decodeWhoareyou reads packet data after the header as a WHOAREYOU packet.
|
|
||||||
func (c *Codec) decodeWhoareyou(head *Header, headerData []byte) (Packet, error) {
|
|
||||||
if len(head.AuthData) != sizeofWhoareyouAuthData {
|
|
||||||
return nil, fmt.Errorf("invalid auth size %d for WHOAREYOU", len(head.AuthData))
|
|
||||||
}
|
|
||||||
var auth whoareyouAuthData
|
|
||||||
c.reader.Reset(head.AuthData)
|
|
||||||
binary.Read(&c.reader, binary.BigEndian, &auth)
|
|
||||||
p := &Whoareyou{
|
|
||||||
Nonce: head.Nonce,
|
|
||||||
IDNonce: auth.IDNonce,
|
|
||||||
RecordSeq: auth.RecordSeq,
|
|
||||||
ChallengeData: make([]byte, len(headerData)),
|
|
||||||
}
|
|
||||||
copy(p.ChallengeData, headerData)
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Codec) decodeHandshakeMessage(fromAddr string, head *Header, headerData, msgData []byte) (n *enode.Node, p Packet, err error) {
|
|
||||||
node, auth, session, err := c.decodeHandshake(fromAddr, head)
|
|
||||||
if err != nil {
|
|
||||||
c.sc.deleteHandshake(auth.h.SrcID, fromAddr)
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypt the message using the new session keys.
|
|
||||||
msg, err := c.decryptMessage(msgData, head.Nonce[:], headerData, session.readKey)
|
|
||||||
if err != nil {
|
|
||||||
c.sc.deleteHandshake(auth.h.SrcID, fromAddr)
|
|
||||||
return node, msg, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handshake OK, drop the challenge and store the new session keys.
|
|
||||||
c.sc.storeNewSession(auth.h.SrcID, fromAddr, session)
|
|
||||||
c.sc.deleteHandshake(auth.h.SrcID, fromAddr)
|
|
||||||
return node, msg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Codec) decodeHandshake(fromAddr string, head *Header) (n *enode.Node, auth handshakeAuthData, s *session, err error) {
|
|
||||||
if auth, err = c.decodeHandshakeAuthData(head); err != nil {
|
|
||||||
return nil, auth, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify against our last WHOAREYOU.
|
|
||||||
challenge := c.sc.getHandshake(auth.h.SrcID, fromAddr)
|
|
||||||
if challenge == nil {
|
|
||||||
return nil, auth, nil, errUnexpectedHandshake
|
|
||||||
}
|
|
||||||
// Get node record.
|
|
||||||
n, err = c.decodeHandshakeRecord(challenge.Node, auth.h.SrcID, auth.record)
|
|
||||||
if err != nil {
|
|
||||||
return nil, auth, nil, err
|
|
||||||
}
|
|
||||||
// Verify ID nonce signature.
|
|
||||||
sig := auth.signature
|
|
||||||
cdata := challenge.ChallengeData
|
|
||||||
err = verifyIDSignature(c.sha256, sig, n, cdata, auth.pubkey, c.localnode.ID())
|
|
||||||
if err != nil {
|
|
||||||
return nil, auth, nil, err
|
|
||||||
}
|
|
||||||
// Verify ephemeral key is on curve.
|
|
||||||
ephkey, err := DecodePubkey(c.privkey.Curve, auth.pubkey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, auth, nil, errInvalidAuthKey
|
|
||||||
}
|
|
||||||
// Derive session keys.
|
|
||||||
session := deriveKeys(sha256.New, c.privkey, ephkey, auth.h.SrcID, c.localnode.ID(), cdata)
|
|
||||||
session = session.keysFlipped()
|
|
||||||
return n, auth, session, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// decodeHandshakeAuthData reads the authdata section of a handshake packet.
|
|
||||||
func (c *Codec) decodeHandshakeAuthData(head *Header) (auth handshakeAuthData, err error) {
|
|
||||||
// Decode fixed size part.
|
|
||||||
if len(head.AuthData) < sizeofHandshakeAuthData {
|
|
||||||
return auth, fmt.Errorf("header authsize %d too low for handshake", head.AuthSize)
|
|
||||||
}
|
|
||||||
c.reader.Reset(head.AuthData)
|
|
||||||
binary.Read(&c.reader, binary.BigEndian, &auth.h)
|
|
||||||
head.src = auth.h.SrcID
|
|
||||||
|
|
||||||
// Decode variable-size part.
|
|
||||||
var (
|
|
||||||
vardata = head.AuthData[sizeofHandshakeAuthData:]
|
|
||||||
sigAndKeySize = int(auth.h.SigSize) + int(auth.h.PubkeySize)
|
|
||||||
keyOffset = int(auth.h.SigSize)
|
|
||||||
recOffset = keyOffset + int(auth.h.PubkeySize)
|
|
||||||
)
|
|
||||||
if len(vardata) < sigAndKeySize {
|
|
||||||
return auth, errTooShort
|
|
||||||
}
|
|
||||||
auth.signature = vardata[:keyOffset]
|
|
||||||
auth.pubkey = vardata[keyOffset:recOffset]
|
|
||||||
auth.record = vardata[recOffset:]
|
|
||||||
return auth, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// decodeHandshakeRecord verifies the node record contained in a handshake packet. The
|
|
||||||
// remote node should include the record if we don't have one or if ours is older than the
|
|
||||||
// latest sequence number.
|
|
||||||
func (c *Codec) decodeHandshakeRecord(local *enode.Node, wantID enode.ID, remote []byte) (*enode.Node, error) {
|
|
||||||
node := local
|
|
||||||
if len(remote) > 0 {
|
|
||||||
var record enr.Record
|
|
||||||
if err := rlp.DecodeBytes(remote, &record); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if local == nil || local.Seq() < record.Seq() {
|
|
||||||
n, err := enode.New(enode.ValidSchemes, &record)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid node record: %v", err)
|
|
||||||
}
|
|
||||||
if n.ID() != wantID {
|
|
||||||
return nil, fmt.Errorf("record in handshake has wrong ID: %v", n.ID())
|
|
||||||
}
|
|
||||||
node = n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if node == nil {
|
|
||||||
return nil, errNoRecord
|
|
||||||
}
|
|
||||||
return node, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// decodeMessage reads packet data following the header as an ordinary message packet.
|
|
||||||
func (c *Codec) decodeMessage(fromAddr string, head *Header, headerData, msgData []byte) (Packet, error) {
|
|
||||||
if len(head.AuthData) != sizeofMessageAuthData {
|
|
||||||
return nil, fmt.Errorf("invalid auth size %d for message packet", len(head.AuthData))
|
|
||||||
}
|
|
||||||
var auth messageAuthData
|
|
||||||
c.reader.Reset(head.AuthData)
|
|
||||||
binary.Read(&c.reader, binary.BigEndian, &auth)
|
|
||||||
head.src = auth.SrcID
|
|
||||||
|
|
||||||
// Try decrypting the message.
|
|
||||||
key := c.sc.readKey(auth.SrcID, fromAddr)
|
|
||||||
msg, err := c.decryptMessage(msgData, head.Nonce[:], headerData, key)
|
|
||||||
if errors.Is(err, errMessageDecrypt) {
|
|
||||||
// It didn't work. Start the handshake since this is an ordinary message packet.
|
|
||||||
return &Unknown{Nonce: head.Nonce}, nil
|
|
||||||
}
|
|
||||||
return msg, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Codec) decryptMessage(input, nonce, headerData, readKey []byte) (Packet, error) {
|
|
||||||
msgdata, err := decryptGCM(readKey, nonce, input, headerData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errMessageDecrypt
|
|
||||||
}
|
|
||||||
if len(msgdata) == 0 {
|
|
||||||
return nil, errMessageTooShort
|
|
||||||
}
|
|
||||||
return DecodeMessage(msgdata[0], msgdata[1:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkValid performs some basic validity checks on the header.
|
|
||||||
// The packetLen here is the length remaining after the static header.
|
|
||||||
func (h *StaticHeader) checkValid(packetLen int, protocolID [6]byte) error {
|
|
||||||
if h.ProtocolID != protocolID {
|
|
||||||
return errInvalidHeader
|
|
||||||
}
|
|
||||||
if h.Version < minVersion {
|
|
||||||
return errMinVersion
|
|
||||||
}
|
|
||||||
if h.Flag != flagWhoareyou && packetLen < minMessageSize {
|
|
||||||
return errMsgTooShort
|
|
||||||
}
|
|
||||||
if int(h.AuthSize) > packetLen {
|
|
||||||
return errAuthSize
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// mask returns a cipher for 'masking' / 'unmasking' packet headers.
|
|
||||||
func (h *Header) mask(destID enode.ID) cipher.Stream {
|
|
||||||
block, err := aes.NewCipher(destID[:16])
|
|
||||||
if err != nil {
|
|
||||||
panic("can't create cipher")
|
|
||||||
}
|
|
||||||
return cipher.NewCTR(block, h.IV[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
func bytesCopy(r *bytes.Buffer) []byte {
|
|
||||||
b := make([]byte, r.Len())
|
|
||||||
copy(b, r.Bytes())
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
@ -1,639 +0,0 @@
|
||||||
// Copyright 2020 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package v5wire
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// To regenerate discv5 test vectors, run
|
|
||||||
//
|
|
||||||
// go test -run TestVectors -write-test-vectors
|
|
||||||
var writeTestVectorsFlag = flag.Bool("write-test-vectors", false, "Overwrite discv5 test vectors in testdata/")
|
|
||||||
|
|
||||||
var (
|
|
||||||
testKeyA, _ = crypto.HexToECDSA("eef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f")
|
|
||||||
testKeyB, _ = crypto.HexToECDSA("66fb62bfbd66b9177a138c1e5cddbe4f7c30c343e94e68df8769459cb1cde628")
|
|
||||||
testEphKey, _ = crypto.HexToECDSA("0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6")
|
|
||||||
testIDnonce = [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
|
|
||||||
)
|
|
||||||
|
|
||||||
// This test checks that the minPacketSize and randomPacketMsgSize constants are well-defined.
|
|
||||||
func TestMinSizes(t *testing.T) {
|
|
||||||
var (
|
|
||||||
gcmTagSize = 16
|
|
||||||
emptyMsg = sizeofMessageAuthData + gcmTagSize
|
|
||||||
)
|
|
||||||
t.Log("static header size", sizeofStaticPacketData)
|
|
||||||
t.Log("whoareyou size", sizeofStaticPacketData+sizeofWhoareyouAuthData)
|
|
||||||
t.Log("empty msg size", sizeofStaticPacketData+emptyMsg)
|
|
||||||
if want := emptyMsg; minMessageSize != want {
|
|
||||||
t.Fatalf("wrong minMessageSize %d, want %d", minMessageSize, want)
|
|
||||||
}
|
|
||||||
if sizeofMessageAuthData+randomPacketMsgSize < minMessageSize {
|
|
||||||
t.Fatalf("randomPacketMsgSize %d too small", randomPacketMsgSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks the basic handshake flow where A talks to B and A has no secrets.
|
|
||||||
func TestHandshake(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
net := newHandshakeTest()
|
|
||||||
defer net.close()
|
|
||||||
|
|
||||||
// A -> B RANDOM PACKET
|
|
||||||
packet, _ := net.nodeA.encode(t, net.nodeB, &Findnode{})
|
|
||||||
resp := net.nodeB.expectDecode(t, UnknownPacket, packet)
|
|
||||||
|
|
||||||
// A <- B WHOAREYOU
|
|
||||||
challenge := &Whoareyou{
|
|
||||||
Nonce: resp.(*Unknown).Nonce,
|
|
||||||
IDNonce: testIDnonce,
|
|
||||||
RecordSeq: 0,
|
|
||||||
}
|
|
||||||
whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge)
|
|
||||||
net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou)
|
|
||||||
|
|
||||||
// A -> B FINDNODE (handshake packet)
|
|
||||||
findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{})
|
|
||||||
net.nodeB.expectDecode(t, FindnodeMsg, findnode)
|
|
||||||
if len(net.nodeB.c.sc.handshakes) > 0 {
|
|
||||||
t.Fatalf("node B didn't remove handshake from challenge map")
|
|
||||||
}
|
|
||||||
|
|
||||||
// A <- B NODES
|
|
||||||
nodes, _ := net.nodeB.encode(t, net.nodeA, &Nodes{RespCount: 1})
|
|
||||||
net.nodeA.expectDecode(t, NodesMsg, nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that handshake attempts are removed within the timeout.
|
|
||||||
func TestHandshake_timeout(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
net := newHandshakeTest()
|
|
||||||
defer net.close()
|
|
||||||
|
|
||||||
// A -> B RANDOM PACKET
|
|
||||||
packet, _ := net.nodeA.encode(t, net.nodeB, &Findnode{})
|
|
||||||
resp := net.nodeB.expectDecode(t, UnknownPacket, packet)
|
|
||||||
|
|
||||||
// A <- B WHOAREYOU
|
|
||||||
challenge := &Whoareyou{
|
|
||||||
Nonce: resp.(*Unknown).Nonce,
|
|
||||||
IDNonce: testIDnonce,
|
|
||||||
RecordSeq: 0,
|
|
||||||
}
|
|
||||||
whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge)
|
|
||||||
net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou)
|
|
||||||
|
|
||||||
// A -> B FINDNODE (handshake packet) after timeout
|
|
||||||
net.clock.Run(handshakeTimeout + 1)
|
|
||||||
findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{})
|
|
||||||
net.nodeB.expectDecodeErr(t, errUnexpectedHandshake, findnode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks handshake behavior when no record is sent in the auth response.
|
|
||||||
func TestHandshake_norecord(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
net := newHandshakeTest()
|
|
||||||
defer net.close()
|
|
||||||
|
|
||||||
// A -> B RANDOM PACKET
|
|
||||||
packet, _ := net.nodeA.encode(t, net.nodeB, &Findnode{})
|
|
||||||
resp := net.nodeB.expectDecode(t, UnknownPacket, packet)
|
|
||||||
|
|
||||||
// A <- B WHOAREYOU
|
|
||||||
nodeA := net.nodeA.n()
|
|
||||||
if nodeA.Seq() == 0 {
|
|
||||||
t.Fatal("need non-zero sequence number")
|
|
||||||
}
|
|
||||||
challenge := &Whoareyou{
|
|
||||||
Nonce: resp.(*Unknown).Nonce,
|
|
||||||
IDNonce: testIDnonce,
|
|
||||||
RecordSeq: nodeA.Seq(),
|
|
||||||
Node: nodeA,
|
|
||||||
}
|
|
||||||
whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge)
|
|
||||||
net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou)
|
|
||||||
|
|
||||||
// A -> B FINDNODE
|
|
||||||
findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{})
|
|
||||||
net.nodeB.expectDecode(t, FindnodeMsg, findnode)
|
|
||||||
|
|
||||||
// A <- B NODES
|
|
||||||
nodes, _ := net.nodeB.encode(t, net.nodeA, &Nodes{RespCount: 1})
|
|
||||||
net.nodeA.expectDecode(t, NodesMsg, nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// In this test, A tries to send FINDNODE with existing secrets but B doesn't know
|
|
||||||
// anything about A.
|
|
||||||
func TestHandshake_rekey(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
net := newHandshakeTest()
|
|
||||||
defer net.close()
|
|
||||||
|
|
||||||
session := &session{
|
|
||||||
readKey: []byte("BBBBBBBBBBBBBBBB"),
|
|
||||||
writeKey: []byte("AAAAAAAAAAAAAAAA"),
|
|
||||||
}
|
|
||||||
net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), session)
|
|
||||||
|
|
||||||
// A -> B FINDNODE (encrypted with zero keys)
|
|
||||||
findnode, authTag := net.nodeA.encode(t, net.nodeB, &Findnode{})
|
|
||||||
net.nodeB.expectDecode(t, UnknownPacket, findnode)
|
|
||||||
|
|
||||||
// A <- B WHOAREYOU
|
|
||||||
challenge := &Whoareyou{Nonce: authTag, IDNonce: testIDnonce}
|
|
||||||
whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge)
|
|
||||||
net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou)
|
|
||||||
|
|
||||||
// Check that new keys haven't been stored yet.
|
|
||||||
sa := net.nodeA.c.sc.session(net.nodeB.id(), net.nodeB.addr())
|
|
||||||
if !bytes.Equal(sa.writeKey, session.writeKey) || !bytes.Equal(sa.readKey, session.readKey) {
|
|
||||||
t.Fatal("node A stored keys too early")
|
|
||||||
}
|
|
||||||
if s := net.nodeB.c.sc.session(net.nodeA.id(), net.nodeA.addr()); s != nil {
|
|
||||||
t.Fatal("node B stored keys too early")
|
|
||||||
}
|
|
||||||
|
|
||||||
// A -> B FINDNODE encrypted with new keys
|
|
||||||
findnode, _ = net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{})
|
|
||||||
net.nodeB.expectDecode(t, FindnodeMsg, findnode)
|
|
||||||
|
|
||||||
// A <- B NODES
|
|
||||||
nodes, _ := net.nodeB.encode(t, net.nodeA, &Nodes{RespCount: 1})
|
|
||||||
net.nodeA.expectDecode(t, NodesMsg, nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// In this test A and B have different keys before the handshake.
|
|
||||||
func TestHandshake_rekey2(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
net := newHandshakeTest()
|
|
||||||
defer net.close()
|
|
||||||
|
|
||||||
initKeysA := &session{
|
|
||||||
readKey: []byte("BBBBBBBBBBBBBBBB"),
|
|
||||||
writeKey: []byte("AAAAAAAAAAAAAAAA"),
|
|
||||||
}
|
|
||||||
initKeysB := &session{
|
|
||||||
readKey: []byte("CCCCCCCCCCCCCCCC"),
|
|
||||||
writeKey: []byte("DDDDDDDDDDDDDDDD"),
|
|
||||||
}
|
|
||||||
net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), initKeysA)
|
|
||||||
net.nodeB.c.sc.storeNewSession(net.nodeA.id(), net.nodeA.addr(), initKeysB)
|
|
||||||
|
|
||||||
// A -> B FINDNODE encrypted with initKeysA
|
|
||||||
findnode, authTag := net.nodeA.encode(t, net.nodeB, &Findnode{Distances: []uint{3}})
|
|
||||||
net.nodeB.expectDecode(t, UnknownPacket, findnode)
|
|
||||||
|
|
||||||
// A <- B WHOAREYOU
|
|
||||||
challenge := &Whoareyou{Nonce: authTag, IDNonce: testIDnonce}
|
|
||||||
whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge)
|
|
||||||
net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou)
|
|
||||||
|
|
||||||
// A -> B FINDNODE (handshake packet)
|
|
||||||
findnode, _ = net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{})
|
|
||||||
net.nodeB.expectDecode(t, FindnodeMsg, findnode)
|
|
||||||
|
|
||||||
// A <- B NODES
|
|
||||||
nodes, _ := net.nodeB.encode(t, net.nodeA, &Nodes{RespCount: 1})
|
|
||||||
net.nodeA.expectDecode(t, NodesMsg, nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandshake_BadHandshakeAttack(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
net := newHandshakeTest()
|
|
||||||
defer net.close()
|
|
||||||
|
|
||||||
// A -> B RANDOM PACKET
|
|
||||||
packet, _ := net.nodeA.encode(t, net.nodeB, &Findnode{})
|
|
||||||
resp := net.nodeB.expectDecode(t, UnknownPacket, packet)
|
|
||||||
|
|
||||||
// A <- B WHOAREYOU
|
|
||||||
challenge := &Whoareyou{
|
|
||||||
Nonce: resp.(*Unknown).Nonce,
|
|
||||||
IDNonce: testIDnonce,
|
|
||||||
RecordSeq: 0,
|
|
||||||
}
|
|
||||||
whoareyou, _ := net.nodeB.encode(t, net.nodeA, challenge)
|
|
||||||
net.nodeA.expectDecode(t, WhoareyouPacket, whoareyou)
|
|
||||||
|
|
||||||
// A -> B FINDNODE
|
|
||||||
incorrect_challenge := &Whoareyou{
|
|
||||||
IDNonce: [16]byte{5, 6, 7, 8, 9, 6, 11, 12},
|
|
||||||
RecordSeq: challenge.RecordSeq,
|
|
||||||
Node: challenge.Node,
|
|
||||||
sent: challenge.sent,
|
|
||||||
}
|
|
||||||
incorrect_findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, incorrect_challenge, &Findnode{})
|
|
||||||
incorrect_findnode2 := make([]byte, len(incorrect_findnode))
|
|
||||||
copy(incorrect_findnode2, incorrect_findnode)
|
|
||||||
|
|
||||||
net.nodeB.expectDecodeErr(t, errInvalidNonceSig, incorrect_findnode)
|
|
||||||
|
|
||||||
// Reject new findnode as previous handshake is now deleted.
|
|
||||||
net.nodeB.expectDecodeErr(t, errUnexpectedHandshake, incorrect_findnode2)
|
|
||||||
|
|
||||||
// The findnode packet is again rejected even with a valid challenge this time.
|
|
||||||
findnode, _ := net.nodeA.encodeWithChallenge(t, net.nodeB, challenge, &Findnode{})
|
|
||||||
net.nodeB.expectDecodeErr(t, errUnexpectedHandshake, findnode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks some malformed packets.
|
|
||||||
func TestDecodeErrorsV5(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
net := newHandshakeTest()
|
|
||||||
defer net.close()
|
|
||||||
|
|
||||||
b := make([]byte, 0)
|
|
||||||
net.nodeA.expectDecodeErr(t, errTooShort, b)
|
|
||||||
|
|
||||||
b = make([]byte, 62)
|
|
||||||
net.nodeA.expectDecodeErr(t, errTooShort, b)
|
|
||||||
|
|
||||||
b = make([]byte, 63)
|
|
||||||
net.nodeA.expectDecodeErr(t, errInvalidHeader, b)
|
|
||||||
|
|
||||||
// TODO some more tests would be nice :)
|
|
||||||
// - check invalid authdata sizes
|
|
||||||
// - check invalid handshake data sizes
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that all test vectors can be decoded.
|
|
||||||
func TestTestVectorsV5(t *testing.T) {
|
|
||||||
var (
|
|
||||||
idA = enode.PubkeyToIDV4(&testKeyA.PublicKey)
|
|
||||||
idB = enode.PubkeyToIDV4(&testKeyB.PublicKey)
|
|
||||||
addr = "127.0.0.1"
|
|
||||||
session = &session{
|
|
||||||
writeKey: hexutil.MustDecode("0x00000000000000000000000000000000"),
|
|
||||||
readKey: hexutil.MustDecode("0x01010101010101010101010101010101"),
|
|
||||||
}
|
|
||||||
challenge0A, challenge1A, challenge0B Whoareyou
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create challenge packets.
|
|
||||||
c := Whoareyou{
|
|
||||||
Nonce: Nonce{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
|
|
||||||
IDNonce: testIDnonce,
|
|
||||||
}
|
|
||||||
challenge0A, challenge1A, challenge0B = c, c, c
|
|
||||||
challenge1A.RecordSeq = 1
|
|
||||||
net := newHandshakeTest()
|
|
||||||
challenge0A.Node = net.nodeA.n()
|
|
||||||
challenge0B.Node = net.nodeB.n()
|
|
||||||
challenge1A.Node = net.nodeA.n()
|
|
||||||
net.close()
|
|
||||||
|
|
||||||
type testVectorTest struct {
|
|
||||||
name string // test vector name
|
|
||||||
packet Packet // the packet to be encoded
|
|
||||||
challenge *Whoareyou // handshake challenge passed to encoder
|
|
||||||
prep func(*handshakeTest) // called before encode/decode
|
|
||||||
}
|
|
||||||
tests := []testVectorTest{
|
|
||||||
{
|
|
||||||
name: "v5.1-whoareyou",
|
|
||||||
packet: &challenge0B,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "v5.1-ping-message",
|
|
||||||
packet: &Ping{
|
|
||||||
ReqID: []byte{0, 0, 0, 1},
|
|
||||||
ENRSeq: 2,
|
|
||||||
},
|
|
||||||
prep: func(net *handshakeTest) {
|
|
||||||
net.nodeA.c.sc.storeNewSession(idB, addr, session)
|
|
||||||
net.nodeB.c.sc.storeNewSession(idA, addr, session.keysFlipped())
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "v5.1-ping-handshake-enr",
|
|
||||||
packet: &Ping{
|
|
||||||
ReqID: []byte{0, 0, 0, 1},
|
|
||||||
ENRSeq: 1,
|
|
||||||
},
|
|
||||||
challenge: &challenge0A,
|
|
||||||
prep: func(net *handshakeTest) {
|
|
||||||
// Update challenge.Header.AuthData.
|
|
||||||
net.nodeA.c.Encode(idB, "", &challenge0A, nil)
|
|
||||||
net.nodeB.c.sc.storeSentHandshake(idA, addr, &challenge0A)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "v5.1-ping-handshake",
|
|
||||||
packet: &Ping{
|
|
||||||
ReqID: []byte{0, 0, 0, 1},
|
|
||||||
ENRSeq: 1,
|
|
||||||
},
|
|
||||||
challenge: &challenge1A,
|
|
||||||
prep: func(net *handshakeTest) {
|
|
||||||
// Update challenge data.
|
|
||||||
net.nodeA.c.Encode(idB, "", &challenge1A, nil)
|
|
||||||
net.nodeB.c.sc.storeSentHandshake(idA, addr, &challenge1A)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
test := test
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
net := newHandshakeTest()
|
|
||||||
defer net.close()
|
|
||||||
|
|
||||||
// Override all random inputs.
|
|
||||||
net.nodeA.c.sc.nonceGen = func(counter uint32) (Nonce, error) {
|
|
||||||
return Nonce{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, nil
|
|
||||||
}
|
|
||||||
net.nodeA.c.sc.maskingIVGen = func(buf []byte) error {
|
|
||||||
return nil // all zero
|
|
||||||
}
|
|
||||||
net.nodeA.c.sc.ephemeralKeyGen = func() (*ecdsa.PrivateKey, error) {
|
|
||||||
return testEphKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prime the codec for encoding/decoding.
|
|
||||||
if test.prep != nil {
|
|
||||||
test.prep(net)
|
|
||||||
}
|
|
||||||
|
|
||||||
file := filepath.Join("testdata", test.name+".txt")
|
|
||||||
if *writeTestVectorsFlag {
|
|
||||||
// Encode the packet.
|
|
||||||
d, nonce := net.nodeA.encodeWithChallenge(t, net.nodeB, test.challenge, test.packet)
|
|
||||||
comment := testVectorComment(net, test.packet, test.challenge, nonce)
|
|
||||||
writeTestVector(file, comment, d)
|
|
||||||
}
|
|
||||||
enc := hexFile(file)
|
|
||||||
net.nodeB.expectDecode(t, test.packet.Kind(), enc)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// testVectorComment creates the commentary for discv5 test vector files.
|
|
||||||
func testVectorComment(net *handshakeTest, p Packet, challenge *Whoareyou, nonce Nonce) string {
|
|
||||||
o := new(strings.Builder)
|
|
||||||
printWhoareyou := func(p *Whoareyou) {
|
|
||||||
fmt.Fprintf(o, "whoareyou.challenge-data = %#x\n", p.ChallengeData)
|
|
||||||
fmt.Fprintf(o, "whoareyou.request-nonce = %#x\n", p.Nonce[:])
|
|
||||||
fmt.Fprintf(o, "whoareyou.id-nonce = %#x\n", p.IDNonce[:])
|
|
||||||
fmt.Fprintf(o, "whoareyou.enr-seq = %d\n", p.RecordSeq)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(o, "src-node-id = %#x\n", net.nodeA.id().Bytes())
|
|
||||||
fmt.Fprintf(o, "dest-node-id = %#x\n", net.nodeB.id().Bytes())
|
|
||||||
switch p := p.(type) {
|
|
||||||
case *Whoareyou:
|
|
||||||
// WHOAREYOU packet.
|
|
||||||
printWhoareyou(p)
|
|
||||||
case *Ping:
|
|
||||||
fmt.Fprintf(o, "nonce = %#x\n", nonce[:])
|
|
||||||
fmt.Fprintf(o, "read-key = %#x\n", net.nodeA.c.sc.session(net.nodeB.id(), net.nodeB.addr()).writeKey)
|
|
||||||
fmt.Fprintf(o, "ping.req-id = %#x\n", p.ReqID)
|
|
||||||
fmt.Fprintf(o, "ping.enr-seq = %d\n", p.ENRSeq)
|
|
||||||
if challenge != nil {
|
|
||||||
// Handshake message packet.
|
|
||||||
fmt.Fprint(o, "\nhandshake inputs:\n\n")
|
|
||||||
printWhoareyou(challenge)
|
|
||||||
fmt.Fprintf(o, "ephemeral-key = %#x\n", testEphKey.D.Bytes())
|
|
||||||
fmt.Fprintf(o, "ephemeral-pubkey = %#x\n", crypto.CompressPubkey(&testEphKey.PublicKey))
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("unhandled packet type %T", p))
|
|
||||||
}
|
|
||||||
return o.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// This benchmark checks performance of handshake packet decoding.
|
|
||||||
func BenchmarkV5_DecodeHandshakePingSecp256k1(b *testing.B) {
|
|
||||||
net := newHandshakeTest()
|
|
||||||
defer net.close()
|
|
||||||
|
|
||||||
var (
|
|
||||||
idA = net.nodeA.id()
|
|
||||||
challenge = &Whoareyou{Node: net.nodeB.n()}
|
|
||||||
message = &Ping{ReqID: []byte("reqid")}
|
|
||||||
)
|
|
||||||
enc, _, err := net.nodeA.c.Encode(net.nodeB.id(), "", message, challenge)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal("can't encode handshake packet")
|
|
||||||
}
|
|
||||||
challenge.Node = nil // force ENR signature verification in decoder
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
input := make([]byte, len(enc))
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
copy(input, enc)
|
|
||||||
net.nodeB.c.sc.storeSentHandshake(idA, "", challenge)
|
|
||||||
_, _, _, err := net.nodeB.c.Decode(input, "")
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This benchmark checks how long it takes to decode an encrypted ping packet.
|
|
||||||
func BenchmarkV5_DecodePing(b *testing.B) {
|
|
||||||
net := newHandshakeTest()
|
|
||||||
defer net.close()
|
|
||||||
|
|
||||||
session := &session{
|
|
||||||
readKey: []byte{233, 203, 93, 195, 86, 47, 177, 186, 227, 43, 2, 141, 244, 230, 120, 17},
|
|
||||||
writeKey: []byte{79, 145, 252, 171, 167, 216, 252, 161, 208, 190, 176, 106, 214, 39, 178, 134},
|
|
||||||
}
|
|
||||||
net.nodeA.c.sc.storeNewSession(net.nodeB.id(), net.nodeB.addr(), session)
|
|
||||||
net.nodeB.c.sc.storeNewSession(net.nodeA.id(), net.nodeA.addr(), session.keysFlipped())
|
|
||||||
addrB := net.nodeA.addr()
|
|
||||||
ping := &Ping{ReqID: []byte("reqid"), ENRSeq: 5}
|
|
||||||
enc, _, err := net.nodeA.c.Encode(net.nodeB.id(), addrB, ping, nil)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatalf("can't encode: %v", err)
|
|
||||||
}
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
input := make([]byte, len(enc))
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
copy(input, enc)
|
|
||||||
_, _, packet, _ := net.nodeB.c.Decode(input, addrB)
|
|
||||||
if _, ok := packet.(*Ping); !ok {
|
|
||||||
b.Fatalf("wrong packet type %T", packet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var pp = spew.NewDefaultConfig()
|
|
||||||
|
|
||||||
type handshakeTest struct {
|
|
||||||
nodeA, nodeB handshakeTestNode
|
|
||||||
clock mclock.Simulated
|
|
||||||
}
|
|
||||||
|
|
||||||
type handshakeTestNode struct {
|
|
||||||
ln *enode.LocalNode
|
|
||||||
c *Codec
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHandshakeTest() *handshakeTest {
|
|
||||||
t := new(handshakeTest)
|
|
||||||
t.nodeA.init(testKeyA, net.IP{127, 0, 0, 1}, &t.clock, DefaultProtocolID)
|
|
||||||
t.nodeB.init(testKeyB, net.IP{127, 0, 0, 1}, &t.clock, DefaultProtocolID)
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *handshakeTest) close() {
|
|
||||||
t.nodeA.ln.Database().Close()
|
|
||||||
t.nodeB.ln.Database().Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *handshakeTestNode) init(key *ecdsa.PrivateKey, ip net.IP, clock mclock.Clock, protocolID [6]byte) {
|
|
||||||
db, _ := enode.OpenDB("")
|
|
||||||
n.ln = enode.NewLocalNode(db, key)
|
|
||||||
n.ln.SetStaticIP(ip)
|
|
||||||
n.c = NewCodec(n.ln, key, clock, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *handshakeTestNode) encode(t testing.TB, to handshakeTestNode, p Packet) ([]byte, Nonce) {
|
|
||||||
t.Helper()
|
|
||||||
return n.encodeWithChallenge(t, to, nil, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *handshakeTestNode) encodeWithChallenge(t testing.TB, to handshakeTestNode, c *Whoareyou, p Packet) ([]byte, Nonce) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
// Copy challenge and add destination node. This avoids sharing 'c' among the two codecs.
|
|
||||||
var challenge *Whoareyou
|
|
||||||
if c != nil {
|
|
||||||
challengeCopy := *c
|
|
||||||
challenge = &challengeCopy
|
|
||||||
challenge.Node = to.n()
|
|
||||||
}
|
|
||||||
// Encode to destination.
|
|
||||||
enc, nonce, err := n.c.Encode(to.id(), to.addr(), p, challenge)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(fmt.Errorf("(%s) %v", n.ln.ID().TerminalString(), err))
|
|
||||||
}
|
|
||||||
t.Logf("(%s) -> (%s) %s\n%s", n.ln.ID().TerminalString(), to.id().TerminalString(), p.Name(), hex.Dump(enc))
|
|
||||||
return enc, nonce
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *handshakeTestNode) expectDecode(t *testing.T, ptype byte, p []byte) Packet {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
dec, err := n.decode(p)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(fmt.Errorf("(%s) %v", n.ln.ID().TerminalString(), err))
|
|
||||||
}
|
|
||||||
t.Logf("(%s) %#v", n.ln.ID().TerminalString(), pp.NewFormatter(dec))
|
|
||||||
if dec.Kind() != ptype {
|
|
||||||
t.Fatalf("expected packet type %d, got %d", ptype, dec.Kind())
|
|
||||||
}
|
|
||||||
return dec
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *handshakeTestNode) expectDecodeErr(t *testing.T, wantErr error, p []byte) {
|
|
||||||
t.Helper()
|
|
||||||
if _, err := n.decode(p); !errors.Is(err, wantErr) {
|
|
||||||
t.Fatal(fmt.Errorf("(%s) got err %q, want %q", n.ln.ID().TerminalString(), err, wantErr))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *handshakeTestNode) decode(input []byte) (Packet, error) {
|
|
||||||
_, _, p, err := n.c.Decode(input, "127.0.0.1")
|
|
||||||
return p, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *handshakeTestNode) n() *enode.Node {
|
|
||||||
return n.ln.Node()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *handshakeTestNode) addr() string {
|
|
||||||
return n.ln.Node().IP().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *handshakeTestNode) id() enode.ID {
|
|
||||||
return n.ln.ID()
|
|
||||||
}
|
|
||||||
|
|
||||||
// hexFile reads the given file and decodes the hex data contained in it.
|
|
||||||
// Whitespace and any lines beginning with the # character are ignored.
|
|
||||||
func hexFile(file string) []byte {
|
|
||||||
fileContent, err := os.ReadFile(file)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gather hex data, ignore comments.
|
|
||||||
var text []byte
|
|
||||||
for _, line := range bytes.Split(fileContent, []byte("\n")) {
|
|
||||||
line = bytes.TrimSpace(line)
|
|
||||||
if len(line) > 0 && line[0] == '#' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
text = append(text, line...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the hex.
|
|
||||||
if bytes.HasPrefix(text, []byte("0x")) {
|
|
||||||
text = text[2:]
|
|
||||||
}
|
|
||||||
data := make([]byte, hex.DecodedLen(len(text)))
|
|
||||||
if _, err := hex.Decode(data, text); err != nil {
|
|
||||||
panic("invalid hex in " + file)
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeTestVector writes a test vector file with the given commentary and binary data.
|
|
||||||
func writeTestVector(file, comment string, data []byte) {
|
|
||||||
fd, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer fd.Close()
|
|
||||||
|
|
||||||
if len(comment) > 0 {
|
|
||||||
for _, line := range strings.Split(strings.TrimSpace(comment), "\n") {
|
|
||||||
fmt.Fprintf(fd, "# %s\n", line)
|
|
||||||
}
|
|
||||||
fmt.Fprintln(fd)
|
|
||||||
}
|
|
||||||
for len(data) > 0 {
|
|
||||||
var chunk []byte
|
|
||||||
if len(data) < 32 {
|
|
||||||
chunk = data
|
|
||||||
} else {
|
|
||||||
chunk = data[:32]
|
|
||||||
}
|
|
||||||
data = data[len(chunk):]
|
|
||||||
fmt.Fprintf(fd, "%x\n", chunk)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,229 +0,0 @@
|
||||||
// Copyright 2020 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package v5wire
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Packet is implemented by all message types.
|
|
||||||
type Packet interface {
|
|
||||||
Name() string // Name returns a string corresponding to the message type.
|
|
||||||
Kind() byte // Kind returns the message type.
|
|
||||||
RequestID() []byte // Returns the request ID.
|
|
||||||
SetRequestID([]byte) // Sets the request ID.
|
|
||||||
|
|
||||||
// AppendLogInfo returns its argument 'ctx' with additional fields
|
|
||||||
// appended for logging purposes.
|
|
||||||
AppendLogInfo(ctx []interface{}) []interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message types.
|
|
||||||
const (
|
|
||||||
PingMsg byte = iota + 1
|
|
||||||
PongMsg
|
|
||||||
FindnodeMsg
|
|
||||||
NodesMsg
|
|
||||||
TalkRequestMsg
|
|
||||||
TalkResponseMsg
|
|
||||||
RequestTicketMsg
|
|
||||||
TicketMsg
|
|
||||||
|
|
||||||
UnknownPacket = byte(255) // any non-decryptable packet
|
|
||||||
WhoareyouPacket = byte(254) // the WHOAREYOU packet
|
|
||||||
)
|
|
||||||
|
|
||||||
// Protocol messages.
|
|
||||||
type (
|
|
||||||
// Unknown represents any packet that can't be decrypted.
|
|
||||||
Unknown struct {
|
|
||||||
Nonce Nonce
|
|
||||||
}
|
|
||||||
|
|
||||||
// WHOAREYOU contains the handshake challenge.
|
|
||||||
Whoareyou struct {
|
|
||||||
ChallengeData []byte // Encoded challenge
|
|
||||||
Nonce Nonce // Nonce of request packet
|
|
||||||
IDNonce [16]byte // Identity proof data
|
|
||||||
RecordSeq uint64 // ENR sequence number of recipient
|
|
||||||
|
|
||||||
// Node is the locally known node record of recipient.
|
|
||||||
// This must be set by the caller of Encode.
|
|
||||||
Node *enode.Node
|
|
||||||
|
|
||||||
sent mclock.AbsTime // for handshake GC.
|
|
||||||
}
|
|
||||||
|
|
||||||
// PING is sent during liveness checks.
|
|
||||||
Ping struct {
|
|
||||||
ReqID []byte
|
|
||||||
ENRSeq uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
// PONG is the reply to PING.
|
|
||||||
Pong struct {
|
|
||||||
ReqID []byte
|
|
||||||
ENRSeq uint64
|
|
||||||
ToIP net.IP // These fields should mirror the UDP envelope address of the ping
|
|
||||||
ToPort uint16 // packet, which provides a way to discover the external address (after NAT).
|
|
||||||
}
|
|
||||||
|
|
||||||
// FINDNODE is a query for nodes in the given bucket.
|
|
||||||
Findnode struct {
|
|
||||||
ReqID []byte
|
|
||||||
Distances []uint
|
|
||||||
|
|
||||||
// OpID is for debugging purposes and is not part of the packet encoding.
|
|
||||||
// It identifies the 'operation' on behalf of which the request was sent.
|
|
||||||
OpID uint64 `rlp:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NODES is a response to FINDNODE.
|
|
||||||
Nodes struct {
|
|
||||||
ReqID []byte
|
|
||||||
RespCount uint8 // total number of responses to the request
|
|
||||||
Nodes []*enr.Record
|
|
||||||
}
|
|
||||||
|
|
||||||
// TALKREQ is an application-level request.
|
|
||||||
TalkRequest struct {
|
|
||||||
ReqID []byte
|
|
||||||
Protocol string
|
|
||||||
Message []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// TALKRESP is the reply to TALKREQ.
|
|
||||||
TalkResponse struct {
|
|
||||||
ReqID []byte
|
|
||||||
Message []byte
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// DecodeMessage decodes the message body of a packet.
|
|
||||||
func DecodeMessage(ptype byte, body []byte) (Packet, error) {
|
|
||||||
var dec Packet
|
|
||||||
switch ptype {
|
|
||||||
case PingMsg:
|
|
||||||
dec = new(Ping)
|
|
||||||
case PongMsg:
|
|
||||||
dec = new(Pong)
|
|
||||||
case FindnodeMsg:
|
|
||||||
dec = new(Findnode)
|
|
||||||
case NodesMsg:
|
|
||||||
dec = new(Nodes)
|
|
||||||
case TalkRequestMsg:
|
|
||||||
dec = new(TalkRequest)
|
|
||||||
case TalkResponseMsg:
|
|
||||||
dec = new(TalkResponse)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown packet type %d", ptype)
|
|
||||||
}
|
|
||||||
if err := rlp.DecodeBytes(body, dec); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if dec.RequestID() != nil && len(dec.RequestID()) > 8 {
|
|
||||||
return nil, ErrInvalidReqID
|
|
||||||
}
|
|
||||||
return dec, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*Whoareyou) Name() string { return "WHOAREYOU/v5" }
|
|
||||||
func (*Whoareyou) Kind() byte { return WhoareyouPacket }
|
|
||||||
func (*Whoareyou) RequestID() []byte { return nil }
|
|
||||||
func (*Whoareyou) SetRequestID([]byte) {}
|
|
||||||
|
|
||||||
func (*Whoareyou) AppendLogInfo(ctx []interface{}) []interface{} {
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*Unknown) Name() string { return "UNKNOWN/v5" }
|
|
||||||
func (*Unknown) Kind() byte { return UnknownPacket }
|
|
||||||
func (*Unknown) RequestID() []byte { return nil }
|
|
||||||
func (*Unknown) SetRequestID([]byte) {}
|
|
||||||
|
|
||||||
func (*Unknown) AppendLogInfo(ctx []interface{}) []interface{} {
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*Ping) Name() string { return "PING/v5" }
|
|
||||||
func (*Ping) Kind() byte { return PingMsg }
|
|
||||||
func (p *Ping) RequestID() []byte { return p.ReqID }
|
|
||||||
func (p *Ping) SetRequestID(id []byte) { p.ReqID = id }
|
|
||||||
|
|
||||||
func (p *Ping) AppendLogInfo(ctx []interface{}) []interface{} {
|
|
||||||
return append(ctx, "req", hexutil.Bytes(p.ReqID), "enrseq", p.ENRSeq)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*Pong) Name() string { return "PONG/v5" }
|
|
||||||
func (*Pong) Kind() byte { return PongMsg }
|
|
||||||
func (p *Pong) RequestID() []byte { return p.ReqID }
|
|
||||||
func (p *Pong) SetRequestID(id []byte) { p.ReqID = id }
|
|
||||||
|
|
||||||
func (p *Pong) AppendLogInfo(ctx []interface{}) []interface{} {
|
|
||||||
return append(ctx, "req", hexutil.Bytes(p.ReqID), "enrseq", p.ENRSeq)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Findnode) Name() string { return "FINDNODE/v5" }
|
|
||||||
func (p *Findnode) Kind() byte { return FindnodeMsg }
|
|
||||||
func (p *Findnode) RequestID() []byte { return p.ReqID }
|
|
||||||
func (p *Findnode) SetRequestID(id []byte) { p.ReqID = id }
|
|
||||||
|
|
||||||
func (p *Findnode) AppendLogInfo(ctx []interface{}) []interface{} {
|
|
||||||
ctx = append(ctx, "req", hexutil.Bytes(p.ReqID))
|
|
||||||
if p.OpID != 0 {
|
|
||||||
ctx = append(ctx, "opid", p.OpID)
|
|
||||||
}
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*Nodes) Name() string { return "NODES/v5" }
|
|
||||||
func (*Nodes) Kind() byte { return NodesMsg }
|
|
||||||
func (p *Nodes) RequestID() []byte { return p.ReqID }
|
|
||||||
func (p *Nodes) SetRequestID(id []byte) { p.ReqID = id }
|
|
||||||
|
|
||||||
func (p *Nodes) AppendLogInfo(ctx []interface{}) []interface{} {
|
|
||||||
return append(ctx,
|
|
||||||
"req", hexutil.Bytes(p.ReqID),
|
|
||||||
"tot", p.RespCount,
|
|
||||||
"n", len(p.Nodes),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*TalkRequest) Name() string { return "TALKREQ/v5" }
|
|
||||||
func (*TalkRequest) Kind() byte { return TalkRequestMsg }
|
|
||||||
func (p *TalkRequest) RequestID() []byte { return p.ReqID }
|
|
||||||
func (p *TalkRequest) SetRequestID(id []byte) { p.ReqID = id }
|
|
||||||
|
|
||||||
func (p *TalkRequest) AppendLogInfo(ctx []interface{}) []interface{} {
|
|
||||||
return append(ctx, "proto", p.Protocol, "req", hexutil.Bytes(p.ReqID), "len", len(p.Message))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*TalkResponse) Name() string { return "TALKRESP/v5" }
|
|
||||||
func (*TalkResponse) Kind() byte { return TalkResponseMsg }
|
|
||||||
func (p *TalkResponse) RequestID() []byte { return p.ReqID }
|
|
||||||
func (p *TalkResponse) SetRequestID(id []byte) { p.ReqID = id }
|
|
||||||
|
|
||||||
func (p *TalkResponse) AppendLogInfo(ctx []interface{}) []interface{} {
|
|
||||||
return append(ctx, "req", hexutil.Bytes(p.ReqID), "len", len(p.Message))
|
|
||||||
}
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
// Copyright 2020 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package v5wire
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
crand "crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/lru"
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
)
|
|
||||||
|
|
||||||
const handshakeTimeout = time.Second
|
|
||||||
|
|
||||||
// The SessionCache keeps negotiated encryption keys and
|
|
||||||
// state for in-progress handshakes in the Discovery v5 wire protocol.
|
|
||||||
type SessionCache struct {
|
|
||||||
sessions lru.BasicLRU[sessionID, *session]
|
|
||||||
handshakes map[sessionID]*Whoareyou
|
|
||||||
clock mclock.Clock
|
|
||||||
|
|
||||||
// hooks for overriding randomness.
|
|
||||||
nonceGen func(uint32) (Nonce, error)
|
|
||||||
maskingIVGen func([]byte) error
|
|
||||||
ephemeralKeyGen func() (*ecdsa.PrivateKey, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sessionID identifies a session or handshake.
|
|
||||||
type sessionID struct {
|
|
||||||
id enode.ID
|
|
||||||
addr string
|
|
||||||
}
|
|
||||||
|
|
||||||
// session contains session information
|
|
||||||
type session struct {
|
|
||||||
writeKey []byte
|
|
||||||
readKey []byte
|
|
||||||
nonceCounter uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// keysFlipped returns a copy of s with the read and write keys flipped.
|
|
||||||
func (s *session) keysFlipped() *session {
|
|
||||||
return &session{s.readKey, s.writeKey, s.nonceCounter}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSessionCache(maxItems int, clock mclock.Clock) *SessionCache {
|
|
||||||
return &SessionCache{
|
|
||||||
sessions: lru.NewBasicLRU[sessionID, *session](maxItems),
|
|
||||||
handshakes: make(map[sessionID]*Whoareyou),
|
|
||||||
clock: clock,
|
|
||||||
nonceGen: generateNonce,
|
|
||||||
maskingIVGen: generateMaskingIV,
|
|
||||||
ephemeralKeyGen: crypto.GenerateKey,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateNonce(counter uint32) (n Nonce, err error) {
|
|
||||||
binary.BigEndian.PutUint32(n[:4], counter)
|
|
||||||
_, err = crand.Read(n[4:])
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateMaskingIV(buf []byte) error {
|
|
||||||
_, err := crand.Read(buf)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// nextNonce creates a nonce for encrypting a message to the given session.
|
|
||||||
func (sc *SessionCache) nextNonce(s *session) (Nonce, error) {
|
|
||||||
s.nonceCounter++
|
|
||||||
return sc.nonceGen(s.nonceCounter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// session returns the current session for the given node, if any.
|
|
||||||
func (sc *SessionCache) session(id enode.ID, addr string) *session {
|
|
||||||
item, _ := sc.sessions.Get(sessionID{id, addr})
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
// readKey returns the current read key for the given node.
|
|
||||||
func (sc *SessionCache) readKey(id enode.ID, addr string) []byte {
|
|
||||||
if s := sc.session(id, addr); s != nil {
|
|
||||||
return s.readKey
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// storeNewSession stores new encryption keys in the cache.
|
|
||||||
func (sc *SessionCache) storeNewSession(id enode.ID, addr string, s *session) {
|
|
||||||
sc.sessions.Add(sessionID{id, addr}, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getHandshake gets the handshake challenge we previously sent to the given remote node.
|
|
||||||
func (sc *SessionCache) getHandshake(id enode.ID, addr string) *Whoareyou {
|
|
||||||
return sc.handshakes[sessionID{id, addr}]
|
|
||||||
}
|
|
||||||
|
|
||||||
// storeSentHandshake stores the handshake challenge sent to the given remote node.
|
|
||||||
func (sc *SessionCache) storeSentHandshake(id enode.ID, addr string, challenge *Whoareyou) {
|
|
||||||
challenge.sent = sc.clock.Now()
|
|
||||||
sc.handshakes[sessionID{id, addr}] = challenge
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteHandshake deletes handshake data for the given node.
|
|
||||||
func (sc *SessionCache) deleteHandshake(id enode.ID, addr string) {
|
|
||||||
delete(sc.handshakes, sessionID{id, addr})
|
|
||||||
}
|
|
||||||
|
|
||||||
// handshakeGC deletes timed-out handshakes.
|
|
||||||
func (sc *SessionCache) handshakeGC() {
|
|
||||||
deadline := sc.clock.Now().Add(-handshakeTimeout)
|
|
||||||
for key, challenge := range sc.handshakes {
|
|
||||||
if challenge.sent < deadline {
|
|
||||||
delete(sc.handshakes, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
# src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb
|
|
||||||
# dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9
|
|
||||||
# nonce = 0xffffffffffffffffffffffff
|
|
||||||
# read-key = 0x53b1c075f41876423154e157470c2f48
|
|
||||||
# ping.req-id = 0x00000001
|
|
||||||
# ping.enr-seq = 1
|
|
||||||
#
|
|
||||||
# handshake inputs:
|
|
||||||
#
|
|
||||||
# whoareyou.challenge-data = 0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000
|
|
||||||
# whoareyou.request-nonce = 0x0102030405060708090a0b0c
|
|
||||||
# whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10
|
|
||||||
# whoareyou.enr-seq = 0
|
|
||||||
# ephemeral-key = 0x0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6
|
|
||||||
# ephemeral-pubkey = 0x039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5
|
|
||||||
|
|
||||||
00000000000000000000000000000000088b3d4342774649305f313964a39e55
|
|
||||||
ea96c005ad539c8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3
|
|
||||||
4c4f53245d08da4bb23698868350aaad22e3ab8dd034f548a1c43cd246be9856
|
|
||||||
2fafa0a1fa86d8e7a3b95ae78cc2b988ded6a5b59eb83ad58097252188b902b2
|
|
||||||
1481e30e5e285f19735796706adff216ab862a9186875f9494150c4ae06fa4d1
|
|
||||||
f0396c93f215fa4ef524e0ed04c3c21e39b1868e1ca8105e585ec17315e755e6
|
|
||||||
cfc4dd6cb7fd8e1a1f55e49b4b5eb024221482105346f3c82b15fdaae36a3bb1
|
|
||||||
2a494683b4a3c7f2ae41306252fed84785e2bbff3b022812d0882f06978df84a
|
|
||||||
80d443972213342d04b9048fc3b1d5fcb1df0f822152eced6da4d3f6df27e70e
|
|
||||||
4539717307a0208cd208d65093ccab5aa596a34d7511401987662d8cf62b1394
|
|
||||||
71
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
# src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb
|
|
||||||
# dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9
|
|
||||||
# nonce = 0xffffffffffffffffffffffff
|
|
||||||
# read-key = 0x4f9fac6de7567d1e3b1241dffe90f662
|
|
||||||
# ping.req-id = 0x00000001
|
|
||||||
# ping.enr-seq = 1
|
|
||||||
#
|
|
||||||
# handshake inputs:
|
|
||||||
#
|
|
||||||
# whoareyou.challenge-data = 0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000001
|
|
||||||
# whoareyou.request-nonce = 0x0102030405060708090a0b0c
|
|
||||||
# whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10
|
|
||||||
# whoareyou.enr-seq = 1
|
|
||||||
# ephemeral-key = 0x0288ef00023598499cb6c940146d050d2b1fb914198c327f76aad590bead68b6
|
|
||||||
# ephemeral-pubkey = 0x039a003ba6517b473fa0cd74aefe99dadfdb34627f90fec6362df85803908f53a5
|
|
||||||
|
|
||||||
00000000000000000000000000000000088b3d4342774649305f313964a39e55
|
|
||||||
ea96c005ad521d8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3
|
|
||||||
4c4f53245d08da4bb252012b2cba3f4f374a90a75cff91f142fa9be3e0a5f3ef
|
|
||||||
268ccb9065aeecfd67a999e7fdc137e062b2ec4a0eb92947f0d9a74bfbf44dfb
|
|
||||||
a776b21301f8b65efd5796706adff216ab862a9186875f9494150c4ae06fa4d1
|
|
||||||
f0396c93f215fa4ef524f1eadf5f0f4126b79336671cbcf7a885b1f8bd2a5d83
|
|
||||||
9cf8
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
# src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb
|
|
||||||
# dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9
|
|
||||||
# nonce = 0xffffffffffffffffffffffff
|
|
||||||
# read-key = 0x00000000000000000000000000000000
|
|
||||||
# ping.req-id = 0x00000001
|
|
||||||
# ping.enr-seq = 2
|
|
||||||
|
|
||||||
00000000000000000000000000000000088b3d4342774649325f313964a39e55
|
|
||||||
ea96c005ad52be8c7560413a7008f16c9e6d2f43bbea8814a546b7409ce783d3
|
|
||||||
4c4f53245d08dab84102ed931f66d1492acb308fa1c6715b9d139b81acbdcc
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
# src-node-id = 0xaaaa8419e9f49d0083561b48287df592939a8d19947d8c0ef88f2a4856a69fbb
|
|
||||||
# dest-node-id = 0xbbbb9d047f0488c0b5a93c1c3f2d8bafc7c8ff337024a55434a0d0555de64db9
|
|
||||||
# whoareyou.challenge-data = 0x000000000000000000000000000000006469736376350001010102030405060708090a0b0c00180102030405060708090a0b0c0d0e0f100000000000000000
|
|
||||||
# whoareyou.request-nonce = 0x0102030405060708090a0b0c
|
|
||||||
# whoareyou.id-nonce = 0x0102030405060708090a0b0c0d0e0f10
|
|
||||||
# whoareyou.enr-seq = 0
|
|
||||||
|
|
||||||
00000000000000000000000000000000088b3d434277464933a1ccc59f5967ad
|
|
||||||
1d6035f15e528627dde75cd68292f9e6c27d6b66c8100a873fcbaed4e16b8d
|
|
||||||
|
|
@ -1,389 +0,0 @@
|
||||||
// Copyright 2019 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package dnsdisc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/lru"
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"golang.org/x/sync/singleflight"
|
|
||||||
"golang.org/x/time/rate"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Client discovers nodes by querying DNS servers.
|
|
||||||
type Client struct {
|
|
||||||
cfg Config
|
|
||||||
clock mclock.Clock
|
|
||||||
entries *lru.Cache[string, entry]
|
|
||||||
ratelimit *rate.Limiter
|
|
||||||
singleflight singleflight.Group
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config holds configuration options for the client.
|
|
||||||
type Config struct {
|
|
||||||
Timeout time.Duration // timeout used for DNS lookups (default 5s)
|
|
||||||
RecheckInterval time.Duration // time between tree root update checks (default 30min)
|
|
||||||
CacheLimit int // maximum number of cached records (default 1000)
|
|
||||||
RateLimit float64 // maximum DNS requests / second (default 3)
|
|
||||||
ValidSchemes enr.IdentityScheme // acceptable ENR identity schemes (default enode.ValidSchemes)
|
|
||||||
Resolver Resolver // the DNS resolver to use (defaults to system DNS)
|
|
||||||
Logger log.Logger // destination of client log messages (defaults to root logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolver is a DNS resolver that can query TXT records.
|
|
||||||
type Resolver interface {
|
|
||||||
LookupTXT(ctx context.Context, domain string) ([]string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg Config) withDefaults() Config {
|
|
||||||
const (
|
|
||||||
defaultTimeout = 5 * time.Second
|
|
||||||
defaultRecheck = 30 * time.Minute
|
|
||||||
defaultRateLimit = 3
|
|
||||||
defaultCache = 1000
|
|
||||||
)
|
|
||||||
if cfg.Timeout == 0 {
|
|
||||||
cfg.Timeout = defaultTimeout
|
|
||||||
}
|
|
||||||
if cfg.RecheckInterval == 0 {
|
|
||||||
cfg.RecheckInterval = defaultRecheck
|
|
||||||
}
|
|
||||||
if cfg.CacheLimit == 0 {
|
|
||||||
cfg.CacheLimit = defaultCache
|
|
||||||
}
|
|
||||||
if cfg.RateLimit == 0 {
|
|
||||||
cfg.RateLimit = defaultRateLimit
|
|
||||||
}
|
|
||||||
if cfg.ValidSchemes == nil {
|
|
||||||
cfg.ValidSchemes = enode.ValidSchemes
|
|
||||||
}
|
|
||||||
if cfg.Resolver == nil {
|
|
||||||
cfg.Resolver = new(net.Resolver)
|
|
||||||
}
|
|
||||||
if cfg.Logger == nil {
|
|
||||||
cfg.Logger = log.Root()
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient creates a client.
|
|
||||||
func NewClient(cfg Config) *Client {
|
|
||||||
cfg = cfg.withDefaults()
|
|
||||||
rlimit := rate.NewLimiter(rate.Limit(cfg.RateLimit), 10)
|
|
||||||
return &Client{
|
|
||||||
cfg: cfg,
|
|
||||||
entries: lru.NewCache[string, entry](cfg.CacheLimit),
|
|
||||||
clock: mclock.System{},
|
|
||||||
ratelimit: rlimit,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncTree downloads the entire node tree at the given URL.
|
|
||||||
func (c *Client) SyncTree(url string) (*Tree, error) {
|
|
||||||
le, err := parseLink(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid enrtree URL: %v", err)
|
|
||||||
}
|
|
||||||
ct := newClientTree(c, new(linkCache), le)
|
|
||||||
t := &Tree{entries: make(map[string]entry)}
|
|
||||||
if err := ct.syncAll(t.entries); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
t.root = ct.root
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewIterator creates an iterator that visits all nodes at the
|
|
||||||
// given tree URLs.
|
|
||||||
func (c *Client) NewIterator(urls ...string) (enode.Iterator, error) {
|
|
||||||
it := c.newRandomIterator()
|
|
||||||
for _, url := range urls {
|
|
||||||
if err := it.addTree(url); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return it, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolveRoot retrieves a root entry via DNS.
|
|
||||||
func (c *Client) resolveRoot(ctx context.Context, loc *linkEntry) (rootEntry, error) {
|
|
||||||
e, err, _ := c.singleflight.Do(loc.str, func() (interface{}, error) {
|
|
||||||
txts, err := c.cfg.Resolver.LookupTXT(ctx, loc.domain)
|
|
||||||
c.cfg.Logger.Trace("Updating DNS discovery root", "tree", loc.domain, "err", err)
|
|
||||||
if err != nil {
|
|
||||||
return rootEntry{}, err
|
|
||||||
}
|
|
||||||
for _, txt := range txts {
|
|
||||||
if strings.HasPrefix(txt, rootPrefix) {
|
|
||||||
return parseAndVerifyRoot(txt, loc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rootEntry{}, nameError{loc.domain, errNoRoot}
|
|
||||||
})
|
|
||||||
return e.(rootEntry), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAndVerifyRoot(txt string, loc *linkEntry) (rootEntry, error) {
|
|
||||||
e, err := parseRoot(txt)
|
|
||||||
if err != nil {
|
|
||||||
return e, err
|
|
||||||
}
|
|
||||||
if !e.verifySignature(loc.pubkey) {
|
|
||||||
return e, entryError{typ: "root", err: errInvalidSig}
|
|
||||||
}
|
|
||||||
return e, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolveEntry retrieves an entry from the cache or fetches it from the network
|
|
||||||
// if it isn't cached.
|
|
||||||
func (c *Client) resolveEntry(ctx context.Context, domain, hash string) (entry, error) {
|
|
||||||
// The rate limit always applies, even when the result might be cached. This is
|
|
||||||
// important because it avoids hot-spinning in consumers of node iterators created on
|
|
||||||
// this client.
|
|
||||||
if err := c.ratelimit.Wait(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cacheKey := truncateHash(hash)
|
|
||||||
if e, ok := c.entries.Get(cacheKey); ok {
|
|
||||||
return e, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ei, err, _ := c.singleflight.Do(cacheKey, func() (interface{}, error) {
|
|
||||||
e, err := c.doResolveEntry(ctx, domain, hash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.entries.Add(cacheKey, e)
|
|
||||||
return e, nil
|
|
||||||
})
|
|
||||||
e, _ := ei.(entry)
|
|
||||||
return e, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// doResolveEntry fetches an entry via DNS.
|
|
||||||
func (c *Client) doResolveEntry(ctx context.Context, domain, hash string) (entry, error) {
|
|
||||||
wantHash, err := b32format.DecodeString(hash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid base32 hash")
|
|
||||||
}
|
|
||||||
name := hash + "." + domain
|
|
||||||
txts, err := c.cfg.Resolver.LookupTXT(ctx, hash+"."+domain)
|
|
||||||
c.cfg.Logger.Trace("DNS discovery lookup", "name", name, "err", err)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, txt := range txts {
|
|
||||||
e, err := parseEntry(txt, c.cfg.ValidSchemes)
|
|
||||||
if errors.Is(err, errUnknownEntry) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !bytes.HasPrefix(crypto.Keccak256([]byte(txt)), wantHash) {
|
|
||||||
err = nameError{name, errHashMismatch}
|
|
||||||
} else if err != nil {
|
|
||||||
err = nameError{name, err}
|
|
||||||
}
|
|
||||||
return e, err
|
|
||||||
}
|
|
||||||
return nil, nameError{name, errNoEntry}
|
|
||||||
}
|
|
||||||
|
|
||||||
// randomIterator traverses a set of trees and returns nodes found in them.
|
|
||||||
type randomIterator struct {
|
|
||||||
cur *enode.Node
|
|
||||||
ctx context.Context
|
|
||||||
cancelFn context.CancelFunc
|
|
||||||
c *Client
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
lc linkCache // tracks tree dependencies
|
|
||||||
trees map[string]*clientTree // all trees
|
|
||||||
// buffers for syncableTrees
|
|
||||||
syncableList []*clientTree
|
|
||||||
disabledList []*clientTree
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) newRandomIterator() *randomIterator {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
return &randomIterator{
|
|
||||||
c: c,
|
|
||||||
ctx: ctx,
|
|
||||||
cancelFn: cancel,
|
|
||||||
trees: make(map[string]*clientTree),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node returns the current node.
|
|
||||||
func (it *randomIterator) Node() *enode.Node {
|
|
||||||
return it.cur
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the iterator.
|
|
||||||
func (it *randomIterator) Close() {
|
|
||||||
it.cancelFn()
|
|
||||||
|
|
||||||
it.mu.Lock()
|
|
||||||
defer it.mu.Unlock()
|
|
||||||
it.trees = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next moves the iterator to the next node.
|
|
||||||
func (it *randomIterator) Next() bool {
|
|
||||||
it.cur = it.nextNode()
|
|
||||||
return it.cur != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// addTree adds an enrtree:// URL to the iterator.
|
|
||||||
func (it *randomIterator) addTree(url string) error {
|
|
||||||
le, err := parseLink(url)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid enrtree URL: %v", err)
|
|
||||||
}
|
|
||||||
it.lc.addLink("", le.str)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// nextNode syncs random tree entries until it finds a node.
|
|
||||||
func (it *randomIterator) nextNode() *enode.Node {
|
|
||||||
for {
|
|
||||||
ct := it.pickTree()
|
|
||||||
if ct == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
n, err := ct.syncRandom(it.ctx)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, it.ctx.Err()) {
|
|
||||||
return nil // context canceled.
|
|
||||||
}
|
|
||||||
it.c.cfg.Logger.Debug("Error in DNS random node sync", "tree", ct.loc.domain, "err", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if n != nil {
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pickTree returns a random tree to sync from.
|
|
||||||
func (it *randomIterator) pickTree() *clientTree {
|
|
||||||
it.mu.Lock()
|
|
||||||
defer it.mu.Unlock()
|
|
||||||
|
|
||||||
// First check if iterator was closed.
|
|
||||||
// Need to do this here to avoid nil map access in rebuildTrees.
|
|
||||||
if it.trees == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rebuild the trees map if any links have changed.
|
|
||||||
if it.lc.changed {
|
|
||||||
it.rebuildTrees()
|
|
||||||
it.lc.changed = false
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
canSync, trees := it.syncableTrees()
|
|
||||||
switch {
|
|
||||||
case canSync:
|
|
||||||
// Pick a random tree.
|
|
||||||
return trees[rand.Intn(len(trees))]
|
|
||||||
case len(trees) > 0:
|
|
||||||
// No sync action can be performed on any tree right now. The only meaningful
|
|
||||||
// thing to do is waiting for any root record to get updated.
|
|
||||||
if !it.waitForRootUpdates(trees) {
|
|
||||||
// Iterator was closed while waiting.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// There are no trees left, the iterator was closed.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// syncableTrees finds trees on which any meaningful sync action can be performed.
|
|
||||||
func (it *randomIterator) syncableTrees() (canSync bool, trees []*clientTree) {
|
|
||||||
// Resize tree lists.
|
|
||||||
it.syncableList = it.syncableList[:0]
|
|
||||||
it.disabledList = it.disabledList[:0]
|
|
||||||
|
|
||||||
// Partition them into the two lists.
|
|
||||||
for _, ct := range it.trees {
|
|
||||||
if ct.canSyncRandom() {
|
|
||||||
it.syncableList = append(it.syncableList, ct)
|
|
||||||
} else {
|
|
||||||
it.disabledList = append(it.disabledList, ct)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(it.syncableList) > 0 {
|
|
||||||
return true, it.syncableList
|
|
||||||
}
|
|
||||||
return false, it.disabledList
|
|
||||||
}
|
|
||||||
|
|
||||||
// waitForRootUpdates waits for the closest scheduled root check time on the given trees.
|
|
||||||
func (it *randomIterator) waitForRootUpdates(trees []*clientTree) bool {
|
|
||||||
var minTree *clientTree
|
|
||||||
var nextCheck mclock.AbsTime
|
|
||||||
for _, ct := range trees {
|
|
||||||
check := ct.nextScheduledRootCheck()
|
|
||||||
if minTree == nil || check < nextCheck {
|
|
||||||
minTree = ct
|
|
||||||
nextCheck = check
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sleep := nextCheck.Sub(it.c.clock.Now())
|
|
||||||
it.c.cfg.Logger.Debug("DNS iterator waiting for root updates", "sleep", sleep, "tree", minTree.loc.domain)
|
|
||||||
timeout := it.c.clock.NewTimer(sleep)
|
|
||||||
defer timeout.Stop()
|
|
||||||
select {
|
|
||||||
case <-timeout.C():
|
|
||||||
return true
|
|
||||||
case <-it.ctx.Done():
|
|
||||||
return false // Iterator was closed.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// rebuildTrees rebuilds the 'trees' map.
|
|
||||||
func (it *randomIterator) rebuildTrees() {
|
|
||||||
// Delete removed trees.
|
|
||||||
for loc := range it.trees {
|
|
||||||
if !it.lc.isReferenced(loc) {
|
|
||||||
delete(it.trees, loc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Add new trees.
|
|
||||||
for loc := range it.lc.backrefs {
|
|
||||||
if it.trees[loc] == nil {
|
|
||||||
link, _ := parseLink(linkPrefix + loc)
|
|
||||||
it.trees[loc] = newClientTree(it.c, &it.lc, link)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,478 +0,0 @@
|
||||||
// Copyright 2019 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package dnsdisc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"errors"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
var signingKeyForTesting, _ = crypto.ToECDSA(hexutil.MustDecode("0xdc599867fc513f8f5e2c2c9c489cde5e71362d1d9ec6e693e0de063236ed1240"))
|
|
||||||
|
|
||||||
func TestClientSyncTree(t *testing.T) {
|
|
||||||
nodes := []string{
|
|
||||||
"enr:-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA",
|
|
||||||
"enr:-HW4QAggRauloj2SDLtIHN1XBkvhFZ1vtf1raYQp9TBW2RD5EEawDzbtSmlXUfnaHcvwOizhVYLtr7e6vw7NAf6mTuoCgmlkgnY0iXNlY3AyNTZrMaECjrXI8TLNXU0f8cthpAMxEshUyQlK-AM0PW2wfrnacNI",
|
|
||||||
"enr:-HW4QLAYqmrwllBEnzWWs7I5Ev2IAs7x_dZlbYdRdMUx5EyKHDXp7AV5CkuPGUPdvbv1_Ms1CPfhcGCvSElSosZmyoqAgmlkgnY0iXNlY3AyNTZrMaECriawHKWdDRk2xeZkrOXBQ0dfMFLHY4eENZwdufn1S1o",
|
|
||||||
}
|
|
||||||
|
|
||||||
r := mapResolver{
|
|
||||||
"n": "enrtree-root:v1 e=JWXYDBPXYWG6FX3GMDIBFA6CJ4 l=C7HRFPF3BLGF3YR4DY5KX3SMBE seq=1 sig=o908WmNp7LibOfPsr4btQwatZJ5URBr2ZAuxvK4UWHlsB9sUOTJQaGAlLPVAhM__XJesCHxLISo94z5Z2a463gA",
|
|
||||||
"C7HRFPF3BLGF3YR4DY5KX3SMBE.n": "enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org",
|
|
||||||
"JWXYDBPXYWG6FX3GMDIBFA6CJ4.n": "enrtree-branch:2XS2367YHAXJFGLZHVAWLQD4ZY,H4FHT4B454P6UXFD7JCYQ5PWDY,MHTDO6TMUBRIA2XWG5LUDACK24",
|
|
||||||
"2XS2367YHAXJFGLZHVAWLQD4ZY.n": nodes[0],
|
|
||||||
"H4FHT4B454P6UXFD7JCYQ5PWDY.n": nodes[1],
|
|
||||||
"MHTDO6TMUBRIA2XWG5LUDACK24.n": nodes[2],
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
wantNodes = sortByID(parseNodes(nodes))
|
|
||||||
wantLinks = []string{"enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org"}
|
|
||||||
wantSeq = uint(1)
|
|
||||||
)
|
|
||||||
|
|
||||||
c := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
|
|
||||||
stree, err := c.SyncTree("enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@n")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("sync error:", err)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(sortByID(stree.Nodes()), wantNodes) {
|
|
||||||
t.Errorf("wrong nodes in synced tree:\nhave %v\nwant %v", spew.Sdump(stree.Nodes()), spew.Sdump(wantNodes))
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(stree.Links(), wantLinks) {
|
|
||||||
t.Errorf("wrong links in synced tree: %v", stree.Links())
|
|
||||||
}
|
|
||||||
if stree.Seq() != wantSeq {
|
|
||||||
t.Errorf("synced tree has wrong seq: %d", stree.Seq())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In this test, syncing the tree fails because it contains an invalid ENR entry.
|
|
||||||
func TestClientSyncTreeBadNode(t *testing.T) {
|
|
||||||
// var b strings.Builder
|
|
||||||
// b.WriteString(enrPrefix)
|
|
||||||
// b.WriteString("-----")
|
|
||||||
// badHash := subdomain(&b)
|
|
||||||
// tree, _ := MakeTree(3, nil, []string{"enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org"})
|
|
||||||
// tree.entries[badHash] = &b
|
|
||||||
// tree.root.eroot = badHash
|
|
||||||
// url, _ := tree.Sign(signingKeyForTesting, "n")
|
|
||||||
// fmt.Println(url)
|
|
||||||
// fmt.Printf("%#v\n", tree.ToTXT("n"))
|
|
||||||
|
|
||||||
r := mapResolver{
|
|
||||||
"n": "enrtree-root:v1 e=INDMVBZEEQ4ESVYAKGIYU74EAA l=C7HRFPF3BLGF3YR4DY5KX3SMBE seq=3 sig=Vl3AmunLur0JZ3sIyJPSH6A3Vvdp4F40jWQeCmkIhmcgwE4VC5U9wpK8C_uL_CMY29fd6FAhspRvq2z_VysTLAA",
|
|
||||||
"C7HRFPF3BLGF3YR4DY5KX3SMBE.n": "enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org",
|
|
||||||
"INDMVBZEEQ4ESVYAKGIYU74EAA.n": "enr:-----",
|
|
||||||
}
|
|
||||||
c := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
|
|
||||||
_, err := c.SyncTree("enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@n")
|
|
||||||
wantErr := nameError{name: "INDMVBZEEQ4ESVYAKGIYU74EAA.n", err: entryError{typ: "enr", err: errInvalidENR}}
|
|
||||||
if err != wantErr {
|
|
||||||
t.Fatalf("expected sync error %q, got %q", wantErr, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that randomIterator finds all entries.
|
|
||||||
func TestIterator(t *testing.T) {
|
|
||||||
var (
|
|
||||||
keys = testKeys(30)
|
|
||||||
nodes = testNodes(keys)
|
|
||||||
tree, url = makeTestTree("n", nodes, nil)
|
|
||||||
r = mapResolver(tree.ToTXT("n"))
|
|
||||||
)
|
|
||||||
|
|
||||||
c := NewClient(Config{
|
|
||||||
Resolver: r,
|
|
||||||
Logger: testlog.Logger(t, log.LvlTrace),
|
|
||||||
RateLimit: 500,
|
|
||||||
})
|
|
||||||
it, err := c.NewIterator(url)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
checkIterator(t, it, nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIteratorCloseWithoutNext(t *testing.T) {
|
|
||||||
tree1, url1 := makeTestTree("t1", nil, nil)
|
|
||||||
c := NewClient(Config{Resolver: newMapResolver(tree1.ToTXT("t1"))})
|
|
||||||
it, err := c.NewIterator(url1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
it.Close()
|
|
||||||
ok := it.Next()
|
|
||||||
if ok {
|
|
||||||
t.Fatal("Next returned true after Close")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks if closing randomIterator races.
|
|
||||||
func TestIteratorClose(t *testing.T) {
|
|
||||||
var (
|
|
||||||
keys = testKeys(500)
|
|
||||||
nodes = testNodes(keys)
|
|
||||||
tree1, url1 = makeTestTree("t1", nodes, nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
c := NewClient(Config{Resolver: newMapResolver(tree1.ToTXT("t1"))})
|
|
||||||
it, err := c.NewIterator(url1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
for it.Next() {
|
|
||||||
_ = it.Node()
|
|
||||||
}
|
|
||||||
close(done)
|
|
||||||
}()
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
it.Close()
|
|
||||||
<-done
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that randomIterator traverses linked trees as well as explicitly added trees.
|
|
||||||
func TestIteratorLinks(t *testing.T) {
|
|
||||||
var (
|
|
||||||
keys = testKeys(40)
|
|
||||||
nodes = testNodes(keys)
|
|
||||||
tree1, url1 = makeTestTree("t1", nodes[:10], nil)
|
|
||||||
tree2, url2 = makeTestTree("t2", nodes[10:], []string{url1})
|
|
||||||
)
|
|
||||||
|
|
||||||
c := NewClient(Config{
|
|
||||||
Resolver: newMapResolver(tree1.ToTXT("t1"), tree2.ToTXT("t2")),
|
|
||||||
Logger: testlog.Logger(t, log.LvlTrace),
|
|
||||||
RateLimit: 500,
|
|
||||||
})
|
|
||||||
it, err := c.NewIterator(url2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
checkIterator(t, it, nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test verifies that randomIterator re-checks the root of the tree to catch
|
|
||||||
// updates to nodes.
|
|
||||||
func TestIteratorNodeUpdates(t *testing.T) {
|
|
||||||
var (
|
|
||||||
clock = new(mclock.Simulated)
|
|
||||||
keys = testKeys(30)
|
|
||||||
nodes = testNodes(keys)
|
|
||||||
resolver = newMapResolver()
|
|
||||||
c = NewClient(Config{
|
|
||||||
Resolver: resolver,
|
|
||||||
Logger: testlog.Logger(t, log.LvlTrace),
|
|
||||||
RecheckInterval: 20 * time.Minute,
|
|
||||||
RateLimit: 500,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
c.clock = clock
|
|
||||||
tree1, url := makeTestTree("n", nodes[:25], nil)
|
|
||||||
it, err := c.NewIterator(url)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync the original tree.
|
|
||||||
resolver.add(tree1.ToTXT("n"))
|
|
||||||
checkIterator(t, it, nodes[:25])
|
|
||||||
|
|
||||||
// Ensure RandomNode returns the new nodes after the tree is updated.
|
|
||||||
updateSomeNodes(keys, nodes)
|
|
||||||
tree2, _ := makeTestTree("n", nodes, nil)
|
|
||||||
resolver.clear()
|
|
||||||
resolver.add(tree2.ToTXT("n"))
|
|
||||||
t.Log("tree updated")
|
|
||||||
|
|
||||||
clock.Run(c.cfg.RecheckInterval + 1*time.Second)
|
|
||||||
checkIterator(t, it, nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that the tree root is rechecked when a couple of leaf
|
|
||||||
// requests have failed. The test is just like TestIteratorNodeUpdates, but
|
|
||||||
// without advancing the clock by recheckInterval after the tree update.
|
|
||||||
func TestIteratorRootRecheckOnFail(t *testing.T) {
|
|
||||||
var (
|
|
||||||
clock = new(mclock.Simulated)
|
|
||||||
keys = testKeys(30)
|
|
||||||
nodes = testNodes(keys)
|
|
||||||
resolver = newMapResolver()
|
|
||||||
c = NewClient(Config{
|
|
||||||
Resolver: resolver,
|
|
||||||
Logger: testlog.Logger(t, log.LvlTrace),
|
|
||||||
RecheckInterval: 20 * time.Minute,
|
|
||||||
RateLimit: 500,
|
|
||||||
// Disabling the cache is required for this test because the client doesn't
|
|
||||||
// notice leaf failures if all records are cached.
|
|
||||||
CacheLimit: 1,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
c.clock = clock
|
|
||||||
tree1, url := makeTestTree("n", nodes[:25], nil)
|
|
||||||
it, err := c.NewIterator(url)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync the original tree.
|
|
||||||
resolver.add(tree1.ToTXT("n"))
|
|
||||||
checkIterator(t, it, nodes[:25])
|
|
||||||
|
|
||||||
// Ensure RandomNode returns the new nodes after the tree is updated.
|
|
||||||
updateSomeNodes(keys, nodes)
|
|
||||||
tree2, _ := makeTestTree("n", nodes, nil)
|
|
||||||
resolver.clear()
|
|
||||||
resolver.add(tree2.ToTXT("n"))
|
|
||||||
t.Log("tree updated")
|
|
||||||
|
|
||||||
checkIterator(t, it, nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that the iterator works correctly when the tree is initially empty.
|
|
||||||
func TestIteratorEmptyTree(t *testing.T) {
|
|
||||||
var (
|
|
||||||
clock = new(mclock.Simulated)
|
|
||||||
keys = testKeys(1)
|
|
||||||
nodes = testNodes(keys)
|
|
||||||
resolver = newMapResolver()
|
|
||||||
c = NewClient(Config{
|
|
||||||
Resolver: resolver,
|
|
||||||
Logger: testlog.Logger(t, log.LvlTrace),
|
|
||||||
RecheckInterval: 20 * time.Minute,
|
|
||||||
RateLimit: 500,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
c.clock = clock
|
|
||||||
tree1, url := makeTestTree("n", nil, nil)
|
|
||||||
tree2, _ := makeTestTree("n", nodes, nil)
|
|
||||||
resolver.add(tree1.ToTXT("n"))
|
|
||||||
|
|
||||||
// Start the iterator.
|
|
||||||
node := make(chan *enode.Node, 1)
|
|
||||||
it, err := c.NewIterator(url)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
it.Next()
|
|
||||||
node <- it.Node()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait for the client to get stuck in waitForRootUpdates.
|
|
||||||
clock.WaitForTimers(1)
|
|
||||||
|
|
||||||
// Now update the root.
|
|
||||||
resolver.add(tree2.ToTXT("n"))
|
|
||||||
|
|
||||||
// Wait for it to pick up the root change.
|
|
||||||
clock.Run(c.cfg.RecheckInterval)
|
|
||||||
select {
|
|
||||||
case n := <-node:
|
|
||||||
if n.ID() != nodes[0].ID() {
|
|
||||||
t.Fatalf("wrong node returned")
|
|
||||||
}
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
t.Fatal("it.Next() did not unblock within 5s of real time")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateSomeNodes applies ENR updates to some of the given nodes.
|
|
||||||
func updateSomeNodes(keys []*ecdsa.PrivateKey, nodes []*enode.Node) {
|
|
||||||
for i, n := range nodes[:len(nodes)/2] {
|
|
||||||
r := n.Record()
|
|
||||||
r.Set(enr.IP{127, 0, 0, 1})
|
|
||||||
r.SetSeq(55)
|
|
||||||
enode.SignV4(r, keys[i])
|
|
||||||
n2, _ := enode.New(enode.ValidSchemes, r)
|
|
||||||
nodes[i] = n2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test verifies that randomIterator re-checks the root of the tree to catch
|
|
||||||
// updates to links.
|
|
||||||
func TestIteratorLinkUpdates(t *testing.T) {
|
|
||||||
var (
|
|
||||||
clock = new(mclock.Simulated)
|
|
||||||
keys = testKeys(30)
|
|
||||||
nodes = testNodes(keys)
|
|
||||||
resolver = newMapResolver()
|
|
||||||
c = NewClient(Config{
|
|
||||||
Resolver: resolver,
|
|
||||||
Logger: testlog.Logger(t, log.LvlTrace),
|
|
||||||
RecheckInterval: 20 * time.Minute,
|
|
||||||
RateLimit: 500,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
c.clock = clock
|
|
||||||
tree3, url3 := makeTestTree("t3", nodes[20:30], nil)
|
|
||||||
tree2, url2 := makeTestTree("t2", nodes[10:20], nil)
|
|
||||||
tree1, url1 := makeTestTree("t1", nodes[0:10], []string{url2})
|
|
||||||
resolver.add(tree1.ToTXT("t1"))
|
|
||||||
resolver.add(tree2.ToTXT("t2"))
|
|
||||||
resolver.add(tree3.ToTXT("t3"))
|
|
||||||
|
|
||||||
it, err := c.NewIterator(url1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync tree1 using RandomNode.
|
|
||||||
checkIterator(t, it, nodes[:20])
|
|
||||||
|
|
||||||
// Add link to tree3, remove link to tree2.
|
|
||||||
tree1, _ = makeTestTree("t1", nodes[:10], []string{url3})
|
|
||||||
resolver.add(tree1.ToTXT("t1"))
|
|
||||||
t.Log("tree1 updated")
|
|
||||||
|
|
||||||
clock.Run(c.cfg.RecheckInterval + 1*time.Second)
|
|
||||||
|
|
||||||
var wantNodes []*enode.Node
|
|
||||||
wantNodes = append(wantNodes, tree1.Nodes()...)
|
|
||||||
wantNodes = append(wantNodes, tree3.Nodes()...)
|
|
||||||
checkIterator(t, it, wantNodes)
|
|
||||||
|
|
||||||
// Check that linked trees are GCed when they're no longer referenced.
|
|
||||||
knownTrees := it.(*randomIterator).trees
|
|
||||||
if len(knownTrees) != 2 {
|
|
||||||
t.Errorf("client knows %d trees, want 2", len(knownTrees))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkIterator(t *testing.T, it enode.Iterator, wantNodes []*enode.Node) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
var (
|
|
||||||
want = make(map[enode.ID]*enode.Node)
|
|
||||||
maxCalls = len(wantNodes) * 3
|
|
||||||
calls = 0
|
|
||||||
)
|
|
||||||
for _, n := range wantNodes {
|
|
||||||
want[n.ID()] = n
|
|
||||||
}
|
|
||||||
for ; len(want) > 0 && calls < maxCalls; calls++ {
|
|
||||||
if !it.Next() {
|
|
||||||
t.Fatalf("Next returned false (call %d)", calls)
|
|
||||||
}
|
|
||||||
n := it.Node()
|
|
||||||
delete(want, n.ID())
|
|
||||||
}
|
|
||||||
t.Logf("checkIterator called Next %d times to find %d nodes", calls, len(wantNodes))
|
|
||||||
for _, n := range want {
|
|
||||||
t.Errorf("iterator didn't discover node %v", n.ID())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeTestTree(domain string, nodes []*enode.Node, links []string) (*Tree, string) {
|
|
||||||
tree, err := MakeTree(1, nodes, links)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
url, err := tree.Sign(signingKeyForTesting, domain)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return tree, url
|
|
||||||
}
|
|
||||||
|
|
||||||
// testKeys creates deterministic private keys for testing.
|
|
||||||
func testKeys(n int) []*ecdsa.PrivateKey {
|
|
||||||
keys := make([]*ecdsa.PrivateKey, n)
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
key, err := crypto.GenerateKey()
|
|
||||||
if err != nil {
|
|
||||||
panic("can't generate key: " + err.Error())
|
|
||||||
}
|
|
||||||
keys[i] = key
|
|
||||||
}
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
|
|
||||||
func testNodes(keys []*ecdsa.PrivateKey) []*enode.Node {
|
|
||||||
nodes := make([]*enode.Node, len(keys))
|
|
||||||
for i, key := range keys {
|
|
||||||
record := new(enr.Record)
|
|
||||||
record.SetSeq(uint64(i))
|
|
||||||
enode.SignV4(record, key)
|
|
||||||
n, err := enode.New(enode.ValidSchemes, record)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
nodes[i] = n
|
|
||||||
}
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
type mapResolver map[string]string
|
|
||||||
|
|
||||||
func newMapResolver(maps ...map[string]string) mapResolver {
|
|
||||||
mr := make(mapResolver, len(maps))
|
|
||||||
for _, m := range maps {
|
|
||||||
mr.add(m)
|
|
||||||
}
|
|
||||||
return mr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mr mapResolver) clear() {
|
|
||||||
for k := range mr {
|
|
||||||
delete(mr, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mr mapResolver) add(m map[string]string) {
|
|
||||||
for k, v := range m {
|
|
||||||
mr[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mr mapResolver) LookupTXT(ctx context.Context, name string) ([]string, error) {
|
|
||||||
if record, ok := mr[name]; ok {
|
|
||||||
return []string{record}, nil
|
|
||||||
}
|
|
||||||
return nil, errors.New("not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseNodes(rec []string) []*enode.Node {
|
|
||||||
var ns []*enode.Node
|
|
||||||
for _, r := range rec {
|
|
||||||
var n enode.Node
|
|
||||||
if err := n.UnmarshalText([]byte(r)); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
ns = append(ns, &n)
|
|
||||||
}
|
|
||||||
return ns
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Package dnsdisc implements node discovery via DNS (EIP-1459).
|
|
||||||
package dnsdisc
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
// Copyright 2019 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package dnsdisc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Entry parse errors.
|
|
||||||
var (
|
|
||||||
errUnknownEntry = errors.New("unknown entry type")
|
|
||||||
errNoPubkey = errors.New("missing public key")
|
|
||||||
errBadPubkey = errors.New("invalid public key")
|
|
||||||
errInvalidENR = errors.New("invalid node record")
|
|
||||||
errInvalidChild = errors.New("invalid child hash")
|
|
||||||
errInvalidSig = errors.New("invalid base64 signature")
|
|
||||||
errSyntax = errors.New("invalid syntax")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Resolver/sync errors
|
|
||||||
var (
|
|
||||||
errNoRoot = errors.New("no valid root found")
|
|
||||||
errNoEntry = errors.New("no valid tree entry found")
|
|
||||||
errHashMismatch = errors.New("hash mismatch")
|
|
||||||
errENRInLinkTree = errors.New("enr entry in link tree")
|
|
||||||
errLinkInENRTree = errors.New("link entry in ENR tree")
|
|
||||||
)
|
|
||||||
|
|
||||||
type nameError struct {
|
|
||||||
name string
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err nameError) Error() string {
|
|
||||||
if ee, ok := err.err.(entryError); ok {
|
|
||||||
return fmt.Sprintf("invalid %s entry at %s: %v", ee.typ, err.name, ee.err)
|
|
||||||
}
|
|
||||||
return err.name + ": " + err.err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
type entryError struct {
|
|
||||||
typ string
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err entryError) Error() string {
|
|
||||||
return fmt.Sprintf("invalid %s entry: %v", err.typ, err.err)
|
|
||||||
}
|
|
||||||
|
|
@ -1,329 +0,0 @@
|
||||||
// Copyright 2019 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package dnsdisc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This is the number of consecutive leaf requests that may fail before
|
|
||||||
// we consider re-resolving the tree root.
|
|
||||||
const rootRecheckFailCount = 5
|
|
||||||
|
|
||||||
// clientTree is a full tree being synced.
|
|
||||||
type clientTree struct {
|
|
||||||
c *Client
|
|
||||||
loc *linkEntry // link to this tree
|
|
||||||
|
|
||||||
lastRootCheck mclock.AbsTime // last revalidation of root
|
|
||||||
leafFailCount int
|
|
||||||
rootFailCount int
|
|
||||||
|
|
||||||
root *rootEntry
|
|
||||||
enrs *subtreeSync
|
|
||||||
links *subtreeSync
|
|
||||||
|
|
||||||
lc *linkCache // tracks all links between all trees
|
|
||||||
curLinks map[string]struct{} // links contained in this tree
|
|
||||||
linkGCRoot string // root on which last link GC has run
|
|
||||||
}
|
|
||||||
|
|
||||||
func newClientTree(c *Client, lc *linkCache, loc *linkEntry) *clientTree {
|
|
||||||
return &clientTree{c: c, lc: lc, loc: loc}
|
|
||||||
}
|
|
||||||
|
|
||||||
// syncAll retrieves all entries of the tree.
|
|
||||||
func (ct *clientTree) syncAll(dest map[string]entry) error {
|
|
||||||
if err := ct.updateRoot(context.Background()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := ct.links.resolveAll(dest); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := ct.enrs.resolveAll(dest); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// syncRandom retrieves a single entry of the tree. The Node return value
|
|
||||||
// is non-nil if the entry was a node.
|
|
||||||
func (ct *clientTree) syncRandom(ctx context.Context) (n *enode.Node, err error) {
|
|
||||||
if ct.rootUpdateDue() {
|
|
||||||
if err := ct.updateRoot(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update fail counter for leaf request errors.
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
ct.leafFailCount++
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Link tree sync has priority, run it to completion before syncing ENRs.
|
|
||||||
if !ct.links.done() {
|
|
||||||
err := ct.syncNextLink(ctx)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ct.gcLinks()
|
|
||||||
|
|
||||||
// Sync next random entry in ENR tree. Once every node has been visited, we simply
|
|
||||||
// start over. This is fine because entries are cached internally by the client LRU
|
|
||||||
// also by DNS resolvers.
|
|
||||||
if ct.enrs.done() {
|
|
||||||
ct.enrs = newSubtreeSync(ct.c, ct.loc, ct.root.eroot, false)
|
|
||||||
}
|
|
||||||
return ct.syncNextRandomENR(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// canSyncRandom checks if any meaningful action can be performed by syncRandom.
|
|
||||||
func (ct *clientTree) canSyncRandom() bool {
|
|
||||||
// Note: the check for non-zero leaf count is very important here.
|
|
||||||
// If we're done syncing all nodes, and no leaves were found, the tree
|
|
||||||
// is empty and we can't use it for sync.
|
|
||||||
return ct.rootUpdateDue() || !ct.links.done() || !ct.enrs.done() || ct.enrs.leaves != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// gcLinks removes outdated links from the global link cache. GC runs once
|
|
||||||
// when the link sync finishes.
|
|
||||||
func (ct *clientTree) gcLinks() {
|
|
||||||
if !ct.links.done() || ct.root.lroot == ct.linkGCRoot {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ct.lc.resetLinks(ct.loc.str, ct.curLinks)
|
|
||||||
ct.linkGCRoot = ct.root.lroot
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ct *clientTree) syncNextLink(ctx context.Context) error {
|
|
||||||
hash := ct.links.missing[0]
|
|
||||||
e, err := ct.links.resolveNext(ctx, hash)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ct.links.missing = ct.links.missing[1:]
|
|
||||||
|
|
||||||
if dest, ok := e.(*linkEntry); ok {
|
|
||||||
ct.lc.addLink(ct.loc.str, dest.str)
|
|
||||||
ct.curLinks[dest.str] = struct{}{}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ct *clientTree) syncNextRandomENR(ctx context.Context) (*enode.Node, error) {
|
|
||||||
index := rand.Intn(len(ct.enrs.missing))
|
|
||||||
hash := ct.enrs.missing[index]
|
|
||||||
e, err := ct.enrs.resolveNext(ctx, hash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ct.enrs.missing = removeHash(ct.enrs.missing, index)
|
|
||||||
if ee, ok := e.(*enrEntry); ok {
|
|
||||||
return ee.node, nil
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ct *clientTree) String() string {
|
|
||||||
return ct.loc.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeHash removes the element at index from h.
|
|
||||||
func removeHash(h []string, index int) []string {
|
|
||||||
if len(h) == 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
last := len(h) - 1
|
|
||||||
if index < last {
|
|
||||||
h[index] = h[last]
|
|
||||||
h[last] = ""
|
|
||||||
}
|
|
||||||
return h[:last]
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateRoot ensures that the given tree has an up-to-date root.
|
|
||||||
func (ct *clientTree) updateRoot(ctx context.Context) error {
|
|
||||||
if !ct.slowdownRootUpdate(ctx) {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
ct.lastRootCheck = ct.c.clock.Now()
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, ct.c.cfg.Timeout)
|
|
||||||
defer cancel()
|
|
||||||
root, err := ct.c.resolveRoot(ctx, ct.loc)
|
|
||||||
if err != nil {
|
|
||||||
ct.rootFailCount++
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ct.root = &root
|
|
||||||
ct.rootFailCount = 0
|
|
||||||
ct.leafFailCount = 0
|
|
||||||
|
|
||||||
// Invalidate subtrees if changed.
|
|
||||||
if ct.links == nil || root.lroot != ct.links.root {
|
|
||||||
ct.links = newSubtreeSync(ct.c, ct.loc, root.lroot, true)
|
|
||||||
ct.curLinks = make(map[string]struct{})
|
|
||||||
}
|
|
||||||
if ct.enrs == nil || root.eroot != ct.enrs.root {
|
|
||||||
ct.enrs = newSubtreeSync(ct.c, ct.loc, root.eroot, false)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// rootUpdateDue returns true when a root update is needed.
|
|
||||||
func (ct *clientTree) rootUpdateDue() bool {
|
|
||||||
tooManyFailures := ct.leafFailCount > rootRecheckFailCount
|
|
||||||
scheduledCheck := ct.c.clock.Now() >= ct.nextScheduledRootCheck()
|
|
||||||
return ct.root == nil || tooManyFailures || scheduledCheck
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ct *clientTree) nextScheduledRootCheck() mclock.AbsTime {
|
|
||||||
return ct.lastRootCheck.Add(ct.c.cfg.RecheckInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
// slowdownRootUpdate applies a delay to root resolution if is tried
|
|
||||||
// too frequently. This avoids busy polling when the client is offline.
|
|
||||||
// Returns true if the timeout passed, false if sync was canceled.
|
|
||||||
func (ct *clientTree) slowdownRootUpdate(ctx context.Context) bool {
|
|
||||||
var delay time.Duration
|
|
||||||
switch {
|
|
||||||
case ct.rootFailCount > 20:
|
|
||||||
delay = 10 * time.Second
|
|
||||||
case ct.rootFailCount > 5:
|
|
||||||
delay = 5 * time.Second
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
timeout := ct.c.clock.NewTimer(delay)
|
|
||||||
defer timeout.Stop()
|
|
||||||
select {
|
|
||||||
case <-timeout.C():
|
|
||||||
return true
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// subtreeSync is the sync of an ENR or link subtree.
|
|
||||||
type subtreeSync struct {
|
|
||||||
c *Client
|
|
||||||
loc *linkEntry
|
|
||||||
root string
|
|
||||||
missing []string // missing tree node hashes
|
|
||||||
link bool // true if this sync is for the link tree
|
|
||||||
leaves int // counter of synced leaves
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSubtreeSync(c *Client, loc *linkEntry, root string, link bool) *subtreeSync {
|
|
||||||
return &subtreeSync{c, loc, root, []string{root}, link, 0}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *subtreeSync) done() bool {
|
|
||||||
return len(ts.missing) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *subtreeSync) resolveAll(dest map[string]entry) error {
|
|
||||||
for !ts.done() {
|
|
||||||
hash := ts.missing[0]
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), ts.c.cfg.Timeout)
|
|
||||||
e, err := ts.resolveNext(ctx, hash)
|
|
||||||
cancel()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dest[hash] = e
|
|
||||||
ts.missing = ts.missing[1:]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts *subtreeSync) resolveNext(ctx context.Context, hash string) (entry, error) {
|
|
||||||
e, err := ts.c.resolveEntry(ctx, ts.loc.domain, hash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
switch e := e.(type) {
|
|
||||||
case *enrEntry:
|
|
||||||
if ts.link {
|
|
||||||
return nil, errENRInLinkTree
|
|
||||||
}
|
|
||||||
ts.leaves++
|
|
||||||
case *linkEntry:
|
|
||||||
if !ts.link {
|
|
||||||
return nil, errLinkInENRTree
|
|
||||||
}
|
|
||||||
ts.leaves++
|
|
||||||
case *branchEntry:
|
|
||||||
ts.missing = append(ts.missing, e.children...)
|
|
||||||
}
|
|
||||||
return e, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// linkCache tracks links between trees.
|
|
||||||
type linkCache struct {
|
|
||||||
backrefs map[string]map[string]struct{}
|
|
||||||
changed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lc *linkCache) isReferenced(r string) bool {
|
|
||||||
return len(lc.backrefs[r]) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lc *linkCache) addLink(from, to string) {
|
|
||||||
if _, ok := lc.backrefs[to][from]; ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if lc.backrefs == nil {
|
|
||||||
lc.backrefs = make(map[string]map[string]struct{})
|
|
||||||
}
|
|
||||||
if _, ok := lc.backrefs[to]; !ok {
|
|
||||||
lc.backrefs[to] = make(map[string]struct{})
|
|
||||||
}
|
|
||||||
lc.backrefs[to][from] = struct{}{}
|
|
||||||
lc.changed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// resetLinks clears all links of the given tree.
|
|
||||||
func (lc *linkCache) resetLinks(from string, keep map[string]struct{}) {
|
|
||||||
stk := []string{from}
|
|
||||||
for len(stk) > 0 {
|
|
||||||
item := stk[len(stk)-1]
|
|
||||||
stk = stk[:len(stk)-1]
|
|
||||||
|
|
||||||
for r, refs := range lc.backrefs {
|
|
||||||
if _, ok := keep[r]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := refs[item]; !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
lc.changed = true
|
|
||||||
delete(refs, item)
|
|
||||||
if len(refs) == 0 {
|
|
||||||
delete(lc.backrefs, r)
|
|
||||||
stk = append(stk, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
// Copyright 2019 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package dnsdisc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"strconv"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLinkCache(t *testing.T) {
|
|
||||||
var lc linkCache
|
|
||||||
|
|
||||||
// Check adding links.
|
|
||||||
lc.addLink("1", "2")
|
|
||||||
if !lc.changed {
|
|
||||||
t.Error("changed flag not set")
|
|
||||||
}
|
|
||||||
lc.changed = false
|
|
||||||
lc.addLink("1", "2")
|
|
||||||
if lc.changed {
|
|
||||||
t.Error("changed flag set after adding link that's already present")
|
|
||||||
}
|
|
||||||
lc.addLink("2", "3")
|
|
||||||
lc.addLink("3", "1")
|
|
||||||
lc.addLink("2", "4")
|
|
||||||
lc.changed = false
|
|
||||||
|
|
||||||
if !lc.isReferenced("3") {
|
|
||||||
t.Error("3 not referenced")
|
|
||||||
}
|
|
||||||
if lc.isReferenced("6") {
|
|
||||||
t.Error("6 is referenced")
|
|
||||||
}
|
|
||||||
|
|
||||||
lc.resetLinks("1", nil)
|
|
||||||
if !lc.changed {
|
|
||||||
t.Error("changed flag not set")
|
|
||||||
}
|
|
||||||
if len(lc.backrefs) != 0 {
|
|
||||||
t.Logf("%+v", lc)
|
|
||||||
t.Error("reference maps should be empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLinkCacheRandom(t *testing.T) {
|
|
||||||
tags := make([]string, 1000)
|
|
||||||
for i := range tags {
|
|
||||||
tags[i] = strconv.Itoa(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create random links.
|
|
||||||
var lc linkCache
|
|
||||||
var remove []string
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
a, b := tags[rand.Intn(len(tags))], tags[rand.Intn(len(tags))]
|
|
||||||
lc.addLink(a, b)
|
|
||||||
remove = append(remove, a)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all the links.
|
|
||||||
for _, s := range remove {
|
|
||||||
lc.resetLinks(s, nil)
|
|
||||||
}
|
|
||||||
if len(lc.backrefs) != 0 {
|
|
||||||
t.Logf("%+v", lc)
|
|
||||||
t.Error("reference maps should be empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,423 +0,0 @@
|
||||||
// Copyright 2019 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package dnsdisc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"encoding/base32"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"golang.org/x/crypto/sha3"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tree is a merkle tree of node records.
|
|
||||||
type Tree struct {
|
|
||||||
root *rootEntry
|
|
||||||
entries map[string]entry
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign signs the tree with the given private key and sets the sequence number.
|
|
||||||
func (t *Tree) Sign(key *ecdsa.PrivateKey, domain string) (url string, err error) {
|
|
||||||
root := *t.root
|
|
||||||
sig, err := crypto.Sign(root.sigHash(), key)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
root.sig = sig
|
|
||||||
t.root = &root
|
|
||||||
link := newLinkEntry(domain, &key.PublicKey)
|
|
||||||
return link.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSignature verifies the given signature and assigns it as the tree's current
|
|
||||||
// signature if valid.
|
|
||||||
func (t *Tree) SetSignature(pubkey *ecdsa.PublicKey, signature string) error {
|
|
||||||
sig, err := b64format.DecodeString(signature)
|
|
||||||
if err != nil || len(sig) != crypto.SignatureLength {
|
|
||||||
return errInvalidSig
|
|
||||||
}
|
|
||||||
root := *t.root
|
|
||||||
root.sig = sig
|
|
||||||
if !root.verifySignature(pubkey) {
|
|
||||||
return errInvalidSig
|
|
||||||
}
|
|
||||||
t.root = &root
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seq returns the sequence number of the tree.
|
|
||||||
func (t *Tree) Seq() uint {
|
|
||||||
return t.root.seq
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signature returns the signature of the tree.
|
|
||||||
func (t *Tree) Signature() string {
|
|
||||||
return b64format.EncodeToString(t.root.sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToTXT returns all DNS TXT records required for the tree.
|
|
||||||
func (t *Tree) ToTXT(domain string) map[string]string {
|
|
||||||
records := map[string]string{domain: t.root.String()}
|
|
||||||
for _, e := range t.entries {
|
|
||||||
sd := subdomain(e)
|
|
||||||
if domain != "" {
|
|
||||||
sd = sd + "." + domain
|
|
||||||
}
|
|
||||||
records[sd] = e.String()
|
|
||||||
}
|
|
||||||
return records
|
|
||||||
}
|
|
||||||
|
|
||||||
// Links returns all links contained in the tree.
|
|
||||||
func (t *Tree) Links() []string {
|
|
||||||
var links []string
|
|
||||||
for _, e := range t.entries {
|
|
||||||
if le, ok := e.(*linkEntry); ok {
|
|
||||||
links = append(links, le.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return links
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nodes returns all nodes contained in the tree.
|
|
||||||
func (t *Tree) Nodes() []*enode.Node {
|
|
||||||
var nodes []*enode.Node
|
|
||||||
for _, e := range t.entries {
|
|
||||||
if ee, ok := e.(*enrEntry); ok {
|
|
||||||
nodes = append(nodes, ee.node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
We want to keep the UDP size below 512 bytes. The UDP size is roughly:
|
|
||||||
UDP length = 8 + UDP payload length ( 229 )
|
|
||||||
UPD Payload length:
|
|
||||||
- dns.id 2
|
|
||||||
- dns.flags 2
|
|
||||||
- dns.count.queries 2
|
|
||||||
- dns.count.answers 2
|
|
||||||
- dns.count.auth_rr 2
|
|
||||||
- dns.count.add_rr 2
|
|
||||||
- queries (query-size + 6)
|
|
||||||
- answers :
|
|
||||||
- dns.resp.name 2
|
|
||||||
- dns.resp.type 2
|
|
||||||
- dns.resp.class 2
|
|
||||||
- dns.resp.ttl 4
|
|
||||||
- dns.resp.len 2
|
|
||||||
- dns.txt.length 1
|
|
||||||
- dns.txt resp_data_size
|
|
||||||
|
|
||||||
So the total size is roughly a fixed overhead of `39`, and the size of the query (domain
|
|
||||||
name) and response. The query size is, for example,
|
|
||||||
FVY6INQ6LZ33WLCHO3BPR3FH6Y.snap.mainnet.ethdisco.net (52)
|
|
||||||
|
|
||||||
We also have some static data in the response, such as `enrtree-branch:`, and potentially
|
|
||||||
splitting the response up with `" "`, leaving us with a size of roughly `400` that we need
|
|
||||||
to stay below.
|
|
||||||
|
|
||||||
The number `370` is used to have some margin for extra overhead (for example, the dns
|
|
||||||
query may be larger - more subdomains).
|
|
||||||
*/
|
|
||||||
const (
|
|
||||||
hashAbbrevSize = 1 + 16*13/8 // Size of an encoded hash (plus comma)
|
|
||||||
maxChildren = 370 / hashAbbrevSize // 13 children
|
|
||||||
minHashLength = 12
|
|
||||||
)
|
|
||||||
|
|
||||||
// MakeTree creates a tree containing the given nodes and links.
|
|
||||||
func MakeTree(seq uint, nodes []*enode.Node, links []string) (*Tree, error) {
|
|
||||||
// Sort records by ID and ensure all nodes have a valid record.
|
|
||||||
records := make([]*enode.Node, len(nodes))
|
|
||||||
|
|
||||||
copy(records, nodes)
|
|
||||||
sortByID(records)
|
|
||||||
for _, n := range records {
|
|
||||||
if len(n.Record().Signature()) == 0 {
|
|
||||||
return nil, fmt.Errorf("can't add node %v: unsigned node record", n.ID())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the leaf list.
|
|
||||||
enrEntries := make([]entry, len(records))
|
|
||||||
for i, r := range records {
|
|
||||||
enrEntries[i] = &enrEntry{r}
|
|
||||||
}
|
|
||||||
linkEntries := make([]entry, len(links))
|
|
||||||
for i, l := range links {
|
|
||||||
le, err := parseLink(l)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
linkEntries[i] = le
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create intermediate nodes.
|
|
||||||
t := &Tree{entries: make(map[string]entry)}
|
|
||||||
eroot := t.build(enrEntries)
|
|
||||||
t.entries[subdomain(eroot)] = eroot
|
|
||||||
lroot := t.build(linkEntries)
|
|
||||||
t.entries[subdomain(lroot)] = lroot
|
|
||||||
t.root = &rootEntry{seq: seq, eroot: subdomain(eroot), lroot: subdomain(lroot)}
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tree) build(entries []entry) entry {
|
|
||||||
if len(entries) == 1 {
|
|
||||||
return entries[0]
|
|
||||||
}
|
|
||||||
if len(entries) <= maxChildren {
|
|
||||||
hashes := make([]string, len(entries))
|
|
||||||
for i, e := range entries {
|
|
||||||
hashes[i] = subdomain(e)
|
|
||||||
t.entries[hashes[i]] = e
|
|
||||||
}
|
|
||||||
return &branchEntry{hashes}
|
|
||||||
}
|
|
||||||
var subtrees []entry
|
|
||||||
for len(entries) > 0 {
|
|
||||||
n := maxChildren
|
|
||||||
if len(entries) < n {
|
|
||||||
n = len(entries)
|
|
||||||
}
|
|
||||||
sub := t.build(entries[:n])
|
|
||||||
entries = entries[n:]
|
|
||||||
subtrees = append(subtrees, sub)
|
|
||||||
t.entries[subdomain(sub)] = sub
|
|
||||||
}
|
|
||||||
return t.build(subtrees)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sortByID(nodes []*enode.Node) []*enode.Node {
|
|
||||||
slices.SortFunc(nodes, func(a, b *enode.Node) int {
|
|
||||||
return bytes.Compare(a.ID().Bytes(), b.ID().Bytes())
|
|
||||||
})
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entry Types
|
|
||||||
|
|
||||||
type entry interface {
|
|
||||||
fmt.Stringer
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
|
||||||
rootEntry struct {
|
|
||||||
eroot string
|
|
||||||
lroot string
|
|
||||||
seq uint
|
|
||||||
sig []byte
|
|
||||||
}
|
|
||||||
branchEntry struct {
|
|
||||||
children []string
|
|
||||||
}
|
|
||||||
enrEntry struct {
|
|
||||||
node *enode.Node
|
|
||||||
}
|
|
||||||
linkEntry struct {
|
|
||||||
str string
|
|
||||||
domain string
|
|
||||||
pubkey *ecdsa.PublicKey
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Entry Encoding
|
|
||||||
|
|
||||||
var (
|
|
||||||
b32format = base32.StdEncoding.WithPadding(base32.NoPadding)
|
|
||||||
b64format = base64.RawURLEncoding
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
rootPrefix = "enrtree-root:v1"
|
|
||||||
linkPrefix = "enrtree://"
|
|
||||||
branchPrefix = "enrtree-branch:"
|
|
||||||
enrPrefix = "enr:"
|
|
||||||
)
|
|
||||||
|
|
||||||
func subdomain(e entry) string {
|
|
||||||
h := sha3.NewLegacyKeccak256()
|
|
||||||
io.WriteString(h, e.String())
|
|
||||||
return b32format.EncodeToString(h.Sum(nil)[:16])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *rootEntry) String() string {
|
|
||||||
return fmt.Sprintf(rootPrefix+" e=%s l=%s seq=%d sig=%s", e.eroot, e.lroot, e.seq, b64format.EncodeToString(e.sig))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *rootEntry) sigHash() []byte {
|
|
||||||
h := sha3.NewLegacyKeccak256()
|
|
||||||
fmt.Fprintf(h, rootPrefix+" e=%s l=%s seq=%d", e.eroot, e.lroot, e.seq)
|
|
||||||
return h.Sum(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *rootEntry) verifySignature(pubkey *ecdsa.PublicKey) bool {
|
|
||||||
sig := e.sig[:crypto.RecoveryIDOffset] // remove recovery id
|
|
||||||
enckey := crypto.FromECDSAPub(pubkey)
|
|
||||||
return crypto.VerifySignature(enckey, e.sigHash(), sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *branchEntry) String() string {
|
|
||||||
return branchPrefix + strings.Join(e.children, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *enrEntry) String() string {
|
|
||||||
return e.node.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *linkEntry) String() string {
|
|
||||||
return linkPrefix + e.str
|
|
||||||
}
|
|
||||||
|
|
||||||
func newLinkEntry(domain string, pubkey *ecdsa.PublicKey) *linkEntry {
|
|
||||||
key := b32format.EncodeToString(crypto.CompressPubkey(pubkey))
|
|
||||||
str := key + "@" + domain
|
|
||||||
return &linkEntry{str, domain, pubkey}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entry Parsing
|
|
||||||
|
|
||||||
func parseEntry(e string, validSchemes enr.IdentityScheme) (entry, error) {
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(e, linkPrefix):
|
|
||||||
return parseLinkEntry(e)
|
|
||||||
case strings.HasPrefix(e, branchPrefix):
|
|
||||||
return parseBranch(e)
|
|
||||||
case strings.HasPrefix(e, enrPrefix):
|
|
||||||
return parseENR(e, validSchemes)
|
|
||||||
default:
|
|
||||||
return nil, errUnknownEntry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseRoot(e string) (rootEntry, error) {
|
|
||||||
var eroot, lroot, sig string
|
|
||||||
var seq uint
|
|
||||||
if _, err := fmt.Sscanf(e, rootPrefix+" e=%s l=%s seq=%d sig=%s", &eroot, &lroot, &seq, &sig); err != nil {
|
|
||||||
return rootEntry{}, entryError{"root", errSyntax}
|
|
||||||
}
|
|
||||||
if !isValidHash(eroot) || !isValidHash(lroot) {
|
|
||||||
return rootEntry{}, entryError{"root", errInvalidChild}
|
|
||||||
}
|
|
||||||
sigb, err := b64format.DecodeString(sig)
|
|
||||||
if err != nil || len(sigb) != crypto.SignatureLength {
|
|
||||||
return rootEntry{}, entryError{"root", errInvalidSig}
|
|
||||||
}
|
|
||||||
return rootEntry{eroot, lroot, seq, sigb}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseLinkEntry(e string) (entry, error) {
|
|
||||||
le, err := parseLink(e)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return le, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseLink(e string) (*linkEntry, error) {
|
|
||||||
if !strings.HasPrefix(e, linkPrefix) {
|
|
||||||
return nil, fmt.Errorf("wrong/missing scheme 'enrtree' in URL")
|
|
||||||
}
|
|
||||||
e = e[len(linkPrefix):]
|
|
||||||
pos := strings.IndexByte(e, '@')
|
|
||||||
if pos == -1 {
|
|
||||||
return nil, entryError{"link", errNoPubkey}
|
|
||||||
}
|
|
||||||
keystring, domain := e[:pos], e[pos+1:]
|
|
||||||
keybytes, err := b32format.DecodeString(keystring)
|
|
||||||
if err != nil {
|
|
||||||
return nil, entryError{"link", errBadPubkey}
|
|
||||||
}
|
|
||||||
key, err := crypto.DecompressPubkey(keybytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, entryError{"link", errBadPubkey}
|
|
||||||
}
|
|
||||||
return &linkEntry{e, domain, key}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseBranch(e string) (entry, error) {
|
|
||||||
e = e[len(branchPrefix):]
|
|
||||||
if e == "" {
|
|
||||||
return &branchEntry{}, nil // empty entry is OK
|
|
||||||
}
|
|
||||||
hashes := make([]string, 0, strings.Count(e, ","))
|
|
||||||
for _, c := range strings.Split(e, ",") {
|
|
||||||
if !isValidHash(c) {
|
|
||||||
return nil, entryError{"branch", errInvalidChild}
|
|
||||||
}
|
|
||||||
hashes = append(hashes, c)
|
|
||||||
}
|
|
||||||
return &branchEntry{hashes}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseENR(e string, validSchemes enr.IdentityScheme) (entry, error) {
|
|
||||||
e = e[len(enrPrefix):]
|
|
||||||
enc, err := b64format.DecodeString(e)
|
|
||||||
if err != nil {
|
|
||||||
return nil, entryError{"enr", errInvalidENR}
|
|
||||||
}
|
|
||||||
var rec enr.Record
|
|
||||||
if err := rlp.DecodeBytes(enc, &rec); err != nil {
|
|
||||||
return nil, entryError{"enr", err}
|
|
||||||
}
|
|
||||||
n, err := enode.New(validSchemes, &rec)
|
|
||||||
if err != nil {
|
|
||||||
return nil, entryError{"enr", err}
|
|
||||||
}
|
|
||||||
return &enrEntry{n}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidHash(s string) bool {
|
|
||||||
dlen := b32format.DecodedLen(len(s))
|
|
||||||
if dlen < minHashLength || dlen > 32 || strings.ContainsAny(s, "\n\r") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
buf := make([]byte, 32)
|
|
||||||
_, err := b32format.Decode(buf, []byte(s))
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// truncateHash truncates the given base32 hash string to the minimum acceptable length.
|
|
||||||
func truncateHash(hash string) string {
|
|
||||||
maxLen := b32format.EncodedLen(minHashLength)
|
|
||||||
if len(hash) < maxLen {
|
|
||||||
panic(fmt.Errorf("dnsdisc: hash %q is too short", hash))
|
|
||||||
}
|
|
||||||
return hash[:maxLen]
|
|
||||||
}
|
|
||||||
|
|
||||||
// URL encoding
|
|
||||||
|
|
||||||
// ParseURL parses an enrtree:// URL and returns its components.
|
|
||||||
func ParseURL(url string) (domain string, pubkey *ecdsa.PublicKey, err error) {
|
|
||||||
le, err := parseLink(url)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
return le.domain, le.pubkey, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
// Copyright 2019 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package dnsdisc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseRoot(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
e rootEntry
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
input: "enrtree-root:v1 e=TO4Q75OQ2N7DX4EOOR7X66A6OM seq=3 sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtw",
|
|
||||||
err: entryError{"root", errSyntax},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enrtree-root:v1 e=TO4Q75OQ2N7DX4EOOR7X66A6OM l=TO4Q75OQ2N7DX4EOOR7X66A6OM seq=3 sig=N-YY6UB9xD0hFx1Gmnt7v0RfSxch5tKyry2SRDoLx7B4GfPXagwLxQqyf7gAMvApFn_ORwZQekMWa_pXrcGCtw",
|
|
||||||
err: entryError{"root", errInvalidSig},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enrtree-root:v1 e=QFT4PBCRX4XQCV3VUYJ6BTCEPU l=JGUFMSAGI7KZYB3P7IZW4S5Y3A seq=3 sig=3FmXuVwpa8Y7OstZTx9PIb1mt8FrW7VpDOFv4AaGCsZ2EIHmhraWhe4NxYhQDlw5MjeFXYMbJjsPeKlHzmJREQE",
|
|
||||||
e: rootEntry{
|
|
||||||
eroot: "QFT4PBCRX4XQCV3VUYJ6BTCEPU",
|
|
||||||
lroot: "JGUFMSAGI7KZYB3P7IZW4S5Y3A",
|
|
||||||
seq: 3,
|
|
||||||
sig: hexutil.MustDecode("0xdc5997b95c296bc63b3acb594f1f4f21bd66b7c16b5bb5690ce16fe006860ac6761081e686b69685ee0dc588500e5c393237855d831b263b0f78a947ce62511101"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for i, test := range tests {
|
|
||||||
e, err := parseRoot(test.input)
|
|
||||||
if !reflect.DeepEqual(e, test.e) {
|
|
||||||
t.Errorf("test %d: wrong entry %s, want %s", i, spew.Sdump(e), spew.Sdump(test.e))
|
|
||||||
}
|
|
||||||
if err != test.err {
|
|
||||||
t.Errorf("test %d: wrong error %q, want %q", i, err, test.err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseEntry(t *testing.T) {
|
|
||||||
testENRs := []string{"enr:-HW4QES8QIeXTYlDzbfr1WEzE-XKY4f8gJFJzjJL-9D7TC9lJb4Z3JPRRz1lP4pL_N_QpT6rGQjAU9Apnc-C1iMP36OAgmlkgnY0iXNlY3AyNTZrMaED5IdwfMxdmR8W37HqSFdQLjDkIwBd4Q_MjxgZifgKSdM"}
|
|
||||||
testNodes := parseNodes(testENRs)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
e entry
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
// Subtrees:
|
|
||||||
{
|
|
||||||
input: "enrtree-branch:1,2",
|
|
||||||
err: entryError{"branch", errInvalidChild},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enrtree-branch:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
|
||||||
err: entryError{"branch", errInvalidChild},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enrtree-branch:",
|
|
||||||
e: &branchEntry{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enrtree-branch:AAAAAAAAAAAAAAAAAAAA",
|
|
||||||
e: &branchEntry{[]string{"AAAAAAAAAAAAAAAAAAAA"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enrtree-branch:AAAAAAAAAAAAAAAAAAAA,BBBBBBBBBBBBBBBBBBBB",
|
|
||||||
e: &branchEntry{[]string{"AAAAAAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBBBBBB"}},
|
|
||||||
},
|
|
||||||
// Links
|
|
||||||
{
|
|
||||||
input: "enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@nodes.example.org",
|
|
||||||
e: &linkEntry{
|
|
||||||
str: "AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@nodes.example.org",
|
|
||||||
domain: "nodes.example.org",
|
|
||||||
pubkey: &signingKeyForTesting.PublicKey,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enrtree://nodes.example.org",
|
|
||||||
err: entryError{"link", errNoPubkey},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enrtree://AP62DT7WOTEQZGQZOU474PP3KMEGVTTE7A7NPRXKX3DUD57@nodes.example.org",
|
|
||||||
err: entryError{"link", errBadPubkey},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enrtree://AP62DT7WONEQZGQZOU474PP3KMEGVTTE7A7NPRXKX3DUD57TQHGIA@nodes.example.org",
|
|
||||||
err: entryError{"link", errBadPubkey},
|
|
||||||
},
|
|
||||||
// ENRs
|
|
||||||
{
|
|
||||||
input: testENRs[0],
|
|
||||||
e: &enrEntry{node: testNodes[0]},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enr:-HW4QLZHjM4vZXkbp-5xJoHsKSbE7W39FPC8283X-y8oHcHPTnDDlIlzL5ArvDUlHZVDPgmFASrh7cWgLOLxj4wprRkHgmlkgnY0iXNlY3AyNTZrMaEC3t2jLMhDpCDX5mbSEwDn4L3iUfyXzoO8G28XvjGRkrAg=",
|
|
||||||
err: entryError{"enr", errInvalidENR},
|
|
||||||
},
|
|
||||||
// Invalid:
|
|
||||||
{input: "", err: errUnknownEntry},
|
|
||||||
{input: "foo", err: errUnknownEntry},
|
|
||||||
{input: "enrtree", err: errUnknownEntry},
|
|
||||||
{input: "enrtree-x=", err: errUnknownEntry},
|
|
||||||
}
|
|
||||||
for i, test := range tests {
|
|
||||||
e, err := parseEntry(test.input, enode.ValidSchemes)
|
|
||||||
if !reflect.DeepEqual(e, test.e) {
|
|
||||||
t.Errorf("test %d: wrong entry %s, want %s", i, spew.Sdump(e), spew.Sdump(test.e))
|
|
||||||
}
|
|
||||||
if err != test.err {
|
|
||||||
t.Errorf("test %d: wrong error %q, want %q", i, err, test.err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMakeTree(t *testing.T) {
|
|
||||||
keys := testKeys(50)
|
|
||||||
nodes := testNodes(keys)
|
|
||||||
tree, err := MakeTree(2, nodes, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
txt := tree.ToTXT("")
|
|
||||||
if len(txt) < len(nodes)+1 {
|
|
||||||
t.Fatal("too few TXT records in output")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package enode
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"golang.org/x/crypto/sha3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ValidSchemes is a List of known secure identity schemes.
|
|
||||||
var ValidSchemes = enr.SchemeMap{
|
|
||||||
"v4": V4ID{},
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidSchemesForTesting is a List of identity schemes for testing.
|
|
||||||
var ValidSchemesForTesting = enr.SchemeMap{
|
|
||||||
"v4": V4ID{},
|
|
||||||
"null": NullID{},
|
|
||||||
}
|
|
||||||
|
|
||||||
// V4ID is the "v4" identity scheme.
|
|
||||||
type V4ID struct{}
|
|
||||||
|
|
||||||
// SignV4 signs a record using the v4 scheme.
|
|
||||||
func SignV4(r *enr.Record, privkey *ecdsa.PrivateKey) error {
|
|
||||||
// Copy r to avoid modifying it if signing fails.
|
|
||||||
cpy := *r
|
|
||||||
cpy.Set(enr.ID("v4"))
|
|
||||||
cpy.Set(Secp256k1(privkey.PublicKey))
|
|
||||||
|
|
||||||
h := sha3.NewLegacyKeccak256()
|
|
||||||
rlp.Encode(h, cpy.AppendElements(nil))
|
|
||||||
sig, err := crypto.Sign(h.Sum(nil), privkey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sig = sig[:len(sig)-1] // remove v
|
|
||||||
if err = cpy.SetSig(V4ID{}, sig); err == nil {
|
|
||||||
*r = cpy
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (V4ID) Verify(r *enr.Record, sig []byte) error {
|
|
||||||
var entry s256raw
|
|
||||||
if err := r.Load(&entry); err != nil {
|
|
||||||
return err
|
|
||||||
} else if len(entry) != 33 {
|
|
||||||
return fmt.Errorf("invalid public key")
|
|
||||||
}
|
|
||||||
|
|
||||||
h := sha3.NewLegacyKeccak256()
|
|
||||||
rlp.Encode(h, r.AppendElements(nil))
|
|
||||||
if !crypto.VerifySignature(entry, h.Sum(nil), sig) {
|
|
||||||
return enr.ErrInvalidSig
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (V4ID) NodeAddr(r *enr.Record) []byte {
|
|
||||||
var pubkey Secp256k1
|
|
||||||
err := r.Load(&pubkey)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
buf := make([]byte, 64)
|
|
||||||
math.ReadBits(pubkey.X, buf[:32])
|
|
||||||
math.ReadBits(pubkey.Y, buf[32:])
|
|
||||||
return crypto.Keccak256(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Secp256k1 is the "secp256k1" key, which holds a public key.
|
|
||||||
type Secp256k1 ecdsa.PublicKey
|
|
||||||
|
|
||||||
func (v Secp256k1) ENRKey() string { return "secp256k1" }
|
|
||||||
|
|
||||||
// EncodeRLP implements rlp.Encoder.
|
|
||||||
func (v Secp256k1) EncodeRLP(w io.Writer) error {
|
|
||||||
return rlp.Encode(w, crypto.CompressPubkey((*ecdsa.PublicKey)(&v)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodeRLP implements rlp.Decoder.
|
|
||||||
func (v *Secp256k1) DecodeRLP(s *rlp.Stream) error {
|
|
||||||
buf, err := s.Bytes()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
pk, err := crypto.DecompressPubkey(buf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*v = (Secp256k1)(*pk)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// s256raw is an unparsed secp256k1 public key entry.
|
|
||||||
type s256raw []byte
|
|
||||||
|
|
||||||
func (s256raw) ENRKey() string { return "secp256k1" }
|
|
||||||
|
|
||||||
// v4CompatID is a weaker and insecure version of the "v4" scheme which only checks for the
|
|
||||||
// presence of a secp256k1 public key, but doesn't verify the signature.
|
|
||||||
type v4CompatID struct {
|
|
||||||
V4ID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v4CompatID) Verify(r *enr.Record, sig []byte) error {
|
|
||||||
var pubkey Secp256k1
|
|
||||||
return r.Load(&pubkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func signV4Compat(r *enr.Record, pubkey *ecdsa.PublicKey) {
|
|
||||||
r.Set((*Secp256k1)(pubkey))
|
|
||||||
if err := r.SetSig(v4CompatID{}, []byte{}); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NullID is the "null" ENR identity scheme. This scheme stores the node
|
|
||||||
// ID in the record without any signature.
|
|
||||||
type NullID struct{}
|
|
||||||
|
|
||||||
func (NullID) Verify(r *enr.Record, sig []byte) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (NullID) NodeAddr(r *enr.Record) []byte {
|
|
||||||
var id ID
|
|
||||||
r.Load(enr.WithEntry("nulladdr", &id))
|
|
||||||
return id[:]
|
|
||||||
}
|
|
||||||
|
|
||||||
func SignNull(r *enr.Record, id ID) *Node {
|
|
||||||
r.Set(enr.ID("null"))
|
|
||||||
r.Set(enr.WithEntry("nulladdr", id))
|
|
||||||
if err := r.SetSig(NullID{}, []byte{}); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return &Node{r: *r, id: id}
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package enode
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"encoding/hex"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
privkey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
|
||||||
pubkey = &privkey.PublicKey
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEmptyNodeID(t *testing.T) {
|
|
||||||
var r enr.Record
|
|
||||||
if addr := ValidSchemes.NodeAddr(&r); addr != nil {
|
|
||||||
t.Errorf("wrong address on empty record: got %v, want %v", addr, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, SignV4(&r, privkey))
|
|
||||||
expected := "a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7"
|
|
||||||
assert.Equal(t, expected, hex.EncodeToString(ValidSchemes.NodeAddr(&r)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks that failure to sign leaves the record unmodified.
|
|
||||||
func TestSignError(t *testing.T) {
|
|
||||||
invalidKey := &ecdsa.PrivateKey{D: new(big.Int), PublicKey: *pubkey}
|
|
||||||
|
|
||||||
var r enr.Record
|
|
||||||
emptyEnc, _ := rlp.EncodeToBytes(&r)
|
|
||||||
if err := SignV4(&r, invalidKey); err == nil {
|
|
||||||
t.Fatal("expected error from SignV4")
|
|
||||||
}
|
|
||||||
newEnc, _ := rlp.EncodeToBytes(&r)
|
|
||||||
if !bytes.Equal(newEnc, emptyEnc) {
|
|
||||||
t.Fatal("record modified even though signing failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGetSetSecp256k1 tests encoding/decoding and setting/getting of the Secp256k1 key.
|
|
||||||
func TestGetSetSecp256k1(t *testing.T) {
|
|
||||||
var r enr.Record
|
|
||||||
if err := SignV4(&r, privkey); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var pk Secp256k1
|
|
||||||
require.NoError(t, r.Load(&pk))
|
|
||||||
assert.EqualValues(t, pubkey, &pk)
|
|
||||||
}
|
|
||||||
|
|
@ -1,295 +0,0 @@
|
||||||
// Copyright 2019 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package enode
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Iterator represents a sequence of nodes. The Next method moves to the next node in the
|
|
||||||
// sequence. It returns false when the sequence has ended or the iterator is closed. Close
|
|
||||||
// may be called concurrently with Next and Node, and interrupts Next if it is blocked.
|
|
||||||
type Iterator interface {
|
|
||||||
Next() bool // moves to next node
|
|
||||||
Node() *Node // returns current node
|
|
||||||
Close() // ends the iterator
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadNodes reads at most n nodes from the given iterator. The return value contains no
|
|
||||||
// duplicates and no nil values. To prevent looping indefinitely for small repeating node
|
|
||||||
// sequences, this function calls Next at most n times.
|
|
||||||
func ReadNodes(it Iterator, n int) []*Node {
|
|
||||||
seen := make(map[ID]*Node, n)
|
|
||||||
for i := 0; i < n && it.Next(); i++ {
|
|
||||||
// Remove duplicates, keeping the node with higher seq.
|
|
||||||
node := it.Node()
|
|
||||||
prevNode, ok := seen[node.ID()]
|
|
||||||
if ok && prevNode.Seq() > node.Seq() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[node.ID()] = node
|
|
||||||
}
|
|
||||||
result := make([]*Node, 0, len(seen))
|
|
||||||
for _, node := range seen {
|
|
||||||
result = append(result, node)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// IterNodes makes an iterator which runs through the given nodes once.
|
|
||||||
func IterNodes(nodes []*Node) Iterator {
|
|
||||||
return &sliceIter{nodes: nodes, index: -1}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CycleNodes makes an iterator which cycles through the given nodes indefinitely.
|
|
||||||
func CycleNodes(nodes []*Node) Iterator {
|
|
||||||
return &sliceIter{nodes: nodes, index: -1, cycle: true}
|
|
||||||
}
|
|
||||||
|
|
||||||
type sliceIter struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
nodes []*Node
|
|
||||||
index int
|
|
||||||
cycle bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *sliceIter) Next() bool {
|
|
||||||
it.mu.Lock()
|
|
||||||
defer it.mu.Unlock()
|
|
||||||
|
|
||||||
if len(it.nodes) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
it.index++
|
|
||||||
if it.index == len(it.nodes) {
|
|
||||||
if it.cycle {
|
|
||||||
it.index = 0
|
|
||||||
} else {
|
|
||||||
it.nodes = nil
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *sliceIter) Node() *Node {
|
|
||||||
it.mu.Lock()
|
|
||||||
defer it.mu.Unlock()
|
|
||||||
if len(it.nodes) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return it.nodes[it.index]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *sliceIter) Close() {
|
|
||||||
it.mu.Lock()
|
|
||||||
defer it.mu.Unlock()
|
|
||||||
|
|
||||||
it.nodes = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter wraps an iterator such that Next only returns nodes for which
|
|
||||||
// the 'check' function returns true.
|
|
||||||
func Filter(it Iterator, check func(*Node) bool) Iterator {
|
|
||||||
return &filterIter{it, check}
|
|
||||||
}
|
|
||||||
|
|
||||||
type filterIter struct {
|
|
||||||
Iterator
|
|
||||||
check func(*Node) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *filterIter) Next() bool {
|
|
||||||
for f.Iterator.Next() {
|
|
||||||
if f.check(f.Node()) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// FairMix aggregates multiple node iterators. The mixer itself is an iterator which ends
|
|
||||||
// only when Close is called. Source iterators added via AddSource are removed from the
|
|
||||||
// mix when they end.
|
|
||||||
//
|
|
||||||
// The distribution of nodes returned by Next is approximately fair, i.e. FairMix
|
|
||||||
// attempts to draw from all sources equally often. However, if a certain source is slow
|
|
||||||
// and doesn't return a node within the configured timeout, a node from any other source
|
|
||||||
// will be returned.
|
|
||||||
//
|
|
||||||
// It's safe to call AddSource and Close concurrently with Next.
|
|
||||||
type FairMix struct {
|
|
||||||
wg sync.WaitGroup
|
|
||||||
fromAny chan *Node
|
|
||||||
timeout time.Duration
|
|
||||||
cur *Node
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
closed chan struct{}
|
|
||||||
sources []*mixSource
|
|
||||||
last int
|
|
||||||
}
|
|
||||||
|
|
||||||
type mixSource struct {
|
|
||||||
it Iterator
|
|
||||||
next chan *Node
|
|
||||||
timeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFairMix creates a mixer.
|
|
||||||
//
|
|
||||||
// The timeout specifies how long the mixer will wait for the next fairly-chosen source
|
|
||||||
// before giving up and taking a node from any other source. A good way to set the timeout
|
|
||||||
// is deciding how long you'd want to wait for a node on average. Passing a negative
|
|
||||||
// timeout makes the mixer completely fair.
|
|
||||||
func NewFairMix(timeout time.Duration) *FairMix {
|
|
||||||
m := &FairMix{
|
|
||||||
fromAny: make(chan *Node),
|
|
||||||
closed: make(chan struct{}),
|
|
||||||
timeout: timeout,
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddSource adds a source of nodes.
|
|
||||||
func (m *FairMix) AddSource(it Iterator) {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
if m.closed == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.wg.Add(1)
|
|
||||||
source := &mixSource{it, make(chan *Node), m.timeout}
|
|
||||||
m.sources = append(m.sources, source)
|
|
||||||
go m.runSource(m.closed, source)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close shuts down the mixer and all current sources.
|
|
||||||
// Calling this is required to release resources associated with the mixer.
|
|
||||||
func (m *FairMix) Close() {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
if m.closed == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, s := range m.sources {
|
|
||||||
s.it.Close()
|
|
||||||
}
|
|
||||||
close(m.closed)
|
|
||||||
m.wg.Wait()
|
|
||||||
close(m.fromAny)
|
|
||||||
m.sources = nil
|
|
||||||
m.closed = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next returns a node from a random source.
|
|
||||||
func (m *FairMix) Next() bool {
|
|
||||||
m.cur = nil
|
|
||||||
|
|
||||||
for {
|
|
||||||
source := m.pickSource()
|
|
||||||
if source == nil {
|
|
||||||
return m.nextFromAny()
|
|
||||||
}
|
|
||||||
|
|
||||||
var timeout <-chan time.Time
|
|
||||||
if source.timeout >= 0 {
|
|
||||||
timer := time.NewTimer(source.timeout)
|
|
||||||
timeout = timer.C
|
|
||||||
defer timer.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case n, ok := <-source.next:
|
|
||||||
if ok {
|
|
||||||
// Here, the timeout is reset to the configured value
|
|
||||||
// because the source delivered a node.
|
|
||||||
source.timeout = m.timeout
|
|
||||||
m.cur = n
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// This source has ended.
|
|
||||||
m.deleteSource(source)
|
|
||||||
case <-timeout:
|
|
||||||
// The selected source did not deliver a node within the timeout, so the
|
|
||||||
// timeout duration is halved for next time. This is supposed to improve
|
|
||||||
// latency with stuck sources.
|
|
||||||
source.timeout /= 2
|
|
||||||
return m.nextFromAny()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node returns the current node.
|
|
||||||
func (m *FairMix) Node() *Node {
|
|
||||||
return m.cur
|
|
||||||
}
|
|
||||||
|
|
||||||
// nextFromAny is used when there are no sources or when the 'fair' choice
|
|
||||||
// doesn't turn up a node quickly enough.
|
|
||||||
func (m *FairMix) nextFromAny() bool {
|
|
||||||
n, ok := <-m.fromAny
|
|
||||||
if ok {
|
|
||||||
m.cur = n
|
|
||||||
}
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// pickSource chooses the next source to read from, cycling through them in order.
|
|
||||||
func (m *FairMix) pickSource() *mixSource {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
if len(m.sources) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
m.last = (m.last + 1) % len(m.sources)
|
|
||||||
return m.sources[m.last]
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteSource deletes a source.
|
|
||||||
func (m *FairMix) deleteSource(s *mixSource) {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
for i := range m.sources {
|
|
||||||
if m.sources[i] == s {
|
|
||||||
copy(m.sources[i:], m.sources[i+1:])
|
|
||||||
m.sources[len(m.sources)-1] = nil
|
|
||||||
m.sources = m.sources[:len(m.sources)-1]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// runSource reads a single source in a loop.
|
|
||||||
func (m *FairMix) runSource(closed chan struct{}, s *mixSource) {
|
|
||||||
defer m.wg.Done()
|
|
||||||
defer close(s.next)
|
|
||||||
for s.it.Next() {
|
|
||||||
n := s.it.Node()
|
|
||||||
select {
|
|
||||||
case s.next <- n:
|
|
||||||
case m.fromAny <- n:
|
|
||||||
case <-closed:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,291 +0,0 @@
|
||||||
// Copyright 2019 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package enode
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"runtime"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestReadNodes(t *testing.T) {
|
|
||||||
nodes := ReadNodes(new(genIter), 10)
|
|
||||||
checkNodes(t, nodes, 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that ReadNodes terminates when reading N nodes from an iterator
|
|
||||||
// which returns less than N nodes in an endless cycle.
|
|
||||||
func TestReadNodesCycle(t *testing.T) {
|
|
||||||
iter := &callCountIter{
|
|
||||||
Iterator: CycleNodes([]*Node{
|
|
||||||
testNode(0, 0),
|
|
||||||
testNode(1, 0),
|
|
||||||
testNode(2, 0),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
nodes := ReadNodes(iter, 10)
|
|
||||||
checkNodes(t, nodes, 3)
|
|
||||||
if iter.count != 10 {
|
|
||||||
t.Fatalf("%d calls to Next, want %d", iter.count, 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilterNodes(t *testing.T) {
|
|
||||||
nodes := make([]*Node, 100)
|
|
||||||
for i := range nodes {
|
|
||||||
nodes[i] = testNode(uint64(i), uint64(i))
|
|
||||||
}
|
|
||||||
|
|
||||||
it := Filter(IterNodes(nodes), func(n *Node) bool {
|
|
||||||
return n.Seq() >= 50
|
|
||||||
})
|
|
||||||
for i := 50; i < len(nodes); i++ {
|
|
||||||
if !it.Next() {
|
|
||||||
t.Fatal("Next returned false")
|
|
||||||
}
|
|
||||||
if it.Node() != nodes[i] {
|
|
||||||
t.Fatalf("iterator returned wrong node %v\nwant %v", it.Node(), nodes[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if it.Next() {
|
|
||||||
t.Fatal("Next returned true after underlying iterator has ended")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkNodes(t *testing.T, nodes []*Node, wantLen int) {
|
|
||||||
if len(nodes) != wantLen {
|
|
||||||
t.Errorf("slice has %d nodes, want %d", len(nodes), wantLen)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seen := make(map[ID]bool, len(nodes))
|
|
||||||
for i, e := range nodes {
|
|
||||||
if e == nil {
|
|
||||||
t.Errorf("nil node at index %d", i)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if seen[e.ID()] {
|
|
||||||
t.Errorf("slice has duplicate node %v", e.ID())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seen[e.ID()] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks fairness of FairMix in the happy case where all sources return nodes
|
|
||||||
// within the context's deadline.
|
|
||||||
func TestFairMix(t *testing.T) {
|
|
||||||
for i := 0; i < 500; i++ {
|
|
||||||
testMixerFairness(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testMixerFairness(t *testing.T) {
|
|
||||||
mix := NewFairMix(1 * time.Second)
|
|
||||||
mix.AddSource(&genIter{index: 1})
|
|
||||||
mix.AddSource(&genIter{index: 2})
|
|
||||||
mix.AddSource(&genIter{index: 3})
|
|
||||||
defer mix.Close()
|
|
||||||
|
|
||||||
nodes := ReadNodes(mix, 500)
|
|
||||||
checkNodes(t, nodes, 500)
|
|
||||||
|
|
||||||
// Verify that the nodes slice contains an approximately equal number of nodes
|
|
||||||
// from each source.
|
|
||||||
d := idPrefixDistribution(nodes)
|
|
||||||
for _, count := range d {
|
|
||||||
if approxEqual(count, len(nodes)/3, 30) {
|
|
||||||
t.Fatalf("ID distribution is unfair: %v", d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that FairMix falls back to an alternative source when
|
|
||||||
// the 'fair' choice doesn't return a node within the timeout.
|
|
||||||
func TestFairMixNextFromAll(t *testing.T) {
|
|
||||||
mix := NewFairMix(1 * time.Millisecond)
|
|
||||||
mix.AddSource(&genIter{index: 1})
|
|
||||||
mix.AddSource(CycleNodes(nil))
|
|
||||||
defer mix.Close()
|
|
||||||
|
|
||||||
nodes := ReadNodes(mix, 500)
|
|
||||||
checkNodes(t, nodes, 500)
|
|
||||||
|
|
||||||
d := idPrefixDistribution(nodes)
|
|
||||||
if len(d) > 1 || d[1] != len(nodes) {
|
|
||||||
t.Fatalf("wrong ID distribution: %v", d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test ensures FairMix works for Next with no sources.
|
|
||||||
func TestFairMixEmpty(t *testing.T) {
|
|
||||||
var (
|
|
||||||
mix = NewFairMix(1 * time.Second)
|
|
||||||
testN = testNode(1, 1)
|
|
||||||
ch = make(chan *Node)
|
|
||||||
)
|
|
||||||
defer mix.Close()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
mix.Next()
|
|
||||||
ch <- mix.Node()
|
|
||||||
}()
|
|
||||||
|
|
||||||
mix.AddSource(CycleNodes([]*Node{testN}))
|
|
||||||
if n := <-ch; n != testN {
|
|
||||||
t.Errorf("got wrong node: %v", n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks closing a source while Next runs.
|
|
||||||
func TestFairMixRemoveSource(t *testing.T) {
|
|
||||||
mix := NewFairMix(1 * time.Second)
|
|
||||||
source := make(blockingIter)
|
|
||||||
mix.AddSource(source)
|
|
||||||
|
|
||||||
sig := make(chan *Node)
|
|
||||||
go func() {
|
|
||||||
<-sig
|
|
||||||
mix.Next()
|
|
||||||
sig <- mix.Node()
|
|
||||||
}()
|
|
||||||
|
|
||||||
sig <- nil
|
|
||||||
runtime.Gosched()
|
|
||||||
source.Close()
|
|
||||||
|
|
||||||
wantNode := testNode(0, 0)
|
|
||||||
mix.AddSource(CycleNodes([]*Node{wantNode}))
|
|
||||||
n := <-sig
|
|
||||||
|
|
||||||
if len(mix.sources) != 1 {
|
|
||||||
t.Fatalf("have %d sources, want one", len(mix.sources))
|
|
||||||
}
|
|
||||||
if n != wantNode {
|
|
||||||
t.Fatalf("mixer returned wrong node")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type blockingIter chan struct{}
|
|
||||||
|
|
||||||
func (it blockingIter) Next() bool {
|
|
||||||
<-it
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it blockingIter) Node() *Node {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it blockingIter) Close() {
|
|
||||||
close(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFairMixClose(t *testing.T) {
|
|
||||||
for i := 0; i < 20 && !t.Failed(); i++ {
|
|
||||||
testMixerClose(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testMixerClose(t *testing.T) {
|
|
||||||
mix := NewFairMix(-1)
|
|
||||||
mix.AddSource(CycleNodes(nil))
|
|
||||||
mix.AddSource(CycleNodes(nil))
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
defer close(done)
|
|
||||||
if mix.Next() {
|
|
||||||
t.Error("Next returned true")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
// This call is supposed to make it more likely that NextNode is
|
|
||||||
// actually executing by the time we call Close.
|
|
||||||
runtime.Gosched()
|
|
||||||
|
|
||||||
mix.Close()
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
case <-time.After(3 * time.Second):
|
|
||||||
t.Fatal("Next didn't unblock on Close")
|
|
||||||
}
|
|
||||||
|
|
||||||
mix.Close() // shouldn't crash
|
|
||||||
}
|
|
||||||
|
|
||||||
func idPrefixDistribution(nodes []*Node) map[uint32]int {
|
|
||||||
d := make(map[uint32]int, len(nodes))
|
|
||||||
for _, node := range nodes {
|
|
||||||
id := node.ID()
|
|
||||||
d[binary.BigEndian.Uint32(id[:4])]++
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
func approxEqual(x, y, ε int) bool {
|
|
||||||
if y > x {
|
|
||||||
x, y = y, x
|
|
||||||
}
|
|
||||||
return x-y > ε
|
|
||||||
}
|
|
||||||
|
|
||||||
// genIter creates fake nodes with numbered IDs based on 'index' and 'gen'
|
|
||||||
type genIter struct {
|
|
||||||
node *Node
|
|
||||||
index, gen uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *genIter) Next() bool {
|
|
||||||
index := atomic.LoadUint32(&s.index)
|
|
||||||
if index == ^uint32(0) {
|
|
||||||
s.node = nil
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
s.node = testNode(uint64(index)<<32|uint64(s.gen), 0)
|
|
||||||
s.gen++
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *genIter) Node() *Node {
|
|
||||||
return s.node
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *genIter) Close() {
|
|
||||||
atomic.StoreUint32(&s.index, ^uint32(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
func testNode(id, seq uint64) *Node {
|
|
||||||
var nodeID ID
|
|
||||||
binary.BigEndian.PutUint64(nodeID[:], id)
|
|
||||||
r := new(enr.Record)
|
|
||||||
r.SetSeq(seq)
|
|
||||||
return SignNull(r, nodeID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// callCountIter counts calls to NextNode.
|
|
||||||
type callCountIter struct {
|
|
||||||
Iterator
|
|
||||||
count int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *callCountIter) Next() bool {
|
|
||||||
it.count++
|
|
||||||
return it.Iterator.Next()
|
|
||||||
}
|
|
||||||
|
|
@ -1,332 +0,0 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package enode
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/netutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// IP tracker configuration
|
|
||||||
iptrackMinStatements = 10
|
|
||||||
iptrackWindow = 5 * time.Minute
|
|
||||||
iptrackContactWindow = 10 * time.Minute
|
|
||||||
|
|
||||||
// time needed to wait between two updates to the local ENR
|
|
||||||
recordUpdateThrottle = time.Millisecond
|
|
||||||
)
|
|
||||||
|
|
||||||
// LocalNode produces the signed node record of a local node, i.e. a node run in the
|
|
||||||
// current process. Setting ENR entries via the Set method updates the record. A new version
|
|
||||||
// of the record is signed on demand when the Node method is called.
|
|
||||||
type LocalNode struct {
|
|
||||||
cur atomic.Value // holds a non-nil node pointer while the record is up-to-date
|
|
||||||
|
|
||||||
id ID
|
|
||||||
key *ecdsa.PrivateKey
|
|
||||||
db *DB
|
|
||||||
|
|
||||||
// everything below is protected by a lock
|
|
||||||
mu sync.RWMutex
|
|
||||||
seq uint64
|
|
||||||
update time.Time // timestamp when the record was last updated
|
|
||||||
entries map[string]enr.Entry
|
|
||||||
endpoint4 lnEndpoint
|
|
||||||
endpoint6 lnEndpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
type lnEndpoint struct {
|
|
||||||
track *netutil.IPTracker
|
|
||||||
staticIP, fallbackIP net.IP
|
|
||||||
fallbackUDP uint16 // port
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLocalNode creates a local node.
|
|
||||||
func NewLocalNode(db *DB, key *ecdsa.PrivateKey) *LocalNode {
|
|
||||||
ln := &LocalNode{
|
|
||||||
id: PubkeyToIDV4(&key.PublicKey),
|
|
||||||
db: db,
|
|
||||||
key: key,
|
|
||||||
entries: make(map[string]enr.Entry),
|
|
||||||
endpoint4: lnEndpoint{
|
|
||||||
track: netutil.NewIPTracker(iptrackWindow, iptrackContactWindow, iptrackMinStatements),
|
|
||||||
},
|
|
||||||
endpoint6: lnEndpoint{
|
|
||||||
track: netutil.NewIPTracker(iptrackWindow, iptrackContactWindow, iptrackMinStatements),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
ln.seq = db.localSeq(ln.id)
|
|
||||||
ln.update = time.Now()
|
|
||||||
ln.cur.Store((*Node)(nil))
|
|
||||||
return ln
|
|
||||||
}
|
|
||||||
|
|
||||||
// Database returns the node database associated with the local node.
|
|
||||||
func (ln *LocalNode) Database() *DB {
|
|
||||||
return ln.db
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node returns the current version of the local node record.
|
|
||||||
func (ln *LocalNode) Node() *Node {
|
|
||||||
// If we have a valid record, return that
|
|
||||||
n := ln.cur.Load().(*Node)
|
|
||||||
if n != nil {
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record was invalidated, sign a new copy.
|
|
||||||
ln.mu.Lock()
|
|
||||||
defer ln.mu.Unlock()
|
|
||||||
|
|
||||||
// Double check the current record, since multiple goroutines might be waiting
|
|
||||||
// on the write mutex.
|
|
||||||
if n = ln.cur.Load().(*Node); n != nil {
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// The initial sequence number is the current timestamp in milliseconds. To ensure
|
|
||||||
// that the initial sequence number will always be higher than any previous sequence
|
|
||||||
// number (assuming the clock is correct), we want to avoid updating the record faster
|
|
||||||
// than once per ms. So we need to sleep here until the next possible update time has
|
|
||||||
// arrived.
|
|
||||||
lastChange := time.Since(ln.update)
|
|
||||||
if lastChange < recordUpdateThrottle {
|
|
||||||
time.Sleep(recordUpdateThrottle - lastChange)
|
|
||||||
}
|
|
||||||
|
|
||||||
ln.sign()
|
|
||||||
ln.update = time.Now()
|
|
||||||
return ln.cur.Load().(*Node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seq returns the current sequence number of the local node record.
|
|
||||||
func (ln *LocalNode) Seq() uint64 {
|
|
||||||
ln.mu.Lock()
|
|
||||||
defer ln.mu.Unlock()
|
|
||||||
|
|
||||||
return ln.seq
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID returns the local node ID.
|
|
||||||
func (ln *LocalNode) ID() ID {
|
|
||||||
return ln.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set puts the given entry into the local record, overwriting any existing value.
|
|
||||||
// Use Set*IP and SetFallbackUDP to set IP addresses and UDP port, otherwise they'll
|
|
||||||
// be overwritten by the endpoint predictor.
|
|
||||||
//
|
|
||||||
// Since node record updates are throttled to one per second, Set is asynchronous.
|
|
||||||
// Any update will be queued up and published when at least one second passes from
|
|
||||||
// the last change.
|
|
||||||
func (ln *LocalNode) Set(e enr.Entry) {
|
|
||||||
ln.mu.Lock()
|
|
||||||
defer ln.mu.Unlock()
|
|
||||||
|
|
||||||
ln.set(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *LocalNode) set(e enr.Entry) {
|
|
||||||
val, exists := ln.entries[e.ENRKey()]
|
|
||||||
if !exists || !reflect.DeepEqual(val, e) {
|
|
||||||
ln.entries[e.ENRKey()] = e
|
|
||||||
ln.invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes the given entry from the local record.
|
|
||||||
func (ln *LocalNode) Delete(e enr.Entry) {
|
|
||||||
ln.mu.Lock()
|
|
||||||
defer ln.mu.Unlock()
|
|
||||||
|
|
||||||
ln.delete(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *LocalNode) delete(e enr.Entry) {
|
|
||||||
_, exists := ln.entries[e.ENRKey()]
|
|
||||||
if exists {
|
|
||||||
delete(ln.entries, e.ENRKey())
|
|
||||||
ln.invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *LocalNode) endpointForIP(ip net.IP) *lnEndpoint {
|
|
||||||
if ip.To4() != nil {
|
|
||||||
return &ln.endpoint4
|
|
||||||
}
|
|
||||||
return &ln.endpoint6
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetStaticIP sets the local IP to the given one unconditionally.
|
|
||||||
// This disables endpoint prediction.
|
|
||||||
func (ln *LocalNode) SetStaticIP(ip net.IP) {
|
|
||||||
ln.mu.Lock()
|
|
||||||
defer ln.mu.Unlock()
|
|
||||||
|
|
||||||
ln.endpointForIP(ip).staticIP = ip
|
|
||||||
ln.updateEndpoints()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFallbackIP sets the last-resort IP address. This address is used
|
|
||||||
// if no endpoint prediction can be made and no static IP is set.
|
|
||||||
func (ln *LocalNode) SetFallbackIP(ip net.IP) {
|
|
||||||
ln.mu.Lock()
|
|
||||||
defer ln.mu.Unlock()
|
|
||||||
|
|
||||||
ln.endpointForIP(ip).fallbackIP = ip
|
|
||||||
ln.updateEndpoints()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFallbackUDP sets the last-resort UDP-on-IPv4 port. This port is used
|
|
||||||
// if no endpoint prediction can be made.
|
|
||||||
func (ln *LocalNode) SetFallbackUDP(port int) {
|
|
||||||
ln.mu.Lock()
|
|
||||||
defer ln.mu.Unlock()
|
|
||||||
|
|
||||||
ln.endpoint4.fallbackUDP = uint16(port)
|
|
||||||
ln.endpoint6.fallbackUDP = uint16(port)
|
|
||||||
ln.updateEndpoints()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UDPEndpointStatement should be called whenever a statement about the local node's
|
|
||||||
// UDP endpoint is received. It feeds the local endpoint predictor.
|
|
||||||
func (ln *LocalNode) UDPEndpointStatement(fromaddr, endpoint *net.UDPAddr) {
|
|
||||||
ln.mu.Lock()
|
|
||||||
defer ln.mu.Unlock()
|
|
||||||
|
|
||||||
ln.endpointForIP(endpoint.IP).track.AddStatement(fromaddr.String(), endpoint.String())
|
|
||||||
ln.updateEndpoints()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UDPContact should be called whenever the local node has announced itself to another node
|
|
||||||
// via UDP. It feeds the local endpoint predictor.
|
|
||||||
func (ln *LocalNode) UDPContact(toaddr *net.UDPAddr) {
|
|
||||||
ln.mu.Lock()
|
|
||||||
defer ln.mu.Unlock()
|
|
||||||
|
|
||||||
ln.endpointForIP(toaddr.IP).track.AddContact(toaddr.String())
|
|
||||||
ln.updateEndpoints()
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateEndpoints updates the record with predicted endpoints.
|
|
||||||
func (ln *LocalNode) updateEndpoints() {
|
|
||||||
ip4, udp4 := ln.endpoint4.get()
|
|
||||||
ip6, udp6 := ln.endpoint6.get()
|
|
||||||
|
|
||||||
if ip4 != nil && !ip4.IsUnspecified() {
|
|
||||||
ln.set(enr.IPv4(ip4))
|
|
||||||
} else {
|
|
||||||
ln.delete(enr.IPv4{})
|
|
||||||
}
|
|
||||||
if ip6 != nil && !ip6.IsUnspecified() {
|
|
||||||
ln.set(enr.IPv6(ip6))
|
|
||||||
} else {
|
|
||||||
ln.delete(enr.IPv6{})
|
|
||||||
}
|
|
||||||
if udp4 != 0 {
|
|
||||||
ln.set(enr.UDP(udp4))
|
|
||||||
} else {
|
|
||||||
ln.delete(enr.UDP(0))
|
|
||||||
}
|
|
||||||
if udp6 != 0 && udp6 != udp4 {
|
|
||||||
ln.set(enr.UDP6(udp6))
|
|
||||||
} else {
|
|
||||||
ln.delete(enr.UDP6(0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get returns the endpoint with highest precedence.
|
|
||||||
func (e *lnEndpoint) get() (newIP net.IP, newPort uint16) {
|
|
||||||
newPort = e.fallbackUDP
|
|
||||||
if e.fallbackIP != nil {
|
|
||||||
newIP = e.fallbackIP
|
|
||||||
}
|
|
||||||
if e.staticIP != nil {
|
|
||||||
newIP = e.staticIP
|
|
||||||
} else if ip, port := predictAddr(e.track); ip != nil {
|
|
||||||
newIP = ip
|
|
||||||
newPort = port
|
|
||||||
}
|
|
||||||
return newIP, newPort
|
|
||||||
}
|
|
||||||
|
|
||||||
// predictAddr wraps IPTracker.PredictEndpoint, converting from its string-based
|
|
||||||
// endpoint representation to IP and port types.
|
|
||||||
func predictAddr(t *netutil.IPTracker) (net.IP, uint16) {
|
|
||||||
ep := t.PredictEndpoint()
|
|
||||||
if ep == "" {
|
|
||||||
return nil, 0
|
|
||||||
}
|
|
||||||
ipString, portString, _ := net.SplitHostPort(ep)
|
|
||||||
ip := net.ParseIP(ipString)
|
|
||||||
port, err := strconv.ParseUint(portString, 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0
|
|
||||||
}
|
|
||||||
return ip, uint16(port)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *LocalNode) invalidate() {
|
|
||||||
ln.cur.Store((*Node)(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *LocalNode) sign() {
|
|
||||||
if n := ln.cur.Load().(*Node); n != nil {
|
|
||||||
return // no changes
|
|
||||||
}
|
|
||||||
|
|
||||||
var r enr.Record
|
|
||||||
for _, e := range ln.entries {
|
|
||||||
r.Set(e)
|
|
||||||
}
|
|
||||||
ln.bumpSeq()
|
|
||||||
r.SetSeq(ln.seq)
|
|
||||||
if err := SignV4(&r, ln.key); err != nil {
|
|
||||||
panic(fmt.Errorf("enode: can't sign record: %v", err))
|
|
||||||
}
|
|
||||||
n, err := New(ValidSchemes, &r)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("enode: can't verify local record: %v", err))
|
|
||||||
}
|
|
||||||
ln.cur.Store(n)
|
|
||||||
log.Info("New local node record", "seq", ln.seq, "id", n.ID(), "ip", n.IP(), "udp", n.UDP(), "tcp", n.TCP())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ln *LocalNode) bumpSeq() {
|
|
||||||
ln.seq++
|
|
||||||
ln.db.storeLocalSeq(ln.id, ln.seq)
|
|
||||||
}
|
|
||||||
|
|
||||||
// nowMilliseconds gives the current timestamp at millisecond precision.
|
|
||||||
func nowMilliseconds() uint64 {
|
|
||||||
ns := time.Now().UnixNano()
|
|
||||||
if ns < 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return uint64(ns / 1000 / 1000)
|
|
||||||
}
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package enode
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newLocalNodeForTesting() (*LocalNode, *DB) {
|
|
||||||
db, _ := OpenDB("")
|
|
||||||
key, _ := crypto.GenerateKey()
|
|
||||||
return NewLocalNode(db, key), db
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLocalNode(t *testing.T) {
|
|
||||||
ln, db := newLocalNodeForTesting()
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
if ln.Node().ID() != ln.ID() {
|
|
||||||
t.Fatal("inconsistent ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
ln.Set(enr.WithEntry("x", uint(3)))
|
|
||||||
var x uint
|
|
||||||
if err := ln.Node().Load(enr.WithEntry("x", &x)); err != nil {
|
|
||||||
t.Fatal("can't load entry 'x':", err)
|
|
||||||
} else if x != 3 {
|
|
||||||
t.Fatal("wrong value for entry 'x':", x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that the sequence number is persisted between restarts.
|
|
||||||
func TestLocalNodeSeqPersist(t *testing.T) {
|
|
||||||
timestamp := nowMilliseconds()
|
|
||||||
|
|
||||||
ln, db := newLocalNodeForTesting()
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
initialSeq := ln.Node().Seq()
|
|
||||||
if initialSeq < timestamp {
|
|
||||||
t.Fatalf("wrong initial seq %d, want at least %d", initialSeq, timestamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
ln.Set(enr.WithEntry("x", uint(1)))
|
|
||||||
if s := ln.Node().Seq(); s != initialSeq+1 {
|
|
||||||
t.Fatalf("wrong seq %d after set, want %d", s, initialSeq+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new instance, it should reload the sequence number.
|
|
||||||
// The number increases just after that because a new record is
|
|
||||||
// created without the "x" entry.
|
|
||||||
ln2 := NewLocalNode(db, ln.key)
|
|
||||||
if s := ln2.Node().Seq(); s != initialSeq+2 {
|
|
||||||
t.Fatalf("wrong seq %d on new instance, want %d", s, initialSeq+2)
|
|
||||||
}
|
|
||||||
|
|
||||||
finalSeq := ln2.Node().Seq()
|
|
||||||
|
|
||||||
// Create a new instance with a different node key on the same database.
|
|
||||||
// This should reset the sequence number.
|
|
||||||
key, _ := crypto.GenerateKey()
|
|
||||||
ln3 := NewLocalNode(db, key)
|
|
||||||
if s := ln3.Node().Seq(); s < finalSeq {
|
|
||||||
t.Fatalf("wrong seq %d on instance with changed key, want >= %d", s, finalSeq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks behavior of the endpoint predictor.
|
|
||||||
func TestLocalNodeEndpoint(t *testing.T) {
|
|
||||||
var (
|
|
||||||
fallback = &net.UDPAddr{IP: net.IP{127, 0, 0, 1}, Port: 80}
|
|
||||||
predicted = &net.UDPAddr{IP: net.IP{127, 0, 1, 2}, Port: 81}
|
|
||||||
staticIP = net.IP{127, 0, 1, 2}
|
|
||||||
)
|
|
||||||
ln, db := newLocalNodeForTesting()
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Nothing is set initially.
|
|
||||||
assert.Equal(t, net.IP(nil), ln.Node().IP())
|
|
||||||
assert.Equal(t, 0, ln.Node().UDP())
|
|
||||||
initialSeq := ln.Node().Seq()
|
|
||||||
|
|
||||||
// Set up fallback address.
|
|
||||||
ln.SetFallbackIP(fallback.IP)
|
|
||||||
ln.SetFallbackUDP(fallback.Port)
|
|
||||||
assert.Equal(t, fallback.IP, ln.Node().IP())
|
|
||||||
assert.Equal(t, fallback.Port, ln.Node().UDP())
|
|
||||||
assert.Equal(t, initialSeq+1, ln.Node().Seq())
|
|
||||||
|
|
||||||
// Add endpoint statements from random hosts.
|
|
||||||
for i := 0; i < iptrackMinStatements; i++ {
|
|
||||||
assert.Equal(t, fallback.IP, ln.Node().IP())
|
|
||||||
assert.Equal(t, fallback.Port, ln.Node().UDP())
|
|
||||||
assert.Equal(t, initialSeq+1, ln.Node().Seq())
|
|
||||||
|
|
||||||
from := &net.UDPAddr{IP: make(net.IP, 4), Port: 90}
|
|
||||||
rand.Read(from.IP)
|
|
||||||
ln.UDPEndpointStatement(from, predicted)
|
|
||||||
}
|
|
||||||
assert.Equal(t, predicted.IP, ln.Node().IP())
|
|
||||||
assert.Equal(t, predicted.Port, ln.Node().UDP())
|
|
||||||
assert.Equal(t, initialSeq+2, ln.Node().Seq())
|
|
||||||
|
|
||||||
// Static IP overrides prediction.
|
|
||||||
ln.SetStaticIP(staticIP)
|
|
||||||
assert.Equal(t, staticIP, ln.Node().IP())
|
|
||||||
assert.Equal(t, fallback.Port, ln.Node().UDP())
|
|
||||||
assert.Equal(t, initialSeq+3, ln.Node().Seq())
|
|
||||||
}
|
|
||||||
|
|
@ -1,279 +0,0 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package enode
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/bits"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errMissingPrefix = errors.New("missing 'enr:' prefix for base64-encoded record")
|
|
||||||
|
|
||||||
// Node represents a host on the network.
|
|
||||||
type Node struct {
|
|
||||||
r enr.Record
|
|
||||||
id ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// New wraps a node record. The record must be valid according to the given
|
|
||||||
// identity scheme.
|
|
||||||
func New(validSchemes enr.IdentityScheme, r *enr.Record) (*Node, error) {
|
|
||||||
if err := r.VerifySignature(validSchemes); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
node := &Node{r: *r}
|
|
||||||
if n := copy(node.id[:], validSchemes.NodeAddr(&node.r)); n != len(ID{}) {
|
|
||||||
return nil, fmt.Errorf("invalid node ID length %d, need %d", n, len(ID{}))
|
|
||||||
}
|
|
||||||
return node, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustParse parses a node record or enode:// URL. It panics if the input is invalid.
|
|
||||||
func MustParse(rawurl string) *Node {
|
|
||||||
n, err := Parse(ValidSchemes, rawurl)
|
|
||||||
if err != nil {
|
|
||||||
panic("invalid node: " + err.Error())
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse decodes and verifies a base64-encoded node record.
|
|
||||||
func Parse(validSchemes enr.IdentityScheme, input string) (*Node, error) {
|
|
||||||
if strings.HasPrefix(input, "enode://") {
|
|
||||||
return ParseV4(input)
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(input, "enr:") {
|
|
||||||
return nil, errMissingPrefix
|
|
||||||
}
|
|
||||||
bin, err := base64.RawURLEncoding.DecodeString(input[4:])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var r enr.Record
|
|
||||||
if err := rlp.DecodeBytes(bin, &r); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return New(validSchemes, &r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID returns the node identifier.
|
|
||||||
func (n *Node) ID() ID {
|
|
||||||
return n.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seq returns the sequence number of the underlying record.
|
|
||||||
func (n *Node) Seq() uint64 {
|
|
||||||
return n.r.Seq()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Incomplete returns true for nodes with no IP address.
|
|
||||||
func (n *Node) Incomplete() bool {
|
|
||||||
return n.IP() == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load retrieves an entry from the underlying record.
|
|
||||||
func (n *Node) Load(k enr.Entry) error {
|
|
||||||
return n.r.Load(k)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IP returns the IP address of the node. This prefers IPv4 addresses.
|
|
||||||
func (n *Node) IP() net.IP {
|
|
||||||
var (
|
|
||||||
ip4 enr.IPv4
|
|
||||||
ip6 enr.IPv6
|
|
||||||
)
|
|
||||||
if n.Load(&ip4) == nil {
|
|
||||||
return net.IP(ip4)
|
|
||||||
}
|
|
||||||
if n.Load(&ip6) == nil {
|
|
||||||
return net.IP(ip6)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UDP returns the UDP port of the node.
|
|
||||||
func (n *Node) UDP() int {
|
|
||||||
var port enr.UDP
|
|
||||||
n.Load(&port)
|
|
||||||
return int(port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TCP returns the TCP port of the node.
|
|
||||||
func (n *Node) TCP() int {
|
|
||||||
var port enr.TCP
|
|
||||||
n.Load(&port)
|
|
||||||
return int(port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pubkey returns the secp256k1 public key of the node, if present.
|
|
||||||
func (n *Node) Pubkey() *ecdsa.PublicKey {
|
|
||||||
var key ecdsa.PublicKey
|
|
||||||
if n.Load((*Secp256k1)(&key)) != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &key
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record returns the node's record. The return value is a copy and may
|
|
||||||
// be modified by the caller.
|
|
||||||
func (n *Node) Record() *enr.Record {
|
|
||||||
cpy := n.r
|
|
||||||
return &cpy
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateComplete checks whether n has a valid IP and UDP port.
|
|
||||||
// Deprecated: don't use this method.
|
|
||||||
func (n *Node) ValidateComplete() error {
|
|
||||||
if n.Incomplete() {
|
|
||||||
return errors.New("missing IP address")
|
|
||||||
}
|
|
||||||
if n.UDP() == 0 {
|
|
||||||
return errors.New("missing UDP port")
|
|
||||||
}
|
|
||||||
ip := n.IP()
|
|
||||||
if ip.IsMulticast() || ip.IsUnspecified() {
|
|
||||||
return errors.New("invalid IP (multicast/unspecified)")
|
|
||||||
}
|
|
||||||
// Validate the node key (on curve, etc.).
|
|
||||||
var key Secp256k1
|
|
||||||
return n.Load(&key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the text representation of the record.
|
|
||||||
func (n *Node) String() string {
|
|
||||||
if isNewV4(n) {
|
|
||||||
return n.URLv4() // backwards-compatibility glue for NewV4 nodes
|
|
||||||
}
|
|
||||||
enc, _ := rlp.EncodeToBytes(&n.r) // always succeeds because record is valid
|
|
||||||
b64 := base64.RawURLEncoding.EncodeToString(enc)
|
|
||||||
return "enr:" + b64
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalText implements encoding.TextMarshaler.
|
|
||||||
func (n *Node) MarshalText() ([]byte, error) {
|
|
||||||
return []byte(n.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
|
||||||
func (n *Node) UnmarshalText(text []byte) error {
|
|
||||||
dec, err := Parse(ValidSchemes, string(text))
|
|
||||||
if err == nil {
|
|
||||||
*n = *dec
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID is a unique identifier for each node.
|
|
||||||
type ID [32]byte
|
|
||||||
|
|
||||||
// Bytes returns a byte slice representation of the ID
|
|
||||||
func (n ID) Bytes() []byte {
|
|
||||||
return n[:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID prints as a long hexadecimal number.
|
|
||||||
func (n ID) String() string {
|
|
||||||
return fmt.Sprintf("%x", n[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// GoString returns the Go syntax representation of a ID is a call to HexID.
|
|
||||||
func (n ID) GoString() string {
|
|
||||||
return fmt.Sprintf("enode.HexID(\"%x\")", n[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// TerminalString returns a shortened hex string for terminal logging.
|
|
||||||
func (n ID) TerminalString() string {
|
|
||||||
return hex.EncodeToString(n[:8])
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalText implements the encoding.TextMarshaler interface.
|
|
||||||
func (n ID) MarshalText() ([]byte, error) {
|
|
||||||
return []byte(hex.EncodeToString(n[:])), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalText implements the encoding.TextUnmarshaler interface.
|
|
||||||
func (n *ID) UnmarshalText(text []byte) error {
|
|
||||||
id, err := ParseID(string(text))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*n = id
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HexID converts a hex string to an ID.
|
|
||||||
// The string may be prefixed with 0x.
|
|
||||||
// It panics if the string is not a valid ID.
|
|
||||||
func HexID(in string) ID {
|
|
||||||
id, err := ParseID(in)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseID(in string) (ID, error) {
|
|
||||||
var id ID
|
|
||||||
b, err := hex.DecodeString(strings.TrimPrefix(in, "0x"))
|
|
||||||
if err != nil {
|
|
||||||
return id, err
|
|
||||||
} else if len(b) != len(id) {
|
|
||||||
return id, fmt.Errorf("wrong length, want %d hex chars", len(id)*2)
|
|
||||||
}
|
|
||||||
copy(id[:], b)
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DistCmp compares the distances a->target and b->target.
|
|
||||||
// Returns -1 if a is closer to target, 1 if b is closer to target
|
|
||||||
// and 0 if they are equal.
|
|
||||||
func DistCmp(target, a, b ID) int {
|
|
||||||
for i := range target {
|
|
||||||
da := a[i] ^ target[i]
|
|
||||||
db := b[i] ^ target[i]
|
|
||||||
if da > db {
|
|
||||||
return 1
|
|
||||||
} else if da < db {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogDist returns the logarithmic distance between a and b, log2(a ^ b).
|
|
||||||
func LogDist(a, b ID) int {
|
|
||||||
lz := 0
|
|
||||||
for i := range a {
|
|
||||||
x := a[i] ^ b[i]
|
|
||||||
if x == 0 {
|
|
||||||
lz += 8
|
|
||||||
} else {
|
|
||||||
lz += bits.LeadingZeros8(x)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len(a)*8 - lz
|
|
||||||
}
|
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package enode
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
"testing/quick"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
var pyRecord, _ = hex.DecodeString("f884b8407098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c01826964827634826970847f00000189736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31388375647082765f")
|
|
||||||
|
|
||||||
// TestPythonInterop checks that we can decode and verify a record produced by the Python
|
|
||||||
// implementation.
|
|
||||||
func TestPythonInterop(t *testing.T) {
|
|
||||||
var r enr.Record
|
|
||||||
if err := rlp.DecodeBytes(pyRecord, &r); err != nil {
|
|
||||||
t.Fatalf("can't decode: %v", err)
|
|
||||||
}
|
|
||||||
n, err := New(ValidSchemes, &r)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("can't verify record: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
wantID = HexID("a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7")
|
|
||||||
wantSeq = uint64(1)
|
|
||||||
wantIP = enr.IPv4{127, 0, 0, 1}
|
|
||||||
wantUDP = enr.UDP(30303)
|
|
||||||
)
|
|
||||||
if n.Seq() != wantSeq {
|
|
||||||
t.Errorf("wrong seq: got %d, want %d", n.Seq(), wantSeq)
|
|
||||||
}
|
|
||||||
if n.ID() != wantID {
|
|
||||||
t.Errorf("wrong id: got %x, want %x", n.ID(), wantID)
|
|
||||||
}
|
|
||||||
want := map[enr.Entry]interface{}{new(enr.IPv4): &wantIP, new(enr.UDP): &wantUDP}
|
|
||||||
for k, v := range want {
|
|
||||||
desc := fmt.Sprintf("loading key %q", k.ENRKey())
|
|
||||||
if assert.NoError(t, n.Load(k), desc) {
|
|
||||||
assert.Equal(t, k, v, desc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHexID(t *testing.T) {
|
|
||||||
ref := ID{0, 0, 0, 0, 0, 0, 0, 128, 106, 217, 182, 31, 165, 174, 1, 67, 7, 235, 220, 150, 66, 83, 173, 205, 159, 44, 10, 57, 42, 161, 26, 188}
|
|
||||||
id1 := HexID("0x00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc")
|
|
||||||
id2 := HexID("00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc")
|
|
||||||
|
|
||||||
if id1 != ref {
|
|
||||||
t.Errorf("wrong id1\ngot %v\nwant %v", id1[:], ref[:])
|
|
||||||
}
|
|
||||||
if id2 != ref {
|
|
||||||
t.Errorf("wrong id2\ngot %v\nwant %v", id2[:], ref[:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestID_textEncoding(t *testing.T) {
|
|
||||||
ref := ID{
|
|
||||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10,
|
|
||||||
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x20,
|
|
||||||
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x30,
|
|
||||||
0x31, 0x32,
|
|
||||||
}
|
|
||||||
hex := "0102030405060708091011121314151617181920212223242526272829303132"
|
|
||||||
|
|
||||||
text, err := ref.MarshalText()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(text, []byte(hex)) {
|
|
||||||
t.Fatalf("text encoding did not match\nexpected: %s\ngot: %s", hex, text)
|
|
||||||
}
|
|
||||||
|
|
||||||
id := new(ID)
|
|
||||||
if err := id.UnmarshalText(text); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if *id != ref {
|
|
||||||
t.Fatalf("text decoding did not match\nexpected: %s\ngot: %s", ref, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestID_distcmp(t *testing.T) {
|
|
||||||
distcmpBig := func(target, a, b ID) int {
|
|
||||||
tbig := new(big.Int).SetBytes(target[:])
|
|
||||||
abig := new(big.Int).SetBytes(a[:])
|
|
||||||
bbig := new(big.Int).SetBytes(b[:])
|
|
||||||
return new(big.Int).Xor(tbig, abig).Cmp(new(big.Int).Xor(tbig, bbig))
|
|
||||||
}
|
|
||||||
if err := quick.CheckEqual(DistCmp, distcmpBig, nil); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The random tests is likely to miss the case where a and b are equal,
|
|
||||||
// this test checks it explicitly.
|
|
||||||
func TestID_distcmpEqual(t *testing.T) {
|
|
||||||
base := ID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
|
|
||||||
x := ID{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
|
|
||||||
if DistCmp(base, x, x) != 0 {
|
|
||||||
t.Errorf("DistCmp(base, x, x) != 0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestID_logdist(t *testing.T) {
|
|
||||||
logdistBig := func(a, b ID) int {
|
|
||||||
abig, bbig := new(big.Int).SetBytes(a[:]), new(big.Int).SetBytes(b[:])
|
|
||||||
return new(big.Int).Xor(abig, bbig).BitLen()
|
|
||||||
}
|
|
||||||
if err := quick.CheckEqual(LogDist, logdistBig, nil); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The random tests is likely to miss the case where a and b are equal,
|
|
||||||
// this test checks it explicitly.
|
|
||||||
func TestID_logdistEqual(t *testing.T) {
|
|
||||||
x := ID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
|
|
||||||
if LogDist(x, x) != 0 {
|
|
||||||
t.Errorf("LogDist(x, x) != 0")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,501 +0,0 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package enode
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
|
||||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
|
||||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
|
||||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
|
||||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
|
||||||
"github.com/syndtr/goleveldb/leveldb/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Keys in the node database.
|
|
||||||
const (
|
|
||||||
dbVersionKey = "version" // Version of the database to flush if changes
|
|
||||||
dbNodePrefix = "n:" // Identifier to prefix node entries with
|
|
||||||
dbLocalPrefix = "local:"
|
|
||||||
dbDiscoverRoot = "v4"
|
|
||||||
dbDiscv5Root = "v5"
|
|
||||||
|
|
||||||
// These fields are stored per ID and IP, the full key is "n:<ID>:v4:<IP>:findfail".
|
|
||||||
// Use nodeItemKey to create those keys.
|
|
||||||
dbNodeFindFails = "findfail"
|
|
||||||
dbNodePing = "lastping"
|
|
||||||
dbNodePong = "lastpong"
|
|
||||||
dbNodeSeq = "seq"
|
|
||||||
|
|
||||||
// Local information is keyed by ID only, the full key is "local:<ID>:seq".
|
|
||||||
// Use localItemKey to create those keys.
|
|
||||||
dbLocalSeq = "seq"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
dbNodeExpiration = 24 * time.Hour // Time after which an unseen node should be dropped.
|
|
||||||
dbCleanupCycle = time.Hour // Time period for running the expiration task.
|
|
||||||
dbVersion = 9
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errInvalidIP = errors.New("invalid IP")
|
|
||||||
)
|
|
||||||
|
|
||||||
var zeroIP = make(net.IP, 16)
|
|
||||||
|
|
||||||
// DB is the node database, storing previously seen nodes and any collected metadata about
|
|
||||||
// them for QoS purposes.
|
|
||||||
type DB struct {
|
|
||||||
lvl *leveldb.DB // Interface to the database itself
|
|
||||||
runner sync.Once // Ensures we can start at most one expirer
|
|
||||||
quit chan struct{} // Channel to signal the expiring thread to stop
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenDB opens a node database for storing and retrieving infos about known peers in the
|
|
||||||
// network. If no path is given an in-memory, temporary database is constructed.
|
|
||||||
func OpenDB(path string) (*DB, error) {
|
|
||||||
if path == "" {
|
|
||||||
return newMemoryDB()
|
|
||||||
}
|
|
||||||
return newPersistentDB(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newMemoryNodeDB creates a new in-memory node database without a persistent backend.
|
|
||||||
func newMemoryDB() (*DB, error) {
|
|
||||||
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &DB{lvl: db, quit: make(chan struct{})}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// newPersistentNodeDB creates/opens a leveldb backed persistent node database,
|
|
||||||
// also flushing its contents in case of a version mismatch.
|
|
||||||
func newPersistentDB(path string) (*DB, error) {
|
|
||||||
opts := &opt.Options{OpenFilesCacheCapacity: 5}
|
|
||||||
db, err := leveldb.OpenFile(path, opts)
|
|
||||||
if _, iscorrupted := err.(*errors.ErrCorrupted); iscorrupted {
|
|
||||||
db, err = leveldb.RecoverFile(path, nil)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// The nodes contained in the cache correspond to a certain protocol version.
|
|
||||||
// Flush all nodes if the version doesn't match.
|
|
||||||
currentVer := make([]byte, binary.MaxVarintLen64)
|
|
||||||
currentVer = currentVer[:binary.PutVarint(currentVer, int64(dbVersion))]
|
|
||||||
|
|
||||||
blob, err := db.Get([]byte(dbVersionKey), nil)
|
|
||||||
switch err {
|
|
||||||
case leveldb.ErrNotFound:
|
|
||||||
// Version not found (i.e. empty cache), insert it
|
|
||||||
if err := db.Put([]byte(dbVersionKey), currentVer, nil); err != nil {
|
|
||||||
db.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
case nil:
|
|
||||||
// Version present, flush if different
|
|
||||||
if !bytes.Equal(blob, currentVer) {
|
|
||||||
db.Close()
|
|
||||||
if err = os.RemoveAll(path); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return newPersistentDB(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &DB{lvl: db, quit: make(chan struct{})}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// nodeKey returns the database key for a node record.
|
|
||||||
func nodeKey(id ID) []byte {
|
|
||||||
key := append([]byte(dbNodePrefix), id[:]...)
|
|
||||||
key = append(key, ':')
|
|
||||||
key = append(key, dbDiscoverRoot...)
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
// splitNodeKey returns the node ID of a key created by nodeKey.
|
|
||||||
func splitNodeKey(key []byte) (id ID, rest []byte) {
|
|
||||||
if !bytes.HasPrefix(key, []byte(dbNodePrefix)) {
|
|
||||||
return ID{}, nil
|
|
||||||
}
|
|
||||||
item := key[len(dbNodePrefix):]
|
|
||||||
copy(id[:], item[:len(id)])
|
|
||||||
return id, item[len(id)+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 = 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
|
|
||||||
}
|
|
||||||
|
|
||||||
func v5Key(id ID, ip net.IP, field string) []byte {
|
|
||||||
return bytes.Join([][]byte{
|
|
||||||
[]byte(dbNodePrefix),
|
|
||||||
id[:],
|
|
||||||
[]byte(dbDiscv5Root),
|
|
||||||
ip.To16(),
|
|
||||||
[]byte(field),
|
|
||||||
}, []byte{':'})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
func (db *DB) fetchInt64(key []byte) int64 {
|
|
||||||
blob, err := db.lvl.Get(key, nil)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
val, read := binary.Varint(blob)
|
|
||||||
if read <= 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
// storeInt64 stores an integer in the given key.
|
|
||||||
func (db *DB) storeInt64(key []byte, n int64) error {
|
|
||||||
blob := make([]byte, binary.MaxVarintLen64)
|
|
||||||
blob = blob[:binary.PutVarint(blob, n)]
|
|
||||||
return db.lvl.Put(key, blob, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchUint64 retrieves an integer associated with a particular key.
|
|
||||||
func (db *DB) fetchUint64(key []byte) uint64 {
|
|
||||||
blob, err := db.lvl.Get(key, nil)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
val, _ := binary.Uvarint(blob)
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
// storeUint64 stores an integer in the given key.
|
|
||||||
func (db *DB) storeUint64(key []byte, n uint64) error {
|
|
||||||
blob := make([]byte, binary.MaxVarintLen64)
|
|
||||||
blob = blob[:binary.PutUvarint(blob, n)]
|
|
||||||
return db.lvl.Put(key, blob, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node retrieves a node with a given id from the database.
|
|
||||||
func (db *DB) Node(id ID) *Node {
|
|
||||||
blob, err := db.lvl.Get(nodeKey(id), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return mustDecodeNode(id[:], blob)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustDecodeNode(id, data []byte) *Node {
|
|
||||||
node := new(Node)
|
|
||||||
if err := rlp.DecodeBytes(data, &node.r); err != nil {
|
|
||||||
panic(fmt.Errorf("p2p/enode: can't decode node %x in DB: %v", id, err))
|
|
||||||
}
|
|
||||||
// Restore node id cache.
|
|
||||||
copy(node.id[:], id)
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateNode inserts - potentially overwriting - a node into the peer database.
|
|
||||||
func (db *DB) UpdateNode(node *Node) error {
|
|
||||||
if node.Seq() < db.NodeSeq(node.ID()) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
blob, err := rlp.EncodeToBytes(&node.r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := db.lvl.Put(nodeKey(node.ID()), blob, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return db.storeUint64(nodeItemKey(node.ID(), zeroIP, dbNodeSeq), node.Seq())
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeSeq returns the stored record sequence number of the given node.
|
|
||||||
func (db *DB) NodeSeq(id ID) uint64 {
|
|
||||||
return db.fetchUint64(nodeItemKey(id, zeroIP, dbNodeSeq))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve returns the stored record of the node if it has a larger sequence
|
|
||||||
// number than n.
|
|
||||||
func (db *DB) Resolve(n *Node) *Node {
|
|
||||||
if n.Seq() > db.NodeSeq(n.ID()) {
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
return db.Node(n.ID())
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteNode deletes all information associated with a node.
|
|
||||||
func (db *DB) DeleteNode(id ID) {
|
|
||||||
deleteRange(db.lvl, nodeKey(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureExpirer is a small helper method ensuring that the data expiration
|
|
||||||
// mechanism is running. If the expiration goroutine is already running, this
|
|
||||||
// method simply returns.
|
|
||||||
//
|
|
||||||
// The goal is to start the data evacuation only after the network successfully
|
|
||||||
// bootstrapped itself (to prevent dumping potentially useful seed nodes). Since
|
|
||||||
// it would require significant overhead to exactly trace the first successful
|
|
||||||
// convergence, it's simpler to "ensure" the correct state when an appropriate
|
|
||||||
// condition occurs (i.e. a successful bonding), and discard further events.
|
|
||||||
func (db *DB) ensureExpirer() {
|
|
||||||
db.runner.Do(func() { go db.expirer() })
|
|
||||||
}
|
|
||||||
|
|
||||||
// expirer should be started in a go routine, and is responsible for looping ad
|
|
||||||
// infinitum and dropping stale data from the database.
|
|
||||||
func (db *DB) expirer() {
|
|
||||||
tick := time.NewTicker(dbCleanupCycle)
|
|
||||||
defer tick.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-tick.C:
|
|
||||||
db.expireNodes()
|
|
||||||
case <-db.quit:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// expireNodes iterates over the database and deletes all nodes that have not
|
|
||||||
// been seen (i.e. received a pong from) for some time.
|
|
||||||
func (db *DB) expireNodes() {
|
|
||||||
it := db.lvl.NewIterator(util.BytesPrefix([]byte(dbNodePrefix)), nil)
|
|
||||||
defer it.Release()
|
|
||||||
if !it.Next() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
threshold = time.Now().Add(-dbNodeExpiration).Unix()
|
|
||||||
youngestPong int64
|
|
||||||
atEnd = false
|
|
||||||
)
|
|
||||||
for !atEnd {
|
|
||||||
id, ip, field := splitNodeItemKey(it.Key())
|
|
||||||
if field == dbNodePong {
|
|
||||||
time, _ := binary.Varint(it.Value())
|
|
||||||
if time > youngestPong {
|
|
||||||
youngestPong = time
|
|
||||||
}
|
|
||||||
if time < threshold {
|
|
||||||
// Last pong from this IP older than threshold, remove fields belonging to it.
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LastPingReceived retrieves the time of the last ping packet received from
|
|
||||||
// a remote node.
|
|
||||||
func (db *DB) LastPingReceived(id ID, ip net.IP) time.Time {
|
|
||||||
if ip = ip.To16(); ip == nil {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
return time.Unix(db.fetchInt64(nodeItemKey(id, ip, dbNodePing)), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateLastPingReceived updates the last time we tried contacting a remote node.
|
|
||||||
func (db *DB) UpdateLastPingReceived(id ID, ip net.IP, instance time.Time) error {
|
|
||||||
if ip = ip.To16(); ip == nil {
|
|
||||||
return errInvalidIP
|
|
||||||
}
|
|
||||||
return db.storeInt64(nodeItemKey(id, ip, dbNodePing), instance.Unix())
|
|
||||||
}
|
|
||||||
|
|
||||||
// LastPongReceived retrieves the time of the last successful pong from remote node.
|
|
||||||
func (db *DB) LastPongReceived(id ID, ip net.IP) time.Time {
|
|
||||||
if ip = ip.To16(); ip == nil {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
// Launch expirer
|
|
||||||
db.ensureExpirer()
|
|
||||||
return time.Unix(db.fetchInt64(nodeItemKey(id, ip, dbNodePong)), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateLastPongReceived updates the last pong time of a node.
|
|
||||||
func (db *DB) UpdateLastPongReceived(id ID, ip net.IP, instance time.Time) error {
|
|
||||||
if ip = ip.To16(); ip == nil {
|
|
||||||
return errInvalidIP
|
|
||||||
}
|
|
||||||
return db.storeInt64(nodeItemKey(id, ip, dbNodePong), instance.Unix())
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindFails retrieves the number of findnode failures since bonding.
|
|
||||||
func (db *DB) FindFails(id ID, ip net.IP) int {
|
|
||||||
if ip = ip.To16(); ip == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return int(db.fetchInt64(nodeItemKey(id, ip, dbNodeFindFails)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateFindFails updates the number of findnode failures since bonding.
|
|
||||||
func (db *DB) UpdateFindFails(id ID, ip net.IP, fails int) error {
|
|
||||||
if ip = ip.To16(); ip == nil {
|
|
||||||
return errInvalidIP
|
|
||||||
}
|
|
||||||
return db.storeInt64(nodeItemKey(id, ip, dbNodeFindFails), int64(fails))
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindFailsV5 retrieves the discv5 findnode failure counter.
|
|
||||||
func (db *DB) FindFailsV5(id ID, ip net.IP) int {
|
|
||||||
if ip = ip.To16(); ip == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return int(db.fetchInt64(v5Key(id, ip, dbNodeFindFails)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateFindFailsV5 stores the discv5 findnode failure counter.
|
|
||||||
func (db *DB) UpdateFindFailsV5(id ID, ip net.IP, fails int) error {
|
|
||||||
if ip = ip.To16(); ip == nil {
|
|
||||||
return errInvalidIP
|
|
||||||
}
|
|
||||||
return db.storeInt64(v5Key(id, ip, dbNodeFindFails), int64(fails))
|
|
||||||
}
|
|
||||||
|
|
||||||
// localSeq retrieves the local record sequence counter, defaulting to the current
|
|
||||||
// timestamp if no previous exists. This ensures that wiping all data associated
|
|
||||||
// with a node (apart from its key) will not generate already used sequence nums.
|
|
||||||
func (db *DB) localSeq(id ID) uint64 {
|
|
||||||
if seq := db.fetchUint64(localItemKey(id, dbLocalSeq)); seq > 0 {
|
|
||||||
return seq
|
|
||||||
}
|
|
||||||
return nowMilliseconds()
|
|
||||||
}
|
|
||||||
|
|
||||||
// storeLocalSeq stores the local record sequence counter.
|
|
||||||
func (db *DB) storeLocalSeq(id ID, n uint64) {
|
|
||||||
db.storeUint64(localItemKey(id, dbLocalSeq), n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// QuerySeeds retrieves random nodes to be used as potential seed nodes
|
|
||||||
// for bootstrapping.
|
|
||||||
func (db *DB) QuerySeeds(n int, maxAge time.Duration) []*Node {
|
|
||||||
var (
|
|
||||||
now = time.Now()
|
|
||||||
nodes = make([]*Node, 0, n)
|
|
||||||
it = db.lvl.NewIterator(nil, nil)
|
|
||||||
id ID
|
|
||||||
)
|
|
||||||
defer it.Release()
|
|
||||||
|
|
||||||
seek:
|
|
||||||
for seeks := 0; len(nodes) < n && seeks < n*5; seeks++ {
|
|
||||||
// Seek to a random entry. The first byte is incremented by a
|
|
||||||
// random amount each time in order to increase the likelihood
|
|
||||||
// of hitting all existing nodes in very small databases.
|
|
||||||
ctr := id[0]
|
|
||||||
rand.Read(id[:])
|
|
||||||
id[0] = ctr + id[0]%16
|
|
||||||
it.Seek(nodeKey(id))
|
|
||||||
|
|
||||||
n := nextNode(it)
|
|
||||||
if n == nil {
|
|
||||||
id[0] = 0
|
|
||||||
continue seek // iterator exhausted
|
|
||||||
}
|
|
||||||
if now.Sub(db.LastPongReceived(n.ID(), n.IP())) > maxAge {
|
|
||||||
continue seek
|
|
||||||
}
|
|
||||||
for i := range nodes {
|
|
||||||
if nodes[i].ID() == n.ID() {
|
|
||||||
continue seek // duplicate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nodes = append(nodes, n)
|
|
||||||
}
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
// reads the next node record from the iterator, skipping over other
|
|
||||||
// database entries.
|
|
||||||
func nextNode(it iterator.Iterator) *Node {
|
|
||||||
for end := false; !end; end = !it.Next() {
|
|
||||||
id, rest := splitNodeKey(it.Key())
|
|
||||||
if string(rest) != dbDiscoverRoot {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return mustDecodeNode(id[:], it.Value())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close flushes and closes the database files.
|
|
||||||
func (db *DB) Close() {
|
|
||||||
close(db.quit)
|
|
||||||
db.lvl.Close()
|
|
||||||
}
|
|
||||||
|
|
@ -1,469 +0,0 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package enode
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var keytestID = HexID("51232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439")
|
|
||||||
|
|
||||||
func TestDBNodeKey(t *testing.T) {
|
|
||||||
enc := nodeKey(keytestID)
|
|
||||||
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',
|
|
||||||
}
|
|
||||||
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 TestDBNodeItemKey(t *testing.T) {
|
|
||||||
wantIP := net.IP{127, 0, 0, 3}
|
|
||||||
wantField := "foobar"
|
|
||||||
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',
|
|
||||||
}
|
|
||||||
if !bytes.Equal(enc, want) {
|
|
||||||
t.Errorf("wrong encoded key:\ngot %q\nwant %q", enc, want)
|
|
||||||
}
|
|
||||||
id, ip, field := splitNodeItemKey(enc)
|
|
||||||
if id != keytestID {
|
|
||||||
t.Errorf("splitNodeItemKey returned wrong ID: %v", id)
|
|
||||||
}
|
|
||||||
if !ip.Equal(wantIP) {
|
|
||||||
t.Errorf("splitNodeItemKey returned wrong IP: %v", ip)
|
|
||||||
}
|
|
||||||
if field != wantField {
|
|
||||||
t.Errorf("splitNodeItemKey returned wrong field: %q", field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var nodeDBInt64Tests = []struct {
|
|
||||||
key []byte
|
|
||||||
value int64
|
|
||||||
}{
|
|
||||||
{key: []byte{0x01}, value: 1},
|
|
||||||
{key: []byte{0x02}, value: 2},
|
|
||||||
{key: []byte{0x03}, value: 3},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDBInt64(t *testing.T) {
|
|
||||||
db, _ := OpenDB("")
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
tests := nodeDBInt64Tests
|
|
||||||
for i := 0; i < len(tests); i++ {
|
|
||||||
// Insert the next value
|
|
||||||
if err := db.storeInt64(tests[i].key, tests[i].value); err != nil {
|
|
||||||
t.Errorf("test %d: failed to store value: %v", i, err)
|
|
||||||
}
|
|
||||||
// Check all existing and non existing values
|
|
||||||
for j := 0; j < len(tests); j++ {
|
|
||||||
num := db.fetchInt64(tests[j].key)
|
|
||||||
switch {
|
|
||||||
case j <= i && num != tests[j].value:
|
|
||||||
t.Errorf("test %d, item %d: value mismatch: have %v, want %v", i, j, num, tests[j].value)
|
|
||||||
case j > i && num != 0:
|
|
||||||
t.Errorf("test %d, item %d: value mismatch: have %v, want %v", i, j, num, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDBFetchStore(t *testing.T) {
|
|
||||||
node := NewV4(
|
|
||||||
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
|
|
||||||
net.IP{192, 168, 0, 1},
|
|
||||||
30303,
|
|
||||||
30303,
|
|
||||||
)
|
|
||||||
inst := time.Now()
|
|
||||||
num := 314
|
|
||||||
|
|
||||||
db, _ := OpenDB("")
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Check fetch/store operations on a node ping object
|
|
||||||
if stored := db.LastPingReceived(node.ID(), node.IP()); stored.Unix() != 0 {
|
|
||||||
t.Errorf("ping: non-existing object: %v", stored)
|
|
||||||
}
|
|
||||||
if err := db.UpdateLastPingReceived(node.ID(), node.IP(), inst); err != nil {
|
|
||||||
t.Errorf("ping: failed to update: %v", err)
|
|
||||||
}
|
|
||||||
if stored := db.LastPingReceived(node.ID(), node.IP()); stored.Unix() != inst.Unix() {
|
|
||||||
t.Errorf("ping: value mismatch: have %v, want %v", stored, inst)
|
|
||||||
}
|
|
||||||
// Check fetch/store operations on a node pong object
|
|
||||||
if stored := db.LastPongReceived(node.ID(), node.IP()); stored.Unix() != 0 {
|
|
||||||
t.Errorf("pong: non-existing object: %v", stored)
|
|
||||||
}
|
|
||||||
if err := db.UpdateLastPongReceived(node.ID(), node.IP(), inst); err != nil {
|
|
||||||
t.Errorf("pong: failed to update: %v", err)
|
|
||||||
}
|
|
||||||
if stored := db.LastPongReceived(node.ID(), node.IP()); stored.Unix() != inst.Unix() {
|
|
||||||
t.Errorf("pong: value mismatch: have %v, want %v", stored, inst)
|
|
||||||
}
|
|
||||||
// Check fetch/store operations on a node findnode-failure object
|
|
||||||
if stored := db.FindFails(node.ID(), node.IP()); stored != 0 {
|
|
||||||
t.Errorf("find-node fails: non-existing object: %v", stored)
|
|
||||||
}
|
|
||||||
if err := db.UpdateFindFails(node.ID(), node.IP(), num); err != nil {
|
|
||||||
t.Errorf("find-node fails: failed to update: %v", err)
|
|
||||||
}
|
|
||||||
if stored := db.FindFails(node.ID(), node.IP()); stored != num {
|
|
||||||
t.Errorf("find-node fails: value mismatch: have %v, want %v", stored, num)
|
|
||||||
}
|
|
||||||
// Check fetch/store operations on an actual node object
|
|
||||||
if stored := db.Node(node.ID()); stored != nil {
|
|
||||||
t.Errorf("node: non-existing object: %v", stored)
|
|
||||||
}
|
|
||||||
if err := db.UpdateNode(node); err != nil {
|
|
||||||
t.Errorf("node: failed to update: %v", err)
|
|
||||||
}
|
|
||||||
if stored := db.Node(node.ID()); stored == nil {
|
|
||||||
t.Errorf("node: not found")
|
|
||||||
} else if !reflect.DeepEqual(stored, node) {
|
|
||||||
t.Errorf("node: data mismatch: have %v, want %v", stored, node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var nodeDBSeedQueryNodes = []struct {
|
|
||||||
node *Node
|
|
||||||
pong time.Time
|
|
||||||
}{
|
|
||||||
// This one should not be in the result set because its last
|
|
||||||
// pong time is too far in the past.
|
|
||||||
{
|
|
||||||
node: NewV4(
|
|
||||||
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
|
|
||||||
net.IP{127, 0, 0, 3},
|
|
||||||
30303,
|
|
||||||
30303,
|
|
||||||
),
|
|
||||||
pong: time.Now().Add(-3 * time.Hour),
|
|
||||||
},
|
|
||||||
// This one shouldn't be in the result set because its
|
|
||||||
// nodeID is the local node's ID.
|
|
||||||
{
|
|
||||||
node: NewV4(
|
|
||||||
hexPubkey("ff93ff820abacd4351b0f14e47b324bc82ff014c226f3f66a53535734a3c150e7e38ca03ef0964ba55acddc768f5e99cd59dea95ddd4defbab1339c92fa319b2"),
|
|
||||||
net.IP{127, 0, 0, 3},
|
|
||||||
30303,
|
|
||||||
30303,
|
|
||||||
),
|
|
||||||
pong: time.Now().Add(-4 * time.Second),
|
|
||||||
},
|
|
||||||
|
|
||||||
// These should be in the result set.
|
|
||||||
{
|
|
||||||
node: NewV4(
|
|
||||||
hexPubkey("c2b5eb3f5dde05f815b63777809ee3e7e0cbb20035a6b00ce327191e6eaa8f26a8d461c9112b7ab94698e7361fa19fd647e603e73239002946d76085b6f928d6"),
|
|
||||||
net.IP{127, 0, 0, 1},
|
|
||||||
30303,
|
|
||||||
30303,
|
|
||||||
),
|
|
||||||
pong: time.Now().Add(-2 * time.Second),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
node: NewV4(
|
|
||||||
hexPubkey("6ca1d400c8ddf8acc94bcb0dd254911ad71a57bed5e0ae5aa205beed59b28c2339908e97990c493499613cff8ecf6c3dc7112a8ead220cdcd00d8847ca3db755"),
|
|
||||||
net.IP{127, 0, 0, 2},
|
|
||||||
30303,
|
|
||||||
30303,
|
|
||||||
),
|
|
||||||
pong: time.Now().Add(-3 * time.Second),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
node: NewV4(
|
|
||||||
hexPubkey("234dc63fe4d131212b38236c4c3411288d7bec61cbf7b120ff12c43dc60c96182882f4291d209db66f8a38e986c9c010ff59231a67f9515c7d1668b86b221a47"),
|
|
||||||
net.IP{127, 0, 0, 3},
|
|
||||||
30303,
|
|
||||||
30303,
|
|
||||||
),
|
|
||||||
pong: time.Now().Add(-1 * time.Second),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
node: NewV4(
|
|
||||||
hexPubkey("c013a50b4d1ebce5c377d8af8cb7114fd933ffc9627f96ad56d90fef5b7253ec736fd07ef9a81dc2955a997e54b7bf50afd0aa9f110595e2bec5bb7ce1657004"),
|
|
||||||
net.IP{127, 0, 0, 3},
|
|
||||||
30303,
|
|
||||||
30303,
|
|
||||||
),
|
|
||||||
pong: time.Now().Add(-2 * time.Second),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
node: NewV4(
|
|
||||||
hexPubkey("f141087e3e08af1aeec261ff75f48b5b1637f594ea9ad670e50051646b0416daa3b134c28788cbe98af26992a47652889cd8577ccc108ac02c6a664db2dc1283"),
|
|
||||||
net.IP{127, 0, 0, 3},
|
|
||||||
30303,
|
|
||||||
30303,
|
|
||||||
),
|
|
||||||
pong: time.Now().Add(-2 * time.Second),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDBSeedQuery(t *testing.T) {
|
|
||||||
// Querying seeds uses seeks an might not find all nodes
|
|
||||||
// every time when the database is small. Run the test multiple
|
|
||||||
// times to avoid flakes.
|
|
||||||
const attempts = 15
|
|
||||||
var err error
|
|
||||||
for i := 0; i < attempts; i++ {
|
|
||||||
if err = testSeedQuery(); err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("no successful run in %d attempts: %v", attempts, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testSeedQuery() error {
|
|
||||||
db, _ := OpenDB("")
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Insert a batch of nodes for querying
|
|
||||||
for i, seed := range nodeDBSeedQueryNodes {
|
|
||||||
if err := db.UpdateNode(seed.node); err != nil {
|
|
||||||
return fmt.Errorf("node %d: failed to insert: %v", i, err)
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the entire batch and check for duplicates
|
|
||||||
seeds := db.QuerySeeds(len(nodeDBSeedQueryNodes)*2, time.Hour)
|
|
||||||
have := make(map[ID]struct{}, len(seeds))
|
|
||||||
for _, seed := range seeds {
|
|
||||||
have[seed.ID()] = struct{}{}
|
|
||||||
}
|
|
||||||
want := make(map[ID]struct{}, len(nodeDBSeedQueryNodes[1:]))
|
|
||||||
for _, seed := range nodeDBSeedQueryNodes[1:] {
|
|
||||||
want[seed.node.ID()] = struct{}{}
|
|
||||||
}
|
|
||||||
if len(seeds) != len(want) {
|
|
||||||
return fmt.Errorf("seed count mismatch: have %v, want %v", len(seeds), len(want))
|
|
||||||
}
|
|
||||||
for id := range have {
|
|
||||||
if _, ok := want[id]; !ok {
|
|
||||||
return fmt.Errorf("extra seed: %v", id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for id := range want {
|
|
||||||
if _, ok := have[id]; !ok {
|
|
||||||
return fmt.Errorf("missing seed: %v", id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDBPersistency(t *testing.T) {
|
|
||||||
root := t.TempDir()
|
|
||||||
|
|
||||||
var (
|
|
||||||
testKey = []byte("somekey")
|
|
||||||
testInt = int64(314)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create a persistent database and store some values
|
|
||||||
db, err := OpenDB(filepath.Join(root, "database"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create persistent database: %v", err)
|
|
||||||
}
|
|
||||||
if err := db.storeInt64(testKey, testInt); err != nil {
|
|
||||||
t.Fatalf("failed to store value: %v.", err)
|
|
||||||
}
|
|
||||||
db.Close()
|
|
||||||
|
|
||||||
// Reopen the database and check the value
|
|
||||||
db, err = OpenDB(filepath.Join(root, "database"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to open persistent database: %v", err)
|
|
||||||
}
|
|
||||||
if val := db.fetchInt64(testKey); val != testInt {
|
|
||||||
t.Fatalf("value mismatch: have %v, want %v", val, testInt)
|
|
||||||
}
|
|
||||||
db.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
var nodeDBExpirationNodes = []struct {
|
|
||||||
node *Node
|
|
||||||
pong time.Time
|
|
||||||
storeNode bool
|
|
||||||
exp bool
|
|
||||||
}{
|
|
||||||
// Node has new enough pong time and isn't expired:
|
|
||||||
{
|
|
||||||
node: NewV4(
|
|
||||||
hexPubkey("8d110e2ed4b446d9b5fb50f117e5f37fb7597af455e1dab0e6f045a6eeaa786a6781141659020d38bdc5e698ed3d4d2bafa8b5061810dfa63e8ac038db2e9b67"),
|
|
||||||
net.IP{127, 0, 0, 1},
|
|
||||||
30303,
|
|
||||||
30303,
|
|
||||||
),
|
|
||||||
storeNode: true,
|
|
||||||
pong: time.Now().Add(-dbNodeExpiration + time.Minute),
|
|
||||||
exp: false,
|
|
||||||
},
|
|
||||||
// Node with pong time before expiration is removed:
|
|
||||||
{
|
|
||||||
node: NewV4(
|
|
||||||
hexPubkey("913a205579c32425b220dfba999d215066e5bdbf900226b11da1907eae5e93eb40616d47412cf819664e9eacbdfcca6b0c6e07e09847a38472d4be46ab0c3672"),
|
|
||||||
net.IP{127, 0, 0, 2},
|
|
||||||
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),
|
|
||||||
exp: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDBExpiration(t *testing.T) {
|
|
||||||
db, _ := OpenDB("")
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Add all the test nodes and set their last pong time.
|
|
||||||
for i, seed := range nodeDBExpirationNodes {
|
|
||||||
if seed.storeNode {
|
|
||||||
if err := db.UpdateNode(seed.node); err != nil {
|
|
||||||
t.Fatalf("node %d: failed to insert: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
db.expireNodes()
|
|
||||||
|
|
||||||
// Check that expired entries have been removed.
|
|
||||||
unixZeroTime := time.Unix(0, 0)
|
|
||||||
for i, seed := range nodeDBExpirationNodes {
|
|
||||||
node := db.Node(seed.node.ID())
|
|
||||||
pong := db.LastPongReceived(seed.node.ID(), seed.node.IP())
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that expiration works when discovery v5 data is present
|
|
||||||
// in the database.
|
|
||||||
func TestDBExpireV5(t *testing.T) {
|
|
||||||
db, _ := OpenDB("")
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
ip := net.IP{127, 0, 0, 1}
|
|
||||||
db.UpdateFindFailsV5(ID{}, ip, 4)
|
|
||||||
db.expireNodes()
|
|
||||||
}
|
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package enode
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/math"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
incompleteNodeURL = regexp.MustCompile("(?i)^(?:enode://)?([0-9a-f]+)$")
|
|
||||||
lookupIPFunc = net.LookupIP
|
|
||||||
)
|
|
||||||
|
|
||||||
// MustParseV4 parses a node URL. It panics if the URL is not valid.
|
|
||||||
func MustParseV4(rawurl string) *Node {
|
|
||||||
n, err := ParseV4(rawurl)
|
|
||||||
if err != nil {
|
|
||||||
panic("invalid node URL: " + err.Error())
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseV4 parses a node URL.
|
|
||||||
//
|
|
||||||
// There are two basic forms of node URLs:
|
|
||||||
//
|
|
||||||
// - incomplete nodes, which only have the public key (node ID)
|
|
||||||
// - complete nodes, which contain the public key and IP/Port information
|
|
||||||
//
|
|
||||||
// For incomplete nodes, the designator must look like one of these
|
|
||||||
//
|
|
||||||
// enode://<hex node id>
|
|
||||||
// <hex node id>
|
|
||||||
//
|
|
||||||
// For complete nodes, the node ID is encoded in the username portion
|
|
||||||
// of the URL, separated from the host by an @ sign. The hostname can
|
|
||||||
// only be given as an IP address or using DNS domain name.
|
|
||||||
// The port in the host name section is the TCP listening port. If the
|
|
||||||
// TCP and UDP (discovery) ports differ, the UDP port is specified as
|
|
||||||
// query parameter "discport".
|
|
||||||
//
|
|
||||||
// In the following example, the node URL describes
|
|
||||||
// a node with IP address 10.3.58.6, TCP listening port 30303
|
|
||||||
// and UDP discovery port 30301.
|
|
||||||
//
|
|
||||||
// enode://<hex node id>@10.3.58.6:30303?discport=30301
|
|
||||||
func ParseV4(rawurl string) (*Node, error) {
|
|
||||||
if m := incompleteNodeURL.FindStringSubmatch(rawurl); m != nil {
|
|
||||||
id, err := parsePubkey(m[1])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid public key (%v)", err)
|
|
||||||
}
|
|
||||||
return NewV4(id, nil, 0, 0), nil
|
|
||||||
}
|
|
||||||
return parseComplete(rawurl)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewV4 creates a node from discovery v4 node information. The record
|
|
||||||
// contained in the node has a zero-length signature.
|
|
||||||
func NewV4(pubkey *ecdsa.PublicKey, ip net.IP, tcp, udp int) *Node {
|
|
||||||
var r enr.Record
|
|
||||||
if len(ip) > 0 {
|
|
||||||
r.Set(enr.IP(ip))
|
|
||||||
}
|
|
||||||
if udp != 0 {
|
|
||||||
r.Set(enr.UDP(udp))
|
|
||||||
}
|
|
||||||
if tcp != 0 {
|
|
||||||
r.Set(enr.TCP(tcp))
|
|
||||||
}
|
|
||||||
signV4Compat(&r, pubkey)
|
|
||||||
n, err := New(v4CompatID{}, &r)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// isNewV4 returns true for nodes created by NewV4.
|
|
||||||
func isNewV4(n *Node) bool {
|
|
||||||
var k s256raw
|
|
||||||
return n.r.IdentityScheme() == "" && n.r.Load(&k) == nil && len(n.r.Signature()) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseComplete(rawurl string) (*Node, error) {
|
|
||||||
var (
|
|
||||||
id *ecdsa.PublicKey
|
|
||||||
tcpPort, udpPort uint64
|
|
||||||
)
|
|
||||||
u, err := url.Parse(rawurl)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if u.Scheme != "enode" {
|
|
||||||
return nil, errors.New("invalid URL scheme, want \"enode\"")
|
|
||||||
}
|
|
||||||
// Parse the Node ID from the user portion.
|
|
||||||
if u.User == nil {
|
|
||||||
return nil, errors.New("does not contain node ID")
|
|
||||||
}
|
|
||||||
if id, err = parsePubkey(u.User.String()); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid public key (%v)", err)
|
|
||||||
}
|
|
||||||
// Parse the IP address.
|
|
||||||
ip := net.ParseIP(u.Hostname())
|
|
||||||
if ip == nil {
|
|
||||||
ips, err := lookupIPFunc(u.Hostname())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ip = ips[0]
|
|
||||||
}
|
|
||||||
// Ensure the IP is 4 bytes long for IPv4 addresses.
|
|
||||||
if ipv4 := ip.To4(); ipv4 != nil {
|
|
||||||
ip = ipv4
|
|
||||||
}
|
|
||||||
// Parse the port numbers.
|
|
||||||
if tcpPort, err = strconv.ParseUint(u.Port(), 10, 16); err != nil {
|
|
||||||
return nil, errors.New("invalid port")
|
|
||||||
}
|
|
||||||
udpPort = tcpPort
|
|
||||||
qv := u.Query()
|
|
||||||
if qv.Get("discport") != "" {
|
|
||||||
udpPort, err = strconv.ParseUint(qv.Get("discport"), 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("invalid discport in query")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NewV4(id, ip, int(tcpPort), int(udpPort)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parsePubkey parses a hex-encoded secp256k1 public key.
|
|
||||||
func parsePubkey(in string) (*ecdsa.PublicKey, error) {
|
|
||||||
b, err := hex.DecodeString(in)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if len(b) != 64 {
|
|
||||||
return nil, fmt.Errorf("wrong length, want %d hex chars", 128)
|
|
||||||
}
|
|
||||||
b = append([]byte{0x4}, b...)
|
|
||||||
return crypto.UnmarshalPubkey(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *Node) URLv4() string {
|
|
||||||
var (
|
|
||||||
scheme enr.ID
|
|
||||||
nodeid string
|
|
||||||
key ecdsa.PublicKey
|
|
||||||
)
|
|
||||||
n.Load(&scheme)
|
|
||||||
n.Load((*Secp256k1)(&key))
|
|
||||||
switch {
|
|
||||||
case scheme == "v4" || key != ecdsa.PublicKey{}:
|
|
||||||
nodeid = fmt.Sprintf("%x", crypto.FromECDSAPub(&key)[1:])
|
|
||||||
default:
|
|
||||||
nodeid = fmt.Sprintf("%s.%x", scheme, n.id[:])
|
|
||||||
}
|
|
||||||
u := url.URL{Scheme: "enode"}
|
|
||||||
if n.Incomplete() {
|
|
||||||
u.Host = nodeid
|
|
||||||
} else {
|
|
||||||
addr := net.TCPAddr{IP: n.IP(), Port: n.TCP()}
|
|
||||||
u.User = url.User(nodeid)
|
|
||||||
u.Host = addr.String()
|
|
||||||
if n.UDP() != n.TCP() {
|
|
||||||
u.RawQuery = "discport=" + strconv.Itoa(n.UDP())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return u.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// PubkeyToIDV4 derives the v4 node address from the given public key.
|
|
||||||
func PubkeyToIDV4(key *ecdsa.PublicKey) ID {
|
|
||||||
e := make([]byte, 64)
|
|
||||||
math.ReadBits(key.X, e[:len(e)/2])
|
|
||||||
math.ReadBits(key.Y, e[len(e)/2:])
|
|
||||||
return ID(crypto.Keccak256Hash(e))
|
|
||||||
}
|
|
||||||
|
|
@ -1,200 +0,0 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package enode
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
lookupIPFunc = func(name string) ([]net.IP, error) {
|
|
||||||
if name == "node.example.org" {
|
|
||||||
return []net.IP{{33, 44, 55, 66}}, nil
|
|
||||||
}
|
|
||||||
return nil, errors.New("no such host")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var parseNodeTests = []struct {
|
|
||||||
input string
|
|
||||||
wantError string
|
|
||||||
wantResult *Node
|
|
||||||
}{
|
|
||||||
// Records
|
|
||||||
{
|
|
||||||
input: "enr:-IS4QGrdq0ugARp5T2BZ41TrZOqLc_oKvZoPuZP5--anqWE_J-Tucc1xgkOL7qXl0puJgT7qc2KSvcupc4NCb0nr4tdjgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQM6UUF2Rm-oFe1IH_rQkRCi00T2ybeMHRSvw1HDpRvjPYN1ZHCCdl8",
|
|
||||||
wantResult: func() *Node {
|
|
||||||
testKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
|
|
||||||
var r enr.Record
|
|
||||||
r.Set(enr.IP{127, 0, 0, 1})
|
|
||||||
r.Set(enr.UDP(30303))
|
|
||||||
r.SetSeq(99)
|
|
||||||
SignV4(&r, testKey)
|
|
||||||
n, _ := New(ValidSchemes, &r)
|
|
||||||
return n
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
// Invalid Records
|
|
||||||
{
|
|
||||||
input: "enr:",
|
|
||||||
wantError: "EOF", // could be nicer
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enr:x",
|
|
||||||
wantError: "illegal base64 data at input byte 0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enr:-EmGZm9vYmFyY4JpZIJ2NIJpcIR_AAABiXNlY3AyNTZrMaEDOlFBdkZvqBXtSB_60JEQotNE9sm3jB0Ur8NRw6Ub4z2DdWRwgnZf",
|
|
||||||
wantError: enr.ErrInvalidSig.Error(),
|
|
||||||
},
|
|
||||||
// Complete node URLs with IP address and ports
|
|
||||||
{
|
|
||||||
input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@invalid.:3",
|
|
||||||
wantError: `no such host`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:foo",
|
|
||||||
wantError: `invalid port`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:3?discport=foo",
|
|
||||||
wantError: `invalid discport in query`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150",
|
|
||||||
wantResult: NewV4(
|
|
||||||
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
|
|
||||||
net.IP{127, 0, 0, 1},
|
|
||||||
52150,
|
|
||||||
52150,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[::]:52150",
|
|
||||||
wantResult: NewV4(
|
|
||||||
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
|
|
||||||
net.ParseIP("::"),
|
|
||||||
52150,
|
|
||||||
52150,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150",
|
|
||||||
wantResult: NewV4(
|
|
||||||
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
|
|
||||||
net.ParseIP("2001:db8:3c4d:15::abcd:ef12"),
|
|
||||||
52150,
|
|
||||||
52150,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150?discport=22334",
|
|
||||||
wantResult: NewV4(
|
|
||||||
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
|
|
||||||
net.IP{0x7f, 0x0, 0x0, 0x1},
|
|
||||||
52150,
|
|
||||||
22334,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
// Incomplete node URLs with no address
|
|
||||||
{
|
|
||||||
input: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439",
|
|
||||||
wantResult: NewV4(
|
|
||||||
hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"),
|
|
||||||
nil, 0, 0,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
// Invalid URLs
|
|
||||||
{
|
|
||||||
input: "",
|
|
||||||
wantError: errMissingPrefix.Error(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439",
|
|
||||||
wantError: errMissingPrefix.Error(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "01010101",
|
|
||||||
wantError: errMissingPrefix.Error(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enode://01010101@123.124.125.126:3",
|
|
||||||
wantError: `invalid public key (wrong length, want 128 hex chars)`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "enode://01010101",
|
|
||||||
wantError: `invalid public key (wrong length, want 128 hex chars)`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "http://foobar",
|
|
||||||
wantError: errMissingPrefix.Error(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "://foo",
|
|
||||||
wantError: errMissingPrefix.Error(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func hexPubkey(h string) *ecdsa.PublicKey {
|
|
||||||
k, err := parsePubkey(h)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return k
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseNode(t *testing.T) {
|
|
||||||
for _, test := range parseNodeTests {
|
|
||||||
n, err := Parse(ValidSchemes, test.input)
|
|
||||||
if test.wantError != "" {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("test %q:\n got nil error, expected %#q", test.input, test.wantError)
|
|
||||||
continue
|
|
||||||
} else if !strings.Contains(err.Error(), test.wantError) {
|
|
||||||
t.Errorf("test %q:\n got error %#q, expected %#q", test.input, err.Error(), test.wantError)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("test %q:\n unexpected error: %v", test.input, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(n, test.wantResult) {
|
|
||||||
t.Errorf("test %q:\n result mismatch:\ngot: %#v\nwant: %#v", test.input, n, test.wantResult)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNodeString(t *testing.T) {
|
|
||||||
for i, test := range parseNodeTests {
|
|
||||||
if test.wantError == "" && strings.HasPrefix(test.input, "enode://") {
|
|
||||||
str := test.wantResult.String()
|
|
||||||
if str != test.input {
|
|
||||||
t.Errorf("test %d: Node.String() mismatch:\ngot: %s\nwant: %s", i, str, test.input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
335
p2p/enr/enr.go
335
p2p/enr/enr.go
|
|
@ -1,335 +0,0 @@
|
||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Package enr implements Ethereum Node Records as defined in EIP-778. A node record holds
|
|
||||||
// arbitrary information about a node on the peer-to-peer network. Node information is
|
|
||||||
// stored in key/value pairs. To store and retrieve key/values in a record, use the Entry
|
|
||||||
// interface.
|
|
||||||
//
|
|
||||||
// # Signature Handling
|
|
||||||
//
|
|
||||||
// Records must be signed before transmitting them to another node.
|
|
||||||
//
|
|
||||||
// Decoding a record doesn't check its signature. Code working with records from an
|
|
||||||
// untrusted source must always verify two things: that the record uses an identity scheme
|
|
||||||
// deemed secure, and that the signature is valid according to the declared scheme.
|
|
||||||
//
|
|
||||||
// When creating a record, set the entries you want and use a signing function provided by
|
|
||||||
// the identity scheme to add the signature. Modifying a record invalidates the signature.
|
|
||||||
//
|
|
||||||
// Package enr supports the "secp256k1-keccak" identity scheme.
|
|
||||||
package enr
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
const SizeLimit = 300 // maximum encoded size of a node record in bytes
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInvalidSig = errors.New("invalid signature on node record")
|
|
||||||
errNotSorted = errors.New("record key/value pairs are not sorted by key")
|
|
||||||
errDuplicateKey = errors.New("record contains duplicate key")
|
|
||||||
errIncompletePair = errors.New("record contains incomplete k/v pair")
|
|
||||||
errIncompleteList = errors.New("record contains less than two list elements")
|
|
||||||
errTooBig = fmt.Errorf("record bigger than %d bytes", SizeLimit)
|
|
||||||
errEncodeUnsigned = errors.New("can't encode unsigned record")
|
|
||||||
errNotFound = errors.New("no such key in record")
|
|
||||||
)
|
|
||||||
|
|
||||||
// An IdentityScheme is capable of verifying record signatures and
|
|
||||||
// deriving node addresses.
|
|
||||||
type IdentityScheme interface {
|
|
||||||
Verify(r *Record, sig []byte) error
|
|
||||||
NodeAddr(r *Record) []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// SchemeMap is a registry of named identity schemes.
|
|
||||||
type SchemeMap map[string]IdentityScheme
|
|
||||||
|
|
||||||
func (m SchemeMap) Verify(r *Record, sig []byte) error {
|
|
||||||
s := m[r.IdentityScheme()]
|
|
||||||
if s == nil {
|
|
||||||
return ErrInvalidSig
|
|
||||||
}
|
|
||||||
return s.Verify(r, sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m SchemeMap) NodeAddr(r *Record) []byte {
|
|
||||||
s := m[r.IdentityScheme()]
|
|
||||||
if s == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return s.NodeAddr(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record represents a node record. The zero value is an empty record.
|
|
||||||
type Record struct {
|
|
||||||
seq uint64 // sequence number
|
|
||||||
signature []byte // the signature
|
|
||||||
raw []byte // RLP encoded record
|
|
||||||
pairs []pair // sorted list of all key/value pairs
|
|
||||||
}
|
|
||||||
|
|
||||||
// pair is a key/value pair in a record.
|
|
||||||
type pair struct {
|
|
||||||
k string
|
|
||||||
v rlp.RawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the encoded size of the record.
|
|
||||||
func (r *Record) Size() uint64 {
|
|
||||||
if r.raw != nil {
|
|
||||||
return uint64(len(r.raw))
|
|
||||||
}
|
|
||||||
return computeSize(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func computeSize(r *Record) uint64 {
|
|
||||||
size := uint64(rlp.IntSize(r.seq))
|
|
||||||
size += rlp.BytesSize(r.signature)
|
|
||||||
for _, p := range r.pairs {
|
|
||||||
size += rlp.StringSize(p.k)
|
|
||||||
size += uint64(len(p.v))
|
|
||||||
}
|
|
||||||
return rlp.ListSize(size)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seq returns the sequence number.
|
|
||||||
func (r *Record) Seq() uint64 {
|
|
||||||
return r.seq
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSeq updates the record sequence number. This invalidates any signature on the record.
|
|
||||||
// Calling SetSeq is usually not required because setting any key in a signed record
|
|
||||||
// increments the sequence number.
|
|
||||||
func (r *Record) SetSeq(s uint64) {
|
|
||||||
r.signature = nil
|
|
||||||
r.raw = nil
|
|
||||||
r.seq = s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load retrieves the value of a key/value pair. The given Entry must be a pointer and will
|
|
||||||
// be set to the value of the entry in the record.
|
|
||||||
//
|
|
||||||
// Errors returned by Load are wrapped in KeyError. You can distinguish decoding errors
|
|
||||||
// from missing keys using the IsNotFound function.
|
|
||||||
func (r *Record) Load(e Entry) error {
|
|
||||||
i := sort.Search(len(r.pairs), func(i int) bool { return r.pairs[i].k >= e.ENRKey() })
|
|
||||||
if i < len(r.pairs) && r.pairs[i].k == e.ENRKey() {
|
|
||||||
if err := rlp.DecodeBytes(r.pairs[i].v, e); err != nil {
|
|
||||||
return &KeyError{Key: e.ENRKey(), Err: err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &KeyError{Key: e.ENRKey(), Err: errNotFound}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set adds or updates the given entry in the record. It panics if the value can't be
|
|
||||||
// encoded. If the record is signed, Set increments the sequence number and invalidates
|
|
||||||
// the sequence number.
|
|
||||||
func (r *Record) Set(e Entry) {
|
|
||||||
blob, err := rlp.EncodeToBytes(e)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("enr: can't encode %s: %v", e.ENRKey(), err))
|
|
||||||
}
|
|
||||||
r.invalidate()
|
|
||||||
|
|
||||||
pairs := make([]pair, len(r.pairs))
|
|
||||||
copy(pairs, r.pairs)
|
|
||||||
i := sort.Search(len(pairs), func(i int) bool { return pairs[i].k >= e.ENRKey() })
|
|
||||||
switch {
|
|
||||||
case i < len(pairs) && pairs[i].k == e.ENRKey():
|
|
||||||
// element is present at r.pairs[i]
|
|
||||||
pairs[i].v = blob
|
|
||||||
case i < len(r.pairs):
|
|
||||||
// insert pair before i-th elem
|
|
||||||
el := pair{e.ENRKey(), blob}
|
|
||||||
pairs = append(pairs, pair{})
|
|
||||||
copy(pairs[i+1:], pairs[i:])
|
|
||||||
pairs[i] = el
|
|
||||||
default:
|
|
||||||
// element should be placed at the end of r.pairs
|
|
||||||
pairs = append(pairs, pair{e.ENRKey(), blob})
|
|
||||||
}
|
|
||||||
r.pairs = pairs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Record) invalidate() {
|
|
||||||
if r.signature != nil {
|
|
||||||
r.seq++
|
|
||||||
}
|
|
||||||
r.signature = nil
|
|
||||||
r.raw = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signature returns the signature of the record.
|
|
||||||
func (r *Record) Signature() []byte {
|
|
||||||
if r.signature == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cpy := make([]byte, len(r.signature))
|
|
||||||
copy(cpy, r.signature)
|
|
||||||
return cpy
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodeRLP implements rlp.Encoder. Encoding fails if
|
|
||||||
// the record is unsigned.
|
|
||||||
func (r Record) EncodeRLP(w io.Writer) error {
|
|
||||||
if r.signature == nil {
|
|
||||||
return errEncodeUnsigned
|
|
||||||
}
|
|
||||||
_, err := w.Write(r.raw)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodeRLP implements rlp.Decoder. Decoding doesn't verify the signature.
|
|
||||||
func (r *Record) DecodeRLP(s *rlp.Stream) error {
|
|
||||||
dec, raw, err := decodeRecord(s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*r = dec
|
|
||||||
r.raw = raw
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeRecord(s *rlp.Stream) (dec Record, raw []byte, err error) {
|
|
||||||
raw, err = s.Raw()
|
|
||||||
if err != nil {
|
|
||||||
return dec, raw, err
|
|
||||||
}
|
|
||||||
if len(raw) > SizeLimit {
|
|
||||||
return dec, raw, errTooBig
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode the RLP container.
|
|
||||||
s = rlp.NewStream(bytes.NewReader(raw), 0)
|
|
||||||
if _, err := s.List(); err != nil {
|
|
||||||
return dec, raw, err
|
|
||||||
}
|
|
||||||
if err = s.Decode(&dec.signature); err != nil {
|
|
||||||
if err == rlp.EOL {
|
|
||||||
err = errIncompleteList
|
|
||||||
}
|
|
||||||
return dec, raw, err
|
|
||||||
}
|
|
||||||
if err = s.Decode(&dec.seq); err != nil {
|
|
||||||
if err == rlp.EOL {
|
|
||||||
err = errIncompleteList
|
|
||||||
}
|
|
||||||
return dec, raw, err
|
|
||||||
}
|
|
||||||
// The rest of the record contains sorted k/v pairs.
|
|
||||||
var prevkey string
|
|
||||||
for i := 0; ; i++ {
|
|
||||||
var kv pair
|
|
||||||
if err := s.Decode(&kv.k); err != nil {
|
|
||||||
if err == rlp.EOL {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return dec, raw, err
|
|
||||||
}
|
|
||||||
if err := s.Decode(&kv.v); err != nil {
|
|
||||||
if err == rlp.EOL {
|
|
||||||
return dec, raw, errIncompletePair
|
|
||||||
}
|
|
||||||
return dec, raw, err
|
|
||||||
}
|
|
||||||
if i > 0 {
|
|
||||||
if kv.k == prevkey {
|
|
||||||
return dec, raw, errDuplicateKey
|
|
||||||
}
|
|
||||||
if kv.k < prevkey {
|
|
||||||
return dec, raw, errNotSorted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dec.pairs = append(dec.pairs, kv)
|
|
||||||
prevkey = kv.k
|
|
||||||
}
|
|
||||||
return dec, raw, s.ListEnd()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IdentityScheme returns the name of the identity scheme in the record.
|
|
||||||
func (r *Record) IdentityScheme() string {
|
|
||||||
var id ID
|
|
||||||
r.Load(&id)
|
|
||||||
return string(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifySignature checks whether the record is signed using the given identity scheme.
|
|
||||||
func (r *Record) VerifySignature(s IdentityScheme) error {
|
|
||||||
return s.Verify(r, r.signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSig sets the record signature. It returns an error if the encoded record is larger
|
|
||||||
// than the size limit or if the signature is invalid according to the passed scheme.
|
|
||||||
//
|
|
||||||
// You can also use SetSig to remove the signature explicitly by passing a nil scheme
|
|
||||||
// and signature.
|
|
||||||
//
|
|
||||||
// SetSig panics when either the scheme or the signature (but not both) are nil.
|
|
||||||
func (r *Record) SetSig(s IdentityScheme, sig []byte) error {
|
|
||||||
switch {
|
|
||||||
// Prevent storing invalid data.
|
|
||||||
case s == nil && sig != nil:
|
|
||||||
panic("enr: invalid call to SetSig with non-nil signature but nil scheme")
|
|
||||||
case s != nil && sig == nil:
|
|
||||||
panic("enr: invalid call to SetSig with nil signature but non-nil scheme")
|
|
||||||
// Verify if we have a scheme.
|
|
||||||
case s != nil:
|
|
||||||
if err := s.Verify(r, sig); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
raw, err := r.encode(sig)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
r.signature, r.raw = sig, raw
|
|
||||||
// Reset otherwise.
|
|
||||||
default:
|
|
||||||
r.signature, r.raw = nil, nil
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendElements appends the sequence number and entries to the given slice.
|
|
||||||
func (r *Record) AppendElements(list []interface{}) []interface{} {
|
|
||||||
list = append(list, r.seq)
|
|
||||||
for _, p := range r.pairs {
|
|
||||||
list = append(list, p.k, p.v)
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Record) encode(sig []byte) (raw []byte, err error) {
|
|
||||||
list := make([]interface{}, 1, 2*len(r.pairs)+2)
|
|
||||||
list[0] = sig
|
|
||||||
list = r.AppendElements(list)
|
|
||||||
if raw, err = rlp.EncodeToBytes(list); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(raw) > SizeLimit {
|
|
||||||
return nil, errTooBig
|
|
||||||
}
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,348 +0,0 @@
|
||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package enr
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
var rnd = rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
||||||
|
|
||||||
func randomString(strlen int) string {
|
|
||||||
b := make([]byte, strlen)
|
|
||||||
rnd.Read(b)
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGetSetID tests encoding/decoding and setting/getting of the ID key.
|
|
||||||
func TestGetSetID(t *testing.T) {
|
|
||||||
id := ID("someid")
|
|
||||||
var r Record
|
|
||||||
r.Set(id)
|
|
||||||
|
|
||||||
var id2 ID
|
|
||||||
require.NoError(t, r.Load(&id2))
|
|
||||||
assert.Equal(t, id, id2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGetSetIP4 tests encoding/decoding and setting/getting of the IP key.
|
|
||||||
func TestGetSetIPv4(t *testing.T) {
|
|
||||||
ip := IPv4{192, 168, 0, 3}
|
|
||||||
var r Record
|
|
||||||
r.Set(ip)
|
|
||||||
|
|
||||||
var ip2 IPv4
|
|
||||||
require.NoError(t, r.Load(&ip2))
|
|
||||||
assert.Equal(t, ip, ip2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGetSetIP6 tests encoding/decoding and setting/getting of the IP6 key.
|
|
||||||
func TestGetSetIPv6(t *testing.T) {
|
|
||||||
ip := IPv6{0x20, 0x01, 0x48, 0x60, 0, 0, 0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x00, 0x68}
|
|
||||||
var r Record
|
|
||||||
r.Set(ip)
|
|
||||||
|
|
||||||
var ip2 IPv6
|
|
||||||
require.NoError(t, r.Load(&ip2))
|
|
||||||
assert.Equal(t, ip, ip2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGetSetUDP tests encoding/decoding and setting/getting of the UDP key.
|
|
||||||
func TestGetSetUDP(t *testing.T) {
|
|
||||||
port := UDP(30309)
|
|
||||||
var r Record
|
|
||||||
r.Set(port)
|
|
||||||
|
|
||||||
var port2 UDP
|
|
||||||
require.NoError(t, r.Load(&port2))
|
|
||||||
assert.Equal(t, port, port2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoadErrors(t *testing.T) {
|
|
||||||
var r Record
|
|
||||||
ip4 := IPv4{127, 0, 0, 1}
|
|
||||||
r.Set(ip4)
|
|
||||||
|
|
||||||
// Check error for missing keys.
|
|
||||||
var udp UDP
|
|
||||||
err := r.Load(&udp)
|
|
||||||
if !IsNotFound(err) {
|
|
||||||
t.Error("IsNotFound should return true for missing key")
|
|
||||||
}
|
|
||||||
assert.Equal(t, &KeyError{Key: udp.ENRKey(), Err: errNotFound}, err)
|
|
||||||
|
|
||||||
// Check error for invalid keys.
|
|
||||||
var list []uint
|
|
||||||
err = r.Load(WithEntry(ip4.ENRKey(), &list))
|
|
||||||
kerr, ok := err.(*KeyError)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected KeyError, got %T", err)
|
|
||||||
}
|
|
||||||
assert.Equal(t, kerr.Key, ip4.ENRKey())
|
|
||||||
assert.Error(t, kerr.Err)
|
|
||||||
if IsNotFound(err) {
|
|
||||||
t.Error("IsNotFound should return false for decoding errors")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSortedGetAndSet tests that Set produced a sorted pairs slice.
|
|
||||||
func TestSortedGetAndSet(t *testing.T) {
|
|
||||||
type pair struct {
|
|
||||||
k string
|
|
||||||
v uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range []struct {
|
|
||||||
input []pair
|
|
||||||
want []pair
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
input: []pair{{"a", 1}, {"c", 2}, {"b", 3}},
|
|
||||||
want: []pair{{"a", 1}, {"b", 3}, {"c", 2}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: []pair{{"a", 1}, {"c", 2}, {"b", 3}, {"d", 4}, {"a", 5}, {"bb", 6}},
|
|
||||||
want: []pair{{"a", 5}, {"b", 3}, {"bb", 6}, {"c", 2}, {"d", 4}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: []pair{{"c", 2}, {"b", 3}, {"d", 4}, {"a", 5}, {"bb", 6}},
|
|
||||||
want: []pair{{"a", 5}, {"b", 3}, {"bb", 6}, {"c", 2}, {"d", 4}},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
var r Record
|
|
||||||
for _, i := range tt.input {
|
|
||||||
r.Set(WithEntry(i.k, &i.v))
|
|
||||||
}
|
|
||||||
for i, w := range tt.want {
|
|
||||||
// set got's key from r.pair[i], so that we preserve order of pairs
|
|
||||||
got := pair{k: r.pairs[i].k}
|
|
||||||
assert.NoError(t, r.Load(WithEntry(w.k, &got.v)))
|
|
||||||
assert.Equal(t, w, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDirty tests record signature removal on setting of new key/value pair in record.
|
|
||||||
func TestDirty(t *testing.T) {
|
|
||||||
var r Record
|
|
||||||
|
|
||||||
if _, err := rlp.EncodeToBytes(r); err != errEncodeUnsigned {
|
|
||||||
t.Errorf("expected errEncodeUnsigned, got %#v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, signTest([]byte{5}, &r))
|
|
||||||
if len(r.signature) == 0 {
|
|
||||||
t.Error("record is not signed")
|
|
||||||
}
|
|
||||||
_, err := rlp.EncodeToBytes(r)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
r.SetSeq(3)
|
|
||||||
if len(r.signature) != 0 {
|
|
||||||
t.Error("signature still set after modification")
|
|
||||||
}
|
|
||||||
if _, err := rlp.EncodeToBytes(r); err != errEncodeUnsigned {
|
|
||||||
t.Errorf("expected errEncodeUnsigned, got %#v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSize(t *testing.T) {
|
|
||||||
var r Record
|
|
||||||
|
|
||||||
// Empty record size is 3 bytes.
|
|
||||||
// Unsigned records cannot be encoded, but they could, the encoding
|
|
||||||
// would be [ 0, 0 ] -> 0xC28080.
|
|
||||||
assert.Equal(t, uint64(3), r.Size())
|
|
||||||
|
|
||||||
// Add one attribute. The size increases to 5, the encoding
|
|
||||||
// would be [ 0, 0, "k", "v" ] -> 0xC58080C26B76.
|
|
||||||
r.Set(WithEntry("k", "v"))
|
|
||||||
assert.Equal(t, uint64(5), r.Size())
|
|
||||||
|
|
||||||
// Now add a signature.
|
|
||||||
nodeid := []byte{1, 2, 3, 4, 5, 6, 7, 8}
|
|
||||||
signTest(nodeid, &r)
|
|
||||||
assert.Equal(t, uint64(45), r.Size())
|
|
||||||
enc, _ := rlp.EncodeToBytes(&r)
|
|
||||||
if r.Size() != uint64(len(enc)) {
|
|
||||||
t.Error("Size() not equal encoded length", len(enc))
|
|
||||||
}
|
|
||||||
if r.Size() != computeSize(&r) {
|
|
||||||
t.Error("Size() not equal computed size", computeSize(&r))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSeq(t *testing.T) {
|
|
||||||
var r Record
|
|
||||||
|
|
||||||
assert.Equal(t, uint64(0), r.Seq())
|
|
||||||
r.Set(UDP(1))
|
|
||||||
assert.Equal(t, uint64(0), r.Seq())
|
|
||||||
signTest([]byte{5}, &r)
|
|
||||||
assert.Equal(t, uint64(0), r.Seq())
|
|
||||||
r.Set(UDP(2))
|
|
||||||
assert.Equal(t, uint64(1), r.Seq())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGetSetOverwrite tests value overwrite when setting a new value with an existing key in record.
|
|
||||||
func TestGetSetOverwrite(t *testing.T) {
|
|
||||||
var r Record
|
|
||||||
|
|
||||||
ip := IPv4{192, 168, 0, 3}
|
|
||||||
r.Set(ip)
|
|
||||||
|
|
||||||
ip2 := IPv4{192, 168, 0, 4}
|
|
||||||
r.Set(ip2)
|
|
||||||
|
|
||||||
var ip3 IPv4
|
|
||||||
require.NoError(t, r.Load(&ip3))
|
|
||||||
assert.Equal(t, ip2, ip3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSignEncodeAndDecode tests signing, RLP encoding and RLP decoding of a record.
|
|
||||||
func TestSignEncodeAndDecode(t *testing.T) {
|
|
||||||
var r Record
|
|
||||||
r.Set(UDP(30303))
|
|
||||||
r.Set(IPv4{127, 0, 0, 1})
|
|
||||||
require.NoError(t, signTest([]byte{5}, &r))
|
|
||||||
|
|
||||||
blob, err := rlp.EncodeToBytes(r)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var r2 Record
|
|
||||||
require.NoError(t, rlp.DecodeBytes(blob, &r2))
|
|
||||||
assert.Equal(t, r, r2)
|
|
||||||
|
|
||||||
blob2, err := rlp.EncodeToBytes(r2)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, blob, blob2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRecordTooBig tests that records bigger than SizeLimit bytes cannot be signed.
|
|
||||||
func TestRecordTooBig(t *testing.T) {
|
|
||||||
var r Record
|
|
||||||
key := randomString(10)
|
|
||||||
|
|
||||||
// set a big value for random key, expect error
|
|
||||||
r.Set(WithEntry(key, randomString(SizeLimit)))
|
|
||||||
if err := signTest([]byte{5}, &r); err != errTooBig {
|
|
||||||
t.Fatalf("expected to get errTooBig, got %#v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set an acceptable value for random key, expect no error
|
|
||||||
r.Set(WithEntry(key, randomString(100)))
|
|
||||||
require.NoError(t, signTest([]byte{5}, &r))
|
|
||||||
}
|
|
||||||
|
|
||||||
// This checks that incomplete RLP inputs are handled correctly.
|
|
||||||
func TestDecodeIncomplete(t *testing.T) {
|
|
||||||
type decTest struct {
|
|
||||||
input []byte
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
tests := []decTest{
|
|
||||||
{[]byte{0xC0}, errIncompleteList},
|
|
||||||
{[]byte{0xC1, 0x1}, errIncompleteList},
|
|
||||||
{[]byte{0xC2, 0x1, 0x2}, nil},
|
|
||||||
{[]byte{0xC3, 0x1, 0x2, 0x3}, errIncompletePair},
|
|
||||||
{[]byte{0xC4, 0x1, 0x2, 0x3, 0x4}, nil},
|
|
||||||
{[]byte{0xC5, 0x1, 0x2, 0x3, 0x4, 0x5}, errIncompletePair},
|
|
||||||
}
|
|
||||||
for _, test := range tests {
|
|
||||||
var r Record
|
|
||||||
err := rlp.DecodeBytes(test.input, &r)
|
|
||||||
if err != test.err {
|
|
||||||
t.Errorf("wrong error for %X: %v", test.input, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSignEncodeAndDecodeRandom tests encoding/decoding of records containing random key/value pairs.
|
|
||||||
func TestSignEncodeAndDecodeRandom(t *testing.T) {
|
|
||||||
var r Record
|
|
||||||
|
|
||||||
// random key/value pairs for testing
|
|
||||||
pairs := map[string]uint32{}
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
key := randomString(7)
|
|
||||||
value := rnd.Uint32()
|
|
||||||
pairs[key] = value
|
|
||||||
r.Set(WithEntry(key, &value))
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, signTest([]byte{5}, &r))
|
|
||||||
|
|
||||||
enc, err := rlp.EncodeToBytes(r)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, uint64(len(enc)), r.Size())
|
|
||||||
require.Equal(t, uint64(len(enc)), computeSize(&r))
|
|
||||||
|
|
||||||
for k, v := range pairs {
|
|
||||||
desc := fmt.Sprintf("key %q", k)
|
|
||||||
var got uint32
|
|
||||||
buf := WithEntry(k, &got)
|
|
||||||
require.NoError(t, r.Load(buf), desc)
|
|
||||||
require.Equal(t, v, got, desc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type testSig struct{}
|
|
||||||
|
|
||||||
type testID []byte
|
|
||||||
|
|
||||||
func (id testID) ENRKey() string { return "testid" }
|
|
||||||
|
|
||||||
func signTest(id []byte, r *Record) error {
|
|
||||||
r.Set(ID("test"))
|
|
||||||
r.Set(testID(id))
|
|
||||||
return r.SetSig(testSig{}, makeTestSig(id, r.Seq()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeTestSig(id []byte, seq uint64) []byte {
|
|
||||||
sig := make([]byte, 8, len(id)+8)
|
|
||||||
binary.BigEndian.PutUint64(sig[:8], seq)
|
|
||||||
sig = append(sig, id...)
|
|
||||||
return sig
|
|
||||||
}
|
|
||||||
|
|
||||||
func (testSig) Verify(r *Record, sig []byte) error {
|
|
||||||
var id []byte
|
|
||||||
if err := r.Load((*testID)(&id)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !bytes.Equal(sig, makeTestSig(id, r.Seq())) {
|
|
||||||
return ErrInvalidSig
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (testSig) NodeAddr(r *Record) []byte {
|
|
||||||
var id []byte
|
|
||||||
if err := r.Load((*testID)(&id)); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
@ -1,196 +0,0 @@
|
||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package enr
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Entry is implemented by known node record entry types.
|
|
||||||
//
|
|
||||||
// To define a new entry that is to be included in a node record,
|
|
||||||
// create a Go type that satisfies this interface. The type should
|
|
||||||
// also implement rlp.Decoder if additional checks are needed on the value.
|
|
||||||
type Entry interface {
|
|
||||||
ENRKey() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type generic struct {
|
|
||||||
key string
|
|
||||||
value interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g generic) ENRKey() string { return g.key }
|
|
||||||
|
|
||||||
func (g generic) EncodeRLP(w io.Writer) error {
|
|
||||||
return rlp.Encode(w, g.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *generic) DecodeRLP(s *rlp.Stream) error {
|
|
||||||
return s.Decode(g.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithEntry wraps any value with a key name. It can be used to set and load arbitrary values
|
|
||||||
// in a record. The value v must be supported by rlp. To use WithEntry with Load, the value
|
|
||||||
// must be a pointer.
|
|
||||||
func WithEntry(k string, v interface{}) Entry {
|
|
||||||
return &generic{key: k, value: v}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TCP is the "tcp" key, which holds the TCP port of the node.
|
|
||||||
type TCP uint16
|
|
||||||
|
|
||||||
func (v TCP) ENRKey() string { return "tcp" }
|
|
||||||
|
|
||||||
// TCP6 is the "tcp6" key, which holds the IPv6-specific tcp6 port of the node.
|
|
||||||
type TCP6 uint16
|
|
||||||
|
|
||||||
func (v TCP6) ENRKey() string { return "tcp6" }
|
|
||||||
|
|
||||||
// UDP is the "udp" key, which holds the UDP port of the node.
|
|
||||||
type UDP uint16
|
|
||||||
|
|
||||||
func (v UDP) ENRKey() string { return "udp" }
|
|
||||||
|
|
||||||
// UDP6 is the "udp6" key, which holds the IPv6-specific UDP port of the node.
|
|
||||||
type UDP6 uint16
|
|
||||||
|
|
||||||
func (v UDP6) ENRKey() string { return "udp6" }
|
|
||||||
|
|
||||||
// ID is the "id" key, which holds the name of the identity scheme.
|
|
||||||
type ID string
|
|
||||||
|
|
||||||
const IDv4 = ID("v4") // the default identity scheme
|
|
||||||
|
|
||||||
func (v ID) ENRKey() string { return "id" }
|
|
||||||
|
|
||||||
// IP is either the "ip" or "ip6" key, depending on the value.
|
|
||||||
// Use this value to encode IP addresses that can be either v4 or v6.
|
|
||||||
// To load an address from a record use the IPv4 or IPv6 types.
|
|
||||||
type IP net.IP
|
|
||||||
|
|
||||||
func (v IP) ENRKey() string {
|
|
||||||
if net.IP(v).To4() == nil {
|
|
||||||
return "ip6"
|
|
||||||
}
|
|
||||||
return "ip"
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodeRLP implements rlp.Encoder.
|
|
||||||
func (v IP) EncodeRLP(w io.Writer) error {
|
|
||||||
if ip4 := net.IP(v).To4(); ip4 != nil {
|
|
||||||
return rlp.Encode(w, ip4)
|
|
||||||
}
|
|
||||||
if ip6 := net.IP(v).To16(); ip6 != nil {
|
|
||||||
return rlp.Encode(w, ip6)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("invalid IP address: %v", net.IP(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodeRLP implements rlp.Decoder.
|
|
||||||
func (v *IP) DecodeRLP(s *rlp.Stream) error {
|
|
||||||
if err := s.Decode((*net.IP)(v)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(*v) != 4 && len(*v) != 16 {
|
|
||||||
return fmt.Errorf("invalid IP address, want 4 or 16 bytes: %v", *v)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPv4 is the "ip" key, which holds the IP address of the node.
|
|
||||||
type IPv4 net.IP
|
|
||||||
|
|
||||||
func (v IPv4) ENRKey() string { return "ip" }
|
|
||||||
|
|
||||||
// EncodeRLP implements rlp.Encoder.
|
|
||||||
func (v IPv4) EncodeRLP(w io.Writer) error {
|
|
||||||
ip4 := net.IP(v).To4()
|
|
||||||
if ip4 == nil {
|
|
||||||
return fmt.Errorf("invalid IPv4 address: %v", net.IP(v))
|
|
||||||
}
|
|
||||||
return rlp.Encode(w, ip4)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodeRLP implements rlp.Decoder.
|
|
||||||
func (v *IPv4) DecodeRLP(s *rlp.Stream) error {
|
|
||||||
if err := s.Decode((*net.IP)(v)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(*v) != 4 {
|
|
||||||
return fmt.Errorf("invalid IPv4 address, want 4 bytes: %v", *v)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPv6 is the "ip6" key, which holds the IP address of the node.
|
|
||||||
type IPv6 net.IP
|
|
||||||
|
|
||||||
func (v IPv6) ENRKey() string { return "ip6" }
|
|
||||||
|
|
||||||
// EncodeRLP implements rlp.Encoder.
|
|
||||||
func (v IPv6) EncodeRLP(w io.Writer) error {
|
|
||||||
ip6 := net.IP(v).To16()
|
|
||||||
if ip6 == nil {
|
|
||||||
return fmt.Errorf("invalid IPv6 address: %v", net.IP(v))
|
|
||||||
}
|
|
||||||
return rlp.Encode(w, ip6)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodeRLP implements rlp.Decoder.
|
|
||||||
func (v *IPv6) DecodeRLP(s *rlp.Stream) error {
|
|
||||||
if err := s.Decode((*net.IP)(v)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(*v) != 16 {
|
|
||||||
return fmt.Errorf("invalid IPv6 address, want 16 bytes: %v", *v)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// KeyError is an error related to a key.
|
|
||||||
type KeyError struct {
|
|
||||||
Key string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error implements error.
|
|
||||||
func (err *KeyError) Error() string {
|
|
||||||
if err.Err == errNotFound {
|
|
||||||
return fmt.Sprintf("missing ENR key %q", err.Key)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("ENR key %q: %v", err.Key, err.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err *KeyError) Unwrap() error {
|
|
||||||
return err.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsNotFound reports whether the given error means that a key/value pair is
|
|
||||||
// missing from a record.
|
|
||||||
func IsNotFound(err error) bool {
|
|
||||||
var ke *KeyError
|
|
||||||
if errors.As(err, &ke) {
|
|
||||||
return ke.Err == errNotFound
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
325
p2p/message.go
325
p2p/message.go
|
|
@ -1,325 +0,0 @@
|
||||||
// Copyright 2014 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package p2p
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/event"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Msg defines the structure of a p2p message.
|
|
||||||
//
|
|
||||||
// Note that a Msg can only be sent once since the Payload reader is
|
|
||||||
// consumed during sending. It is not possible to create a Msg and
|
|
||||||
// send it any number of times. If you want to reuse an encoded
|
|
||||||
// structure, encode the payload into a byte array and create a
|
|
||||||
// separate Msg with a bytes.Reader as Payload for each send.
|
|
||||||
type Msg struct {
|
|
||||||
Code uint64
|
|
||||||
Size uint32 // Size of the raw payload
|
|
||||||
Payload io.Reader
|
|
||||||
ReceivedAt time.Time
|
|
||||||
|
|
||||||
meterCap Cap // Protocol name and version for egress metering
|
|
||||||
meterCode uint64 // Message within protocol for egress metering
|
|
||||||
meterSize uint32 // Compressed message size for ingress metering
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode parses the RLP content of a message into
|
|
||||||
// the given value, which must be a pointer.
|
|
||||||
//
|
|
||||||
// For the decoding rules, please see package rlp.
|
|
||||||
func (msg Msg) Decode(val interface{}) error {
|
|
||||||
s := rlp.NewStream(msg.Payload, uint64(msg.Size))
|
|
||||||
if err := s.Decode(val); err != nil {
|
|
||||||
return newPeerError(errInvalidMsg, "(code %x) (size %d) %v", msg.Code, msg.Size, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msg Msg) String() string {
|
|
||||||
return fmt.Sprintf("msg #%v (%v bytes)", msg.Code, msg.Size)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discard reads any remaining payload data into a black hole.
|
|
||||||
func (msg Msg) Discard() error {
|
|
||||||
_, err := io.Copy(io.Discard, msg.Payload)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (msg Msg) Time() time.Time {
|
|
||||||
return msg.ReceivedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
type MsgReader interface {
|
|
||||||
ReadMsg() (Msg, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type MsgWriter interface {
|
|
||||||
// WriteMsg sends a message. It will block until the message's
|
|
||||||
// Payload has been consumed by the other end.
|
|
||||||
//
|
|
||||||
// Note that messages can be sent only once because their
|
|
||||||
// payload reader is drained.
|
|
||||||
WriteMsg(Msg) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// MsgReadWriter provides reading and writing of encoded messages.
|
|
||||||
// Implementations should ensure that ReadMsg and WriteMsg can be
|
|
||||||
// called simultaneously from multiple goroutines.
|
|
||||||
type MsgReadWriter interface {
|
|
||||||
MsgReader
|
|
||||||
MsgWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send writes an RLP-encoded message with the given code.
|
|
||||||
// data should encode as an RLP list.
|
|
||||||
func Send(w MsgWriter, msgcode uint64, data interface{}) error {
|
|
||||||
size, r, err := rlp.EncodeToReader(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return w.WriteMsg(Msg{Code: msgcode, Size: uint32(size), Payload: r})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendItems writes an RLP with the given code and data elements.
|
|
||||||
// For a call such as:
|
|
||||||
//
|
|
||||||
// SendItems(w, code, e1, e2, e3)
|
|
||||||
//
|
|
||||||
// the message payload will be an RLP list containing the items:
|
|
||||||
//
|
|
||||||
// [e1, e2, e3]
|
|
||||||
func SendItems(w MsgWriter, msgcode uint64, elems ...interface{}) error {
|
|
||||||
return Send(w, msgcode, elems)
|
|
||||||
}
|
|
||||||
|
|
||||||
// eofSignal wraps a reader with eof signaling. the eof channel is
|
|
||||||
// closed when the wrapped reader returns an error or when count bytes
|
|
||||||
// have been read.
|
|
||||||
type eofSignal struct {
|
|
||||||
wrapped io.Reader
|
|
||||||
count uint32 // number of bytes left
|
|
||||||
eof chan<- struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// note: when using eofSignal to detect whether a message payload
|
|
||||||
// has been read, Read might not be called for zero sized messages.
|
|
||||||
func (r *eofSignal) Read(buf []byte) (int, error) {
|
|
||||||
if r.count == 0 {
|
|
||||||
if r.eof != nil {
|
|
||||||
r.eof <- struct{}{}
|
|
||||||
r.eof = nil
|
|
||||||
}
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
max := len(buf)
|
|
||||||
if int(r.count) < len(buf) {
|
|
||||||
max = int(r.count)
|
|
||||||
}
|
|
||||||
n, err := r.wrapped.Read(buf[:max])
|
|
||||||
r.count -= uint32(n)
|
|
||||||
if (err != nil || r.count == 0) && r.eof != nil {
|
|
||||||
r.eof <- struct{}{} // tell Peer that msg has been consumed
|
|
||||||
r.eof = nil
|
|
||||||
}
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// MsgPipe creates a message pipe. Reads on one end are matched
|
|
||||||
// with writes on the other. The pipe is full-duplex, both ends
|
|
||||||
// implement MsgReadWriter.
|
|
||||||
func MsgPipe() (*MsgPipeRW, *MsgPipeRW) {
|
|
||||||
var (
|
|
||||||
c1, c2 = make(chan Msg), make(chan Msg)
|
|
||||||
closing = make(chan struct{})
|
|
||||||
closed = new(atomic.Bool)
|
|
||||||
rw1 = &MsgPipeRW{c1, c2, closing, closed}
|
|
||||||
rw2 = &MsgPipeRW{c2, c1, closing, closed}
|
|
||||||
)
|
|
||||||
return rw1, rw2
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrPipeClosed is returned from pipe operations after the
|
|
||||||
// pipe has been closed.
|
|
||||||
var ErrPipeClosed = errors.New("p2p: read or write on closed message pipe")
|
|
||||||
|
|
||||||
// MsgPipeRW is an endpoint of a MsgReadWriter pipe.
|
|
||||||
type MsgPipeRW struct {
|
|
||||||
w chan<- Msg
|
|
||||||
r <-chan Msg
|
|
||||||
closing chan struct{}
|
|
||||||
closed *atomic.Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteMsg sends a message on the pipe.
|
|
||||||
// It blocks until the receiver has consumed the message payload.
|
|
||||||
func (p *MsgPipeRW) WriteMsg(msg Msg) error {
|
|
||||||
if !p.closed.Load() {
|
|
||||||
consumed := make(chan struct{}, 1)
|
|
||||||
msg.Payload = &eofSignal{msg.Payload, msg.Size, consumed}
|
|
||||||
select {
|
|
||||||
case p.w <- msg:
|
|
||||||
if msg.Size > 0 {
|
|
||||||
// wait for payload read or discard
|
|
||||||
select {
|
|
||||||
case <-consumed:
|
|
||||||
case <-p.closing:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case <-p.closing:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ErrPipeClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadMsg returns a message sent on the other end of the pipe.
|
|
||||||
func (p *MsgPipeRW) ReadMsg() (Msg, error) {
|
|
||||||
if !p.closed.Load() {
|
|
||||||
select {
|
|
||||||
case msg := <-p.r:
|
|
||||||
return msg, nil
|
|
||||||
case <-p.closing:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Msg{}, ErrPipeClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close unblocks any pending ReadMsg and WriteMsg calls on both ends
|
|
||||||
// of the pipe. They will return ErrPipeClosed. Close also
|
|
||||||
// interrupts any reads from a message payload.
|
|
||||||
func (p *MsgPipeRW) Close() error {
|
|
||||||
if p.closed.Swap(true) {
|
|
||||||
// someone else is already closing
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
close(p.closing)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExpectMsg reads a message from r and verifies that its
|
|
||||||
// code and encoded RLP content match the provided values.
|
|
||||||
// If content is nil, the payload is discarded and not verified.
|
|
||||||
func ExpectMsg(r MsgReader, code uint64, content interface{}) error {
|
|
||||||
msg, err := r.ReadMsg()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if msg.Code != code {
|
|
||||||
return fmt.Errorf("message code mismatch: got %d, expected %d", msg.Code, code)
|
|
||||||
}
|
|
||||||
if content == nil {
|
|
||||||
return msg.Discard()
|
|
||||||
}
|
|
||||||
contentEnc, err := rlp.EncodeToBytes(content)
|
|
||||||
if err != nil {
|
|
||||||
panic("content encode error: " + err.Error())
|
|
||||||
}
|
|
||||||
if int(msg.Size) != len(contentEnc) {
|
|
||||||
return fmt.Errorf("message size mismatch: got %d, want %d", msg.Size, len(contentEnc))
|
|
||||||
}
|
|
||||||
actualContent, err := io.ReadAll(msg.Payload)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !bytes.Equal(actualContent, contentEnc) {
|
|
||||||
return fmt.Errorf("message payload mismatch:\ngot: %x\nwant: %x", actualContent, contentEnc)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// msgEventer wraps a MsgReadWriter and sends events whenever a message is sent
|
|
||||||
// or received
|
|
||||||
type msgEventer struct {
|
|
||||||
MsgReadWriter
|
|
||||||
|
|
||||||
feed *event.Feed
|
|
||||||
peerID enode.ID
|
|
||||||
Protocol string
|
|
||||||
localAddress string
|
|
||||||
remoteAddress string
|
|
||||||
}
|
|
||||||
|
|
||||||
// newMsgEventer returns a msgEventer which sends message events to the given
|
|
||||||
// feed
|
|
||||||
func newMsgEventer(rw MsgReadWriter, feed *event.Feed, peerID enode.ID, proto, remote, local string) *msgEventer {
|
|
||||||
return &msgEventer{
|
|
||||||
MsgReadWriter: rw,
|
|
||||||
feed: feed,
|
|
||||||
peerID: peerID,
|
|
||||||
Protocol: proto,
|
|
||||||
remoteAddress: remote,
|
|
||||||
localAddress: local,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadMsg reads a message from the underlying MsgReadWriter and emits a
|
|
||||||
// "message received" event
|
|
||||||
func (ev *msgEventer) ReadMsg() (Msg, error) {
|
|
||||||
msg, err := ev.MsgReadWriter.ReadMsg()
|
|
||||||
if err != nil {
|
|
||||||
return msg, err
|
|
||||||
}
|
|
||||||
ev.feed.Send(&PeerEvent{
|
|
||||||
Type: PeerEventTypeMsgRecv,
|
|
||||||
Peer: ev.peerID,
|
|
||||||
Protocol: ev.Protocol,
|
|
||||||
MsgCode: &msg.Code,
|
|
||||||
MsgSize: &msg.Size,
|
|
||||||
LocalAddress: ev.localAddress,
|
|
||||||
RemoteAddress: ev.remoteAddress,
|
|
||||||
})
|
|
||||||
return msg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteMsg writes a message to the underlying MsgReadWriter and emits a
|
|
||||||
// "message sent" event
|
|
||||||
func (ev *msgEventer) WriteMsg(msg Msg) error {
|
|
||||||
err := ev.MsgReadWriter.WriteMsg(msg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ev.feed.Send(&PeerEvent{
|
|
||||||
Type: PeerEventTypeMsgSend,
|
|
||||||
Peer: ev.peerID,
|
|
||||||
Protocol: ev.Protocol,
|
|
||||||
MsgCode: &msg.Code,
|
|
||||||
MsgSize: &msg.Size,
|
|
||||||
LocalAddress: ev.localAddress,
|
|
||||||
RemoteAddress: ev.remoteAddress,
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the underlying MsgReadWriter if it implements the io.Closer
|
|
||||||
// interface
|
|
||||||
func (ev *msgEventer) Close() error {
|
|
||||||
if v, ok := ev.MsgReadWriter.(io.Closer); ok {
|
|
||||||
return v.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
// Copyright 2014 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package p2p
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"runtime"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExampleMsgPipe() {
|
|
||||||
rw1, rw2 := MsgPipe()
|
|
||||||
go func() {
|
|
||||||
Send(rw1, 8, [][]byte{{0, 0}})
|
|
||||||
Send(rw1, 5, [][]byte{{1, 1}})
|
|
||||||
rw1.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
msg, err := rw2.ReadMsg()
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
var data [][]byte
|
|
||||||
msg.Decode(&data)
|
|
||||||
fmt.Printf("msg: %d, %x\n", msg.Code, data[0])
|
|
||||||
}
|
|
||||||
// Output:
|
|
||||||
// msg: 8, 0000
|
|
||||||
// msg: 5, 0101
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMsgPipeUnblockWrite(t *testing.T) {
|
|
||||||
loop:
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
rw1, rw2 := MsgPipe()
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
if err := SendItems(rw1, 1); err == nil {
|
|
||||||
t.Error("EncodeMsg returned nil error")
|
|
||||||
} else if err != ErrPipeClosed {
|
|
||||||
t.Errorf("EncodeMsg returned wrong error: got %v, want %v", err, ErrPipeClosed)
|
|
||||||
}
|
|
||||||
close(done)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// this call should ensure that EncodeMsg is waiting to
|
|
||||||
// deliver sometimes. if this isn't done, Close is likely to
|
|
||||||
// be executed before EncodeMsg starts and then we won't test
|
|
||||||
// all the cases.
|
|
||||||
runtime.Gosched()
|
|
||||||
|
|
||||||
rw2.Close()
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
case <-time.After(200 * time.Millisecond):
|
|
||||||
t.Errorf("write didn't unblock")
|
|
||||||
break loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test should panic if concurrent close isn't implemented correctly.
|
|
||||||
func TestMsgPipeConcurrentClose(t *testing.T) {
|
|
||||||
rw1, _ := MsgPipe()
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
go rw1.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEOFSignal(t *testing.T) {
|
|
||||||
rb := make([]byte, 10)
|
|
||||||
|
|
||||||
// empty reader
|
|
||||||
eof := make(chan struct{}, 1)
|
|
||||||
sig := &eofSignal{new(bytes.Buffer), 0, eof}
|
|
||||||
if n, err := sig.Read(rb); n != 0 || err != io.EOF {
|
|
||||||
t.Errorf("Read returned unexpected values: (%v, %v)", n, err)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-eof:
|
|
||||||
default:
|
|
||||||
t.Error("EOF chan not signaled")
|
|
||||||
}
|
|
||||||
|
|
||||||
// count before error
|
|
||||||
eof = make(chan struct{}, 1)
|
|
||||||
sig = &eofSignal{bytes.NewBufferString("aaaaaaaa"), 4, eof}
|
|
||||||
if n, err := sig.Read(rb); n != 4 || err != nil {
|
|
||||||
t.Errorf("Read returned unexpected values: (%v, %v)", n, err)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-eof:
|
|
||||||
default:
|
|
||||||
t.Error("EOF chan not signaled")
|
|
||||||
}
|
|
||||||
|
|
||||||
// error before count
|
|
||||||
eof = make(chan struct{}, 1)
|
|
||||||
sig = &eofSignal{bytes.NewBufferString("aaaa"), 999, eof}
|
|
||||||
if n, err := sig.Read(rb); n != 4 || err != nil {
|
|
||||||
t.Errorf("Read returned unexpected values: (%v, %v)", n, err)
|
|
||||||
}
|
|
||||||
if n, err := sig.Read(rb); n != 0 || err != io.EOF {
|
|
||||||
t.Errorf("Read returned unexpected values: (%v, %v)", n, err)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-eof:
|
|
||||||
default:
|
|
||||||
t.Error("EOF chan not signaled")
|
|
||||||
}
|
|
||||||
|
|
||||||
// no signal if neither occurs
|
|
||||||
eof = make(chan struct{}, 1)
|
|
||||||
sig = &eofSignal{bytes.NewBufferString("aaaaaaaaaaaaaaaaaaaaa"), 999, eof}
|
|
||||||
if n, err := sig.Read(rb); n != 10 || err != nil {
|
|
||||||
t.Errorf("Read returned unexpected values: (%v, %v)", n, err)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-eof:
|
|
||||||
t.Error("unexpected EOF signal")
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
132
p2p/metrics.go
132
p2p/metrics.go
|
|
@ -1,132 +0,0 @@
|
||||||
// Copyright 2015 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Contains the meters and timers used by the networking layer.
|
|
||||||
|
|
||||||
package p2p
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/metrics"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// HandleHistName is the prefix of the per-packet serving time histograms.
|
|
||||||
HandleHistName = "p2p/handle"
|
|
||||||
|
|
||||||
// ingressMeterName is the prefix of the per-packet inbound metrics.
|
|
||||||
ingressMeterName = "p2p/ingress"
|
|
||||||
|
|
||||||
// egressMeterName is the prefix of the per-packet outbound metrics.
|
|
||||||
egressMeterName = "p2p/egress"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
activePeerGauge metrics.Gauge = metrics.NilGauge{}
|
|
||||||
|
|
||||||
ingressTrafficMeter = metrics.NewRegisteredMeter("p2p/ingress", nil)
|
|
||||||
egressTrafficMeter = metrics.NewRegisteredMeter("p2p/egress", nil)
|
|
||||||
|
|
||||||
// general ingress/egress connection meters
|
|
||||||
serveMeter metrics.Meter = metrics.NilMeter{}
|
|
||||||
serveSuccessMeter metrics.Meter = metrics.NilMeter{}
|
|
||||||
dialMeter metrics.Meter = metrics.NilMeter{}
|
|
||||||
dialSuccessMeter metrics.Meter = metrics.NilMeter{}
|
|
||||||
dialConnectionError metrics.Meter = metrics.NilMeter{}
|
|
||||||
|
|
||||||
// handshake error meters
|
|
||||||
dialTooManyPeers = metrics.NewRegisteredMeter("p2p/dials/error/saturated", nil)
|
|
||||||
dialAlreadyConnected = metrics.NewRegisteredMeter("p2p/dials/error/known", nil)
|
|
||||||
dialSelf = metrics.NewRegisteredMeter("p2p/dials/error/self", nil)
|
|
||||||
dialUselessPeer = metrics.NewRegisteredMeter("p2p/dials/error/useless", nil)
|
|
||||||
dialUnexpectedIdentity = metrics.NewRegisteredMeter("p2p/dials/error/id/unexpected", nil)
|
|
||||||
dialEncHandshakeError = metrics.NewRegisteredMeter("p2p/dials/error/rlpx/enc", nil)
|
|
||||||
dialProtoHandshakeError = metrics.NewRegisteredMeter("p2p/dials/error/rlpx/proto", nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
if !metrics.Enabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
activePeerGauge = metrics.NewRegisteredGauge("p2p/peers", nil)
|
|
||||||
serveMeter = metrics.NewRegisteredMeter("p2p/serves", nil)
|
|
||||||
serveSuccessMeter = metrics.NewRegisteredMeter("p2p/serves/success", nil)
|
|
||||||
dialMeter = metrics.NewRegisteredMeter("p2p/dials", nil)
|
|
||||||
dialSuccessMeter = metrics.NewRegisteredMeter("p2p/dials/success", nil)
|
|
||||||
dialConnectionError = metrics.NewRegisteredMeter("p2p/dials/error/connection", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// markDialError matches errors that occur while setting up a dial connection
|
|
||||||
// to the corresponding meter.
|
|
||||||
func markDialError(err error) {
|
|
||||||
if !metrics.Enabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err2 := errors.Unwrap(err); err2 != nil {
|
|
||||||
err = err2
|
|
||||||
}
|
|
||||||
switch err {
|
|
||||||
case DiscTooManyPeers:
|
|
||||||
dialTooManyPeers.Mark(1)
|
|
||||||
case DiscAlreadyConnected:
|
|
||||||
dialAlreadyConnected.Mark(1)
|
|
||||||
case DiscSelf:
|
|
||||||
dialSelf.Mark(1)
|
|
||||||
case DiscUselessPeer:
|
|
||||||
dialUselessPeer.Mark(1)
|
|
||||||
case DiscUnexpectedIdentity:
|
|
||||||
dialUnexpectedIdentity.Mark(1)
|
|
||||||
case errEncHandshakeError:
|
|
||||||
dialEncHandshakeError.Mark(1)
|
|
||||||
case errProtoHandshakeError:
|
|
||||||
dialProtoHandshakeError.Mark(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// meteredConn is a wrapper around a net.Conn that meters both the
|
|
||||||
// inbound and outbound network traffic.
|
|
||||||
type meteredConn struct {
|
|
||||||
net.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
// newMeteredConn creates a new metered connection, bumps the ingress or egress
|
|
||||||
// connection meter and also increases the metered peer count. If the metrics
|
|
||||||
// system is disabled, function returns the original connection.
|
|
||||||
func newMeteredConn(conn net.Conn) net.Conn {
|
|
||||||
if !metrics.Enabled {
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
return &meteredConn{Conn: conn}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read delegates a network read to the underlying connection, bumping the common
|
|
||||||
// and the peer ingress traffic meters along the way.
|
|
||||||
func (c *meteredConn) Read(b []byte) (n int, err error) {
|
|
||||||
n, err = c.Conn.Read(b)
|
|
||||||
ingressTrafficMeter.Mark(int64(n))
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write delegates a network write to the underlying connection, bumping the common
|
|
||||||
// and the peer egress traffic meters along the way.
|
|
||||||
func (c *meteredConn) Write(b []byte) (n int, err error) {
|
|
||||||
n, err = c.Conn.Write(b)
|
|
||||||
egressTrafficMeter.Mark(int64(n))
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
@ -1,465 +0,0 @@
|
||||||
// Copyright 2021 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Package msgrate allows estimating the throughput of peers for more balanced syncs.
|
|
||||||
package msgrate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"math"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// measurementImpact is the impact a single measurement has on a peer's final
|
|
||||||
// capacity value. A value closer to 0 reacts slower to sudden network changes,
|
|
||||||
// but it is also more stable against temporary hiccups. 0.1 worked well for
|
|
||||||
// most of Ethereum's existence, so might as well go with it.
|
|
||||||
const measurementImpact = 0.1
|
|
||||||
|
|
||||||
// capacityOverestimation is the ratio of items to over-estimate when retrieving
|
|
||||||
// a peer's capacity to avoid locking into a lower value due to never attempting
|
|
||||||
// to fetch more than some local stable value.
|
|
||||||
const capacityOverestimation = 1.01
|
|
||||||
|
|
||||||
// rttMinEstimate is the minimal round trip time to target requests for. Since
|
|
||||||
// every request entails a 2 way latency + bandwidth + serving database lookups,
|
|
||||||
// it should be generous enough to permit meaningful work to be done on top of
|
|
||||||
// the transmission costs.
|
|
||||||
const rttMinEstimate = 2 * time.Second
|
|
||||||
|
|
||||||
// rttMaxEstimate is the maximal round trip time to target requests for. Although
|
|
||||||
// the expectation is that a well connected node will never reach this, certain
|
|
||||||
// special connectivity ones might experience significant delays (e.g. satellite
|
|
||||||
// uplink with 3s RTT). This value should be low enough to forbid stalling the
|
|
||||||
// pipeline too long, but large enough to cover the worst of the worst links.
|
|
||||||
const rttMaxEstimate = 20 * time.Second
|
|
||||||
|
|
||||||
// rttPushdownFactor is a multiplier to attempt forcing quicker requests than
|
|
||||||
// what the message rate tracker estimates. The reason is that message rate
|
|
||||||
// tracking adapts queries to the RTT, but multiple RTT values can be perfectly
|
|
||||||
// valid, they just result in higher packet sizes. Since smaller packets almost
|
|
||||||
// always result in stabler download streams, this factor hones in on the lowest
|
|
||||||
// RTT from all the functional ones.
|
|
||||||
const rttPushdownFactor = 0.9
|
|
||||||
|
|
||||||
// rttMinConfidence is the minimum value the roundtrip confidence factor may drop
|
|
||||||
// to. Since the target timeouts are based on how confident the tracker is in the
|
|
||||||
// true roundtrip, it's important to not allow too huge fluctuations.
|
|
||||||
const rttMinConfidence = 0.1
|
|
||||||
|
|
||||||
// ttlScaling is the multiplier that converts the estimated roundtrip time to a
|
|
||||||
// timeout cap for network requests. The expectation is that peers' response time
|
|
||||||
// will fluctuate around the estimated roundtrip, but depending in their load at
|
|
||||||
// request time, it might be higher than anticipated. This scaling factor ensures
|
|
||||||
// that we allow remote connections some slack but at the same time do enforce a
|
|
||||||
// behavior similar to our median peers.
|
|
||||||
const ttlScaling = 3
|
|
||||||
|
|
||||||
// ttlLimit is the maximum timeout allowance to prevent reaching crazy numbers
|
|
||||||
// if some unforeseen network events happen. As much as we try to hone in on
|
|
||||||
// the most optimal values, it doesn't make any sense to go above a threshold,
|
|
||||||
// even if everything is slow and screwy.
|
|
||||||
const ttlLimit = time.Minute
|
|
||||||
|
|
||||||
// tuningConfidenceCap is the number of active peers above which to stop detuning
|
|
||||||
// the confidence number. The idea here is that once we hone in on the capacity
|
|
||||||
// of a meaningful number of peers, adding one more should ot have a significant
|
|
||||||
// impact on things, so just ron with the originals.
|
|
||||||
const tuningConfidenceCap = 10
|
|
||||||
|
|
||||||
// tuningImpact is the influence that a new tuning target has on the previously
|
|
||||||
// cached value. This number is mostly just an out-of-the-blue heuristic that
|
|
||||||
// prevents the estimates from jumping around. There's no particular reason for
|
|
||||||
// the current value.
|
|
||||||
const tuningImpact = 0.25
|
|
||||||
|
|
||||||
// Tracker estimates the throughput capacity of a peer with regard to each data
|
|
||||||
// type it can deliver. The goal is to dynamically adjust request sizes to max
|
|
||||||
// out network throughput without overloading either the peer or the local node.
|
|
||||||
//
|
|
||||||
// By tracking in real time the latencies and bandwidths peers exhibit for each
|
|
||||||
// packet type, it's possible to prevent overloading by detecting a slowdown on
|
|
||||||
// one type when another type is pushed too hard.
|
|
||||||
//
|
|
||||||
// Similarly, real time measurements also help avoid overloading the local net
|
|
||||||
// connection if our peers would otherwise be capable to deliver more, but the
|
|
||||||
// local link is saturated. In that case, the live measurements will force us
|
|
||||||
// to reduce request sizes until the throughput gets stable.
|
|
||||||
//
|
|
||||||
// Lastly, message rate measurements allows us to detect if a peer is unusually
|
|
||||||
// slow compared to other peers, in which case we can decide to keep it around
|
|
||||||
// or free up the slot so someone closer.
|
|
||||||
//
|
|
||||||
// Since throughput tracking and estimation adapts dynamically to live network
|
|
||||||
// conditions, it's fine to have multiple trackers locally track the same peer
|
|
||||||
// in different subsystem. The throughput will simply be distributed across the
|
|
||||||
// two trackers if both are highly active.
|
|
||||||
type Tracker struct {
|
|
||||||
// capacity is the number of items retrievable per second of a given type.
|
|
||||||
// It is analogous to bandwidth, but we deliberately avoided using bytes
|
|
||||||
// as the unit, since serving nodes also spend a lot of time loading data
|
|
||||||
// from disk, which is linear in the number of items, but mostly constant
|
|
||||||
// in their sizes.
|
|
||||||
//
|
|
||||||
// Callers of course are free to use the item counter as a byte counter if
|
|
||||||
// or when their protocol of choice if capped by bytes instead of items.
|
|
||||||
// (eg. eth.getHeaders vs snap.getAccountRange).
|
|
||||||
capacity map[uint64]float64
|
|
||||||
|
|
||||||
// roundtrip is the latency a peer in general responds to data requests.
|
|
||||||
// This number is not used inside the tracker, but is exposed to compare
|
|
||||||
// peers to each other and filter out slow ones. Note however, it only
|
|
||||||
// makes sense to compare RTTs if the caller caters request sizes for
|
|
||||||
// each peer to target the same RTT. There's no need to make this number
|
|
||||||
// the real networking RTT, we just need a number to compare peers with.
|
|
||||||
roundtrip time.Duration
|
|
||||||
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTracker creates a new message rate tracker for a specific peer. An initial
|
|
||||||
// RTT is needed to avoid a peer getting marked as an outlier compared to others
|
|
||||||
// right after joining. It's suggested to use the median rtt across all peers to
|
|
||||||
// init a new peer tracker.
|
|
||||||
func NewTracker(caps map[uint64]float64, rtt time.Duration) *Tracker {
|
|
||||||
if caps == nil {
|
|
||||||
caps = make(map[uint64]float64)
|
|
||||||
}
|
|
||||||
return &Tracker{
|
|
||||||
capacity: caps,
|
|
||||||
roundtrip: rtt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capacity calculates the number of items the peer is estimated to be able to
|
|
||||||
// retrieve within the allotted time slot. The method will round up any division
|
|
||||||
// errors and will add an additional overestimation ratio on top. The reason for
|
|
||||||
// overshooting the capacity is because certain message types might not increase
|
|
||||||
// the load proportionally to the requested items, so fetching a bit more might
|
|
||||||
// still take the same RTT. By forcefully overshooting by a small amount, we can
|
|
||||||
// avoid locking into a lower-that-real capacity.
|
|
||||||
func (t *Tracker) Capacity(kind uint64, targetRTT time.Duration) int {
|
|
||||||
t.lock.RLock()
|
|
||||||
defer t.lock.RUnlock()
|
|
||||||
|
|
||||||
// Calculate the actual measured throughput
|
|
||||||
throughput := t.capacity[kind] * float64(targetRTT) / float64(time.Second)
|
|
||||||
|
|
||||||
// Return an overestimation to force the peer out of a stuck minima, adding
|
|
||||||
// +1 in case the item count is too low for the overestimator to dent
|
|
||||||
return roundCapacity(1 + capacityOverestimation*throughput)
|
|
||||||
}
|
|
||||||
|
|
||||||
// roundCapacity gives the integer value of a capacity.
|
|
||||||
// The result fits int32, and is guaranteed to be positive.
|
|
||||||
func roundCapacity(cap float64) int {
|
|
||||||
const maxInt32 = float64(1<<31 - 1)
|
|
||||||
return int(math.Min(maxInt32, math.Max(1, math.Ceil(cap))))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update modifies the peer's capacity values for a specific data type with a new
|
|
||||||
// measurement. If the delivery is zero, the peer is assumed to have either timed
|
|
||||||
// out or to not have the requested data, resulting in a slash to 0 capacity. This
|
|
||||||
// avoids assigning the peer retrievals that it won't be able to honour.
|
|
||||||
func (t *Tracker) Update(kind uint64, elapsed time.Duration, items int) {
|
|
||||||
t.lock.Lock()
|
|
||||||
defer t.lock.Unlock()
|
|
||||||
|
|
||||||
// If nothing was delivered (timeout / unavailable data), reduce throughput
|
|
||||||
// to minimum
|
|
||||||
if items == 0 {
|
|
||||||
t.capacity[kind] = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Otherwise update the throughput with a new measurement
|
|
||||||
if elapsed <= 0 {
|
|
||||||
elapsed = 1 // +1 (ns) to ensure non-zero divisor
|
|
||||||
}
|
|
||||||
measured := float64(items) / (float64(elapsed) / float64(time.Second))
|
|
||||||
|
|
||||||
t.capacity[kind] = (1-measurementImpact)*(t.capacity[kind]) + measurementImpact*measured
|
|
||||||
t.roundtrip = time.Duration((1-measurementImpact)*float64(t.roundtrip) + measurementImpact*float64(elapsed))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trackers is a set of message rate trackers across a number of peers with the
|
|
||||||
// goal of aggregating certain measurements across the entire set for outlier
|
|
||||||
// filtering and newly joining initialization.
|
|
||||||
type Trackers struct {
|
|
||||||
trackers map[string]*Tracker
|
|
||||||
|
|
||||||
// roundtrip is the current best guess as to what is a stable round trip time
|
|
||||||
// across the entire collection of connected peers. This is derived from the
|
|
||||||
// various trackers added, but is used as a cache to avoid recomputing on each
|
|
||||||
// network request. The value is updated once every RTT to avoid fluctuations
|
|
||||||
// caused by hiccups or peer events.
|
|
||||||
roundtrip time.Duration
|
|
||||||
|
|
||||||
// confidence represents the probability that the estimated roundtrip value
|
|
||||||
// is the real one across all our peers. The confidence value is used as an
|
|
||||||
// impact factor of new measurements on old estimates. As our connectivity
|
|
||||||
// stabilizes, this value gravitates towards 1, new measurements having
|
|
||||||
// almost no impact. If there's a large peer churn and few peers, then new
|
|
||||||
// measurements will impact it more. The confidence is increased with every
|
|
||||||
// packet and dropped with every new connection.
|
|
||||||
confidence float64
|
|
||||||
|
|
||||||
// tuned is the time instance the tracker recalculated its cached roundtrip
|
|
||||||
// value and confidence values. A cleaner way would be to have a heartbeat
|
|
||||||
// goroutine do it regularly, but that requires a lot of maintenance to just
|
|
||||||
// run every now and again.
|
|
||||||
tuned time.Time
|
|
||||||
|
|
||||||
// The fields below can be used to override certain default values. Their
|
|
||||||
// purpose is to allow quicker tests. Don't use them in production.
|
|
||||||
OverrideTTLLimit time.Duration
|
|
||||||
|
|
||||||
log log.Logger
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTrackers creates an empty set of trackers to be filled with peers.
|
|
||||||
func NewTrackers(log log.Logger) *Trackers {
|
|
||||||
return &Trackers{
|
|
||||||
trackers: make(map[string]*Tracker),
|
|
||||||
roundtrip: rttMaxEstimate,
|
|
||||||
confidence: 1,
|
|
||||||
tuned: time.Now(),
|
|
||||||
OverrideTTLLimit: ttlLimit,
|
|
||||||
log: log,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track inserts a new tracker into the set.
|
|
||||||
func (t *Trackers) Track(id string, tracker *Tracker) error {
|
|
||||||
t.lock.Lock()
|
|
||||||
defer t.lock.Unlock()
|
|
||||||
|
|
||||||
if _, ok := t.trackers[id]; ok {
|
|
||||||
return errors.New("already tracking")
|
|
||||||
}
|
|
||||||
t.trackers[id] = tracker
|
|
||||||
t.detune()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Untrack stops tracking a previously added peer.
|
|
||||||
func (t *Trackers) Untrack(id string) error {
|
|
||||||
t.lock.Lock()
|
|
||||||
defer t.lock.Unlock()
|
|
||||||
|
|
||||||
if _, ok := t.trackers[id]; !ok {
|
|
||||||
return errors.New("not tracking")
|
|
||||||
}
|
|
||||||
delete(t.trackers, id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MedianRoundTrip returns the median RTT across all known trackers. The purpose
|
|
||||||
// of the median RTT is to initialize a new peer with sane statistics that it will
|
|
||||||
// hopefully outperform. If it seriously underperforms, there's a risk of dropping
|
|
||||||
// the peer, but that is ok as we're aiming for a strong median.
|
|
||||||
func (t *Trackers) MedianRoundTrip() time.Duration {
|
|
||||||
t.lock.RLock()
|
|
||||||
defer t.lock.RUnlock()
|
|
||||||
|
|
||||||
return t.medianRoundTrip()
|
|
||||||
}
|
|
||||||
|
|
||||||
// medianRoundTrip is the internal lockless version of MedianRoundTrip to be used
|
|
||||||
// by the QoS tuner.
|
|
||||||
func (t *Trackers) medianRoundTrip() time.Duration {
|
|
||||||
// Gather all the currently measured round trip times
|
|
||||||
rtts := make([]float64, 0, len(t.trackers))
|
|
||||||
for _, tt := range t.trackers {
|
|
||||||
tt.lock.RLock()
|
|
||||||
rtts = append(rtts, float64(tt.roundtrip))
|
|
||||||
tt.lock.RUnlock()
|
|
||||||
}
|
|
||||||
sort.Float64s(rtts)
|
|
||||||
|
|
||||||
var median time.Duration
|
|
||||||
switch len(rtts) {
|
|
||||||
case 0:
|
|
||||||
median = rttMaxEstimate
|
|
||||||
case 1:
|
|
||||||
median = time.Duration(rtts[0])
|
|
||||||
default:
|
|
||||||
idx := int(math.Sqrt(float64(len(rtts))))
|
|
||||||
median = time.Duration(rtts[idx])
|
|
||||||
}
|
|
||||||
// Restrict the RTT into some QoS defaults, irrelevant of true RTT
|
|
||||||
if median < rttMinEstimate {
|
|
||||||
median = rttMinEstimate
|
|
||||||
}
|
|
||||||
if median > rttMaxEstimate {
|
|
||||||
median = rttMaxEstimate
|
|
||||||
}
|
|
||||||
return median
|
|
||||||
}
|
|
||||||
|
|
||||||
// MeanCapacities returns the capacities averaged across all the added trackers.
|
|
||||||
// The purpose of the mean capacities are to initialize a new peer with some sane
|
|
||||||
// starting values that it will hopefully outperform. If the mean overshoots, the
|
|
||||||
// peer will be cut back to minimal capacity and given another chance.
|
|
||||||
func (t *Trackers) MeanCapacities() map[uint64]float64 {
|
|
||||||
t.lock.RLock()
|
|
||||||
defer t.lock.RUnlock()
|
|
||||||
|
|
||||||
return t.meanCapacities()
|
|
||||||
}
|
|
||||||
|
|
||||||
// meanCapacities is the internal lockless version of MeanCapacities used for
|
|
||||||
// debug logging.
|
|
||||||
func (t *Trackers) meanCapacities() map[uint64]float64 {
|
|
||||||
capacities := make(map[uint64]float64, len(t.trackers))
|
|
||||||
for _, tt := range t.trackers {
|
|
||||||
tt.lock.RLock()
|
|
||||||
for key, val := range tt.capacity {
|
|
||||||
capacities[key] += val
|
|
||||||
}
|
|
||||||
tt.lock.RUnlock()
|
|
||||||
}
|
|
||||||
for key, val := range capacities {
|
|
||||||
capacities[key] = val / float64(len(t.trackers))
|
|
||||||
}
|
|
||||||
return capacities
|
|
||||||
}
|
|
||||||
|
|
||||||
// TargetRoundTrip returns the current target round trip time for a request to
|
|
||||||
// complete in.The returned RTT is slightly under the estimated RTT. The reason
|
|
||||||
// is that message rate estimation is a 2 dimensional problem which is solvable
|
|
||||||
// for any RTT. The goal is to gravitate towards smaller RTTs instead of large
|
|
||||||
// messages, to result in a stabler download stream.
|
|
||||||
func (t *Trackers) TargetRoundTrip() time.Duration {
|
|
||||||
// Recalculate the internal caches if it's been a while
|
|
||||||
t.tune()
|
|
||||||
|
|
||||||
// Caches surely recent, return target roundtrip
|
|
||||||
t.lock.RLock()
|
|
||||||
defer t.lock.RUnlock()
|
|
||||||
|
|
||||||
return time.Duration(float64(t.roundtrip) * rttPushdownFactor)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TargetTimeout returns the timeout allowance for a single request to finish
|
|
||||||
// under. The timeout is proportional to the roundtrip, but also takes into
|
|
||||||
// consideration the tracker's confidence in said roundtrip and scales it
|
|
||||||
// accordingly. The final value is capped to avoid runaway requests.
|
|
||||||
func (t *Trackers) TargetTimeout() time.Duration {
|
|
||||||
// Recalculate the internal caches if it's been a while
|
|
||||||
t.tune()
|
|
||||||
|
|
||||||
// Caches surely recent, return target timeout
|
|
||||||
t.lock.RLock()
|
|
||||||
defer t.lock.RUnlock()
|
|
||||||
|
|
||||||
return t.targetTimeout()
|
|
||||||
}
|
|
||||||
|
|
||||||
// targetTimeout is the internal lockless version of TargetTimeout to be used
|
|
||||||
// during QoS tuning.
|
|
||||||
func (t *Trackers) targetTimeout() time.Duration {
|
|
||||||
timeout := time.Duration(ttlScaling * float64(t.roundtrip) / t.confidence)
|
|
||||||
if timeout > t.OverrideTTLLimit {
|
|
||||||
timeout = t.OverrideTTLLimit
|
|
||||||
}
|
|
||||||
return timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
// tune gathers the individual tracker statistics and updates the estimated
|
|
||||||
// request round trip time.
|
|
||||||
func (t *Trackers) tune() {
|
|
||||||
// Tune may be called concurrently all over the place, but we only want to
|
|
||||||
// periodically update and even then only once. First check if it was updated
|
|
||||||
// recently and abort if so.
|
|
||||||
t.lock.RLock()
|
|
||||||
dirty := time.Since(t.tuned) > t.roundtrip
|
|
||||||
t.lock.RUnlock()
|
|
||||||
if !dirty {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// If an update is needed, obtain a write lock but make sure we don't update
|
|
||||||
// it on all concurrent threads one by one.
|
|
||||||
t.lock.Lock()
|
|
||||||
defer t.lock.Unlock()
|
|
||||||
|
|
||||||
if dirty := time.Since(t.tuned) > t.roundtrip; !dirty {
|
|
||||||
return // A concurrent request beat us to the tuning
|
|
||||||
}
|
|
||||||
// First thread reaching the tuning point, update the estimates and return
|
|
||||||
t.roundtrip = time.Duration((1-tuningImpact)*float64(t.roundtrip) + tuningImpact*float64(t.medianRoundTrip()))
|
|
||||||
t.confidence = t.confidence + (1-t.confidence)/2
|
|
||||||
|
|
||||||
t.tuned = time.Now()
|
|
||||||
t.log.Debug("Recalculated msgrate QoS values", "rtt", t.roundtrip, "confidence", t.confidence, "ttl", t.targetTimeout(), "next", t.tuned.Add(t.roundtrip))
|
|
||||||
if t.log.Enabled(context.Background(), log.LevelTrace) {
|
|
||||||
t.log.Trace("Debug dump of mean capacities", "caps", t.meanCapacities())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// detune reduces the tracker's confidence in order to make fresh measurements
|
|
||||||
// have a larger impact on the estimates. It is meant to be used during new peer
|
|
||||||
// connections so they can have a proper impact on the estimates.
|
|
||||||
func (t *Trackers) detune() {
|
|
||||||
// If we have a single peer, confidence is always 1
|
|
||||||
if len(t.trackers) == 1 {
|
|
||||||
t.confidence = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// If we have a ton of peers, don't drop the confidence since there's enough
|
|
||||||
// remaining to retain the same throughput
|
|
||||||
if len(t.trackers) >= tuningConfidenceCap {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Otherwise drop the confidence factor
|
|
||||||
peers := float64(len(t.trackers))
|
|
||||||
|
|
||||||
t.confidence = t.confidence * (peers - 1) / peers
|
|
||||||
if t.confidence < rttMinConfidence {
|
|
||||||
t.confidence = rttMinConfidence
|
|
||||||
}
|
|
||||||
t.log.Debug("Relaxed msgrate QoS values", "rtt", t.roundtrip, "confidence", t.confidence, "ttl", t.targetTimeout())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capacity is a helper function to access a specific tracker without having to
|
|
||||||
// track it explicitly outside.
|
|
||||||
func (t *Trackers) Capacity(id string, kind uint64, targetRTT time.Duration) int {
|
|
||||||
t.lock.RLock()
|
|
||||||
defer t.lock.RUnlock()
|
|
||||||
|
|
||||||
tracker := t.trackers[id]
|
|
||||||
if tracker == nil {
|
|
||||||
return 1 // Unregister race, don't return 0, it's a dangerous number
|
|
||||||
}
|
|
||||||
return tracker.Capacity(kind, targetRTT)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update is a helper function to access a specific tracker without having to
|
|
||||||
// track it explicitly outside.
|
|
||||||
func (t *Trackers) Update(id string, kind uint64, elapsed time.Duration, items int) {
|
|
||||||
t.lock.RLock()
|
|
||||||
defer t.lock.RUnlock()
|
|
||||||
|
|
||||||
if tracker := t.trackers[id]; tracker != nil {
|
|
||||||
tracker.Update(kind, elapsed, items)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
// Copyright 2021 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package msgrate
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestCapacityOverflow(t *testing.T) {
|
|
||||||
tracker := NewTracker(nil, 1)
|
|
||||||
tracker.Update(1, 1, 100000)
|
|
||||||
cap := tracker.Capacity(1, 10000000)
|
|
||||||
if int32(cap) < 0 {
|
|
||||||
t.Fatalf("Negative: %v", int32(cap))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
240
p2p/nat/nat.go
240
p2p/nat/nat.go
|
|
@ -1,240 +0,0 @@
|
||||||
// Copyright 2015 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Package nat provides access to common network port mapping protocols.
|
|
||||||
package nat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
natpmp "github.com/jackpal/go-nat-pmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Interface An implementation of nat.Interface can map local ports to ports
|
|
||||||
// accessible from the Internet.
|
|
||||||
type Interface interface {
|
|
||||||
// These methods manage a mapping between a port on the local
|
|
||||||
// machine to a port that can be connected to from the internet.
|
|
||||||
//
|
|
||||||
// protocol is "UDP" or "TCP". Some implementations allow setting
|
|
||||||
// a display name for the mapping. The mapping may be removed by
|
|
||||||
// the gateway when its lifetime ends.
|
|
||||||
AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) (uint16, error)
|
|
||||||
DeleteMapping(protocol string, extport, intport int) error
|
|
||||||
|
|
||||||
// ExternalIP should return the external (Internet-facing)
|
|
||||||
// address of the gateway device.
|
|
||||||
ExternalIP() (net.IP, error)
|
|
||||||
|
|
||||||
// String should return name of the method. This is used for logging.
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse parses a NAT interface description.
|
|
||||||
// The following formats are currently accepted.
|
|
||||||
// Note that mechanism names are not case-sensitive.
|
|
||||||
//
|
|
||||||
// "" or "none" return nil
|
|
||||||
// "extip:77.12.33.4" will assume the local machine is reachable on the given IP
|
|
||||||
// "any" uses the first auto-detected mechanism
|
|
||||||
// "upnp" uses the Universal Plug and Play protocol
|
|
||||||
// "pmp" uses NAT-PMP with an auto-detected gateway address
|
|
||||||
// "pmp:192.168.0.1" uses NAT-PMP with the given gateway address
|
|
||||||
func Parse(spec string) (Interface, error) {
|
|
||||||
var (
|
|
||||||
before, after, found = strings.Cut(spec, ":")
|
|
||||||
mech = strings.ToLower(before)
|
|
||||||
ip net.IP
|
|
||||||
)
|
|
||||||
if found {
|
|
||||||
ip = net.ParseIP(after)
|
|
||||||
if ip == nil {
|
|
||||||
return nil, errors.New("invalid IP address")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch mech {
|
|
||||||
case "", "none", "off":
|
|
||||||
return nil, nil
|
|
||||||
case "any", "auto", "on":
|
|
||||||
return Any(), nil
|
|
||||||
case "extip", "ip":
|
|
||||||
if ip == nil {
|
|
||||||
return nil, errors.New("missing IP address")
|
|
||||||
}
|
|
||||||
return ExtIP(ip), nil
|
|
||||||
case "upnp":
|
|
||||||
return UPnP(), nil
|
|
||||||
case "pmp", "natpmp", "nat-pmp":
|
|
||||||
return PMP(ip), nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown mechanism %q", before)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultMapTimeout = 10 * time.Minute
|
|
||||||
)
|
|
||||||
|
|
||||||
// Map adds a port mapping on m and keeps it alive until c is closed.
|
|
||||||
// This function is typically invoked in its own goroutine.
|
|
||||||
//
|
|
||||||
// Note that Map does not handle the situation where the NAT interface assigns a different
|
|
||||||
// external port than the requested one.
|
|
||||||
func Map(m Interface, c <-chan struct{}, protocol string, extport, intport int, name string) {
|
|
||||||
log := log.New("proto", protocol, "extport", extport, "intport", intport, "interface", m)
|
|
||||||
refresh := time.NewTimer(DefaultMapTimeout)
|
|
||||||
defer func() {
|
|
||||||
refresh.Stop()
|
|
||||||
log.Debug("Deleting port mapping")
|
|
||||||
m.DeleteMapping(protocol, extport, intport)
|
|
||||||
}()
|
|
||||||
if _, err := m.AddMapping(protocol, extport, intport, name, DefaultMapTimeout); err != nil {
|
|
||||||
log.Debug("Couldn't add port mapping", "err", err)
|
|
||||||
} else {
|
|
||||||
log.Info("Mapped network port")
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case _, ok := <-c:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-refresh.C:
|
|
||||||
log.Trace("Refreshing port mapping")
|
|
||||||
if _, err := m.AddMapping(protocol, extport, intport, name, DefaultMapTimeout); err != nil {
|
|
||||||
log.Debug("Couldn't add port mapping", "err", err)
|
|
||||||
}
|
|
||||||
refresh.Reset(DefaultMapTimeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtIP assumes that the local machine is reachable on the given
|
|
||||||
// external IP address, and that any required ports were mapped manually.
|
|
||||||
// Mapping operations will not return an error but won't actually do anything.
|
|
||||||
type ExtIP net.IP
|
|
||||||
|
|
||||||
func (n ExtIP) ExternalIP() (net.IP, error) { return net.IP(n), nil }
|
|
||||||
func (n ExtIP) String() string { return fmt.Sprintf("ExtIP(%v)", net.IP(n)) }
|
|
||||||
|
|
||||||
// These do nothing.
|
|
||||||
|
|
||||||
func (ExtIP) AddMapping(string, int, int, string, time.Duration) (uint16, error) { return 0, nil }
|
|
||||||
func (ExtIP) DeleteMapping(string, int, int) error { return nil }
|
|
||||||
|
|
||||||
// Any returns a port mapper that tries to discover any supported
|
|
||||||
// mechanism on the local network.
|
|
||||||
func Any() Interface {
|
|
||||||
// TODO: attempt to discover whether the local machine has an
|
|
||||||
// Internet-class address. Return ExtIP in this case.
|
|
||||||
return startautodisc("UPnP or NAT-PMP", func() Interface {
|
|
||||||
found := make(chan Interface, 2)
|
|
||||||
go func() { found <- discoverUPnP() }()
|
|
||||||
go func() { found <- discoverPMP() }()
|
|
||||||
for i := 0; i < cap(found); i++ {
|
|
||||||
if c := <-found; c != nil {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UPnP returns a port mapper that uses UPnP. It will attempt to
|
|
||||||
// discover the address of your router using UDP broadcasts.
|
|
||||||
func UPnP() Interface {
|
|
||||||
return startautodisc("UPnP", discoverUPnP)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PMP returns a port mapper that uses NAT-PMP. The provided gateway
|
|
||||||
// address should be the IP of your router. If the given gateway
|
|
||||||
// address is nil, PMP will attempt to auto-discover the router.
|
|
||||||
func PMP(gateway net.IP) Interface {
|
|
||||||
if gateway != nil {
|
|
||||||
return &pmp{gw: gateway, c: natpmp.NewClient(gateway)}
|
|
||||||
}
|
|
||||||
return startautodisc("NAT-PMP", discoverPMP)
|
|
||||||
}
|
|
||||||
|
|
||||||
// autodisc represents a port mapping mechanism that is still being
|
|
||||||
// auto-discovered. Calls to the Interface methods on this type will
|
|
||||||
// wait until the discovery is done and then call the method on the
|
|
||||||
// discovered mechanism.
|
|
||||||
//
|
|
||||||
// This type is useful because discovery can take a while but we
|
|
||||||
// want return an Interface value from UPnP, PMP and Auto immediately.
|
|
||||||
type autodisc struct {
|
|
||||||
what string // type of interface being autodiscovered
|
|
||||||
once sync.Once
|
|
||||||
doit func() Interface
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
found Interface
|
|
||||||
}
|
|
||||||
|
|
||||||
func startautodisc(what string, doit func() Interface) Interface {
|
|
||||||
// TODO: monitor network configuration and rerun doit when it changes.
|
|
||||||
return &autodisc{what: what, doit: doit}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *autodisc) AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) (uint16, error) {
|
|
||||||
if err := n.wait(); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return n.found.AddMapping(protocol, extport, intport, name, lifetime)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *autodisc) DeleteMapping(protocol string, extport, intport int) error {
|
|
||||||
if err := n.wait(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return n.found.DeleteMapping(protocol, extport, intport)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *autodisc) ExternalIP() (net.IP, error) {
|
|
||||||
if err := n.wait(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return n.found.ExternalIP()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *autodisc) String() string {
|
|
||||||
n.mu.Lock()
|
|
||||||
defer n.mu.Unlock()
|
|
||||||
if n.found == nil {
|
|
||||||
return n.what
|
|
||||||
}
|
|
||||||
return n.found.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait blocks until auto-discovery has been performed.
|
|
||||||
func (n *autodisc) wait() error {
|
|
||||||
n.once.Do(func() {
|
|
||||||
n.mu.Lock()
|
|
||||||
n.found = n.doit()
|
|
||||||
n.mu.Unlock()
|
|
||||||
})
|
|
||||||
if n.found == nil {
|
|
||||||
return fmt.Errorf("no %s router discovered", n.what)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
// Copyright 2015 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package nat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This test checks that autodisc doesn't hang and returns
|
|
||||||
// consistent results when multiple goroutines call its methods
|
|
||||||
// concurrently.
|
|
||||||
func TestAutoDiscRace(t *testing.T) {
|
|
||||||
ad := startautodisc("thing", func() Interface {
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
return ExtIP{33, 44, 55, 66}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Spawn a few concurrent calls to ad.ExternalIP.
|
|
||||||
type rval struct {
|
|
||||||
ip net.IP
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
results := make(chan rval, 50)
|
|
||||||
for i := 0; i < cap(results); i++ {
|
|
||||||
go func() {
|
|
||||||
ip, err := ad.ExternalIP()
|
|
||||||
results <- rval{ip, err}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that they all return the correct result within the deadline.
|
|
||||||
deadline := time.After(2 * time.Second)
|
|
||||||
for i := 0; i < cap(results); i++ {
|
|
||||||
select {
|
|
||||||
case <-deadline:
|
|
||||||
t.Fatal("deadline exceeded")
|
|
||||||
case rval := <-results:
|
|
||||||
if rval.err != nil {
|
|
||||||
t.Errorf("result %d: unexpected error: %v", i, rval.err)
|
|
||||||
}
|
|
||||||
wantIP := net.IP{33, 44, 55, 66}
|
|
||||||
if !rval.ip.Equal(wantIP) {
|
|
||||||
t.Errorf("result %d: got IP %v, want %v", i, rval.ip, wantIP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
// Copyright 2015 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package nat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
natpmp "github.com/jackpal/go-nat-pmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// natPMPClient adapts the NAT-PMP protocol implementation so it conforms to
|
|
||||||
// the common interface.
|
|
||||||
type pmp struct {
|
|
||||||
gw net.IP
|
|
||||||
c *natpmp.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *pmp) String() string {
|
|
||||||
return fmt.Sprintf("NAT-PMP(%v)", n.gw)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *pmp) ExternalIP() (net.IP, error) {
|
|
||||||
response, err := n.c.GetExternalAddress()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return response.ExternalIPAddress[:], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *pmp) AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) (uint16, error) {
|
|
||||||
if lifetime <= 0 {
|
|
||||||
return 0, fmt.Errorf("lifetime must not be <= 0")
|
|
||||||
}
|
|
||||||
// Note order of port arguments is switched between our
|
|
||||||
// AddMapping and the client's AddPortMapping.
|
|
||||||
res, err := n.c.AddPortMapping(strings.ToLower(protocol), intport, extport, int(lifetime/time.Second))
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// NAT-PMP maps an alternative available port number if the requested port
|
|
||||||
// is already mapped to another address and returns success. Handling of
|
|
||||||
// alternate port numbers is done by the caller.
|
|
||||||
return res.MappedExternalPort, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *pmp) DeleteMapping(protocol string, extport, intport int) (err error) {
|
|
||||||
// To destroy a mapping, send an add-port with an internalPort of
|
|
||||||
// the internal port to destroy, an external port of zero and a
|
|
||||||
// time of zero.
|
|
||||||
_, err = n.c.AddPortMapping(strings.ToLower(protocol), intport, 0, 0)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func discoverPMP() Interface {
|
|
||||||
// run external address lookups on all potential gateways
|
|
||||||
gws := potentialGateways()
|
|
||||||
found := make(chan *pmp, len(gws))
|
|
||||||
for i := range gws {
|
|
||||||
gw := gws[i]
|
|
||||||
go func() {
|
|
||||||
c := natpmp.NewClient(gw)
|
|
||||||
if _, err := c.GetExternalAddress(); err != nil {
|
|
||||||
found <- nil
|
|
||||||
} else {
|
|
||||||
found <- &pmp{gw, c}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
// return the one that responds first.
|
|
||||||
// discovery needs to be quick, so we stop caring about
|
|
||||||
// any responses after a very short timeout.
|
|
||||||
timeout := time.NewTimer(1 * time.Second)
|
|
||||||
defer timeout.Stop()
|
|
||||||
for range gws {
|
|
||||||
select {
|
|
||||||
case c := <-found:
|
|
||||||
if c != nil {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
case <-timeout.C:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: improve this. We currently assume that (on most networks)
|
|
||||||
// the router is X.X.X.1 in a local LAN range.
|
|
||||||
func potentialGateways() (gws []net.IP) {
|
|
||||||
ifaces, err := net.Interfaces()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, iface := range ifaces {
|
|
||||||
ifaddrs, err := iface.Addrs()
|
|
||||||
if err != nil {
|
|
||||||
return gws
|
|
||||||
}
|
|
||||||
for _, addr := range ifaddrs {
|
|
||||||
if x, ok := addr.(*net.IPNet); ok {
|
|
||||||
if x.IP.IsPrivate() {
|
|
||||||
ip := x.IP.Mask(x.Mask).To4()
|
|
||||||
if ip != nil {
|
|
||||||
ip[3] = ip[3] | 0x01
|
|
||||||
gws = append(gws, ip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return gws
|
|
||||||
}
|
|
||||||
|
|
@ -1,250 +0,0 @@
|
||||||
// Copyright 2015 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package nat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"math/rand"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/huin/goupnp"
|
|
||||||
"github.com/huin/goupnp/dcps/internetgateway1"
|
|
||||||
"github.com/huin/goupnp/dcps/internetgateway2"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
soapRequestTimeout = 3 * time.Second
|
|
||||||
rateLimit = 200 * time.Millisecond
|
|
||||||
)
|
|
||||||
|
|
||||||
type upnp struct {
|
|
||||||
dev *goupnp.RootDevice
|
|
||||||
service string
|
|
||||||
client upnpClient
|
|
||||||
mu sync.Mutex
|
|
||||||
lastReqTime time.Time
|
|
||||||
rand *rand.Rand
|
|
||||||
}
|
|
||||||
|
|
||||||
type upnpClient interface {
|
|
||||||
GetExternalIPAddress() (string, error)
|
|
||||||
AddPortMapping(string, uint16, string, uint16, string, bool, string, uint32) error
|
|
||||||
DeletePortMapping(string, uint16, string) error
|
|
||||||
GetNATRSIPStatus() (sip bool, nat bool, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *upnp) natEnabled() bool {
|
|
||||||
var ok bool
|
|
||||||
var err error
|
|
||||||
n.withRateLimit(func() error {
|
|
||||||
_, ok, err = n.client.GetNATRSIPStatus()
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
return err == nil && ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *upnp) ExternalIP() (addr net.IP, err error) {
|
|
||||||
var ipString string
|
|
||||||
n.withRateLimit(func() error {
|
|
||||||
ipString, err = n.client.GetExternalIPAddress()
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ip := net.ParseIP(ipString)
|
|
||||||
if ip == nil {
|
|
||||||
return nil, errors.New("bad IP in response")
|
|
||||||
}
|
|
||||||
return ip, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *upnp) AddMapping(protocol string, extport, intport int, desc string, lifetime time.Duration) (uint16, error) {
|
|
||||||
ip, err := n.internalAddress()
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil // TODO: Shouldn't we return the error?
|
|
||||||
}
|
|
||||||
protocol = strings.ToUpper(protocol)
|
|
||||||
lifetimeS := uint32(lifetime / time.Second)
|
|
||||||
n.DeleteMapping(protocol, extport, intport)
|
|
||||||
|
|
||||||
err = n.withRateLimit(func() error {
|
|
||||||
return n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
return uint16(extport), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return uint16(extport), n.withRateLimit(func() error {
|
|
||||||
p, err := n.addAnyPortMapping(protocol, extport, intport, ip, desc, lifetimeS)
|
|
||||||
if err == nil {
|
|
||||||
extport = int(p)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *upnp) addAnyPortMapping(protocol string, extport, intport int, ip net.IP, desc string, lifetimeS uint32) (uint16, error) {
|
|
||||||
if client, ok := n.client.(*internetgateway2.WANIPConnection2); ok {
|
|
||||||
return client.AddAnyPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
|
|
||||||
}
|
|
||||||
// It will retry with a random port number if the client does
|
|
||||||
// not support AddAnyPortMapping.
|
|
||||||
extport = n.randomPort()
|
|
||||||
err := n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return uint16(extport), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *upnp) randomPort() int {
|
|
||||||
if n.rand == nil {
|
|
||||||
n.rand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
||||||
}
|
|
||||||
return n.rand.Intn(math.MaxUint16-10000) + 10000
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *upnp) internalAddress() (net.IP, error) {
|
|
||||||
devaddr, err := net.ResolveUDPAddr("udp4", n.dev.URLBase.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ifaces, err := net.Interfaces()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, iface := range ifaces {
|
|
||||||
addrs, err := iface.Addrs()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, addr := range addrs {
|
|
||||||
if x, ok := addr.(*net.IPNet); ok && x.Contains(devaddr.IP) {
|
|
||||||
return x.IP, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("could not find local address in same net as %v", devaddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *upnp) DeleteMapping(protocol string, extport, intport int) error {
|
|
||||||
return n.withRateLimit(func() error {
|
|
||||||
return n.client.DeletePortMapping("", uint16(extport), strings.ToUpper(protocol))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *upnp) String() string {
|
|
||||||
return "UPNP " + n.service
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *upnp) withRateLimit(fn func() error) error {
|
|
||||||
n.mu.Lock()
|
|
||||||
defer n.mu.Unlock()
|
|
||||||
|
|
||||||
lastreq := time.Since(n.lastReqTime)
|
|
||||||
if lastreq < rateLimit {
|
|
||||||
time.Sleep(rateLimit - lastreq)
|
|
||||||
}
|
|
||||||
err := fn()
|
|
||||||
n.lastReqTime = time.Now()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// discoverUPnP searches for Internet Gateway Devices
|
|
||||||
// and returns the first one it can find on the local network.
|
|
||||||
func discoverUPnP() Interface {
|
|
||||||
found := make(chan *upnp, 2)
|
|
||||||
// IGDv1
|
|
||||||
go discover(found, internetgateway1.URN_WANConnectionDevice_1, func(sc goupnp.ServiceClient) *upnp {
|
|
||||||
switch sc.Service.ServiceType {
|
|
||||||
case internetgateway1.URN_WANIPConnection_1:
|
|
||||||
return &upnp{service: "IGDv1-IP1", client: &internetgateway1.WANIPConnection1{ServiceClient: sc}}
|
|
||||||
case internetgateway1.URN_WANPPPConnection_1:
|
|
||||||
return &upnp{service: "IGDv1-PPP1", client: &internetgateway1.WANPPPConnection1{ServiceClient: sc}}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
// IGDv2
|
|
||||||
go discover(found, internetgateway2.URN_WANConnectionDevice_2, func(sc goupnp.ServiceClient) *upnp {
|
|
||||||
switch sc.Service.ServiceType {
|
|
||||||
case internetgateway2.URN_WANIPConnection_1:
|
|
||||||
return &upnp{service: "IGDv2-IP1", client: &internetgateway2.WANIPConnection1{ServiceClient: sc}}
|
|
||||||
case internetgateway2.URN_WANIPConnection_2:
|
|
||||||
return &upnp{service: "IGDv2-IP2", client: &internetgateway2.WANIPConnection2{ServiceClient: sc}}
|
|
||||||
case internetgateway2.URN_WANPPPConnection_1:
|
|
||||||
return &upnp{service: "IGDv2-PPP1", client: &internetgateway2.WANPPPConnection1{ServiceClient: sc}}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
for i := 0; i < cap(found); i++ {
|
|
||||||
if c := <-found; c != nil {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// finds devices matching the given target and calls matcher for all
|
|
||||||
// advertised services of each device. The first non-nil service found
|
|
||||||
// is sent into out. If no service matched, nil is sent.
|
|
||||||
func discover(out chan<- *upnp, target string, matcher func(goupnp.ServiceClient) *upnp) {
|
|
||||||
devs, err := goupnp.DiscoverDevices(target)
|
|
||||||
if err != nil {
|
|
||||||
out <- nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
found := false
|
|
||||||
for i := 0; i < len(devs) && !found; i++ {
|
|
||||||
if devs[i].Root == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
devs[i].Root.Device.VisitServices(func(service *goupnp.Service) {
|
|
||||||
if found {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// check for a matching IGD service
|
|
||||||
sc := goupnp.ServiceClient{
|
|
||||||
SOAPClient: service.NewSOAPClient(),
|
|
||||||
RootDevice: devs[i].Root,
|
|
||||||
Location: devs[i].Location,
|
|
||||||
Service: service,
|
|
||||||
}
|
|
||||||
sc.SOAPClient.HTTPClient.Timeout = soapRequestTimeout
|
|
||||||
upnp := matcher(sc)
|
|
||||||
if upnp == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
upnp.dev = devs[i].Root
|
|
||||||
|
|
||||||
// check whether port mapping is enabled
|
|
||||||
if upnp.natEnabled() {
|
|
||||||
out <- upnp
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
out <- nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,249 +0,0 @@
|
||||||
// Copyright 2015 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package nat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/huin/goupnp/httpu"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUPNP_DDWRT(t *testing.T) {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
t.Skipf("disabled to avoid firewall prompt")
|
|
||||||
}
|
|
||||||
|
|
||||||
dev := &fakeIGD{
|
|
||||||
t: t,
|
|
||||||
ssdpResp: "HTTP/1.1 200 OK\r\n" +
|
|
||||||
"Cache-Control: max-age=300\r\n" +
|
|
||||||
"Date: Sun, 10 May 2015 10:05:33 GMT\r\n" +
|
|
||||||
"Ext: \r\n" +
|
|
||||||
"Location: http://{{listenAddr}}/InternetGatewayDevice.xml\r\n" +
|
|
||||||
"Server: POSIX UPnP/1.0 DD-WRT Linux/V24\r\n" +
|
|
||||||
"ST: urn:schemas-upnp-org:device:WANConnectionDevice:1\r\n" +
|
|
||||||
"USN: uuid:CB2471CC-CF2E-9795-8D9C-E87B34C16800::urn:schemas-upnp-org:device:WANConnectionDevice:1\r\n" +
|
|
||||||
"\r\n",
|
|
||||||
httpResps: map[string]string{
|
|
||||||
"GET /InternetGatewayDevice.xml": `
|
|
||||||
<?xml version="1.0"?>
|
|
||||||
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
|
||||||
<specVersion>
|
|
||||||
<major>1</major>
|
|
||||||
<minor>0</minor>
|
|
||||||
</specVersion>
|
|
||||||
<device>
|
|
||||||
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
|
|
||||||
<manufacturer>DD-WRT</manufacturer>
|
|
||||||
<manufacturerURL>http://www.dd-wrt.com</manufacturerURL>
|
|
||||||
<modelDescription>Gateway</modelDescription>
|
|
||||||
<friendlyName>Asus RT-N16:DD-WRT</friendlyName>
|
|
||||||
<modelName>Asus RT-N16</modelName>
|
|
||||||
<modelNumber>V24</modelNumber>
|
|
||||||
<serialNumber>0000001</serialNumber>
|
|
||||||
<modelURL>http://www.dd-wrt.com</modelURL>
|
|
||||||
<UDN>uuid:A13AB4C3-3A14-E386-DE6A-EFEA923A06FE</UDN>
|
|
||||||
<serviceList>
|
|
||||||
<service>
|
|
||||||
<serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType>
|
|
||||||
<serviceId>urn:upnp-org:serviceId:L3Forwarding1</serviceId>
|
|
||||||
<SCPDURL>/x_layer3forwarding.xml</SCPDURL>
|
|
||||||
<controlURL>/control?Layer3Forwarding</controlURL>
|
|
||||||
<eventSubURL>/event?Layer3Forwarding</eventSubURL>
|
|
||||||
</service>
|
|
||||||
</serviceList>
|
|
||||||
<deviceList>
|
|
||||||
<device>
|
|
||||||
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
|
|
||||||
<friendlyName>WANDevice</friendlyName>
|
|
||||||
<manufacturer>DD-WRT</manufacturer>
|
|
||||||
<manufacturerURL>http://www.dd-wrt.com</manufacturerURL>
|
|
||||||
<modelDescription>Gateway</modelDescription>
|
|
||||||
<modelName>router</modelName>
|
|
||||||
<modelURL>http://www.dd-wrt.com</modelURL>
|
|
||||||
<UDN>uuid:48FD569B-F9A9-96AE-4EE6-EB403D3DB91A</UDN>
|
|
||||||
<serviceList>
|
|
||||||
<service>
|
|
||||||
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
|
|
||||||
<serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
|
|
||||||
<SCPDURL>/x_wancommoninterfaceconfig.xml</SCPDURL>
|
|
||||||
<controlURL>/control?WANCommonInterfaceConfig</controlURL>
|
|
||||||
<eventSubURL>/event?WANCommonInterfaceConfig</eventSubURL>
|
|
||||||
</service>
|
|
||||||
</serviceList>
|
|
||||||
<deviceList>
|
|
||||||
<device>
|
|
||||||
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
|
|
||||||
<friendlyName>WAN Connection Device</friendlyName>
|
|
||||||
<manufacturer>DD-WRT</manufacturer>
|
|
||||||
<manufacturerURL>http://www.dd-wrt.com</manufacturerURL>
|
|
||||||
<modelDescription>Gateway</modelDescription>
|
|
||||||
<modelName>router</modelName>
|
|
||||||
<modelURL>http://www.dd-wrt.com</modelURL>
|
|
||||||
<UDN>uuid:CB2471CC-CF2E-9795-8D9C-E87B34C16800</UDN>
|
|
||||||
<serviceList>
|
|
||||||
<service>
|
|
||||||
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
|
|
||||||
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
|
|
||||||
<SCPDURL>/x_wanipconnection.xml</SCPDURL>
|
|
||||||
<controlURL>/control?WANIPConnection</controlURL>
|
|
||||||
<eventSubURL>/event?WANIPConnection</eventSubURL>
|
|
||||||
</service>
|
|
||||||
</serviceList>
|
|
||||||
</device>
|
|
||||||
</deviceList>
|
|
||||||
</device>
|
|
||||||
<device>
|
|
||||||
<deviceType>urn:schemas-upnp-org:device:LANDevice:1</deviceType>
|
|
||||||
<friendlyName>LANDevice</friendlyName>
|
|
||||||
<manufacturer>DD-WRT</manufacturer>
|
|
||||||
<manufacturerURL>http://www.dd-wrt.com</manufacturerURL>
|
|
||||||
<modelDescription>Gateway</modelDescription>
|
|
||||||
<modelName>router</modelName>
|
|
||||||
<modelURL>http://www.dd-wrt.com</modelURL>
|
|
||||||
<UDN>uuid:04021998-3B35-2BDB-7B3C-99DA4435DA09</UDN>
|
|
||||||
<serviceList>
|
|
||||||
<service>
|
|
||||||
<serviceType>urn:schemas-upnp-org:service:LANHostConfigManagement:1</serviceType>
|
|
||||||
<serviceId>urn:upnp-org:serviceId:LANHostCfg1</serviceId>
|
|
||||||
<SCPDURL>/x_lanhostconfigmanagement.xml</SCPDURL>
|
|
||||||
<controlURL>/control?LANHostConfigManagement</controlURL>
|
|
||||||
<eventSubURL>/event?LANHostConfigManagement</eventSubURL>
|
|
||||||
</service>
|
|
||||||
</serviceList>
|
|
||||||
</device>
|
|
||||||
</deviceList>
|
|
||||||
<presentationURL>http://{{listenAddr}}</presentationURL>
|
|
||||||
</device>
|
|
||||||
</root>
|
|
||||||
`,
|
|
||||||
// The response to our GetNATRSIPStatus call. This
|
|
||||||
// particular implementation has a bug where the elements
|
|
||||||
// inside u:GetNATRSIPStatusResponse are not properly
|
|
||||||
// namespaced.
|
|
||||||
"POST /control?WANIPConnection": `
|
|
||||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
||||||
<s:Body>
|
|
||||||
<u:GetNATRSIPStatusResponse xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
|
|
||||||
<NewRSIPAvailable>0</NewRSIPAvailable>
|
|
||||||
<NewNATEnabled>1</NewNATEnabled>
|
|
||||||
</u:GetNATRSIPStatusResponse>
|
|
||||||
</s:Body>
|
|
||||||
</s:Envelope>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if err := dev.listen(); err != nil {
|
|
||||||
t.Skipf("cannot listen: %v", err)
|
|
||||||
}
|
|
||||||
dev.serve()
|
|
||||||
defer dev.close()
|
|
||||||
|
|
||||||
// Attempt to discover the fake device.
|
|
||||||
discovered := discoverUPnP()
|
|
||||||
if discovered == nil {
|
|
||||||
if os.Getenv("CI") != "" {
|
|
||||||
t.Fatalf("not discovered")
|
|
||||||
} else {
|
|
||||||
t.Skipf("UPnP not discovered (known issue, see https://github.com/ethereum/go-ethereum/issues/21476)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
upnp, _ := discovered.(*upnp)
|
|
||||||
if upnp.service != "IGDv1-IP1" {
|
|
||||||
t.Errorf("upnp.service mismatch: got %q, want %q", upnp.service, "IGDv1-IP1")
|
|
||||||
}
|
|
||||||
wantURL := "http://" + dev.listener.Addr().String() + "/InternetGatewayDevice.xml"
|
|
||||||
if upnp.dev.URLBaseStr != wantURL {
|
|
||||||
t.Errorf("upnp.dev.URLBaseStr mismatch: got %q, want %q", upnp.dev.URLBaseStr, wantURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fakeIGD presents itself as a discoverable UPnP device which sends
|
|
||||||
// canned responses to HTTPU and HTTP requests.
|
|
||||||
type fakeIGD struct {
|
|
||||||
t *testing.T // for logging
|
|
||||||
|
|
||||||
listener net.Listener
|
|
||||||
mcastListener *net.UDPConn
|
|
||||||
|
|
||||||
// This should be a complete HTTP response (including headers).
|
|
||||||
// It is sent as the response to any sspd packet. Any occurrence
|
|
||||||
// of "{{listenAddr}}" is replaced with the actual TCP listen
|
|
||||||
// address of the HTTP server.
|
|
||||||
ssdpResp string
|
|
||||||
// This one should contain XML payloads for all requests
|
|
||||||
// performed. The keys contain method and path, e.g. "GET /foo/bar".
|
|
||||||
// As with ssdpResp, "{{listenAddr}}" is replaced with the TCP
|
|
||||||
// listen address.
|
|
||||||
httpResps map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// httpu.Handler
|
|
||||||
func (dev *fakeIGD) ServeMessage(r *http.Request) {
|
|
||||||
dev.t.Logf(`HTTPU request %s %s`, r.Method, r.RequestURI)
|
|
||||||
conn, err := net.Dial("udp4", r.RemoteAddr)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("reply Dial error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
io.WriteString(conn, dev.replaceListenAddr(dev.ssdpResp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// http.Handler
|
|
||||||
func (dev *fakeIGD) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if resp, ok := dev.httpResps[r.Method+" "+r.RequestURI]; ok {
|
|
||||||
dev.t.Logf(`HTTP request "%s %s" --> %d`, r.Method, r.RequestURI, 200)
|
|
||||||
io.WriteString(w, dev.replaceListenAddr(resp))
|
|
||||||
} else {
|
|
||||||
dev.t.Logf(`HTTP request "%s %s" --> %d`, r.Method, r.RequestURI, 404)
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dev *fakeIGD) replaceListenAddr(resp string) string {
|
|
||||||
return strings.ReplaceAll(resp, "{{listenAddr}}", dev.listener.Addr().String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dev *fakeIGD) listen() (err error) {
|
|
||||||
if dev.listener, err = net.Listen("tcp", "127.0.0.1:0"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
laddr := &net.UDPAddr{IP: net.ParseIP("239.255.255.250"), Port: 1900}
|
|
||||||
if dev.mcastListener, err = net.ListenMulticastUDP("udp", nil, laddr); err != nil {
|
|
||||||
dev.listener.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dev *fakeIGD) serve() {
|
|
||||||
go httpu.Serve(dev.mcastListener, dev)
|
|
||||||
go http.Serve(dev.listener, dev)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dev *fakeIGD) close() {
|
|
||||||
dev.mcastListener.Close()
|
|
||||||
dev.listener.Close()
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
// Copyright 2019 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package netutil
|
|
||||||
|
|
||||||
import "net"
|
|
||||||
|
|
||||||
// AddrIP gets the IP address contained in addr. It returns nil if no address is present.
|
|
||||||
func AddrIP(addr net.Addr) net.IP {
|
|
||||||
switch a := addr.(type) {
|
|
||||||
case *net.IPAddr:
|
|
||||||
return a.IP
|
|
||||||
case *net.TCPAddr:
|
|
||||||
return a.IP
|
|
||||||
case *net.UDPAddr:
|
|
||||||
return a.IP
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
// Copyright 2016 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package netutil
|
|
||||||
|
|
||||||
// IsTemporaryError checks whether the given error should be considered temporary.
|
|
||||||
func IsTemporaryError(err error) bool {
|
|
||||||
tempErr, ok := err.(interface {
|
|
||||||
Temporary() bool
|
|
||||||
})
|
|
||||||
return ok && tempErr.Temporary() || isPacketTooBig(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsTimeout checks whether the given error is a timeout.
|
|
||||||
func IsTimeout(err error) bool {
|
|
||||||
timeoutErr, ok := err.(interface {
|
|
||||||
Timeout() bool
|
|
||||||
})
|
|
||||||
return ok && timeoutErr.Timeout()
|
|
||||||
}
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
// Copyright 2016 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package netutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This test checks that isPacketTooBig correctly identifies
|
|
||||||
// errors that result from receiving a UDP packet larger
|
|
||||||
// than the supplied receive buffer.
|
|
||||||
func TestIsPacketTooBig(t *testing.T) {
|
|
||||||
listener, err := net.ListenPacket("udp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer listener.Close()
|
|
||||||
sender, err := net.Dial("udp", listener.LocalAddr().String())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer sender.Close()
|
|
||||||
|
|
||||||
sendN := 1800
|
|
||||||
recvN := 300
|
|
||||||
for i := 0; i < 20; i++ {
|
|
||||||
go func() {
|
|
||||||
buf := make([]byte, sendN)
|
|
||||||
for i := range buf {
|
|
||||||
buf[i] = byte(i)
|
|
||||||
}
|
|
||||||
sender.Write(buf)
|
|
||||||
}()
|
|
||||||
|
|
||||||
buf := make([]byte, recvN)
|
|
||||||
listener.SetDeadline(time.Now().Add(1 * time.Second))
|
|
||||||
n, _, err := listener.ReadFrom(buf)
|
|
||||||
if err != nil {
|
|
||||||
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !isPacketTooBig(err) {
|
|
||||||
t.Fatalf("unexpected read error: %v", err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if n != recvN {
|
|
||||||
t.Fatalf("short read: %d, want %d", n, recvN)
|
|
||||||
}
|
|
||||||
for i := range buf {
|
|
||||||
if buf[i] != byte(i) {
|
|
||||||
t.Fatalf("error in pattern")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package netutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IPTracker predicts the external endpoint, i.e. IP address and port, of the local host
|
|
||||||
// based on statements made by other hosts.
|
|
||||||
type IPTracker struct {
|
|
||||||
window time.Duration
|
|
||||||
contactWindow time.Duration
|
|
||||||
minStatements int
|
|
||||||
clock mclock.Clock
|
|
||||||
statements map[string]ipStatement
|
|
||||||
contact map[string]mclock.AbsTime
|
|
||||||
lastStatementGC mclock.AbsTime
|
|
||||||
lastContactGC mclock.AbsTime
|
|
||||||
}
|
|
||||||
|
|
||||||
type ipStatement struct {
|
|
||||||
endpoint string
|
|
||||||
time mclock.AbsTime
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewIPTracker creates an IP tracker.
|
|
||||||
//
|
|
||||||
// The window parameters configure the amount of past network events which are kept. The
|
|
||||||
// minStatements parameter enforces a minimum number of statements which must be recorded
|
|
||||||
// before any prediction is made. Higher values for these parameters decrease 'flapping' of
|
|
||||||
// predictions as network conditions change. Window duration values should typically be in
|
|
||||||
// the range of minutes.
|
|
||||||
func NewIPTracker(window, contactWindow time.Duration, minStatements int) *IPTracker {
|
|
||||||
return &IPTracker{
|
|
||||||
window: window,
|
|
||||||
contactWindow: contactWindow,
|
|
||||||
statements: make(map[string]ipStatement),
|
|
||||||
minStatements: minStatements,
|
|
||||||
contact: make(map[string]mclock.AbsTime),
|
|
||||||
clock: mclock.System{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PredictFullConeNAT checks whether the local host is behind full cone NAT. It predicts by
|
|
||||||
// checking whether any statement has been received from a node we didn't contact before
|
|
||||||
// the statement was made.
|
|
||||||
func (it *IPTracker) PredictFullConeNAT() bool {
|
|
||||||
now := it.clock.Now()
|
|
||||||
it.gcContact(now)
|
|
||||||
it.gcStatements(now)
|
|
||||||
for host, st := range it.statements {
|
|
||||||
if c, ok := it.contact[host]; !ok || c > st.time {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// PredictEndpoint returns the current prediction of the external endpoint.
|
|
||||||
func (it *IPTracker) PredictEndpoint() string {
|
|
||||||
it.gcStatements(it.clock.Now())
|
|
||||||
|
|
||||||
// The current strategy is simple: find the endpoint with most statements.
|
|
||||||
counts := make(map[string]int, len(it.statements))
|
|
||||||
maxcount, max := 0, ""
|
|
||||||
for _, s := range it.statements {
|
|
||||||
c := counts[s.endpoint] + 1
|
|
||||||
counts[s.endpoint] = c
|
|
||||||
if c > maxcount && c >= it.minStatements {
|
|
||||||
maxcount, max = c, s.endpoint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddStatement records that a certain host thinks our external endpoint is the one given.
|
|
||||||
func (it *IPTracker) AddStatement(host, endpoint string) {
|
|
||||||
now := it.clock.Now()
|
|
||||||
it.statements[host] = ipStatement{endpoint, now}
|
|
||||||
if time.Duration(now-it.lastStatementGC) >= it.window {
|
|
||||||
it.gcStatements(now)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddContact records that a packet containing our endpoint information has been sent to a
|
|
||||||
// certain host.
|
|
||||||
func (it *IPTracker) AddContact(host string) {
|
|
||||||
now := it.clock.Now()
|
|
||||||
it.contact[host] = now
|
|
||||||
if time.Duration(now-it.lastContactGC) >= it.contactWindow {
|
|
||||||
it.gcContact(now)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *IPTracker) gcStatements(now mclock.AbsTime) {
|
|
||||||
it.lastStatementGC = now
|
|
||||||
cutoff := now.Add(-it.window)
|
|
||||||
for host, s := range it.statements {
|
|
||||||
if s.time < cutoff {
|
|
||||||
delete(it.statements, host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *IPTracker) gcContact(now mclock.AbsTime) {
|
|
||||||
it.lastContactGC = now
|
|
||||||
cutoff := now.Add(-it.contactWindow)
|
|
||||||
for host, ct := range it.contact {
|
|
||||||
if ct < cutoff {
|
|
||||||
delete(it.contact, host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package netutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
crand "crypto/rand"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
opStatement = iota
|
|
||||||
opContact
|
|
||||||
opPredict
|
|
||||||
opCheckFullCone
|
|
||||||
)
|
|
||||||
|
|
||||||
type iptrackTestEvent struct {
|
|
||||||
op int
|
|
||||||
time int // absolute, in milliseconds
|
|
||||||
ip, from string
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIPTracker(t *testing.T) {
|
|
||||||
tests := map[string][]iptrackTestEvent{
|
|
||||||
"minStatements": {
|
|
||||||
{opPredict, 0, "", ""},
|
|
||||||
{opStatement, 0, "127.0.0.1", "127.0.0.2"},
|
|
||||||
{opPredict, 1000, "", ""},
|
|
||||||
{opStatement, 1000, "127.0.0.1", "127.0.0.3"},
|
|
||||||
{opPredict, 1000, "", ""},
|
|
||||||
{opStatement, 1000, "127.0.0.1", "127.0.0.4"},
|
|
||||||
{opPredict, 1000, "127.0.0.1", ""},
|
|
||||||
},
|
|
||||||
"window": {
|
|
||||||
{opStatement, 0, "127.0.0.1", "127.0.0.2"},
|
|
||||||
{opStatement, 2000, "127.0.0.1", "127.0.0.3"},
|
|
||||||
{opStatement, 3000, "127.0.0.1", "127.0.0.4"},
|
|
||||||
{opPredict, 10000, "127.0.0.1", ""},
|
|
||||||
{opPredict, 10001, "", ""}, // first statement expired
|
|
||||||
{opStatement, 10100, "127.0.0.1", "127.0.0.2"},
|
|
||||||
{opPredict, 10200, "127.0.0.1", ""},
|
|
||||||
},
|
|
||||||
"fullcone": {
|
|
||||||
{opContact, 0, "", "127.0.0.2"},
|
|
||||||
{opStatement, 10, "127.0.0.1", "127.0.0.2"},
|
|
||||||
{opContact, 2000, "", "127.0.0.3"},
|
|
||||||
{opStatement, 2010, "127.0.0.1", "127.0.0.3"},
|
|
||||||
{opContact, 3000, "", "127.0.0.4"},
|
|
||||||
{opStatement, 3010, "127.0.0.1", "127.0.0.4"},
|
|
||||||
{opCheckFullCone, 3500, "false", ""},
|
|
||||||
},
|
|
||||||
"fullcone_2": {
|
|
||||||
{opContact, 0, "", "127.0.0.2"},
|
|
||||||
{opStatement, 10, "127.0.0.1", "127.0.0.2"},
|
|
||||||
{opContact, 2000, "", "127.0.0.3"},
|
|
||||||
{opStatement, 2010, "127.0.0.1", "127.0.0.3"},
|
|
||||||
{opStatement, 3000, "127.0.0.1", "127.0.0.4"},
|
|
||||||
{opContact, 3010, "", "127.0.0.4"},
|
|
||||||
{opCheckFullCone, 3500, "true", ""},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for name, test := range tests {
|
|
||||||
t.Run(name, func(t *testing.T) { runIPTrackerTest(t, test) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runIPTrackerTest(t *testing.T, evs []iptrackTestEvent) {
|
|
||||||
var (
|
|
||||||
clock mclock.Simulated
|
|
||||||
it = NewIPTracker(10*time.Second, 10*time.Second, 3)
|
|
||||||
)
|
|
||||||
it.clock = &clock
|
|
||||||
for i, ev := range evs {
|
|
||||||
evtime := time.Duration(ev.time) * time.Millisecond
|
|
||||||
clock.Run(evtime - time.Duration(clock.Now()))
|
|
||||||
switch ev.op {
|
|
||||||
case opStatement:
|
|
||||||
it.AddStatement(ev.from, ev.ip)
|
|
||||||
case opContact:
|
|
||||||
it.AddContact(ev.from)
|
|
||||||
case opPredict:
|
|
||||||
if pred := it.PredictEndpoint(); pred != ev.ip {
|
|
||||||
t.Errorf("op %d: wrong prediction %q, want %q", i, pred, ev.ip)
|
|
||||||
}
|
|
||||||
case opCheckFullCone:
|
|
||||||
pred := fmt.Sprintf("%t", it.PredictFullConeNAT())
|
|
||||||
if pred != ev.ip {
|
|
||||||
t.Errorf("op %d: wrong prediction %s, want %s", i, pred, ev.ip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This checks that old statements and contacts are GCed even if Predict* isn't called.
|
|
||||||
func TestIPTrackerForceGC(t *testing.T) {
|
|
||||||
var (
|
|
||||||
clock mclock.Simulated
|
|
||||||
window = 10 * time.Second
|
|
||||||
rate = 50 * time.Millisecond
|
|
||||||
max = int(window/rate) + 1
|
|
||||||
it = NewIPTracker(window, window, 3)
|
|
||||||
)
|
|
||||||
it.clock = &clock
|
|
||||||
|
|
||||||
for i := 0; i < 5*max; i++ {
|
|
||||||
e1 := make([]byte, 4)
|
|
||||||
e2 := make([]byte, 4)
|
|
||||||
crand.Read(e1)
|
|
||||||
crand.Read(e2)
|
|
||||||
it.AddStatement(string(e1), string(e2))
|
|
||||||
it.AddContact(string(e1))
|
|
||||||
clock.Run(rate)
|
|
||||||
}
|
|
||||||
if len(it.contact) > 2*max {
|
|
||||||
t.Errorf("contacts not GCed, have %d", len(it.contact))
|
|
||||||
}
|
|
||||||
if len(it.statements) > 2*max {
|
|
||||||
t.Errorf("statements not GCed, have %d", len(it.statements))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,322 +0,0 @@
|
||||||
// Copyright 2016 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Package netutil contains extensions to the net package.
|
|
||||||
package netutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var lan4, lan6, special4, special6 Netlist
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Lists from RFC 5735, RFC 5156,
|
|
||||||
// https://www.iana.org/assignments/iana-ipv4-special-registry/
|
|
||||||
lan4.Add("0.0.0.0/8") // "This" network
|
|
||||||
lan4.Add("10.0.0.0/8") // Private Use
|
|
||||||
lan4.Add("172.16.0.0/12") // Private Use
|
|
||||||
lan4.Add("192.168.0.0/16") // Private Use
|
|
||||||
lan6.Add("fe80::/10") // Link-Local
|
|
||||||
lan6.Add("fc00::/7") // Unique-Local
|
|
||||||
special4.Add("192.0.0.0/29") // IPv4 Service Continuity
|
|
||||||
special4.Add("192.0.0.9/32") // PCP Anycast
|
|
||||||
special4.Add("192.0.0.170/32") // NAT64/DNS64 Discovery
|
|
||||||
special4.Add("192.0.0.171/32") // NAT64/DNS64 Discovery
|
|
||||||
special4.Add("192.0.2.0/24") // TEST-NET-1
|
|
||||||
special4.Add("192.31.196.0/24") // AS112
|
|
||||||
special4.Add("192.52.193.0/24") // AMT
|
|
||||||
special4.Add("192.88.99.0/24") // 6to4 Relay Anycast
|
|
||||||
special4.Add("192.175.48.0/24") // AS112
|
|
||||||
special4.Add("198.18.0.0/15") // Device Benchmark Testing
|
|
||||||
special4.Add("198.51.100.0/24") // TEST-NET-2
|
|
||||||
special4.Add("203.0.113.0/24") // TEST-NET-3
|
|
||||||
special4.Add("255.255.255.255/32") // Limited Broadcast
|
|
||||||
|
|
||||||
// http://www.iana.org/assignments/iana-ipv6-special-registry/
|
|
||||||
special6.Add("100::/64")
|
|
||||||
special6.Add("2001::/32")
|
|
||||||
special6.Add("2001:1::1/128")
|
|
||||||
special6.Add("2001:2::/48")
|
|
||||||
special6.Add("2001:3::/32")
|
|
||||||
special6.Add("2001:4:112::/48")
|
|
||||||
special6.Add("2001:5::/32")
|
|
||||||
special6.Add("2001:10::/28")
|
|
||||||
special6.Add("2001:20::/28")
|
|
||||||
special6.Add("2001:db8::/32")
|
|
||||||
special6.Add("2002::/16")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Netlist is a list of IP networks.
|
|
||||||
type Netlist []net.IPNet
|
|
||||||
|
|
||||||
// ParseNetlist parses a comma-separated list of CIDR masks.
|
|
||||||
// Whitespace and extra commas are ignored.
|
|
||||||
func ParseNetlist(s string) (*Netlist, error) {
|
|
||||||
ws := strings.NewReplacer(" ", "", "\n", "", "\t", "")
|
|
||||||
masks := strings.Split(ws.Replace(s), ",")
|
|
||||||
l := make(Netlist, 0)
|
|
||||||
for _, mask := range masks {
|
|
||||||
if mask == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
_, n, err := net.ParseCIDR(mask)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
l = append(l, *n)
|
|
||||||
}
|
|
||||||
return &l, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalTOML implements toml.MarshalerRec.
|
|
||||||
func (l Netlist) MarshalTOML() interface{} {
|
|
||||||
list := make([]string, 0, len(l))
|
|
||||||
for _, net := range l {
|
|
||||||
list = append(list, net.String())
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalTOML implements toml.UnmarshalerRec.
|
|
||||||
func (l *Netlist) UnmarshalTOML(fn func(interface{}) error) error {
|
|
||||||
var masks []string
|
|
||||||
if err := fn(&masks); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, mask := range masks {
|
|
||||||
_, n, err := net.ParseCIDR(mask)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*l = append(*l, *n)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add parses a CIDR mask and appends it to the list. It panics for invalid masks and is
|
|
||||||
// intended to be used for setting up static lists.
|
|
||||||
func (l *Netlist) Add(cidr string) {
|
|
||||||
_, n, err := net.ParseCIDR(cidr)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
*l = append(*l, *n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains reports whether the given IP is contained in the list.
|
|
||||||
func (l *Netlist) Contains(ip net.IP) bool {
|
|
||||||
if l == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, net := range *l {
|
|
||||||
if net.Contains(ip) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsLAN reports whether an IP is a local network address.
|
|
||||||
func IsLAN(ip net.IP) bool {
|
|
||||||
if ip.IsLoopback() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if v4 := ip.To4(); v4 != nil {
|
|
||||||
return lan4.Contains(v4)
|
|
||||||
}
|
|
||||||
return lan6.Contains(ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsSpecialNetwork reports whether an IP is located in a special-use network range
|
|
||||||
// This includes broadcast, multicast and documentation addresses.
|
|
||||||
func IsSpecialNetwork(ip net.IP) bool {
|
|
||||||
if ip.IsMulticast() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if v4 := ip.To4(); v4 != nil {
|
|
||||||
return special4.Contains(v4)
|
|
||||||
}
|
|
||||||
return special6.Contains(ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
errInvalid = errors.New("invalid IP")
|
|
||||||
errUnspecified = errors.New("zero address")
|
|
||||||
errSpecial = errors.New("special network")
|
|
||||||
errLoopback = errors.New("loopback address from non-loopback host")
|
|
||||||
errLAN = errors.New("LAN address from WAN host")
|
|
||||||
)
|
|
||||||
|
|
||||||
// CheckRelayIP reports whether an IP relayed from the given sender IP
|
|
||||||
// is a valid connection target.
|
|
||||||
//
|
|
||||||
// There are four rules:
|
|
||||||
// - Special network addresses are never valid.
|
|
||||||
// - Loopback addresses are OK if relayed by a loopback host.
|
|
||||||
// - LAN addresses are OK if relayed by a LAN host.
|
|
||||||
// - All other addresses are always acceptable.
|
|
||||||
func CheckRelayIP(sender, addr net.IP) error {
|
|
||||||
if len(addr) != net.IPv4len && len(addr) != net.IPv6len {
|
|
||||||
return errInvalid
|
|
||||||
}
|
|
||||||
if addr.IsUnspecified() {
|
|
||||||
return errUnspecified
|
|
||||||
}
|
|
||||||
if IsSpecialNetwork(addr) {
|
|
||||||
return errSpecial
|
|
||||||
}
|
|
||||||
if addr.IsLoopback() && !sender.IsLoopback() {
|
|
||||||
return errLoopback
|
|
||||||
}
|
|
||||||
if IsLAN(addr) && !IsLAN(sender) {
|
|
||||||
return errLAN
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SameNet reports whether two IP addresses have an equal prefix of the given bit length.
|
|
||||||
func SameNet(bits uint, ip, other net.IP) bool {
|
|
||||||
ip4, other4 := ip.To4(), other.To4()
|
|
||||||
switch {
|
|
||||||
case (ip4 == nil) != (other4 == nil):
|
|
||||||
return false
|
|
||||||
case ip4 != nil:
|
|
||||||
return sameNet(bits, ip4, other4)
|
|
||||||
default:
|
|
||||||
return sameNet(bits, ip.To16(), other.To16())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sameNet(bits uint, ip, other net.IP) bool {
|
|
||||||
nb := int(bits / 8)
|
|
||||||
mask := ^byte(0xFF >> (bits % 8))
|
|
||||||
if mask != 0 && nb < len(ip) && ip[nb]&mask != other[nb]&mask {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return nb <= len(ip) && ip[:nb].Equal(other[:nb])
|
|
||||||
}
|
|
||||||
|
|
||||||
// DistinctNetSet tracks IPs, ensuring that at most N of them
|
|
||||||
// fall into the same network range.
|
|
||||||
type DistinctNetSet struct {
|
|
||||||
Subnet uint // number of common prefix bits
|
|
||||||
Limit uint // maximum number of IPs in each subnet
|
|
||||||
|
|
||||||
members map[string]uint
|
|
||||||
buf net.IP
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds an IP address to the set. It returns false (and doesn't add the IP) if the
|
|
||||||
// number of existing IPs in the defined range exceeds the limit.
|
|
||||||
func (s *DistinctNetSet) Add(ip net.IP) bool {
|
|
||||||
key := s.key(ip)
|
|
||||||
n := s.members[string(key)]
|
|
||||||
if n < s.Limit {
|
|
||||||
s.members[string(key)] = n + 1
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes an IP from the set.
|
|
||||||
func (s *DistinctNetSet) Remove(ip net.IP) {
|
|
||||||
key := s.key(ip)
|
|
||||||
if n, ok := s.members[string(key)]; ok {
|
|
||||||
if n == 1 {
|
|
||||||
delete(s.members, string(key))
|
|
||||||
} else {
|
|
||||||
s.members[string(key)] = n - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains whether the given IP is contained in the set.
|
|
||||||
func (s DistinctNetSet) Contains(ip net.IP) bool {
|
|
||||||
key := s.key(ip)
|
|
||||||
_, ok := s.members[string(key)]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the number of tracked IPs.
|
|
||||||
func (s DistinctNetSet) Len() int {
|
|
||||||
n := uint(0)
|
|
||||||
for _, i := range s.members {
|
|
||||||
n += i
|
|
||||||
}
|
|
||||||
return int(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// key encodes the map key for an address into a temporary buffer.
|
|
||||||
//
|
|
||||||
// The first byte of key is '4' or '6' to distinguish IPv4/IPv6 address types.
|
|
||||||
// The remainder of the key is the IP, truncated to the number of bits.
|
|
||||||
func (s *DistinctNetSet) key(ip net.IP) net.IP {
|
|
||||||
// Lazily initialize storage.
|
|
||||||
if s.members == nil {
|
|
||||||
s.members = make(map[string]uint)
|
|
||||||
s.buf = make(net.IP, 17)
|
|
||||||
}
|
|
||||||
// Canonicalize ip and bits.
|
|
||||||
typ := byte('6')
|
|
||||||
if ip4 := ip.To4(); ip4 != nil {
|
|
||||||
typ, ip = '4', ip4
|
|
||||||
}
|
|
||||||
bits := s.Subnet
|
|
||||||
if bits > uint(len(ip)*8) {
|
|
||||||
bits = uint(len(ip) * 8)
|
|
||||||
}
|
|
||||||
// Encode the prefix into s.buf.
|
|
||||||
nb := int(bits / 8)
|
|
||||||
mask := ^byte(0xFF >> (bits % 8))
|
|
||||||
s.buf[0] = typ
|
|
||||||
buf := append(s.buf[:1], ip[:nb]...)
|
|
||||||
if nb < len(ip) && mask != 0 {
|
|
||||||
buf = append(buf, ip[nb]&mask)
|
|
||||||
}
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
// String implements fmt.Stringer
|
|
||||||
func (s DistinctNetSet) String() string {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
buf.WriteString("{")
|
|
||||||
keys := make([]string, 0, len(s.members))
|
|
||||||
for k := range s.members {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
for i, k := range keys {
|
|
||||||
var ip net.IP
|
|
||||||
if k[0] == '4' {
|
|
||||||
ip = make(net.IP, 4)
|
|
||||||
} else {
|
|
||||||
ip = make(net.IP, 16)
|
|
||||||
}
|
|
||||||
copy(ip, k[1:])
|
|
||||||
fmt.Fprintf(&buf, "%v×%d", ip, s.members[k])
|
|
||||||
if i != len(keys)-1 {
|
|
||||||
buf.WriteString(" ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buf.WriteString("}")
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
@ -1,262 +0,0 @@
|
||||||
// Copyright 2016 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package netutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"testing/quick"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseNetlist(t *testing.T) {
|
|
||||||
var tests = []struct {
|
|
||||||
input string
|
|
||||||
wantErr error
|
|
||||||
wantList *Netlist
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
input: "",
|
|
||||||
wantList: &Netlist{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "127.0.0.0/8",
|
|
||||||
wantErr: nil,
|
|
||||||
wantList: &Netlist{{IP: net.IP{127, 0, 0, 0}, Mask: net.CIDRMask(8, 32)}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "127.0.0.0/44",
|
|
||||||
wantErr: &net.ParseError{Type: "CIDR address", Text: "127.0.0.0/44"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "127.0.0.0/16, 23.23.23.23/24,",
|
|
||||||
wantList: &Netlist{
|
|
||||||
{IP: net.IP{127, 0, 0, 0}, Mask: net.CIDRMask(16, 32)},
|
|
||||||
{IP: net.IP{23, 23, 23, 0}, Mask: net.CIDRMask(24, 32)},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
l, err := ParseNetlist(test.input)
|
|
||||||
if !reflect.DeepEqual(err, test.wantErr) {
|
|
||||||
t.Errorf("%q: got error %q, want %q", test.input, err, test.wantErr)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(l, test.wantList) {
|
|
||||||
spew.Dump(l)
|
|
||||||
spew.Dump(test.wantList)
|
|
||||||
t.Errorf("%q: got %v, want %v", test.input, l, test.wantList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNilNetListContains(t *testing.T) {
|
|
||||||
var list *Netlist
|
|
||||||
checkContains(t, list.Contains, nil, []string{"1.2.3.4"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsLAN(t *testing.T) {
|
|
||||||
checkContains(t, IsLAN,
|
|
||||||
[]string{ // included
|
|
||||||
"0.0.0.0",
|
|
||||||
"0.2.0.8",
|
|
||||||
"127.0.0.1",
|
|
||||||
"10.0.1.1",
|
|
||||||
"10.22.0.3",
|
|
||||||
"172.31.252.251",
|
|
||||||
"192.168.1.4",
|
|
||||||
"fe80::f4a1:8eff:fec5:9d9d",
|
|
||||||
"febf::ab32:2233",
|
|
||||||
"fc00::4",
|
|
||||||
},
|
|
||||||
[]string{ // excluded
|
|
||||||
"192.0.2.1",
|
|
||||||
"1.0.0.0",
|
|
||||||
"172.32.0.1",
|
|
||||||
"fec0::2233",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsSpecialNetwork(t *testing.T) {
|
|
||||||
checkContains(t, IsSpecialNetwork,
|
|
||||||
[]string{ // included
|
|
||||||
"192.0.2.1",
|
|
||||||
"192.0.2.44",
|
|
||||||
"2001:db8:85a3:8d3:1319:8a2e:370:7348",
|
|
||||||
"255.255.255.255",
|
|
||||||
"224.0.0.22", // IPv4 multicast
|
|
||||||
"ff05::1:3", // IPv6 multicast
|
|
||||||
},
|
|
||||||
[]string{ // excluded
|
|
||||||
"192.0.3.1",
|
|
||||||
"1.0.0.0",
|
|
||||||
"172.32.0.1",
|
|
||||||
"fec0::2233",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkContains(t *testing.T, fn func(net.IP) bool, inc, exc []string) {
|
|
||||||
for _, s := range inc {
|
|
||||||
if !fn(parseIP(s)) {
|
|
||||||
t.Error("returned false for included address", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, s := range exc {
|
|
||||||
if fn(parseIP(s)) {
|
|
||||||
t.Error("returned true for excluded address", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseIP(s string) net.IP {
|
|
||||||
ip := net.ParseIP(s)
|
|
||||||
if ip == nil {
|
|
||||||
panic("invalid " + s)
|
|
||||||
}
|
|
||||||
return ip
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckRelayIP(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
sender, addr string
|
|
||||||
want error
|
|
||||||
}{
|
|
||||||
{"127.0.0.1", "0.0.0.0", errUnspecified},
|
|
||||||
{"192.168.0.1", "0.0.0.0", errUnspecified},
|
|
||||||
{"23.55.1.242", "0.0.0.0", errUnspecified},
|
|
||||||
{"127.0.0.1", "255.255.255.255", errSpecial},
|
|
||||||
{"192.168.0.1", "255.255.255.255", errSpecial},
|
|
||||||
{"23.55.1.242", "255.255.255.255", errSpecial},
|
|
||||||
{"192.168.0.1", "127.0.2.19", errLoopback},
|
|
||||||
{"23.55.1.242", "192.168.0.1", errLAN},
|
|
||||||
|
|
||||||
{"127.0.0.1", "127.0.2.19", nil},
|
|
||||||
{"127.0.0.1", "192.168.0.1", nil},
|
|
||||||
{"127.0.0.1", "23.55.1.242", nil},
|
|
||||||
{"192.168.0.1", "192.168.0.1", nil},
|
|
||||||
{"192.168.0.1", "23.55.1.242", nil},
|
|
||||||
{"23.55.1.242", "23.55.1.242", nil},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
err := CheckRelayIP(parseIP(test.sender), parseIP(test.addr))
|
|
||||||
if err != test.want {
|
|
||||||
t.Errorf("%s from %s: got %q, want %q", test.addr, test.sender, err, test.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkCheckRelayIP(b *testing.B) {
|
|
||||||
sender := parseIP("23.55.1.242")
|
|
||||||
addr := parseIP("23.55.1.2")
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
CheckRelayIP(sender, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSameNet(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
ip, other string
|
|
||||||
bits uint
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{"0.0.0.0", "0.0.0.0", 32, true},
|
|
||||||
{"0.0.0.0", "0.0.0.1", 0, true},
|
|
||||||
{"0.0.0.0", "0.0.0.1", 31, true},
|
|
||||||
{"0.0.0.0", "0.0.0.1", 32, false},
|
|
||||||
{"0.33.0.1", "0.34.0.2", 8, true},
|
|
||||||
{"0.33.0.1", "0.34.0.2", 13, true},
|
|
||||||
{"0.33.0.1", "0.34.0.2", 15, false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
if ok := SameNet(test.bits, parseIP(test.ip), parseIP(test.other)); ok != test.want {
|
|
||||||
t.Errorf("SameNet(%d, %s, %s) == %t, want %t", test.bits, test.ip, test.other, ok, test.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleSameNet() {
|
|
||||||
// This returns true because the IPs are in the same /24 network:
|
|
||||||
fmt.Println(SameNet(24, net.IP{127, 0, 0, 1}, net.IP{127, 0, 0, 3}))
|
|
||||||
// This call returns false:
|
|
||||||
fmt.Println(SameNet(24, net.IP{127, 3, 0, 1}, net.IP{127, 5, 0, 3}))
|
|
||||||
// Output:
|
|
||||||
// true
|
|
||||||
// false
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDistinctNetSet(t *testing.T) {
|
|
||||||
ops := []struct {
|
|
||||||
add, remove string
|
|
||||||
fails bool
|
|
||||||
}{
|
|
||||||
{add: "127.0.0.1"},
|
|
||||||
{add: "127.0.0.2"},
|
|
||||||
{add: "127.0.0.3", fails: true},
|
|
||||||
{add: "127.32.0.1"},
|
|
||||||
{add: "127.32.0.2"},
|
|
||||||
{add: "127.32.0.3", fails: true},
|
|
||||||
{add: "127.33.0.1", fails: true},
|
|
||||||
{add: "127.34.0.1"},
|
|
||||||
{add: "127.34.0.2"},
|
|
||||||
{add: "127.34.0.3", fails: true},
|
|
||||||
// Make room for an address, then add again.
|
|
||||||
{remove: "127.0.0.1"},
|
|
||||||
{add: "127.0.0.3"},
|
|
||||||
{add: "127.0.0.3", fails: true},
|
|
||||||
}
|
|
||||||
|
|
||||||
set := DistinctNetSet{Subnet: 15, Limit: 2}
|
|
||||||
for _, op := range ops {
|
|
||||||
var desc string
|
|
||||||
if op.add != "" {
|
|
||||||
desc = fmt.Sprintf("Add(%s)", op.add)
|
|
||||||
if ok := set.Add(parseIP(op.add)); ok != !op.fails {
|
|
||||||
t.Errorf("%s == %t, want %t", desc, ok, !op.fails)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
desc = fmt.Sprintf("Remove(%s)", op.remove)
|
|
||||||
set.Remove(parseIP(op.remove))
|
|
||||||
}
|
|
||||||
t.Logf("%s: %v", desc, set)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDistinctNetSetAddRemove(t *testing.T) {
|
|
||||||
cfg := &quick.Config{}
|
|
||||||
fn := func(ips []net.IP) bool {
|
|
||||||
s := DistinctNetSet{Limit: 3, Subnet: 2}
|
|
||||||
for _, ip := range ips {
|
|
||||||
s.Add(ip)
|
|
||||||
}
|
|
||||||
for _, ip := range ips {
|
|
||||||
s.Remove(ip)
|
|
||||||
}
|
|
||||||
return s.Len() == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := quick.Check(fn, cfg); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
// Copyright 2016 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
//go:build !windows
|
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package netutil
|
|
||||||
|
|
||||||
// isPacketTooBig reports whether err indicates that a UDP packet didn't
|
|
||||||
// fit the receive buffer. There is no such error on
|
|
||||||
// non-Windows platforms.
|
|
||||||
func isPacketTooBig(err error) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
// Copyright 2016 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
//go:build windows
|
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package netutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
const _WSAEMSGSIZE = syscall.Errno(10040)
|
|
||||||
|
|
||||||
// isPacketTooBig reports whether err indicates that a UDP packet didn't
|
|
||||||
// fit the receive buffer. On Windows, WSARecvFrom returns
|
|
||||||
// code WSAEMSGSIZE and no data if this happens.
|
|
||||||
func isPacketTooBig(err error) bool {
|
|
||||||
if opErr, ok := err.(*net.OpError); ok {
|
|
||||||
if scErr, ok := opErr.Err.(*os.SyscallError); ok {
|
|
||||||
return scErr.Err == _WSAEMSGSIZE
|
|
||||||
}
|
|
||||||
return opErr.Err == _WSAEMSGSIZE
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,407 +0,0 @@
|
||||||
// Copyright 2020 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package nodestate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func testSetup(flagPersist []bool, fieldType []reflect.Type) (*Setup, []Flags, []Field) {
|
|
||||||
setup := &Setup{}
|
|
||||||
flags := make([]Flags, len(flagPersist))
|
|
||||||
for i, persist := range flagPersist {
|
|
||||||
if persist {
|
|
||||||
flags[i] = setup.NewPersistentFlag(fmt.Sprintf("flag-%d", i))
|
|
||||||
} else {
|
|
||||||
flags[i] = setup.NewFlag(fmt.Sprintf("flag-%d", i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fields := make([]Field, len(fieldType))
|
|
||||||
for i, ftype := range fieldType {
|
|
||||||
switch ftype {
|
|
||||||
case reflect.TypeOf(uint64(0)):
|
|
||||||
fields[i] = setup.NewPersistentField(fmt.Sprintf("field-%d", i), ftype, uint64FieldEnc, uint64FieldDec)
|
|
||||||
case reflect.TypeOf(""):
|
|
||||||
fields[i] = setup.NewPersistentField(fmt.Sprintf("field-%d", i), ftype, stringFieldEnc, stringFieldDec)
|
|
||||||
default:
|
|
||||||
fields[i] = setup.NewField(fmt.Sprintf("field-%d", i), ftype)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return setup, flags, fields
|
|
||||||
}
|
|
||||||
|
|
||||||
func testNode(b byte) *enode.Node {
|
|
||||||
r := &enr.Record{}
|
|
||||||
r.SetSig(dummyIdentity{b}, []byte{42})
|
|
||||||
n, _ := enode.New(dummyIdentity{b}, r)
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCallback(t *testing.T) {
|
|
||||||
mdb, clock := rawdb.NewMemoryDatabase(), &mclock.Simulated{}
|
|
||||||
|
|
||||||
s, flags, _ := testSetup([]bool{false, false, false}, nil)
|
|
||||||
ns := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
|
||||||
|
|
||||||
set0 := make(chan struct{}, 1)
|
|
||||||
set1 := make(chan struct{}, 1)
|
|
||||||
set2 := make(chan struct{}, 1)
|
|
||||||
ns.SubscribeState(flags[0], func(n *enode.Node, oldState, newState Flags) { set0 <- struct{}{} })
|
|
||||||
ns.SubscribeState(flags[1], func(n *enode.Node, oldState, newState Flags) { set1 <- struct{}{} })
|
|
||||||
ns.SubscribeState(flags[2], func(n *enode.Node, oldState, newState Flags) { set2 <- struct{}{} })
|
|
||||||
|
|
||||||
ns.Start()
|
|
||||||
|
|
||||||
ns.SetState(testNode(1), flags[0], Flags{}, 0)
|
|
||||||
ns.SetState(testNode(1), flags[1], Flags{}, time.Second)
|
|
||||||
ns.SetState(testNode(1), flags[2], Flags{}, 2*time.Second)
|
|
||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
select {
|
|
||||||
case <-set0:
|
|
||||||
case <-set1:
|
|
||||||
case <-set2:
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatalf("failed to invoke callback")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPersistentFlags(t *testing.T) {
|
|
||||||
mdb, clock := rawdb.NewMemoryDatabase(), &mclock.Simulated{}
|
|
||||||
|
|
||||||
s, flags, _ := testSetup([]bool{true, true, true, false}, nil)
|
|
||||||
ns := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
|
||||||
|
|
||||||
saveNode := make(chan *nodeInfo, 5)
|
|
||||||
ns.saveNodeHook = func(node *nodeInfo) {
|
|
||||||
saveNode <- node
|
|
||||||
}
|
|
||||||
|
|
||||||
ns.Start()
|
|
||||||
|
|
||||||
ns.SetState(testNode(1), flags[0], Flags{}, time.Second) // state with timeout should not be saved
|
|
||||||
ns.SetState(testNode(2), flags[1], Flags{}, 0)
|
|
||||||
ns.SetState(testNode(3), flags[2], Flags{}, 0)
|
|
||||||
ns.SetState(testNode(4), flags[3], Flags{}, 0)
|
|
||||||
ns.SetState(testNode(5), flags[0], Flags{}, 0)
|
|
||||||
ns.Persist(testNode(5))
|
|
||||||
select {
|
|
||||||
case <-saveNode:
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatalf("Timeout")
|
|
||||||
}
|
|
||||||
ns.Stop()
|
|
||||||
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
select {
|
|
||||||
case <-saveNode:
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatalf("Timeout")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-saveNode:
|
|
||||||
t.Fatalf("Unexpected saveNode")
|
|
||||||
case <-time.After(time.Millisecond * 100):
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetField(t *testing.T) {
|
|
||||||
mdb, clock := rawdb.NewMemoryDatabase(), &mclock.Simulated{}
|
|
||||||
|
|
||||||
s, flags, fields := testSetup([]bool{true}, []reflect.Type{reflect.TypeOf("")})
|
|
||||||
ns := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
|
||||||
|
|
||||||
saveNode := make(chan *nodeInfo, 1)
|
|
||||||
ns.saveNodeHook = func(node *nodeInfo) {
|
|
||||||
saveNode <- node
|
|
||||||
}
|
|
||||||
|
|
||||||
ns.Start()
|
|
||||||
|
|
||||||
// Set field before setting state
|
|
||||||
ns.SetField(testNode(1), fields[0], "hello world")
|
|
||||||
field := ns.GetField(testNode(1), fields[0])
|
|
||||||
if field == nil {
|
|
||||||
t.Fatalf("Field should be set before setting states")
|
|
||||||
}
|
|
||||||
ns.SetField(testNode(1), fields[0], nil)
|
|
||||||
field = ns.GetField(testNode(1), fields[0])
|
|
||||||
if field != nil {
|
|
||||||
t.Fatalf("Field should be unset")
|
|
||||||
}
|
|
||||||
// Set field after setting state
|
|
||||||
ns.SetState(testNode(1), flags[0], Flags{}, 0)
|
|
||||||
ns.SetField(testNode(1), fields[0], "hello world")
|
|
||||||
field = ns.GetField(testNode(1), fields[0])
|
|
||||||
if field == nil {
|
|
||||||
t.Fatalf("Field should be set after setting states")
|
|
||||||
}
|
|
||||||
if err := ns.SetField(testNode(1), fields[0], 123); err == nil {
|
|
||||||
t.Fatalf("Invalid field should be rejected")
|
|
||||||
}
|
|
||||||
// Dirty node should be written back
|
|
||||||
ns.Stop()
|
|
||||||
select {
|
|
||||||
case <-saveNode:
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatalf("Timeout")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetState(t *testing.T) {
|
|
||||||
mdb, clock := rawdb.NewMemoryDatabase(), &mclock.Simulated{}
|
|
||||||
|
|
||||||
s, flags, _ := testSetup([]bool{false, false, false}, nil)
|
|
||||||
ns := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
|
||||||
|
|
||||||
type change struct{ old, new Flags }
|
|
||||||
set := make(chan change, 1)
|
|
||||||
ns.SubscribeState(flags[0].Or(flags[1]), func(n *enode.Node, oldState, newState Flags) {
|
|
||||||
set <- change{
|
|
||||||
old: oldState,
|
|
||||||
new: newState,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ns.Start()
|
|
||||||
|
|
||||||
check := func(expectOld, expectNew Flags, expectChange bool) {
|
|
||||||
if expectChange {
|
|
||||||
select {
|
|
||||||
case c := <-set:
|
|
||||||
if !c.old.Equals(expectOld) {
|
|
||||||
t.Fatalf("Old state mismatch")
|
|
||||||
}
|
|
||||||
if !c.new.Equals(expectNew) {
|
|
||||||
t.Fatalf("New state mismatch")
|
|
||||||
}
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-set:
|
|
||||||
t.Fatalf("Unexpected change")
|
|
||||||
case <-time.After(time.Millisecond * 100):
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns.SetState(testNode(1), flags[0], Flags{}, 0)
|
|
||||||
check(Flags{}, flags[0], true)
|
|
||||||
|
|
||||||
ns.SetState(testNode(1), flags[1], Flags{}, 0)
|
|
||||||
check(flags[0], flags[0].Or(flags[1]), true)
|
|
||||||
|
|
||||||
ns.SetState(testNode(1), flags[2], Flags{}, 0)
|
|
||||||
check(Flags{}, Flags{}, false)
|
|
||||||
|
|
||||||
ns.SetState(testNode(1), Flags{}, flags[0], 0)
|
|
||||||
check(flags[0].Or(flags[1]), flags[1], true)
|
|
||||||
|
|
||||||
ns.SetState(testNode(1), Flags{}, flags[1], 0)
|
|
||||||
check(flags[1], Flags{}, true)
|
|
||||||
|
|
||||||
ns.SetState(testNode(1), Flags{}, flags[2], 0)
|
|
||||||
check(Flags{}, Flags{}, false)
|
|
||||||
|
|
||||||
ns.SetState(testNode(1), flags[0].Or(flags[1]), Flags{}, time.Second)
|
|
||||||
check(Flags{}, flags[0].Or(flags[1]), true)
|
|
||||||
clock.Run(time.Second)
|
|
||||||
check(flags[0].Or(flags[1]), Flags{}, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func uint64FieldEnc(field interface{}) ([]byte, error) {
|
|
||||||
if u, ok := field.(uint64); ok {
|
|
||||||
enc, err := rlp.EncodeToBytes(&u)
|
|
||||||
return enc, err
|
|
||||||
}
|
|
||||||
return nil, errors.New("invalid field type")
|
|
||||||
}
|
|
||||||
|
|
||||||
func uint64FieldDec(enc []byte) (interface{}, error) {
|
|
||||||
var u uint64
|
|
||||||
err := rlp.DecodeBytes(enc, &u)
|
|
||||||
return u, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringFieldEnc(field interface{}) ([]byte, error) {
|
|
||||||
if s, ok := field.(string); ok {
|
|
||||||
return []byte(s), nil
|
|
||||||
}
|
|
||||||
return nil, errors.New("invalid field type")
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringFieldDec(enc []byte) (interface{}, error) {
|
|
||||||
return string(enc), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPersistentFields(t *testing.T) {
|
|
||||||
mdb, clock := rawdb.NewMemoryDatabase(), &mclock.Simulated{}
|
|
||||||
|
|
||||||
s, flags, fields := testSetup([]bool{true}, []reflect.Type{reflect.TypeOf(uint64(0)), reflect.TypeOf("")})
|
|
||||||
ns := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
|
||||||
|
|
||||||
ns.Start()
|
|
||||||
ns.SetState(testNode(1), flags[0], Flags{}, 0)
|
|
||||||
ns.SetField(testNode(1), fields[0], uint64(100))
|
|
||||||
ns.SetField(testNode(1), fields[1], "hello world")
|
|
||||||
ns.Stop()
|
|
||||||
|
|
||||||
ns2 := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
|
||||||
|
|
||||||
ns2.Start()
|
|
||||||
field0 := ns2.GetField(testNode(1), fields[0])
|
|
||||||
if !reflect.DeepEqual(field0, uint64(100)) {
|
|
||||||
t.Fatalf("Field changed")
|
|
||||||
}
|
|
||||||
field1 := ns2.GetField(testNode(1), fields[1])
|
|
||||||
if !reflect.DeepEqual(field1, "hello world") {
|
|
||||||
t.Fatalf("Field changed")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Version++
|
|
||||||
ns3 := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
|
||||||
ns3.Start()
|
|
||||||
if ns3.GetField(testNode(1), fields[0]) != nil {
|
|
||||||
t.Fatalf("Old field version should have been discarded")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFieldSub(t *testing.T) {
|
|
||||||
mdb, clock := rawdb.NewMemoryDatabase(), &mclock.Simulated{}
|
|
||||||
|
|
||||||
s, flags, fields := testSetup([]bool{true}, []reflect.Type{reflect.TypeOf(uint64(0))})
|
|
||||||
ns := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
|
||||||
|
|
||||||
var (
|
|
||||||
lastState Flags
|
|
||||||
lastOldValue, lastNewValue interface{}
|
|
||||||
)
|
|
||||||
ns.SubscribeField(fields[0], func(n *enode.Node, state Flags, oldValue, newValue interface{}) {
|
|
||||||
lastState, lastOldValue, lastNewValue = state, oldValue, newValue
|
|
||||||
})
|
|
||||||
check := func(state Flags, oldValue, newValue interface{}) {
|
|
||||||
if !lastState.Equals(state) || lastOldValue != oldValue || lastNewValue != newValue {
|
|
||||||
t.Fatalf("Incorrect field sub callback (expected [%v %v %v], got [%v %v %v])", state, oldValue, newValue, lastState, lastOldValue, lastNewValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns.Start()
|
|
||||||
ns.SetState(testNode(1), flags[0], Flags{}, 0)
|
|
||||||
ns.SetField(testNode(1), fields[0], uint64(100))
|
|
||||||
check(flags[0], nil, uint64(100))
|
|
||||||
ns.Stop()
|
|
||||||
check(s.OfflineFlag(), uint64(100), nil)
|
|
||||||
|
|
||||||
ns2 := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
|
||||||
ns2.SubscribeField(fields[0], func(n *enode.Node, state Flags, oldValue, newValue interface{}) {
|
|
||||||
lastState, lastOldValue, lastNewValue = state, oldValue, newValue
|
|
||||||
})
|
|
||||||
ns2.Start()
|
|
||||||
check(s.OfflineFlag(), nil, uint64(100))
|
|
||||||
ns2.SetState(testNode(1), Flags{}, flags[0], 0)
|
|
||||||
ns2.SetField(testNode(1), fields[0], nil)
|
|
||||||
check(Flags{}, uint64(100), nil)
|
|
||||||
ns2.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDuplicatedFlags(t *testing.T) {
|
|
||||||
mdb, clock := rawdb.NewMemoryDatabase(), &mclock.Simulated{}
|
|
||||||
|
|
||||||
s, flags, _ := testSetup([]bool{true}, nil)
|
|
||||||
ns := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
|
||||||
|
|
||||||
type change struct{ old, new Flags }
|
|
||||||
set := make(chan change, 1)
|
|
||||||
ns.SubscribeState(flags[0], func(n *enode.Node, oldState, newState Flags) {
|
|
||||||
set <- change{oldState, newState}
|
|
||||||
})
|
|
||||||
|
|
||||||
ns.Start()
|
|
||||||
defer ns.Stop()
|
|
||||||
|
|
||||||
check := func(expectOld, expectNew Flags, expectChange bool) {
|
|
||||||
if expectChange {
|
|
||||||
select {
|
|
||||||
case c := <-set:
|
|
||||||
if !c.old.Equals(expectOld) {
|
|
||||||
t.Fatalf("Old state mismatch")
|
|
||||||
}
|
|
||||||
if !c.new.Equals(expectNew) {
|
|
||||||
t.Fatalf("New state mismatch")
|
|
||||||
}
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-set:
|
|
||||||
t.Fatalf("Unexpected change")
|
|
||||||
case <-time.After(time.Millisecond * 100):
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns.SetState(testNode(1), flags[0], Flags{}, time.Second)
|
|
||||||
check(Flags{}, flags[0], true)
|
|
||||||
ns.SetState(testNode(1), flags[0], Flags{}, 2*time.Second) // extend the timeout to 2s
|
|
||||||
check(Flags{}, flags[0], false)
|
|
||||||
|
|
||||||
clock.Run(2 * time.Second)
|
|
||||||
check(flags[0], Flags{}, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCallbackOrder(t *testing.T) {
|
|
||||||
mdb, clock := rawdb.NewMemoryDatabase(), &mclock.Simulated{}
|
|
||||||
|
|
||||||
s, flags, _ := testSetup([]bool{false, false, false, false}, nil)
|
|
||||||
ns := NewNodeStateMachine(mdb, []byte("-ns"), clock, s)
|
|
||||||
|
|
||||||
ns.SubscribeState(flags[0], func(n *enode.Node, oldState, newState Flags) {
|
|
||||||
if newState.Equals(flags[0]) {
|
|
||||||
ns.SetStateSub(n, flags[1], Flags{}, 0)
|
|
||||||
ns.SetStateSub(n, flags[2], Flags{}, 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
ns.SubscribeState(flags[1], func(n *enode.Node, oldState, newState Flags) {
|
|
||||||
if newState.Equals(flags[1]) {
|
|
||||||
ns.SetStateSub(n, flags[3], Flags{}, 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
lastState := Flags{}
|
|
||||||
ns.SubscribeState(MergeFlags(flags[1], flags[2], flags[3]), func(n *enode.Node, oldState, newState Flags) {
|
|
||||||
if !oldState.Equals(lastState) {
|
|
||||||
t.Fatalf("Wrong callback order")
|
|
||||||
}
|
|
||||||
lastState = newState
|
|
||||||
})
|
|
||||||
|
|
||||||
ns.Start()
|
|
||||||
defer ns.Stop()
|
|
||||||
|
|
||||||
ns.SetState(testNode(1), flags[0], Flags{}, 0)
|
|
||||||
}
|
|
||||||
548
p2p/peer.go
548
p2p/peer.go
|
|
@ -1,548 +0,0 @@
|
||||||
// Copyright 2014 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package p2p
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
"github.com/ethereum/go-ethereum/event"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/metrics"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrShuttingDown = errors.New("shutting down")
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
baseProtocolVersion = 5
|
|
||||||
baseProtocolLength = uint64(16)
|
|
||||||
baseProtocolMaxMsgSize = 2 * 1024
|
|
||||||
|
|
||||||
snappyProtocolVersion = 5
|
|
||||||
|
|
||||||
pingInterval = 15 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// devp2p message codes
|
|
||||||
handshakeMsg = 0x00
|
|
||||||
discMsg = 0x01
|
|
||||||
pingMsg = 0x02
|
|
||||||
pongMsg = 0x03
|
|
||||||
)
|
|
||||||
|
|
||||||
// protoHandshake is the RLP structure of the protocol handshake.
|
|
||||||
type protoHandshake struct {
|
|
||||||
Version uint64
|
|
||||||
Name string
|
|
||||||
Caps []Cap
|
|
||||||
ListenPort uint64
|
|
||||||
ID []byte // secp256k1 public key
|
|
||||||
|
|
||||||
// Ignore additional fields (for forward compatibility).
|
|
||||||
Rest []rlp.RawValue `rlp:"tail"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PeerEventType is the type of peer events emitted by a p2p.Server
|
|
||||||
type PeerEventType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// PeerEventTypeAdd is the type of event emitted when a peer is added
|
|
||||||
// to a p2p.Server
|
|
||||||
PeerEventTypeAdd PeerEventType = "add"
|
|
||||||
|
|
||||||
// PeerEventTypeDrop is the type of event emitted when a peer is
|
|
||||||
// dropped from a p2p.Server
|
|
||||||
PeerEventTypeDrop PeerEventType = "drop"
|
|
||||||
|
|
||||||
// PeerEventTypeMsgSend is the type of event emitted when a
|
|
||||||
// message is successfully sent to a peer
|
|
||||||
PeerEventTypeMsgSend PeerEventType = "msgsend"
|
|
||||||
|
|
||||||
// PeerEventTypeMsgRecv is the type of event emitted when a
|
|
||||||
// message is received from a peer
|
|
||||||
PeerEventTypeMsgRecv PeerEventType = "msgrecv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PeerEvent is an event emitted when peers are either added or dropped from
|
|
||||||
// a p2p.Server or when a message is sent or received on a peer connection
|
|
||||||
type PeerEvent struct {
|
|
||||||
Type PeerEventType `json:"type"`
|
|
||||||
Peer enode.ID `json:"peer"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
Protocol string `json:"protocol,omitempty"`
|
|
||||||
MsgCode *uint64 `json:"msg_code,omitempty"`
|
|
||||||
MsgSize *uint32 `json:"msg_size,omitempty"`
|
|
||||||
LocalAddress string `json:"local,omitempty"`
|
|
||||||
RemoteAddress string `json:"remote,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Peer represents a connected remote node.
|
|
||||||
type Peer struct {
|
|
||||||
rw *conn
|
|
||||||
running map[string]*protoRW
|
|
||||||
log log.Logger
|
|
||||||
created mclock.AbsTime
|
|
||||||
|
|
||||||
wg sync.WaitGroup
|
|
||||||
protoErr chan error
|
|
||||||
closed chan struct{}
|
|
||||||
pingRecv chan struct{}
|
|
||||||
disc chan DiscReason
|
|
||||||
|
|
||||||
// events receives message send / receive events if set
|
|
||||||
events *event.Feed
|
|
||||||
testPipe *MsgPipeRW // for testing
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPeer returns a peer for testing purposes.
|
|
||||||
func NewPeer(id enode.ID, name string, caps []Cap) *Peer {
|
|
||||||
// Generate a fake set of local protocols to match as running caps. Almost
|
|
||||||
// no fields needs to be meaningful here as we're only using it to cross-
|
|
||||||
// check with the "remote" caps array.
|
|
||||||
protos := make([]Protocol, len(caps))
|
|
||||||
for i, cap := range caps {
|
|
||||||
protos[i].Name = cap.Name
|
|
||||||
protos[i].Version = cap.Version
|
|
||||||
}
|
|
||||||
pipe, _ := net.Pipe()
|
|
||||||
node := enode.SignNull(new(enr.Record), id)
|
|
||||||
conn := &conn{fd: pipe, transport: nil, node: node, caps: caps, name: name}
|
|
||||||
peer := newPeer(log.Root(), conn, protos)
|
|
||||||
close(peer.closed) // ensures Disconnect doesn't block
|
|
||||||
return peer
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPeerPipe creates a peer for testing purposes.
|
|
||||||
// The message pipe given as the last parameter is closed when
|
|
||||||
// Disconnect is called on the peer.
|
|
||||||
func NewPeerPipe(id enode.ID, name string, caps []Cap, pipe *MsgPipeRW) *Peer {
|
|
||||||
p := NewPeer(id, name, caps)
|
|
||||||
p.testPipe = pipe
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID returns the node's public key.
|
|
||||||
func (p *Peer) ID() enode.ID {
|
|
||||||
return p.rw.node.ID()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node returns the peer's node descriptor.
|
|
||||||
func (p *Peer) Node() *enode.Node {
|
|
||||||
return p.rw.node
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns an abbreviated form of the name
|
|
||||||
func (p *Peer) Name() string {
|
|
||||||
s := p.rw.name
|
|
||||||
if len(s) > 20 {
|
|
||||||
return s[:20] + "..."
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fullname returns the node name that the remote node advertised.
|
|
||||||
func (p *Peer) Fullname() string {
|
|
||||||
return p.rw.name
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caps returns the capabilities (supported subprotocols) of the remote peer.
|
|
||||||
func (p *Peer) Caps() []Cap {
|
|
||||||
// TODO: maybe return copy
|
|
||||||
return p.rw.caps
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunningCap returns true if the peer is actively connected using any of the
|
|
||||||
// enumerated versions of a specific protocol, meaning that at least one of the
|
|
||||||
// versions is supported by both this node and the peer p.
|
|
||||||
func (p *Peer) RunningCap(protocol string, versions []uint) bool {
|
|
||||||
if proto, ok := p.running[protocol]; ok {
|
|
||||||
for _, ver := range versions {
|
|
||||||
if proto.Version == ver {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoteAddr returns the remote address of the network connection.
|
|
||||||
func (p *Peer) RemoteAddr() net.Addr {
|
|
||||||
return p.rw.fd.RemoteAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// LocalAddr returns the local address of the network connection.
|
|
||||||
func (p *Peer) LocalAddr() net.Addr {
|
|
||||||
return p.rw.fd.LocalAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disconnect terminates the peer connection with the given reason.
|
|
||||||
// It returns immediately and does not wait until the connection is closed.
|
|
||||||
func (p *Peer) Disconnect(reason DiscReason) {
|
|
||||||
if p.testPipe != nil {
|
|
||||||
p.testPipe.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case p.disc <- reason:
|
|
||||||
case <-p.closed:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String implements fmt.Stringer.
|
|
||||||
func (p *Peer) String() string {
|
|
||||||
id := p.ID()
|
|
||||||
return fmt.Sprintf("Peer %x %v", id[:8], p.RemoteAddr())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inbound returns true if the peer is an inbound connection
|
|
||||||
func (p *Peer) Inbound() bool {
|
|
||||||
return p.rw.is(inboundConn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPeer(log log.Logger, conn *conn, protocols []Protocol) *Peer {
|
|
||||||
protomap := matchProtocols(protocols, conn.caps, conn)
|
|
||||||
p := &Peer{
|
|
||||||
rw: conn,
|
|
||||||
running: protomap,
|
|
||||||
created: mclock.Now(),
|
|
||||||
disc: make(chan DiscReason),
|
|
||||||
protoErr: make(chan error, len(protomap)+1), // protocols + pingLoop
|
|
||||||
closed: make(chan struct{}),
|
|
||||||
pingRecv: make(chan struct{}, 16),
|
|
||||||
log: log.New("id", conn.node.ID(), "conn", conn.flags),
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Peer) Log() log.Logger {
|
|
||||||
return p.log
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Peer) run() (remoteRequested bool, err error) {
|
|
||||||
var (
|
|
||||||
writeStart = make(chan struct{}, 1)
|
|
||||||
writeErr = make(chan error, 1)
|
|
||||||
readErr = make(chan error, 1)
|
|
||||||
reason DiscReason // sent to the peer
|
|
||||||
)
|
|
||||||
p.wg.Add(2)
|
|
||||||
go p.readLoop(readErr)
|
|
||||||
go p.pingLoop()
|
|
||||||
|
|
||||||
// Start all protocol handlers.
|
|
||||||
writeStart <- struct{}{}
|
|
||||||
p.startProtocols(writeStart, writeErr)
|
|
||||||
|
|
||||||
// Wait for an error or disconnect.
|
|
||||||
loop:
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case err = <-writeErr:
|
|
||||||
// A write finished. Allow the next write to start if
|
|
||||||
// there was no error.
|
|
||||||
if err != nil {
|
|
||||||
reason = DiscNetworkError
|
|
||||||
break loop
|
|
||||||
}
|
|
||||||
writeStart <- struct{}{}
|
|
||||||
case err = <-readErr:
|
|
||||||
if r, ok := err.(DiscReason); ok {
|
|
||||||
remoteRequested = true
|
|
||||||
reason = r
|
|
||||||
} else {
|
|
||||||
reason = DiscNetworkError
|
|
||||||
}
|
|
||||||
break loop
|
|
||||||
case err = <-p.protoErr:
|
|
||||||
reason = discReasonForError(err)
|
|
||||||
break loop
|
|
||||||
case err = <-p.disc:
|
|
||||||
reason = discReasonForError(err)
|
|
||||||
break loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close(p.closed)
|
|
||||||
p.rw.close(reason)
|
|
||||||
p.wg.Wait()
|
|
||||||
return remoteRequested, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Peer) pingLoop() {
|
|
||||||
defer p.wg.Done()
|
|
||||||
|
|
||||||
ping := time.NewTimer(pingInterval)
|
|
||||||
defer ping.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ping.C:
|
|
||||||
if err := SendItems(p.rw, pingMsg); err != nil {
|
|
||||||
p.protoErr <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ping.Reset(pingInterval)
|
|
||||||
|
|
||||||
case <-p.pingRecv:
|
|
||||||
SendItems(p.rw, pongMsg)
|
|
||||||
|
|
||||||
case <-p.closed:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Peer) readLoop(errc chan<- error) {
|
|
||||||
defer p.wg.Done()
|
|
||||||
for {
|
|
||||||
msg, err := p.rw.ReadMsg()
|
|
||||||
if err != nil {
|
|
||||||
errc <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg.ReceivedAt = time.Now()
|
|
||||||
if err = p.handle(msg); err != nil {
|
|
||||||
errc <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Peer) handle(msg Msg) error {
|
|
||||||
switch {
|
|
||||||
case msg.Code == pingMsg:
|
|
||||||
msg.Discard()
|
|
||||||
select {
|
|
||||||
case p.pingRecv <- struct{}{}:
|
|
||||||
case <-p.closed:
|
|
||||||
}
|
|
||||||
case msg.Code == discMsg:
|
|
||||||
// This is the last message. We don't need to discard or
|
|
||||||
// check errors because, the connection will be closed after it.
|
|
||||||
var m struct{ R DiscReason }
|
|
||||||
rlp.Decode(msg.Payload, &m)
|
|
||||||
return m.R
|
|
||||||
case msg.Code < baseProtocolLength:
|
|
||||||
// ignore other base protocol messages
|
|
||||||
return msg.Discard()
|
|
||||||
default:
|
|
||||||
// it's a subprotocol message
|
|
||||||
proto, err := p.getProto(msg.Code)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("msg code out of range: %v", msg.Code)
|
|
||||||
}
|
|
||||||
if metrics.Enabled {
|
|
||||||
m := fmt.Sprintf("%s/%s/%d/%#02x", ingressMeterName, proto.Name, proto.Version, msg.Code-proto.offset)
|
|
||||||
metrics.GetOrRegisterMeter(m, nil).Mark(int64(msg.meterSize))
|
|
||||||
metrics.GetOrRegisterMeter(m+"/packets", nil).Mark(1)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case proto.in <- msg:
|
|
||||||
return nil
|
|
||||||
case <-p.closed:
|
|
||||||
return io.EOF
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func countMatchingProtocols(protocols []Protocol, caps []Cap) int {
|
|
||||||
n := 0
|
|
||||||
for _, cap := range caps {
|
|
||||||
for _, proto := range protocols {
|
|
||||||
if proto.Name == cap.Name && proto.Version == cap.Version {
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchProtocols creates structures for matching named subprotocols.
|
|
||||||
func matchProtocols(protocols []Protocol, caps []Cap, rw MsgReadWriter) map[string]*protoRW {
|
|
||||||
slices.SortFunc(caps, Cap.Cmp)
|
|
||||||
offset := baseProtocolLength
|
|
||||||
result := make(map[string]*protoRW)
|
|
||||||
|
|
||||||
outer:
|
|
||||||
for _, cap := range caps {
|
|
||||||
for _, proto := range protocols {
|
|
||||||
if proto.Name == cap.Name && proto.Version == cap.Version {
|
|
||||||
// If an old protocol version matched, revert it
|
|
||||||
if old := result[cap.Name]; old != nil {
|
|
||||||
offset -= old.Length
|
|
||||||
}
|
|
||||||
// Assign the new match
|
|
||||||
result[cap.Name] = &protoRW{Protocol: proto, offset: offset, in: make(chan Msg), w: rw}
|
|
||||||
offset += proto.Length
|
|
||||||
|
|
||||||
continue outer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Peer) startProtocols(writeStart <-chan struct{}, writeErr chan<- error) {
|
|
||||||
p.wg.Add(len(p.running))
|
|
||||||
for _, proto := range p.running {
|
|
||||||
proto := proto
|
|
||||||
proto.closed = p.closed
|
|
||||||
proto.wstart = writeStart
|
|
||||||
proto.werr = writeErr
|
|
||||||
var rw MsgReadWriter = proto
|
|
||||||
if p.events != nil {
|
|
||||||
rw = newMsgEventer(rw, p.events, p.ID(), proto.Name, p.Info().Network.RemoteAddress, p.Info().Network.LocalAddress)
|
|
||||||
}
|
|
||||||
p.log.Trace(fmt.Sprintf("Starting protocol %s/%d", proto.Name, proto.Version))
|
|
||||||
go func() {
|
|
||||||
defer p.wg.Done()
|
|
||||||
err := proto.Run(p, rw)
|
|
||||||
if err == nil {
|
|
||||||
p.log.Trace(fmt.Sprintf("Protocol %s/%d returned", proto.Name, proto.Version))
|
|
||||||
err = errProtocolReturned
|
|
||||||
} else if !errors.Is(err, io.EOF) {
|
|
||||||
p.log.Trace(fmt.Sprintf("Protocol %s/%d failed", proto.Name, proto.Version), "err", err)
|
|
||||||
}
|
|
||||||
p.protoErr <- err
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getProto finds the protocol responsible for handling
|
|
||||||
// the given message code.
|
|
||||||
func (p *Peer) getProto(code uint64) (*protoRW, error) {
|
|
||||||
for _, proto := range p.running {
|
|
||||||
if code >= proto.offset && code < proto.offset+proto.Length {
|
|
||||||
return proto, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, newPeerError(errInvalidMsgCode, "%d", code)
|
|
||||||
}
|
|
||||||
|
|
||||||
type protoRW struct {
|
|
||||||
Protocol
|
|
||||||
in chan Msg // receives read messages
|
|
||||||
closed <-chan struct{} // receives when peer is shutting down
|
|
||||||
wstart <-chan struct{} // receives when write may start
|
|
||||||
werr chan<- error // for write results
|
|
||||||
offset uint64
|
|
||||||
w MsgWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rw *protoRW) WriteMsg(msg Msg) (err error) {
|
|
||||||
if msg.Code >= rw.Length {
|
|
||||||
return newPeerError(errInvalidMsgCode, "not handled")
|
|
||||||
}
|
|
||||||
msg.meterCap = rw.cap()
|
|
||||||
msg.meterCode = msg.Code
|
|
||||||
|
|
||||||
msg.Code += rw.offset
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-rw.wstart:
|
|
||||||
err = rw.w.WriteMsg(msg)
|
|
||||||
// Report write status back to Peer.run. It will initiate
|
|
||||||
// shutdown if the error is non-nil and unblock the next write
|
|
||||||
// otherwise. The calling protocol code should exit for errors
|
|
||||||
// as well but we don't want to rely on that.
|
|
||||||
rw.werr <- err
|
|
||||||
case <-rw.closed:
|
|
||||||
err = ErrShuttingDown
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rw *protoRW) ReadMsg() (Msg, error) {
|
|
||||||
select {
|
|
||||||
case msg := <-rw.in:
|
|
||||||
msg.Code -= rw.offset
|
|
||||||
return msg, nil
|
|
||||||
case <-rw.closed:
|
|
||||||
return Msg{}, io.EOF
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PeerInfo represents a short summary of the information known about a connected
|
|
||||||
// peer. Sub-protocol independent fields are contained and initialized here, with
|
|
||||||
// protocol specifics delegated to all connected sub-protocols.
|
|
||||||
type PeerInfo struct {
|
|
||||||
ENR string `json:"enr,omitempty"` // Ethereum Node Record
|
|
||||||
Enode string `json:"enode"` // Node URL
|
|
||||||
ID string `json:"id"` // Unique node identifier
|
|
||||||
Name string `json:"name"` // Name of the node, including client type, version, OS, custom data
|
|
||||||
Caps []string `json:"caps"` // Protocols advertised by this peer
|
|
||||||
Network struct {
|
|
||||||
LocalAddress string `json:"localAddress"` // Local endpoint of the TCP data connection
|
|
||||||
RemoteAddress string `json:"remoteAddress"` // Remote endpoint of the TCP data connection
|
|
||||||
Inbound bool `json:"inbound"`
|
|
||||||
Trusted bool `json:"trusted"`
|
|
||||||
Static bool `json:"static"`
|
|
||||||
} `json:"network"`
|
|
||||||
Protocols map[string]interface{} `json:"protocols"` // Sub-protocol specific metadata fields
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info gathers and returns a collection of metadata known about a peer.
|
|
||||||
func (p *Peer) Info() *PeerInfo {
|
|
||||||
// Gather the protocol capabilities
|
|
||||||
var caps []string
|
|
||||||
for _, cap := range p.Caps() {
|
|
||||||
caps = append(caps, cap.String())
|
|
||||||
}
|
|
||||||
// Assemble the generic peer metadata
|
|
||||||
info := &PeerInfo{
|
|
||||||
Enode: p.Node().URLv4(),
|
|
||||||
ID: p.ID().String(),
|
|
||||||
Name: p.Fullname(),
|
|
||||||
Caps: caps,
|
|
||||||
Protocols: make(map[string]interface{}, len(p.running)),
|
|
||||||
}
|
|
||||||
if p.Node().Seq() > 0 {
|
|
||||||
info.ENR = p.Node().String()
|
|
||||||
}
|
|
||||||
info.Network.LocalAddress = p.LocalAddr().String()
|
|
||||||
info.Network.RemoteAddress = p.RemoteAddr().String()
|
|
||||||
info.Network.Inbound = p.rw.is(inboundConn)
|
|
||||||
info.Network.Trusted = p.rw.is(trustedConn)
|
|
||||||
info.Network.Static = p.rw.is(staticDialedConn)
|
|
||||||
|
|
||||||
// Gather all the running protocol infos
|
|
||||||
for _, proto := range p.running {
|
|
||||||
protoInfo := interface{}("unknown")
|
|
||||||
if query := proto.Protocol.PeerInfo; query != nil {
|
|
||||||
if metadata := query(p.ID()); metadata != nil {
|
|
||||||
protoInfo = metadata
|
|
||||||
} else {
|
|
||||||
protoInfo = "handshake"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info.Protocols[proto.Name] = protoInfo
|
|
||||||
}
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
// Copyright 2014 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package p2p
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
errInvalidMsgCode = iota
|
|
||||||
errInvalidMsg
|
|
||||||
)
|
|
||||||
|
|
||||||
var errorToString = map[int]string{
|
|
||||||
errInvalidMsgCode: "invalid message code",
|
|
||||||
errInvalidMsg: "invalid message",
|
|
||||||
}
|
|
||||||
|
|
||||||
type peerError struct {
|
|
||||||
code int
|
|
||||||
message string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPeerError(code int, format string, v ...interface{}) *peerError {
|
|
||||||
desc, ok := errorToString[code]
|
|
||||||
if !ok {
|
|
||||||
panic("invalid error code")
|
|
||||||
}
|
|
||||||
err := &peerError{code, desc}
|
|
||||||
if format != "" {
|
|
||||||
err.message += ": " + fmt.Sprintf(format, v...)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pe *peerError) Error() string {
|
|
||||||
return pe.message
|
|
||||||
}
|
|
||||||
|
|
||||||
var errProtocolReturned = errors.New("protocol returned")
|
|
||||||
|
|
||||||
type DiscReason uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
DiscRequested DiscReason = iota
|
|
||||||
DiscNetworkError
|
|
||||||
DiscProtocolError
|
|
||||||
DiscUselessPeer
|
|
||||||
DiscTooManyPeers
|
|
||||||
DiscAlreadyConnected
|
|
||||||
DiscIncompatibleVersion
|
|
||||||
DiscInvalidIdentity
|
|
||||||
DiscQuitting
|
|
||||||
DiscUnexpectedIdentity
|
|
||||||
DiscSelf
|
|
||||||
DiscReadTimeout
|
|
||||||
DiscSubprotocolError = DiscReason(0x10)
|
|
||||||
)
|
|
||||||
|
|
||||||
var discReasonToString = [...]string{
|
|
||||||
DiscRequested: "disconnect requested",
|
|
||||||
DiscNetworkError: "network error",
|
|
||||||
DiscProtocolError: "breach of protocol",
|
|
||||||
DiscUselessPeer: "useless peer",
|
|
||||||
DiscTooManyPeers: "too many peers",
|
|
||||||
DiscAlreadyConnected: "already connected",
|
|
||||||
DiscIncompatibleVersion: "incompatible p2p protocol version",
|
|
||||||
DiscInvalidIdentity: "invalid node identity",
|
|
||||||
DiscQuitting: "client quitting",
|
|
||||||
DiscUnexpectedIdentity: "unexpected identity",
|
|
||||||
DiscSelf: "connected to self",
|
|
||||||
DiscReadTimeout: "read timeout",
|
|
||||||
DiscSubprotocolError: "subprotocol error",
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DiscReason) String() string {
|
|
||||||
if len(discReasonToString) <= int(d) {
|
|
||||||
return fmt.Sprintf("unknown disconnect reason %d", d)
|
|
||||||
}
|
|
||||||
return discReasonToString[d]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DiscReason) Error() string {
|
|
||||||
return d.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func discReasonForError(err error) DiscReason {
|
|
||||||
if reason, ok := err.(DiscReason); ok {
|
|
||||||
return reason
|
|
||||||
}
|
|
||||||
if errors.Is(err, errProtocolReturned) {
|
|
||||||
return DiscQuitting
|
|
||||||
}
|
|
||||||
peerError, ok := err.(*peerError)
|
|
||||||
if ok {
|
|
||||||
switch peerError.code {
|
|
||||||
case errInvalidMsgCode, errInvalidMsg:
|
|
||||||
return DiscProtocolError
|
|
||||||
default:
|
|
||||||
return DiscSubprotocolError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return DiscSubprotocolError
|
|
||||||
}
|
|
||||||
362
p2p/peer_test.go
362
p2p/peer_test.go
|
|
@ -1,362 +0,0 @@
|
||||||
// Copyright 2014 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package p2p
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
)
|
|
||||||
|
|
||||||
var discard = Protocol{
|
|
||||||
Name: "discard",
|
|
||||||
Length: 1,
|
|
||||||
Run: func(p *Peer, rw MsgReadWriter) error {
|
|
||||||
for {
|
|
||||||
msg, err := rw.ReadMsg()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Printf("discarding %d\n", msg.Code)
|
|
||||||
if err = msg.Discard(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// uintID encodes i into a node ID.
|
|
||||||
func uintID(i uint16) enode.ID {
|
|
||||||
var id enode.ID
|
|
||||||
binary.BigEndian.PutUint16(id[:], i)
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
// newNode creates a node record with the given address.
|
|
||||||
func newNode(id enode.ID, addr string) *enode.Node {
|
|
||||||
var r enr.Record
|
|
||||||
if addr != "" {
|
|
||||||
// Set the port if present.
|
|
||||||
if strings.Contains(addr, ":") {
|
|
||||||
hs, ps, err := net.SplitHostPort(addr)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("invalid address %q", addr))
|
|
||||||
}
|
|
||||||
port, err := strconv.Atoi(ps)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("invalid port in %q", addr))
|
|
||||||
}
|
|
||||||
r.Set(enr.TCP(port))
|
|
||||||
r.Set(enr.UDP(port))
|
|
||||||
addr = hs
|
|
||||||
}
|
|
||||||
// Set the IP.
|
|
||||||
ip := net.ParseIP(addr)
|
|
||||||
if ip == nil {
|
|
||||||
panic(fmt.Errorf("invalid IP %q", addr))
|
|
||||||
}
|
|
||||||
r.Set(enr.IP(ip))
|
|
||||||
}
|
|
||||||
return enode.SignNull(&r, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPeer(protos []Protocol) (func(), *conn, *Peer, <-chan error) {
|
|
||||||
var (
|
|
||||||
fd1, fd2 = net.Pipe()
|
|
||||||
key1, key2 = newkey(), newkey()
|
|
||||||
t1 = newTestTransport(&key2.PublicKey, fd1, nil)
|
|
||||||
t2 = newTestTransport(&key1.PublicKey, fd2, &key1.PublicKey)
|
|
||||||
)
|
|
||||||
|
|
||||||
c1 := &conn{fd: fd1, node: newNode(uintID(1), ""), transport: t1}
|
|
||||||
c2 := &conn{fd: fd2, node: newNode(uintID(2), ""), transport: t2}
|
|
||||||
for _, p := range protos {
|
|
||||||
c1.caps = append(c1.caps, p.cap())
|
|
||||||
c2.caps = append(c2.caps, p.cap())
|
|
||||||
}
|
|
||||||
|
|
||||||
peer := newPeer(log.Root(), c1, protos)
|
|
||||||
errc := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
_, err := peer.run()
|
|
||||||
errc <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
closer := func() { c2.close(errors.New("close func called")) }
|
|
||||||
return closer, c2, peer, errc
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPeerProtoReadMsg(t *testing.T) {
|
|
||||||
proto := Protocol{
|
|
||||||
Name: "a",
|
|
||||||
Length: 5,
|
|
||||||
Run: func(peer *Peer, rw MsgReadWriter) error {
|
|
||||||
if err := ExpectMsg(rw, 2, []uint{1}); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
if err := ExpectMsg(rw, 3, []uint{2}); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
if err := ExpectMsg(rw, 4, []uint{3}); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
closer, rw, _, errc := testPeer([]Protocol{proto})
|
|
||||||
defer closer()
|
|
||||||
|
|
||||||
Send(rw, baseProtocolLength+2, []uint{1})
|
|
||||||
Send(rw, baseProtocolLength+3, []uint{2})
|
|
||||||
Send(rw, baseProtocolLength+4, []uint{3})
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err := <-errc:
|
|
||||||
if err != errProtocolReturned {
|
|
||||||
t.Errorf("peer returned error: %v", err)
|
|
||||||
}
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
t.Errorf("receive timeout")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPeerProtoEncodeMsg(t *testing.T) {
|
|
||||||
proto := Protocol{
|
|
||||||
Name: "a",
|
|
||||||
Length: 2,
|
|
||||||
Run: func(peer *Peer, rw MsgReadWriter) error {
|
|
||||||
if err := SendItems(rw, 2); err == nil {
|
|
||||||
t.Error("expected error for out-of-range msg code, got nil")
|
|
||||||
}
|
|
||||||
if err := SendItems(rw, 1, "foo", "bar"); err != nil {
|
|
||||||
t.Errorf("write error: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
closer, rw, _, _ := testPeer([]Protocol{proto})
|
|
||||||
defer closer()
|
|
||||||
|
|
||||||
if err := ExpectMsg(rw, 17, []string{"foo", "bar"}); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPeerPing(t *testing.T) {
|
|
||||||
closer, rw, _, _ := testPeer(nil)
|
|
||||||
defer closer()
|
|
||||||
if err := SendItems(rw, pingMsg); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := ExpectMsg(rw, pongMsg, nil); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that a disconnect message sent by a peer is returned
|
|
||||||
// as the error from Peer.run.
|
|
||||||
func TestPeerDisconnect(t *testing.T) {
|
|
||||||
closer, rw, _, disc := testPeer(nil)
|
|
||||||
defer closer()
|
|
||||||
|
|
||||||
if err := SendItems(rw, discMsg, DiscQuitting); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case reason := <-disc:
|
|
||||||
if reason != DiscQuitting {
|
|
||||||
t.Errorf("run returned wrong reason: got %v, want %v", reason, DiscQuitting)
|
|
||||||
}
|
|
||||||
case <-time.After(500 * time.Millisecond):
|
|
||||||
t.Error("peer did not return")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test is supposed to verify that Peer can reliably handle
|
|
||||||
// multiple causes of disconnection occurring at the same time.
|
|
||||||
func TestPeerDisconnectRace(t *testing.T) {
|
|
||||||
maybe := func() bool { return rand.Intn(2) == 1 }
|
|
||||||
|
|
||||||
for i := 0; i < 1000; i++ {
|
|
||||||
protoclose := make(chan error)
|
|
||||||
protodisc := make(chan DiscReason)
|
|
||||||
closer, rw, p, disc := testPeer([]Protocol{
|
|
||||||
{
|
|
||||||
Name: "closereq",
|
|
||||||
Run: func(p *Peer, rw MsgReadWriter) error { return <-protoclose },
|
|
||||||
Length: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "disconnect",
|
|
||||||
Run: func(p *Peer, rw MsgReadWriter) error { p.Disconnect(<-protodisc); return nil },
|
|
||||||
Length: 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Simulate incoming messages.
|
|
||||||
go SendItems(rw, baseProtocolLength+1)
|
|
||||||
go SendItems(rw, baseProtocolLength+2)
|
|
||||||
// Close the network connection.
|
|
||||||
go closer()
|
|
||||||
// Make protocol "closereq" return.
|
|
||||||
protoclose <- errors.New("protocol closed")
|
|
||||||
// Make protocol "disconnect" call peer.Disconnect
|
|
||||||
protodisc <- DiscAlreadyConnected
|
|
||||||
// In some cases, simulate something else calling peer.Disconnect.
|
|
||||||
if maybe() {
|
|
||||||
go p.Disconnect(DiscInvalidIdentity)
|
|
||||||
}
|
|
||||||
// In some cases, simulate remote requesting a disconnect.
|
|
||||||
if maybe() {
|
|
||||||
go SendItems(rw, discMsg, DiscQuitting)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-disc:
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
// Peer.run should return quickly. If it doesn't the Peer
|
|
||||||
// goroutines are probably deadlocked. Call panic in order to
|
|
||||||
// show the stacks.
|
|
||||||
panic("Peer.run took to long to return.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewPeer(t *testing.T) {
|
|
||||||
name := "nodename"
|
|
||||||
caps := []Cap{{"foo", 2}, {"bar", 3}}
|
|
||||||
id := randomID()
|
|
||||||
p := NewPeer(id, name, caps)
|
|
||||||
if p.ID() != id {
|
|
||||||
t.Errorf("ID mismatch: got %v, expected %v", p.ID(), id)
|
|
||||||
}
|
|
||||||
if p.Name() != name {
|
|
||||||
t.Errorf("Name mismatch: got %v, expected %v", p.Name(), name)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(p.Caps(), caps) {
|
|
||||||
t.Errorf("Caps mismatch: got %v, expected %v", p.Caps(), caps)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Disconnect(DiscAlreadyConnected) // Should not hang
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMatchProtocols(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
Remote []Cap
|
|
||||||
Local []Protocol
|
|
||||||
Match map[string]protoRW
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
// No remote capabilities
|
|
||||||
Local: []Protocol{{Name: "a"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// No local protocols
|
|
||||||
Remote: []Cap{{Name: "a"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// No mutual protocols
|
|
||||||
Remote: []Cap{{Name: "a"}},
|
|
||||||
Local: []Protocol{{Name: "b"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Some matches, some differences
|
|
||||||
Remote: []Cap{{Name: "local"}, {Name: "match1"}, {Name: "match2"}},
|
|
||||||
Local: []Protocol{{Name: "match1"}, {Name: "match2"}, {Name: "remote"}},
|
|
||||||
Match: map[string]protoRW{"match1": {Protocol: Protocol{Name: "match1"}}, "match2": {Protocol: Protocol{Name: "match2"}}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Various alphabetical ordering
|
|
||||||
Remote: []Cap{{Name: "aa"}, {Name: "ab"}, {Name: "bb"}, {Name: "ba"}},
|
|
||||||
Local: []Protocol{{Name: "ba"}, {Name: "bb"}, {Name: "ab"}, {Name: "aa"}},
|
|
||||||
Match: map[string]protoRW{"aa": {Protocol: Protocol{Name: "aa"}}, "ab": {Protocol: Protocol{Name: "ab"}}, "ba": {Protocol: Protocol{Name: "ba"}}, "bb": {Protocol: Protocol{Name: "bb"}}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// No mutual versions
|
|
||||||
Remote: []Cap{{Version: 1}},
|
|
||||||
Local: []Protocol{{Version: 2}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Multiple versions, single common
|
|
||||||
Remote: []Cap{{Version: 1}, {Version: 2}},
|
|
||||||
Local: []Protocol{{Version: 2}, {Version: 3}},
|
|
||||||
Match: map[string]protoRW{"": {Protocol: Protocol{Version: 2}}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Multiple versions, multiple common
|
|
||||||
Remote: []Cap{{Version: 1}, {Version: 2}, {Version: 3}, {Version: 4}},
|
|
||||||
Local: []Protocol{{Version: 2}, {Version: 3}},
|
|
||||||
Match: map[string]protoRW{"": {Protocol: Protocol{Version: 3}}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Various version orderings
|
|
||||||
Remote: []Cap{{Version: 4}, {Version: 1}, {Version: 3}, {Version: 2}},
|
|
||||||
Local: []Protocol{{Version: 2}, {Version: 3}, {Version: 1}},
|
|
||||||
Match: map[string]protoRW{"": {Protocol: Protocol{Version: 3}}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Versions overriding sub-protocol lengths
|
|
||||||
Remote: []Cap{{Version: 1}, {Version: 2}, {Version: 3}, {Name: "a"}},
|
|
||||||
Local: []Protocol{{Version: 1, Length: 1}, {Version: 2, Length: 2}, {Version: 3, Length: 3}, {Name: "a"}},
|
|
||||||
Match: map[string]protoRW{"": {Protocol: Protocol{Version: 3}}, "a": {Protocol: Protocol{Name: "a"}, offset: 3}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tt := range tests {
|
|
||||||
result := matchProtocols(tt.Local, tt.Remote, nil)
|
|
||||||
if len(result) != len(tt.Match) {
|
|
||||||
t.Errorf("test %d: negotiation mismatch: have %v, want %v", i, len(result), len(tt.Match))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Make sure all negotiated protocols are needed and correct
|
|
||||||
for name, proto := range result {
|
|
||||||
match, ok := tt.Match[name]
|
|
||||||
if !ok {
|
|
||||||
t.Errorf("test %d, proto '%s': negotiated but shouldn't have", i, name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if proto.Name != match.Name {
|
|
||||||
t.Errorf("test %d, proto '%s': name mismatch: have %v, want %v", i, name, proto.Name, match.Name)
|
|
||||||
}
|
|
||||||
if proto.Version != match.Version {
|
|
||||||
t.Errorf("test %d, proto '%s': version mismatch: have %v, want %v", i, name, proto.Version, match.Version)
|
|
||||||
}
|
|
||||||
if proto.offset-baseProtocolLength != match.offset {
|
|
||||||
t.Errorf("test %d, proto '%s': offset mismatch: have %v, want %v", i, name, proto.offset-baseProtocolLength, match.offset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Make sure no protocols missed negotiation
|
|
||||||
for name := range tt.Match {
|
|
||||||
if _, ok := result[name]; !ok {
|
|
||||||
t.Errorf("test %d, proto '%s': not negotiated, should have", i, name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
// Copyright 2014 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package p2p
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Protocol represents a P2P subprotocol implementation.
|
|
||||||
type Protocol struct {
|
|
||||||
// Name should contain the official protocol name,
|
|
||||||
// often a three-letter word.
|
|
||||||
Name string
|
|
||||||
|
|
||||||
// Version should contain the version number of the protocol.
|
|
||||||
Version uint
|
|
||||||
|
|
||||||
// Length should contain the number of message codes used
|
|
||||||
// by the protocol.
|
|
||||||
Length uint64
|
|
||||||
|
|
||||||
// Run is called in a new goroutine when the protocol has been
|
|
||||||
// negotiated with a peer. It should read and write messages from
|
|
||||||
// rw. The Payload for each message must be fully consumed.
|
|
||||||
//
|
|
||||||
// The peer connection is closed when Start returns. It should return
|
|
||||||
// any protocol-level error (such as an I/O error) that is
|
|
||||||
// encountered.
|
|
||||||
Run func(peer *Peer, rw MsgReadWriter) error
|
|
||||||
|
|
||||||
// NodeInfo is an optional helper method to retrieve protocol specific metadata
|
|
||||||
// about the host node.
|
|
||||||
NodeInfo func() interface{}
|
|
||||||
|
|
||||||
// PeerInfo is an optional helper method to retrieve protocol specific metadata
|
|
||||||
// about a certain peer in the network. If an info retrieval function is set,
|
|
||||||
// but returns nil, it is assumed that the protocol handshake is still running.
|
|
||||||
PeerInfo func(id enode.ID) interface{}
|
|
||||||
|
|
||||||
// DialCandidates, if non-nil, is a way to tell Server about protocol-specific nodes
|
|
||||||
// that should be dialed. The server continuously reads nodes from the iterator and
|
|
||||||
// attempts to create connections to them.
|
|
||||||
DialCandidates enode.Iterator
|
|
||||||
|
|
||||||
// Attributes contains protocol specific information for the node record.
|
|
||||||
Attributes []enr.Entry
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Protocol) cap() Cap {
|
|
||||||
return Cap{p.Name, p.Version}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cap is the structure of a peer capability.
|
|
||||||
type Cap struct {
|
|
||||||
Name string
|
|
||||||
Version uint
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cap Cap) String() string {
|
|
||||||
return fmt.Sprintf("%s/%d", cap.Name, cap.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cmp defines the canonical sorting order of capabilities.
|
|
||||||
func (cap Cap) Cmp(other Cap) int {
|
|
||||||
if cap.Name == other.Name {
|
|
||||||
if cap.Version < other.Version {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if cap.Version > other.Version {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return strings.Compare(cap.Name, other.Name)
|
|
||||||
}
|
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
// Copyright 2021 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rlpx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// readBuffer implements buffering for network reads. This type is similar to bufio.Reader,
|
|
||||||
// with two crucial differences: the buffer slice is exposed, and the buffer keeps all
|
|
||||||
// read data available until reset.
|
|
||||||
//
|
|
||||||
// How to use this type:
|
|
||||||
//
|
|
||||||
// Keep a readBuffer b alongside the underlying network connection. When reading a packet
|
|
||||||
// from the connection, first call b.reset(). This empties b.data. Now perform reads
|
|
||||||
// through b.read() until the end of the packet is reached. The complete packet data is
|
|
||||||
// now available in b.data.
|
|
||||||
type readBuffer struct {
|
|
||||||
data []byte
|
|
||||||
end int
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset removes all processed data which was read since the last call to reset.
|
|
||||||
// After reset, len(b.data) is zero.
|
|
||||||
func (b *readBuffer) reset() {
|
|
||||||
unprocessed := b.end - len(b.data)
|
|
||||||
copy(b.data[:unprocessed], b.data[len(b.data):b.end])
|
|
||||||
b.end = unprocessed
|
|
||||||
b.data = b.data[:0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// read reads at least n bytes from r, returning the bytes.
|
|
||||||
// The returned slice is valid until the next call to reset.
|
|
||||||
func (b *readBuffer) read(r io.Reader, n int) ([]byte, error) {
|
|
||||||
offset := len(b.data)
|
|
||||||
have := b.end - len(b.data)
|
|
||||||
|
|
||||||
// If n bytes are available in the buffer, there is no need to read from r at all.
|
|
||||||
if have >= n {
|
|
||||||
b.data = b.data[:offset+n]
|
|
||||||
return b.data[offset : offset+n], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make buffer space available.
|
|
||||||
need := n - have
|
|
||||||
b.grow(need)
|
|
||||||
|
|
||||||
// Read.
|
|
||||||
rn, err := io.ReadAtLeast(r, b.data[b.end:cap(b.data)], need)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
b.end += rn
|
|
||||||
b.data = b.data[:offset+n]
|
|
||||||
return b.data[offset : offset+n], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// grow ensures the buffer has at least n bytes of unused space.
|
|
||||||
func (b *readBuffer) grow(n int) {
|
|
||||||
if cap(b.data)-b.end >= n {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
need := n - (cap(b.data) - b.end)
|
|
||||||
offset := len(b.data)
|
|
||||||
b.data = append(b.data[:cap(b.data)], make([]byte, need)...)
|
|
||||||
b.data = b.data[:offset]
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeBuffer implements buffering for network writes. This is essentially
|
|
||||||
// a convenience wrapper around a byte slice.
|
|
||||||
type writeBuffer struct {
|
|
||||||
data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *writeBuffer) reset() {
|
|
||||||
b.data = b.data[:0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *writeBuffer) appendZero(n int) []byte {
|
|
||||||
offset := len(b.data)
|
|
||||||
b.data = append(b.data, make([]byte, n)...)
|
|
||||||
return b.data[offset : offset+n]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *writeBuffer) Write(data []byte) (int, error) {
|
|
||||||
b.data = append(b.data, data...)
|
|
||||||
return len(data), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxUint24 = int(^uint32(0) >> 8)
|
|
||||||
|
|
||||||
func readUint24(b []byte) uint32 {
|
|
||||||
return uint32(b[2]) | uint32(b[1])<<8 | uint32(b[0])<<16
|
|
||||||
}
|
|
||||||
|
|
||||||
func putUint24(v uint32, b []byte) {
|
|
||||||
b[0] = byte(v >> 16)
|
|
||||||
b[1] = byte(v >> 8)
|
|
||||||
b[2] = byte(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// growslice ensures b has the wanted length by either expanding it to its capacity
|
|
||||||
// or allocating a new slice if b has insufficient capacity.
|
|
||||||
func growslice(b []byte, wantLength int) []byte {
|
|
||||||
if len(b) >= wantLength {
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
if cap(b) >= wantLength {
|
|
||||||
return b[:cap(b)]
|
|
||||||
}
|
|
||||||
return make([]byte, wantLength)
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
// Copyright 2021 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rlpx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestReadBufferReset(t *testing.T) {
|
|
||||||
reader := bytes.NewReader(hexutil.MustDecode("0x010202030303040505"))
|
|
||||||
var b readBuffer
|
|
||||||
|
|
||||||
s1, _ := b.read(reader, 1)
|
|
||||||
s2, _ := b.read(reader, 2)
|
|
||||||
s3, _ := b.read(reader, 3)
|
|
||||||
|
|
||||||
assert.Equal(t, []byte{1}, s1)
|
|
||||||
assert.Equal(t, []byte{2, 2}, s2)
|
|
||||||
assert.Equal(t, []byte{3, 3, 3}, s3)
|
|
||||||
|
|
||||||
b.reset()
|
|
||||||
|
|
||||||
s4, _ := b.read(reader, 1)
|
|
||||||
s5, _ := b.read(reader, 2)
|
|
||||||
|
|
||||||
assert.Equal(t, []byte{4}, s4)
|
|
||||||
assert.Equal(t, []byte{5, 5}, s5)
|
|
||||||
|
|
||||||
s6, err := b.read(reader, 2)
|
|
||||||
|
|
||||||
assert.EqualError(t, err, "EOF")
|
|
||||||
assert.Nil(t, s6)
|
|
||||||
}
|
|
||||||
676
p2p/rlpx/rlpx.go
676
p2p/rlpx/rlpx.go
|
|
@ -1,676 +0,0 @@
|
||||||
// Copyright 2020 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Package rlpx implements the RLPx transport protocol.
|
|
||||||
package rlpx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"hash"
|
|
||||||
"io"
|
|
||||||
mrand "math/rand"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto/ecies"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/golang/snappy"
|
|
||||||
"golang.org/x/crypto/sha3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Conn is an RLPx network connection. It wraps a low-level network connection. The
|
|
||||||
// underlying connection should not be used for other activity when it is wrapped by Conn.
|
|
||||||
//
|
|
||||||
// Before sending messages, a handshake must be performed by calling the Handshake method.
|
|
||||||
// This type is not generally safe for concurrent use, but reading and writing of messages
|
|
||||||
// may happen concurrently after the handshake.
|
|
||||||
type Conn struct {
|
|
||||||
dialDest *ecdsa.PublicKey
|
|
||||||
conn net.Conn
|
|
||||||
session *sessionState
|
|
||||||
|
|
||||||
// These are the buffers for snappy compression.
|
|
||||||
// Compression is enabled if they are non-nil.
|
|
||||||
snappyReadBuffer []byte
|
|
||||||
snappyWriteBuffer []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// sessionState contains the session keys.
|
|
||||||
type sessionState struct {
|
|
||||||
enc cipher.Stream
|
|
||||||
dec cipher.Stream
|
|
||||||
|
|
||||||
egressMAC hashMAC
|
|
||||||
ingressMAC hashMAC
|
|
||||||
rbuf readBuffer
|
|
||||||
wbuf writeBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
// hashMAC holds the state of the RLPx v4 MAC contraption.
|
|
||||||
type hashMAC struct {
|
|
||||||
cipher cipher.Block
|
|
||||||
hash hash.Hash
|
|
||||||
aesBuffer [16]byte
|
|
||||||
hashBuffer [32]byte
|
|
||||||
seedBuffer [32]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHashMAC(cipher cipher.Block, h hash.Hash) hashMAC {
|
|
||||||
m := hashMAC{cipher: cipher, hash: h}
|
|
||||||
if cipher.BlockSize() != len(m.aesBuffer) {
|
|
||||||
panic(fmt.Errorf("invalid MAC cipher block size %d", cipher.BlockSize()))
|
|
||||||
}
|
|
||||||
if h.Size() != len(m.hashBuffer) {
|
|
||||||
panic(fmt.Errorf("invalid MAC digest size %d", h.Size()))
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewConn wraps the given network connection. If dialDest is non-nil, the connection
|
|
||||||
// behaves as the initiator during the handshake.
|
|
||||||
func NewConn(conn net.Conn, dialDest *ecdsa.PublicKey) *Conn {
|
|
||||||
return &Conn{
|
|
||||||
dialDest: dialDest,
|
|
||||||
conn: conn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSnappy enables or disables snappy compression of messages. This is usually called
|
|
||||||
// after the devp2p Hello message exchange when the negotiated version indicates that
|
|
||||||
// compression is available on both ends of the connection.
|
|
||||||
func (c *Conn) SetSnappy(snappy bool) {
|
|
||||||
if snappy {
|
|
||||||
c.snappyReadBuffer = []byte{}
|
|
||||||
c.snappyWriteBuffer = []byte{}
|
|
||||||
} else {
|
|
||||||
c.snappyReadBuffer = nil
|
|
||||||
c.snappyWriteBuffer = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReadDeadline sets the deadline for all future read operations.
|
|
||||||
func (c *Conn) SetReadDeadline(time time.Time) error {
|
|
||||||
return c.conn.SetReadDeadline(time)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetWriteDeadline sets the deadline for all future write operations.
|
|
||||||
func (c *Conn) SetWriteDeadline(time time.Time) error {
|
|
||||||
return c.conn.SetWriteDeadline(time)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDeadline sets the deadline for all future read and write operations.
|
|
||||||
func (c *Conn) SetDeadline(time time.Time) error {
|
|
||||||
return c.conn.SetDeadline(time)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read reads a message from the connection.
|
|
||||||
// The returned data buffer is valid until the next call to Read.
|
|
||||||
func (c *Conn) Read() (code uint64, data []byte, wireSize int, err error) {
|
|
||||||
if c.session == nil {
|
|
||||||
panic("can't ReadMsg before handshake")
|
|
||||||
}
|
|
||||||
|
|
||||||
frame, err := c.session.readFrame(c.conn)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, 0, err
|
|
||||||
}
|
|
||||||
code, data, err = rlp.SplitUint64(frame)
|
|
||||||
if err != nil {
|
|
||||||
return 0, nil, 0, fmt.Errorf("invalid message code: %v", err)
|
|
||||||
}
|
|
||||||
wireSize = len(data)
|
|
||||||
|
|
||||||
// If snappy is enabled, verify and decompress message.
|
|
||||||
if c.snappyReadBuffer != nil {
|
|
||||||
var actualSize int
|
|
||||||
actualSize, err = snappy.DecodedLen(data)
|
|
||||||
if err != nil {
|
|
||||||
return code, nil, 0, err
|
|
||||||
}
|
|
||||||
if actualSize > maxUint24 {
|
|
||||||
return code, nil, 0, errPlainMessageTooLarge
|
|
||||||
}
|
|
||||||
c.snappyReadBuffer = growslice(c.snappyReadBuffer, actualSize)
|
|
||||||
data, err = snappy.Decode(c.snappyReadBuffer, data)
|
|
||||||
}
|
|
||||||
return code, data, wireSize, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *sessionState) readFrame(conn io.Reader) ([]byte, error) {
|
|
||||||
h.rbuf.reset()
|
|
||||||
|
|
||||||
// Read the frame header.
|
|
||||||
header, err := h.rbuf.read(conn, 32)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify header MAC.
|
|
||||||
wantHeaderMAC := h.ingressMAC.computeHeader(header[:16])
|
|
||||||
if !hmac.Equal(wantHeaderMAC, header[16:]) {
|
|
||||||
return nil, errors.New("bad header MAC")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypt the frame header to get the frame size.
|
|
||||||
h.dec.XORKeyStream(header[:16], header[:16])
|
|
||||||
fsize := readUint24(header[:16])
|
|
||||||
// Frame size rounded up to 16 byte boundary for padding.
|
|
||||||
rsize := fsize
|
|
||||||
if padding := fsize % 16; padding > 0 {
|
|
||||||
rsize += 16 - padding
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the frame content.
|
|
||||||
frame, err := h.rbuf.read(conn, int(rsize))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate frame MAC.
|
|
||||||
frameMAC, err := h.rbuf.read(conn, 16)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
wantFrameMAC := h.ingressMAC.computeFrame(frame)
|
|
||||||
if !hmac.Equal(wantFrameMAC, frameMAC) {
|
|
||||||
return nil, errors.New("bad frame MAC")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypt the frame data.
|
|
||||||
h.dec.XORKeyStream(frame, frame)
|
|
||||||
return frame[:fsize], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write writes a message to the connection.
|
|
||||||
//
|
|
||||||
// Write returns the written size of the message data. This may be less than or equal to
|
|
||||||
// len(data) depending on whether snappy compression is enabled.
|
|
||||||
func (c *Conn) Write(code uint64, data []byte) (uint32, error) {
|
|
||||||
if c.session == nil {
|
|
||||||
panic("can't WriteMsg before handshake")
|
|
||||||
}
|
|
||||||
if len(data) > maxUint24 {
|
|
||||||
return 0, errPlainMessageTooLarge
|
|
||||||
}
|
|
||||||
if c.snappyWriteBuffer != nil {
|
|
||||||
// Ensure the buffer has sufficient size.
|
|
||||||
// Package snappy will allocate its own buffer if the provided
|
|
||||||
// one is smaller than MaxEncodedLen.
|
|
||||||
c.snappyWriteBuffer = growslice(c.snappyWriteBuffer, snappy.MaxEncodedLen(len(data)))
|
|
||||||
data = snappy.Encode(c.snappyWriteBuffer, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
wireSize := uint32(len(data))
|
|
||||||
err := c.session.writeFrame(c.conn, code, data)
|
|
||||||
return wireSize, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *sessionState) writeFrame(conn io.Writer, code uint64, data []byte) error {
|
|
||||||
h.wbuf.reset()
|
|
||||||
|
|
||||||
// Write header.
|
|
||||||
fsize := rlp.IntSize(code) + len(data)
|
|
||||||
if fsize > maxUint24 {
|
|
||||||
return errPlainMessageTooLarge
|
|
||||||
}
|
|
||||||
header := h.wbuf.appendZero(16)
|
|
||||||
putUint24(uint32(fsize), header)
|
|
||||||
copy(header[3:], zeroHeader)
|
|
||||||
h.enc.XORKeyStream(header, header)
|
|
||||||
|
|
||||||
// Write header MAC.
|
|
||||||
h.wbuf.Write(h.egressMAC.computeHeader(header))
|
|
||||||
|
|
||||||
// Encode and encrypt the frame data.
|
|
||||||
offset := len(h.wbuf.data)
|
|
||||||
h.wbuf.data = rlp.AppendUint64(h.wbuf.data, code)
|
|
||||||
h.wbuf.Write(data)
|
|
||||||
if padding := fsize % 16; padding > 0 {
|
|
||||||
h.wbuf.appendZero(16 - padding)
|
|
||||||
}
|
|
||||||
framedata := h.wbuf.data[offset:]
|
|
||||||
h.enc.XORKeyStream(framedata, framedata)
|
|
||||||
|
|
||||||
// Write frame MAC.
|
|
||||||
h.wbuf.Write(h.egressMAC.computeFrame(framedata))
|
|
||||||
|
|
||||||
_, err := conn.Write(h.wbuf.data)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// computeHeader computes the MAC of a frame header.
|
|
||||||
func (m *hashMAC) computeHeader(header []byte) []byte {
|
|
||||||
sum1 := m.hash.Sum(m.hashBuffer[:0])
|
|
||||||
return m.compute(sum1, header)
|
|
||||||
}
|
|
||||||
|
|
||||||
// computeFrame computes the MAC of framedata.
|
|
||||||
func (m *hashMAC) computeFrame(framedata []byte) []byte {
|
|
||||||
m.hash.Write(framedata)
|
|
||||||
seed := m.hash.Sum(m.seedBuffer[:0])
|
|
||||||
return m.compute(seed, seed[:16])
|
|
||||||
}
|
|
||||||
|
|
||||||
// compute computes the MAC of a 16-byte 'seed'.
|
|
||||||
//
|
|
||||||
// To do this, it encrypts the current value of the hash state, then XORs the ciphertext
|
|
||||||
// with seed. The obtained value is written back into the hash state and hash output is
|
|
||||||
// taken again. The first 16 bytes of the resulting sum are the MAC value.
|
|
||||||
//
|
|
||||||
// This MAC construction is a horrible, legacy thing.
|
|
||||||
func (m *hashMAC) compute(sum1, seed []byte) []byte {
|
|
||||||
if len(seed) != len(m.aesBuffer) {
|
|
||||||
panic("invalid MAC seed")
|
|
||||||
}
|
|
||||||
|
|
||||||
m.cipher.Encrypt(m.aesBuffer[:], sum1)
|
|
||||||
for i := range m.aesBuffer {
|
|
||||||
m.aesBuffer[i] ^= seed[i]
|
|
||||||
}
|
|
||||||
m.hash.Write(m.aesBuffer[:])
|
|
||||||
sum2 := m.hash.Sum(m.hashBuffer[:0])
|
|
||||||
return sum2[:16]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handshake performs the handshake. This must be called before any data is written
|
|
||||||
// or read from the connection.
|
|
||||||
func (c *Conn) Handshake(prv *ecdsa.PrivateKey) (*ecdsa.PublicKey, error) {
|
|
||||||
var (
|
|
||||||
sec Secrets
|
|
||||||
err error
|
|
||||||
h handshakeState
|
|
||||||
)
|
|
||||||
if c.dialDest != nil {
|
|
||||||
sec, err = h.runInitiator(c.conn, prv, c.dialDest)
|
|
||||||
} else {
|
|
||||||
sec, err = h.runRecipient(c.conn, prv)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.InitWithSecrets(sec)
|
|
||||||
c.session.rbuf = h.rbuf
|
|
||||||
c.session.wbuf = h.wbuf
|
|
||||||
return sec.remote, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitWithSecrets injects connection secrets as if a handshake had
|
|
||||||
// been performed. This cannot be called after the handshake.
|
|
||||||
func (c *Conn) InitWithSecrets(sec Secrets) {
|
|
||||||
if c.session != nil {
|
|
||||||
panic("can't handshake twice")
|
|
||||||
}
|
|
||||||
macc, err := aes.NewCipher(sec.MAC)
|
|
||||||
if err != nil {
|
|
||||||
panic("invalid MAC secret: " + err.Error())
|
|
||||||
}
|
|
||||||
encc, err := aes.NewCipher(sec.AES)
|
|
||||||
if err != nil {
|
|
||||||
panic("invalid AES secret: " + err.Error())
|
|
||||||
}
|
|
||||||
// we use an all-zeroes IV for AES because the key used
|
|
||||||
// for encryption is ephemeral.
|
|
||||||
iv := make([]byte, encc.BlockSize())
|
|
||||||
c.session = &sessionState{
|
|
||||||
enc: cipher.NewCTR(encc, iv),
|
|
||||||
dec: cipher.NewCTR(encc, iv),
|
|
||||||
egressMAC: newHashMAC(macc, sec.EgressMAC),
|
|
||||||
ingressMAC: newHashMAC(macc, sec.IngressMAC),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the underlying network connection.
|
|
||||||
func (c *Conn) Close() error {
|
|
||||||
return c.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constants for the handshake.
|
|
||||||
const (
|
|
||||||
sskLen = 16 // ecies.MaxSharedKeyLength(pubKey) / 2
|
|
||||||
sigLen = crypto.SignatureLength // elliptic S256
|
|
||||||
pubLen = 64 // 512 bit pubkey in uncompressed representation without format byte
|
|
||||||
shaLen = 32 // hash length (for nonce etc)
|
|
||||||
|
|
||||||
eciesOverhead = 65 /* pubkey */ + 16 /* IV */ + 32 /* MAC */
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// this is used in place of actual frame header data.
|
|
||||||
// TODO: replace this when Msg contains the protocol type code.
|
|
||||||
zeroHeader = []byte{0xC2, 0x80, 0x80}
|
|
||||||
|
|
||||||
// errPlainMessageTooLarge is returned if a decompressed message length exceeds
|
|
||||||
// the allowed 24 bits (i.e. length >= 16MB).
|
|
||||||
errPlainMessageTooLarge = errors.New("message length >= 16MB")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Secrets represents the connection secrets which are negotiated during the handshake.
|
|
||||||
type Secrets struct {
|
|
||||||
AES, MAC []byte
|
|
||||||
EgressMAC, IngressMAC hash.Hash
|
|
||||||
remote *ecdsa.PublicKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// handshakeState contains the state of the encryption handshake.
|
|
||||||
type handshakeState struct {
|
|
||||||
initiator bool
|
|
||||||
remote *ecies.PublicKey // remote-pubk
|
|
||||||
initNonce, respNonce []byte // nonce
|
|
||||||
randomPrivKey *ecies.PrivateKey // ecdhe-random
|
|
||||||
remoteRandomPub *ecies.PublicKey // ecdhe-random-pubk
|
|
||||||
|
|
||||||
rbuf readBuffer
|
|
||||||
wbuf writeBuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
// RLPx v4 handshake auth (defined in EIP-8).
|
|
||||||
type authMsgV4 struct {
|
|
||||||
Signature [sigLen]byte
|
|
||||||
InitiatorPubkey [pubLen]byte
|
|
||||||
Nonce [shaLen]byte
|
|
||||||
Version uint
|
|
||||||
|
|
||||||
// Ignore additional fields (forward-compatibility)
|
|
||||||
Rest []rlp.RawValue `rlp:"tail"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RLPx v4 handshake response (defined in EIP-8).
|
|
||||||
type authRespV4 struct {
|
|
||||||
RandomPubkey [pubLen]byte
|
|
||||||
Nonce [shaLen]byte
|
|
||||||
Version uint
|
|
||||||
|
|
||||||
// Ignore additional fields (forward-compatibility)
|
|
||||||
Rest []rlp.RawValue `rlp:"tail"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// runRecipient negotiates a session token on conn.
|
|
||||||
// it should be called on the listening side of the connection.
|
|
||||||
//
|
|
||||||
// prv is the local client's private key.
|
|
||||||
func (h *handshakeState) runRecipient(conn io.ReadWriter, prv *ecdsa.PrivateKey) (s Secrets, err error) {
|
|
||||||
authMsg := new(authMsgV4)
|
|
||||||
authPacket, err := h.readMsg(authMsg, prv, conn)
|
|
||||||
if err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
if err := h.handleAuthMsg(authMsg, prv); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
|
|
||||||
authRespMsg, err := h.makeAuthResp()
|
|
||||||
if err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
authRespPacket, err := h.sealEIP8(authRespMsg)
|
|
||||||
if err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
if _, err = conn.Write(authRespPacket); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.secrets(authPacket, authRespPacket)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handshakeState) handleAuthMsg(msg *authMsgV4, prv *ecdsa.PrivateKey) error {
|
|
||||||
// Import the remote identity.
|
|
||||||
rpub, err := importPublicKey(msg.InitiatorPubkey[:])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
h.initNonce = msg.Nonce[:]
|
|
||||||
h.remote = rpub
|
|
||||||
|
|
||||||
// Generate random keypair for ECDH.
|
|
||||||
// If a private key is already set, use it instead of generating one (for testing).
|
|
||||||
if h.randomPrivKey == nil {
|
|
||||||
h.randomPrivKey, err = ecies.GenerateKey(rand.Reader, crypto.S256(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the signature.
|
|
||||||
token, err := h.staticSharedSecret(prv)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
signedMsg := xor(token, h.initNonce)
|
|
||||||
remoteRandomPub, err := crypto.Ecrecover(signedMsg, msg.Signature[:])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
h.remoteRandomPub, _ = importPublicKey(remoteRandomPub)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// secrets is called after the handshake is completed.
|
|
||||||
// It extracts the connection secrets from the handshake values.
|
|
||||||
func (h *handshakeState) secrets(auth, authResp []byte) (Secrets, error) {
|
|
||||||
ecdheSecret, err := h.randomPrivKey.GenerateShared(h.remoteRandomPub, sskLen, sskLen)
|
|
||||||
if err != nil {
|
|
||||||
return Secrets{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// derive base secrets from ephemeral key agreement
|
|
||||||
sharedSecret := crypto.Keccak256(ecdheSecret, crypto.Keccak256(h.respNonce, h.initNonce))
|
|
||||||
aesSecret := crypto.Keccak256(ecdheSecret, sharedSecret)
|
|
||||||
s := Secrets{
|
|
||||||
remote: h.remote.ExportECDSA(),
|
|
||||||
AES: aesSecret,
|
|
||||||
MAC: crypto.Keccak256(ecdheSecret, aesSecret),
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup sha3 instances for the MACs
|
|
||||||
mac1 := sha3.NewLegacyKeccak256()
|
|
||||||
mac1.Write(xor(s.MAC, h.respNonce))
|
|
||||||
mac1.Write(auth)
|
|
||||||
mac2 := sha3.NewLegacyKeccak256()
|
|
||||||
mac2.Write(xor(s.MAC, h.initNonce))
|
|
||||||
mac2.Write(authResp)
|
|
||||||
if h.initiator {
|
|
||||||
s.EgressMAC, s.IngressMAC = mac1, mac2
|
|
||||||
} else {
|
|
||||||
s.EgressMAC, s.IngressMAC = mac2, mac1
|
|
||||||
}
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// staticSharedSecret returns the static shared secret, the result
|
|
||||||
// of key agreement between the local and remote static node key.
|
|
||||||
func (h *handshakeState) staticSharedSecret(prv *ecdsa.PrivateKey) ([]byte, error) {
|
|
||||||
return ecies.ImportECDSA(prv).GenerateShared(h.remote, sskLen, sskLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
// runInitiator negotiates a session token on conn.
|
|
||||||
// it should be called on the dialing side of the connection.
|
|
||||||
//
|
|
||||||
// prv is the local client's private key.
|
|
||||||
func (h *handshakeState) runInitiator(conn io.ReadWriter, prv *ecdsa.PrivateKey, remote *ecdsa.PublicKey) (s Secrets, err error) {
|
|
||||||
h.initiator = true
|
|
||||||
h.remote = ecies.ImportECDSAPublic(remote)
|
|
||||||
|
|
||||||
authMsg, err := h.makeAuthMsg(prv)
|
|
||||||
if err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
authPacket, err := h.sealEIP8(authMsg)
|
|
||||||
if err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = conn.Write(authPacket); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
|
|
||||||
authRespMsg := new(authRespV4)
|
|
||||||
authRespPacket, err := h.readMsg(authRespMsg, prv, conn)
|
|
||||||
if err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
if err := h.handleAuthResp(authRespMsg); err != nil {
|
|
||||||
return s, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.secrets(authPacket, authRespPacket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeAuthMsg creates the initiator handshake message.
|
|
||||||
func (h *handshakeState) makeAuthMsg(prv *ecdsa.PrivateKey) (*authMsgV4, error) {
|
|
||||||
// Generate random initiator nonce.
|
|
||||||
h.initNonce = make([]byte, shaLen)
|
|
||||||
_, err := rand.Read(h.initNonce)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Generate random keypair to for ECDH.
|
|
||||||
h.randomPrivKey, err = ecies.GenerateKey(rand.Reader, crypto.S256(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign known message: static-shared-secret ^ nonce
|
|
||||||
token, err := h.staticSharedSecret(prv)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
signed := xor(token, h.initNonce)
|
|
||||||
signature, err := crypto.Sign(signed, h.randomPrivKey.ExportECDSA())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := new(authMsgV4)
|
|
||||||
copy(msg.Signature[:], signature)
|
|
||||||
copy(msg.InitiatorPubkey[:], crypto.FromECDSAPub(&prv.PublicKey)[1:])
|
|
||||||
copy(msg.Nonce[:], h.initNonce)
|
|
||||||
msg.Version = 4
|
|
||||||
return msg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handshakeState) handleAuthResp(msg *authRespV4) (err error) {
|
|
||||||
h.respNonce = msg.Nonce[:]
|
|
||||||
h.remoteRandomPub, err = importPublicKey(msg.RandomPubkey[:])
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handshakeState) makeAuthResp() (msg *authRespV4, err error) {
|
|
||||||
// Generate random nonce.
|
|
||||||
h.respNonce = make([]byte, shaLen)
|
|
||||||
if _, err = rand.Read(h.respNonce); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
msg = new(authRespV4)
|
|
||||||
copy(msg.Nonce[:], h.respNonce)
|
|
||||||
copy(msg.RandomPubkey[:], exportPubkey(&h.randomPrivKey.PublicKey))
|
|
||||||
msg.Version = 4
|
|
||||||
return msg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// readMsg reads an encrypted handshake message, decoding it into msg.
|
|
||||||
func (h *handshakeState) readMsg(msg interface{}, prv *ecdsa.PrivateKey, r io.Reader) ([]byte, error) {
|
|
||||||
h.rbuf.reset()
|
|
||||||
h.rbuf.grow(512)
|
|
||||||
|
|
||||||
// Read the size prefix.
|
|
||||||
prefix, err := h.rbuf.read(r, 2)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
size := binary.BigEndian.Uint16(prefix)
|
|
||||||
|
|
||||||
// Read the handshake packet.
|
|
||||||
packet, err := h.rbuf.read(r, int(size))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
dec, err := ecies.ImportECDSA(prv).Decrypt(packet, nil, prefix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Can't use rlp.DecodeBytes here because it rejects
|
|
||||||
// trailing data (forward-compatibility).
|
|
||||||
s := rlp.NewStream(bytes.NewReader(dec), 0)
|
|
||||||
err = s.Decode(msg)
|
|
||||||
return h.rbuf.data[:len(prefix)+len(packet)], err
|
|
||||||
}
|
|
||||||
|
|
||||||
// sealEIP8 encrypts a handshake message.
|
|
||||||
func (h *handshakeState) sealEIP8(msg interface{}) ([]byte, error) {
|
|
||||||
h.wbuf.reset()
|
|
||||||
|
|
||||||
// Write the message plaintext.
|
|
||||||
if err := rlp.Encode(&h.wbuf, msg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Pad with random amount of data. the amount needs to be at least 100 bytes to make
|
|
||||||
// the message distinguishable from pre-EIP-8 handshakes.
|
|
||||||
h.wbuf.appendZero(mrand.Intn(100) + 100)
|
|
||||||
|
|
||||||
prefix := make([]byte, 2)
|
|
||||||
binary.BigEndian.PutUint16(prefix, uint16(len(h.wbuf.data)+eciesOverhead))
|
|
||||||
|
|
||||||
enc, err := ecies.Encrypt(rand.Reader, h.remote, h.wbuf.data, nil, prefix)
|
|
||||||
return append(prefix, enc...), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// importPublicKey unmarshals 512 bit public keys.
|
|
||||||
func importPublicKey(pubKey []byte) (*ecies.PublicKey, error) {
|
|
||||||
var pubKey65 []byte
|
|
||||||
switch len(pubKey) {
|
|
||||||
case 64:
|
|
||||||
// add 'uncompressed key' flag
|
|
||||||
pubKey65 = append([]byte{0x04}, pubKey...)
|
|
||||||
case 65:
|
|
||||||
pubKey65 = pubKey
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid public key length %v (expect 64/65)", len(pubKey))
|
|
||||||
}
|
|
||||||
// TODO: fewer pointless conversions
|
|
||||||
pub, err := crypto.UnmarshalPubkey(pubKey65)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return ecies.ImportECDSAPublic(pub), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func exportPubkey(pub *ecies.PublicKey) []byte {
|
|
||||||
if pub == nil {
|
|
||||||
panic("nil pubkey")
|
|
||||||
}
|
|
||||||
return elliptic.Marshal(pub.Curve, pub.X, pub.Y)[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
func xor(one, other []byte) (xor []byte) {
|
|
||||||
xor = make([]byte, len(one))
|
|
||||||
for i := 0; i < len(one); i++ {
|
|
||||||
xor[i] = one[i] ^ other[i]
|
|
||||||
}
|
|
||||||
return xor
|
|
||||||
}
|
|
||||||
|
|
@ -1,453 +0,0 @@
|
||||||
// Copyright 2020 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package rlpx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math/rand"
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/crypto/ecies"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/pipes"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
type message struct {
|
|
||||||
code uint64
|
|
||||||
data []byte
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandshake(t *testing.T) {
|
|
||||||
p1, p2 := createPeers(t)
|
|
||||||
p1.Close()
|
|
||||||
p2.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that messages can be sent and received through WriteMsg/ReadMsg.
|
|
||||||
func TestReadWriteMsg(t *testing.T) {
|
|
||||||
peer1, peer2 := createPeers(t)
|
|
||||||
defer peer1.Close()
|
|
||||||
defer peer2.Close()
|
|
||||||
|
|
||||||
testCode := uint64(23)
|
|
||||||
testData := []byte("test")
|
|
||||||
checkMsgReadWrite(t, peer1, peer2, testCode, testData)
|
|
||||||
|
|
||||||
t.Log("enabling snappy")
|
|
||||||
peer1.SetSnappy(true)
|
|
||||||
peer2.SetSnappy(true)
|
|
||||||
checkMsgReadWrite(t, peer1, peer2, testCode, testData)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkMsgReadWrite(t *testing.T, p1, p2 *Conn, msgCode uint64, msgData []byte) {
|
|
||||||
// Set up the reader.
|
|
||||||
ch := make(chan message, 1)
|
|
||||||
go func() {
|
|
||||||
var msg message
|
|
||||||
msg.code, msg.data, _, msg.err = p1.Read()
|
|
||||||
ch <- msg
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Write the message.
|
|
||||||
_, err := p2.Write(msgCode, msgData)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check it was received correctly.
|
|
||||||
msg := <-ch
|
|
||||||
assert.Equal(t, msgCode, msg.code, "wrong message code returned from ReadMsg")
|
|
||||||
assert.Equal(t, msgData, msg.data, "wrong message data returned from ReadMsg")
|
|
||||||
}
|
|
||||||
|
|
||||||
func createPeers(t *testing.T) (peer1, peer2 *Conn) {
|
|
||||||
conn1, conn2 := net.Pipe()
|
|
||||||
key1, key2 := newkey(), newkey()
|
|
||||||
peer1 = NewConn(conn1, &key2.PublicKey) // dialer
|
|
||||||
peer2 = NewConn(conn2, nil) // listener
|
|
||||||
doHandshake(t, peer1, peer2, key1, key2)
|
|
||||||
return peer1, peer2
|
|
||||||
}
|
|
||||||
|
|
||||||
func doHandshake(t *testing.T, peer1, peer2 *Conn, key1, key2 *ecdsa.PrivateKey) {
|
|
||||||
keyChan := make(chan *ecdsa.PublicKey, 1)
|
|
||||||
go func() {
|
|
||||||
pubKey, err := peer2.Handshake(key2)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("peer2 could not do handshake: %v", err)
|
|
||||||
}
|
|
||||||
keyChan <- pubKey
|
|
||||||
}()
|
|
||||||
|
|
||||||
pubKey2, err := peer1.Handshake(key1)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("peer1 could not do handshake: %v", err)
|
|
||||||
}
|
|
||||||
pubKey1 := <-keyChan
|
|
||||||
|
|
||||||
// Confirm the handshake was successful.
|
|
||||||
if !reflect.DeepEqual(pubKey1, &key1.PublicKey) || !reflect.DeepEqual(pubKey2, &key2.PublicKey) {
|
|
||||||
t.Fatal("unsuccessful handshake")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks the frame data of written messages.
|
|
||||||
func TestFrameReadWrite(t *testing.T) {
|
|
||||||
conn := NewConn(nil, nil)
|
|
||||||
hash := fakeHash([]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1})
|
|
||||||
conn.InitWithSecrets(Secrets{
|
|
||||||
AES: crypto.Keccak256(),
|
|
||||||
MAC: crypto.Keccak256(),
|
|
||||||
IngressMAC: hash,
|
|
||||||
EgressMAC: hash,
|
|
||||||
})
|
|
||||||
h := conn.session
|
|
||||||
|
|
||||||
golden := unhex(`
|
|
||||||
00828ddae471818bb0bfa6b551d1cb42
|
|
||||||
01010101010101010101010101010101
|
|
||||||
ba628a4ba590cb43f7848f41c4382885
|
|
||||||
01010101010101010101010101010101
|
|
||||||
`)
|
|
||||||
msgCode := uint64(8)
|
|
||||||
msg := []uint{1, 2, 3, 4}
|
|
||||||
msgEnc, _ := rlp.EncodeToBytes(msg)
|
|
||||||
|
|
||||||
// Check writeFrame. The frame that's written should be equal to the test vector.
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
if err := h.writeFrame(buf, msgCode, msgEnc); err != nil {
|
|
||||||
t.Fatalf("WriteMsg error: %v", err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(buf.Bytes(), golden) {
|
|
||||||
t.Fatalf("output mismatch:\n got: %x\n want: %x", buf.Bytes(), golden)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check readFrame on the test vector.
|
|
||||||
content, err := h.readFrame(bytes.NewReader(golden))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ReadMsg error: %v", err)
|
|
||||||
}
|
|
||||||
wantContent := unhex("08C401020304")
|
|
||||||
if !bytes.Equal(content, wantContent) {
|
|
||||||
t.Errorf("frame content mismatch:\ngot %x\nwant %x", content, wantContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeHash []byte
|
|
||||||
|
|
||||||
func (fakeHash) Write(p []byte) (int, error) { return len(p), nil }
|
|
||||||
func (fakeHash) Reset() {}
|
|
||||||
func (fakeHash) BlockSize() int { return 0 }
|
|
||||||
func (h fakeHash) Size() int { return len(h) }
|
|
||||||
func (h fakeHash) Sum(b []byte) []byte { return append(b, h...) }
|
|
||||||
|
|
||||||
type handshakeAuthTest struct {
|
|
||||||
input string
|
|
||||||
wantVersion uint
|
|
||||||
wantRest []rlp.RawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
var eip8HandshakeAuthTests = []handshakeAuthTest{
|
|
||||||
// (Auth₂) EIP-8 encoding
|
|
||||||
{
|
|
||||||
input: `
|
|
||||||
01b304ab7578555167be8154d5cc456f567d5ba302662433674222360f08d5f1534499d3678b513b
|
|
||||||
0fca474f3a514b18e75683032eb63fccb16c156dc6eb2c0b1593f0d84ac74f6e475f1b8d56116b84
|
|
||||||
9634a8c458705bf83a626ea0384d4d7341aae591fae42ce6bd5c850bfe0b999a694a49bbbaf3ef6c
|
|
||||||
da61110601d3b4c02ab6c30437257a6e0117792631a4b47c1d52fc0f8f89caadeb7d02770bf999cc
|
|
||||||
147d2df3b62e1ffb2c9d8c125a3984865356266bca11ce7d3a688663a51d82defaa8aad69da39ab6
|
|
||||||
d5470e81ec5f2a7a47fb865ff7cca21516f9299a07b1bc63ba56c7a1a892112841ca44b6e0034dee
|
|
||||||
70c9adabc15d76a54f443593fafdc3b27af8059703f88928e199cb122362a4b35f62386da7caad09
|
|
||||||
c001edaeb5f8a06d2b26fb6cb93c52a9fca51853b68193916982358fe1e5369e249875bb8d0d0ec3
|
|
||||||
6f917bc5e1eafd5896d46bd61ff23f1a863a8a8dcd54c7b109b771c8e61ec9c8908c733c0263440e
|
|
||||||
2aa067241aaa433f0bb053c7b31a838504b148f570c0ad62837129e547678c5190341e4f1693956c
|
|
||||||
3bf7678318e2d5b5340c9e488eefea198576344afbdf66db5f51204a6961a63ce072c8926c
|
|
||||||
`,
|
|
||||||
wantVersion: 4,
|
|
||||||
wantRest: []rlp.RawValue{},
|
|
||||||
},
|
|
||||||
// (Auth₃) RLPx v4 EIP-8 encoding with version 56, additional list elements
|
|
||||||
{
|
|
||||||
input: `
|
|
||||||
01b8044c6c312173685d1edd268aa95e1d495474c6959bcdd10067ba4c9013df9e40ff45f5bfd6f7
|
|
||||||
2471f93a91b493f8e00abc4b80f682973de715d77ba3a005a242eb859f9a211d93a347fa64b597bf
|
|
||||||
280a6b88e26299cf263b01b8dfdb712278464fd1c25840b995e84d367d743f66c0e54a586725b7bb
|
|
||||||
f12acca27170ae3283c1073adda4b6d79f27656993aefccf16e0d0409fe07db2dc398a1b7e8ee93b
|
|
||||||
cd181485fd332f381d6a050fba4c7641a5112ac1b0b61168d20f01b479e19adf7fdbfa0905f63352
|
|
||||||
bfc7e23cf3357657455119d879c78d3cf8c8c06375f3f7d4861aa02a122467e069acaf513025ff19
|
|
||||||
6641f6d2810ce493f51bee9c966b15c5043505350392b57645385a18c78f14669cc4d960446c1757
|
|
||||||
1b7c5d725021babbcd786957f3d17089c084907bda22c2b2675b4378b114c601d858802a55345a15
|
|
||||||
116bc61da4193996187ed70d16730e9ae6b3bb8787ebcaea1871d850997ddc08b4f4ea668fbf3740
|
|
||||||
7ac044b55be0908ecb94d4ed172ece66fd31bfdadf2b97a8bc690163ee11f5b575a4b44e36e2bfb2
|
|
||||||
f0fce91676fd64c7773bac6a003f481fddd0bae0a1f31aa27504e2a533af4cef3b623f4791b2cca6
|
|
||||||
d490
|
|
||||||
`,
|
|
||||||
wantVersion: 56,
|
|
||||||
wantRest: []rlp.RawValue{{0x01}, {0x02}, {0xC2, 0x04, 0x05}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
type handshakeAckTest struct {
|
|
||||||
input string
|
|
||||||
wantVersion uint
|
|
||||||
wantRest []rlp.RawValue
|
|
||||||
}
|
|
||||||
|
|
||||||
var eip8HandshakeRespTests = []handshakeAckTest{
|
|
||||||
// (Ack₂) EIP-8 encoding
|
|
||||||
{
|
|
||||||
input: `
|
|
||||||
01ea0451958701280a56482929d3b0757da8f7fbe5286784beead59d95089c217c9b917788989470
|
|
||||||
b0e330cc6e4fb383c0340ed85fab836ec9fb8a49672712aeabbdfd1e837c1ff4cace34311cd7f4de
|
|
||||||
05d59279e3524ab26ef753a0095637ac88f2b499b9914b5f64e143eae548a1066e14cd2f4bd7f814
|
|
||||||
c4652f11b254f8a2d0191e2f5546fae6055694aed14d906df79ad3b407d94692694e259191cde171
|
|
||||||
ad542fc588fa2b7333313d82a9f887332f1dfc36cea03f831cb9a23fea05b33deb999e85489e645f
|
|
||||||
6aab1872475d488d7bd6c7c120caf28dbfc5d6833888155ed69d34dbdc39c1f299be1057810f34fb
|
|
||||||
e754d021bfca14dc989753d61c413d261934e1a9c67ee060a25eefb54e81a4d14baff922180c395d
|
|
||||||
3f998d70f46f6b58306f969627ae364497e73fc27f6d17ae45a413d322cb8814276be6ddd13b885b
|
|
||||||
201b943213656cde498fa0e9ddc8e0b8f8a53824fbd82254f3e2c17e8eaea009c38b4aa0a3f306e8
|
|
||||||
797db43c25d68e86f262e564086f59a2fc60511c42abfb3057c247a8a8fe4fb3ccbadde17514b7ac
|
|
||||||
8000cdb6a912778426260c47f38919a91f25f4b5ffb455d6aaaf150f7e5529c100ce62d6d92826a7
|
|
||||||
1778d809bdf60232ae21ce8a437eca8223f45ac37f6487452ce626f549b3b5fdee26afd2072e4bc7
|
|
||||||
5833c2464c805246155289f4
|
|
||||||
`,
|
|
||||||
wantVersion: 4,
|
|
||||||
wantRest: []rlp.RawValue{},
|
|
||||||
},
|
|
||||||
// (Ack₃) EIP-8 encoding with version 57, additional list elements
|
|
||||||
{
|
|
||||||
input: `
|
|
||||||
01f004076e58aae772bb101ab1a8e64e01ee96e64857ce82b1113817c6cdd52c09d26f7b90981cd7
|
|
||||||
ae835aeac72e1573b8a0225dd56d157a010846d888dac7464baf53f2ad4e3d584531fa203658fab0
|
|
||||||
3a06c9fd5e35737e417bc28c1cbf5e5dfc666de7090f69c3b29754725f84f75382891c561040ea1d
|
|
||||||
dc0d8f381ed1b9d0d4ad2a0ec021421d847820d6fa0ba66eaf58175f1b235e851c7e2124069fbc20
|
|
||||||
2888ddb3ac4d56bcbd1b9b7eab59e78f2e2d400905050f4a92dec1c4bdf797b3fc9b2f8e84a482f3
|
|
||||||
d800386186712dae00d5c386ec9387a5e9c9a1aca5a573ca91082c7d68421f388e79127a5177d4f8
|
|
||||||
590237364fd348c9611fa39f78dcdceee3f390f07991b7b47e1daa3ebcb6ccc9607811cb17ce51f1
|
|
||||||
c8c2c5098dbdd28fca547b3f58c01a424ac05f869f49c6a34672ea2cbbc558428aa1fe48bbfd6115
|
|
||||||
8b1b735a65d99f21e70dbc020bfdface9f724a0d1fb5895db971cc81aa7608baa0920abb0a565c9c
|
|
||||||
436e2fd13323428296c86385f2384e408a31e104670df0791d93e743a3a5194ee6b076fb6323ca59
|
|
||||||
3011b7348c16cf58f66b9633906ba54a2ee803187344b394f75dd2e663a57b956cb830dd7a908d4f
|
|
||||||
39a2336a61ef9fda549180d4ccde21514d117b6c6fd07a9102b5efe710a32af4eeacae2cb3b1dec0
|
|
||||||
35b9593b48b9d3ca4c13d245d5f04169b0b1
|
|
||||||
`,
|
|
||||||
wantVersion: 57,
|
|
||||||
wantRest: []rlp.RawValue{{0x06}, {0xC2, 0x07, 0x08}, {0x81, 0xFA}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
keyA, _ = crypto.HexToECDSA("49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee")
|
|
||||||
keyB, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHandshakeForwardCompatibility(t *testing.T) {
|
|
||||||
var (
|
|
||||||
pubA = crypto.FromECDSAPub(&keyA.PublicKey)[1:]
|
|
||||||
pubB = crypto.FromECDSAPub(&keyB.PublicKey)[1:]
|
|
||||||
ephA, _ = crypto.HexToECDSA("869d6ecf5211f1cc60418a13b9d870b22959d0c16f02bec714c960dd2298a32d")
|
|
||||||
ephB, _ = crypto.HexToECDSA("e238eb8e04fee6511ab04c6dd3c89ce097b11f25d584863ac2b6d5b35b1847e4")
|
|
||||||
ephPubA = crypto.FromECDSAPub(&ephA.PublicKey)[1:]
|
|
||||||
ephPubB = crypto.FromECDSAPub(&ephB.PublicKey)[1:]
|
|
||||||
nonceA = unhex("7e968bba13b6c50e2c4cd7f241cc0d64d1ac25c7f5952df231ac6a2bda8ee5d6")
|
|
||||||
nonceB = unhex("559aead08264d5795d3909718cdd05abd49572e84fe55590eef31a88a08fdffd")
|
|
||||||
_, _, _, _ = pubA, pubB, ephPubA, ephPubB
|
|
||||||
authSignature = unhex("299ca6acfd35e3d72d8ba3d1e2b60b5561d5af5218eb5bc182045769eb4226910a301acae3b369fffc4a4899d6b02531e89fd4fe36a2cf0d93607ba470b50f7800")
|
|
||||||
_ = authSignature
|
|
||||||
)
|
|
||||||
makeAuth := func(test handshakeAuthTest) *authMsgV4 {
|
|
||||||
msg := &authMsgV4{Version: test.wantVersion, Rest: test.wantRest}
|
|
||||||
copy(msg.Signature[:], authSignature)
|
|
||||||
copy(msg.InitiatorPubkey[:], pubA)
|
|
||||||
copy(msg.Nonce[:], nonceA)
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
makeAck := func(test handshakeAckTest) *authRespV4 {
|
|
||||||
msg := &authRespV4{Version: test.wantVersion, Rest: test.wantRest}
|
|
||||||
copy(msg.RandomPubkey[:], ephPubB)
|
|
||||||
copy(msg.Nonce[:], nonceB)
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
|
|
||||||
// check auth msg parsing
|
|
||||||
for _, test := range eip8HandshakeAuthTests {
|
|
||||||
var h handshakeState
|
|
||||||
r := bytes.NewReader(unhex(test.input))
|
|
||||||
msg := new(authMsgV4)
|
|
||||||
ciphertext, err := h.readMsg(msg, keyB, r)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("error for input %x:\n %v", unhex(test.input), err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !bytes.Equal(ciphertext, unhex(test.input)) {
|
|
||||||
t.Errorf("wrong ciphertext for input %x:\n %x", unhex(test.input), ciphertext)
|
|
||||||
}
|
|
||||||
want := makeAuth(test)
|
|
||||||
if !reflect.DeepEqual(msg, want) {
|
|
||||||
t.Errorf("wrong msg for input %x:\ngot %s\nwant %s", unhex(test.input), spew.Sdump(msg), spew.Sdump(want))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check auth resp parsing
|
|
||||||
for _, test := range eip8HandshakeRespTests {
|
|
||||||
var h handshakeState
|
|
||||||
input := unhex(test.input)
|
|
||||||
r := bytes.NewReader(input)
|
|
||||||
msg := new(authRespV4)
|
|
||||||
ciphertext, err := h.readMsg(msg, keyA, r)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("error for input %x:\n %v", input, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !bytes.Equal(ciphertext, input) {
|
|
||||||
t.Errorf("wrong ciphertext for input %x:\n %x", input, err)
|
|
||||||
}
|
|
||||||
want := makeAck(test)
|
|
||||||
if !reflect.DeepEqual(msg, want) {
|
|
||||||
t.Errorf("wrong msg for input %x:\ngot %s\nwant %s", input, spew.Sdump(msg), spew.Sdump(want))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check derivation for (Auth₂, Ack₂) on recipient side
|
|
||||||
var (
|
|
||||||
hs = &handshakeState{
|
|
||||||
initiator: false,
|
|
||||||
respNonce: nonceB,
|
|
||||||
randomPrivKey: ecies.ImportECDSA(ephB),
|
|
||||||
}
|
|
||||||
authCiphertext = unhex(eip8HandshakeAuthTests[0].input)
|
|
||||||
authRespCiphertext = unhex(eip8HandshakeRespTests[0].input)
|
|
||||||
authMsg = makeAuth(eip8HandshakeAuthTests[0])
|
|
||||||
wantAES = unhex("80e8632c05fed6fc2a13b0f8d31a3cf645366239170ea067065aba8e28bac487")
|
|
||||||
wantMAC = unhex("2ea74ec5dae199227dff1af715362700e989d889d7a493cb0639691efb8e5f98")
|
|
||||||
wantFooIngressHash = unhex("0c7ec6340062cc46f5e9f1e3cf86f8c8c403c5a0964f5df0ebd34a75ddc86db5")
|
|
||||||
)
|
|
||||||
if err := hs.handleAuthMsg(authMsg, keyB); err != nil {
|
|
||||||
t.Fatalf("handleAuthMsg: %v", err)
|
|
||||||
}
|
|
||||||
derived, err := hs.secrets(authCiphertext, authRespCiphertext)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("secrets: %v", err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(derived.AES, wantAES) {
|
|
||||||
t.Errorf("aes-secret mismatch:\ngot %x\nwant %x", derived.AES, wantAES)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(derived.MAC, wantMAC) {
|
|
||||||
t.Errorf("mac-secret mismatch:\ngot %x\nwant %x", derived.MAC, wantMAC)
|
|
||||||
}
|
|
||||||
io.WriteString(derived.IngressMAC, "foo")
|
|
||||||
fooIngressHash := derived.IngressMAC.Sum(nil)
|
|
||||||
if !bytes.Equal(fooIngressHash, wantFooIngressHash) {
|
|
||||||
t.Errorf("ingress-mac('foo') mismatch:\ngot %x\nwant %x", fooIngressHash, wantFooIngressHash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkHandshakeRead(b *testing.B) {
|
|
||||||
var input = unhex(eip8HandshakeAuthTests[0].input)
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
var (
|
|
||||||
h handshakeState
|
|
||||||
r = bytes.NewReader(input)
|
|
||||||
msg = new(authMsgV4)
|
|
||||||
)
|
|
||||||
if _, err := h.readMsg(msg, keyB, r); err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkThroughput(b *testing.B) {
|
|
||||||
pipe1, pipe2, err := pipes.TCPPipe()
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
conn1, conn2 = NewConn(pipe1, nil), NewConn(pipe2, &keyA.PublicKey)
|
|
||||||
handshakeDone = make(chan error, 1)
|
|
||||||
msgdata = make([]byte, 1024)
|
|
||||||
rand = rand.New(rand.NewSource(1337))
|
|
||||||
)
|
|
||||||
rand.Read(msgdata)
|
|
||||||
|
|
||||||
// Server side.
|
|
||||||
go func() {
|
|
||||||
defer conn1.Close()
|
|
||||||
// Perform handshake.
|
|
||||||
_, err := conn1.Handshake(keyA)
|
|
||||||
handshakeDone <- err
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
conn1.SetSnappy(true)
|
|
||||||
// Keep sending messages until connection closed.
|
|
||||||
for {
|
|
||||||
if _, err := conn1.Write(0, msgdata); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Set up client side.
|
|
||||||
defer conn2.Close()
|
|
||||||
if _, err := conn2.Handshake(keyB); err != nil {
|
|
||||||
b.Fatal("client handshake error:", err)
|
|
||||||
}
|
|
||||||
conn2.SetSnappy(true)
|
|
||||||
if err := <-handshakeDone; err != nil {
|
|
||||||
b.Fatal("server handshake error:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read N messages.
|
|
||||||
b.SetBytes(int64(len(msgdata)))
|
|
||||||
b.ReportAllocs()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, _, _, err := conn2.Read()
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal("read error:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func unhex(str string) []byte {
|
|
||||||
r := strings.NewReplacer("\t", "", " ", "", "\n", "")
|
|
||||||
b, err := hex.DecodeString(r.Replace(str))
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("invalid hex string: %q", str))
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func newkey() *ecdsa.PrivateKey {
|
|
||||||
key, err := crypto.GenerateKey()
|
|
||||||
if err != nil {
|
|
||||||
panic("couldn't generate key: " + err.Error())
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
1134
p2p/server.go
1134
p2p/server.go
File diff suppressed because it is too large
Load diff
|
|
@ -1,187 +0,0 @@
|
||||||
// Copyright 2023 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package p2p
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/nat"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
portMapDuration = 10 * time.Minute
|
|
||||||
portMapRefreshInterval = 8 * time.Minute
|
|
||||||
portMapRetryInterval = 5 * time.Minute
|
|
||||||
extipRetryInterval = 2 * time.Minute
|
|
||||||
)
|
|
||||||
|
|
||||||
type portMapping struct {
|
|
||||||
protocol string
|
|
||||||
name string
|
|
||||||
port int
|
|
||||||
|
|
||||||
// for use by the portMappingLoop goroutine:
|
|
||||||
extPort int // the mapped port returned by the NAT interface
|
|
||||||
nextTime mclock.AbsTime
|
|
||||||
}
|
|
||||||
|
|
||||||
// setupPortMapping starts the port mapping loop if necessary.
|
|
||||||
// Note: this needs to be called after the LocalNode instance has been set on the server.
|
|
||||||
func (srv *Server) setupPortMapping() {
|
|
||||||
// portMappingRegister will receive up to two values: one for the TCP port if
|
|
||||||
// listening is enabled, and one more for enabling UDP port mapping if discovery is
|
|
||||||
// enabled. We make it buffered to avoid blocking setup while a mapping request is in
|
|
||||||
// progress.
|
|
||||||
srv.portMappingRegister = make(chan *portMapping, 2)
|
|
||||||
|
|
||||||
switch srv.NAT.(type) {
|
|
||||||
case nil:
|
|
||||||
// No NAT interface configured.
|
|
||||||
srv.loopWG.Add(1)
|
|
||||||
go srv.consumePortMappingRequests()
|
|
||||||
|
|
||||||
case nat.ExtIP:
|
|
||||||
// ExtIP doesn't block, set the IP right away.
|
|
||||||
ip, _ := srv.NAT.ExternalIP()
|
|
||||||
srv.localnode.SetStaticIP(ip)
|
|
||||||
srv.loopWG.Add(1)
|
|
||||||
go srv.consumePortMappingRequests()
|
|
||||||
|
|
||||||
default:
|
|
||||||
srv.loopWG.Add(1)
|
|
||||||
go srv.portMappingLoop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *Server) consumePortMappingRequests() {
|
|
||||||
defer srv.loopWG.Done()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-srv.quit:
|
|
||||||
return
|
|
||||||
case <-srv.portMappingRegister:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// portMappingLoop manages port mappings for UDP and TCP.
|
|
||||||
func (srv *Server) portMappingLoop() {
|
|
||||||
defer srv.loopWG.Done()
|
|
||||||
|
|
||||||
newLogger := func(p string, e int, i int) log.Logger {
|
|
||||||
return log.New("proto", p, "extport", e, "intport", i, "interface", srv.NAT)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
mappings = make(map[string]*portMapping, 2)
|
|
||||||
refresh = mclock.NewAlarm(srv.clock)
|
|
||||||
extip = mclock.NewAlarm(srv.clock)
|
|
||||||
lastExtIP net.IP
|
|
||||||
)
|
|
||||||
extip.Schedule(srv.clock.Now())
|
|
||||||
defer func() {
|
|
||||||
refresh.Stop()
|
|
||||||
extip.Stop()
|
|
||||||
for _, m := range mappings {
|
|
||||||
if m.extPort != 0 {
|
|
||||||
log := newLogger(m.protocol, m.extPort, m.port)
|
|
||||||
log.Debug("Deleting port mapping")
|
|
||||||
srv.NAT.DeleteMapping(m.protocol, m.extPort, m.port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
// Schedule refresh of existing mappings.
|
|
||||||
for _, m := range mappings {
|
|
||||||
refresh.Schedule(m.nextTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-srv.quit:
|
|
||||||
return
|
|
||||||
|
|
||||||
case <-extip.C():
|
|
||||||
extip.Schedule(srv.clock.Now().Add(extipRetryInterval))
|
|
||||||
ip, err := srv.NAT.ExternalIP()
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("Couldn't get external IP", "err", err, "interface", srv.NAT)
|
|
||||||
} else if !ip.Equal(lastExtIP) {
|
|
||||||
log.Debug("External IP changed", "ip", extip, "interface", srv.NAT)
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Here, we either failed to get the external IP, or it has changed.
|
|
||||||
lastExtIP = ip
|
|
||||||
srv.localnode.SetStaticIP(ip)
|
|
||||||
// Ensure port mappings are refreshed in case we have moved to a new network.
|
|
||||||
for _, m := range mappings {
|
|
||||||
m.nextTime = srv.clock.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
case m := <-srv.portMappingRegister:
|
|
||||||
if m.protocol != "TCP" && m.protocol != "UDP" {
|
|
||||||
panic("unknown NAT protocol name: " + m.protocol)
|
|
||||||
}
|
|
||||||
mappings[m.protocol] = m
|
|
||||||
m.nextTime = srv.clock.Now()
|
|
||||||
|
|
||||||
case <-refresh.C():
|
|
||||||
for _, m := range mappings {
|
|
||||||
if srv.clock.Now() < m.nextTime {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
external := m.port
|
|
||||||
if m.extPort != 0 {
|
|
||||||
external = m.extPort
|
|
||||||
}
|
|
||||||
log := newLogger(m.protocol, external, m.port)
|
|
||||||
|
|
||||||
log.Trace("Attempting port mapping")
|
|
||||||
p, err := srv.NAT.AddMapping(m.protocol, external, m.port, m.name, portMapDuration)
|
|
||||||
if err != nil {
|
|
||||||
log.Debug("Couldn't add port mapping", "err", err)
|
|
||||||
m.extPort = 0
|
|
||||||
m.nextTime = srv.clock.Now().Add(portMapRetryInterval)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// It was mapped!
|
|
||||||
m.extPort = int(p)
|
|
||||||
m.nextTime = srv.clock.Now().Add(portMapRefreshInterval)
|
|
||||||
if external != m.extPort {
|
|
||||||
log = newLogger(m.protocol, m.extPort, m.port)
|
|
||||||
log.Info("NAT mapped alternative port")
|
|
||||||
} else {
|
|
||||||
log.Info("NAT mapped port")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update port in local ENR.
|
|
||||||
switch m.protocol {
|
|
||||||
case "TCP":
|
|
||||||
srv.localnode.Set(enr.TCP(m.extPort))
|
|
||||||
case "UDP":
|
|
||||||
srv.localnode.SetFallbackUDP(m.extPort)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
// Copyright 2023 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package p2p
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/mclock"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/testlog"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestServerPortMapping(t *testing.T) {
|
|
||||||
clock := new(mclock.Simulated)
|
|
||||||
mockNAT := &mockNAT{mappedPort: 30000}
|
|
||||||
srv := Server{
|
|
||||||
Config: Config{
|
|
||||||
PrivateKey: newkey(),
|
|
||||||
NoDial: true,
|
|
||||||
ListenAddr: ":0",
|
|
||||||
NAT: mockNAT,
|
|
||||||
Logger: testlog.Logger(t, log.LvlTrace),
|
|
||||||
clock: clock,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := srv.Start()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer srv.Stop()
|
|
||||||
|
|
||||||
// Wait for the port mapping to be registered. Synchronization with the port mapping
|
|
||||||
// goroutine works like this: For each iteration, we allow other goroutines to run and
|
|
||||||
// also advance the virtual clock by 1 second. Waiting stops when the NAT interface
|
|
||||||
// has received some requests, or when the clock reaches a timeout.
|
|
||||||
deadline := clock.Now().Add(portMapRefreshInterval)
|
|
||||||
for clock.Now() < deadline && mockNAT.mapRequests.Load() < 2 {
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
clock.Run(1 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mockNAT.ipRequests.Load() == 0 {
|
|
||||||
t.Fatal("external IP was never requested")
|
|
||||||
}
|
|
||||||
reqCount := mockNAT.mapRequests.Load()
|
|
||||||
if reqCount != 2 {
|
|
||||||
t.Error("wrong request count:", reqCount)
|
|
||||||
}
|
|
||||||
enr := srv.LocalNode().Node()
|
|
||||||
if enr.IP().String() != "192.0.2.0" {
|
|
||||||
t.Error("wrong IP in ENR:", enr.IP())
|
|
||||||
}
|
|
||||||
if enr.TCP() != 30000 {
|
|
||||||
t.Error("wrong TCP port in ENR:", enr.TCP())
|
|
||||||
}
|
|
||||||
if enr.UDP() != 30000 {
|
|
||||||
t.Error("wrong UDP port in ENR:", enr.UDP())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockNAT struct {
|
|
||||||
mappedPort uint16
|
|
||||||
mapRequests atomic.Int32
|
|
||||||
unmapRequests atomic.Int32
|
|
||||||
ipRequests atomic.Int32
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockNAT) AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) (uint16, error) {
|
|
||||||
m.mapRequests.Add(1)
|
|
||||||
return m.mappedPort, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockNAT) DeleteMapping(protocol string, extport, intport int) error {
|
|
||||||
m.unmapRequests.Add(1)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockNAT) ExternalIP() (net.IP, error) {
|
|
||||||
m.ipRequests.Add(1)
|
|
||||||
return net.ParseIP("192.0.2.0"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockNAT) String() string {
|
|
||||||
return "mockNAT"
|
|
||||||
}
|
|
||||||
|
|
@ -1,631 +0,0 @@
|
||||||
// Copyright 2014 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package p2p
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/sha256"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"math/rand"
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"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/p2p/rlpx"
|
|
||||||
)
|
|
||||||
|
|
||||||
type testTransport struct {
|
|
||||||
*rlpxTransport
|
|
||||||
rpub *ecdsa.PublicKey
|
|
||||||
closeErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestTransport(rpub *ecdsa.PublicKey, fd net.Conn, dialDest *ecdsa.PublicKey) transport {
|
|
||||||
wrapped := newRLPX(fd, dialDest).(*rlpxTransport)
|
|
||||||
wrapped.conn.InitWithSecrets(rlpx.Secrets{
|
|
||||||
AES: make([]byte, 16),
|
|
||||||
MAC: make([]byte, 16),
|
|
||||||
EgressMAC: sha256.New(),
|
|
||||||
IngressMAC: sha256.New(),
|
|
||||||
})
|
|
||||||
return &testTransport{rpub: rpub, rlpxTransport: wrapped}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testTransport) doEncHandshake(prv *ecdsa.PrivateKey) (*ecdsa.PublicKey, error) {
|
|
||||||
return c.rpub, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testTransport) doProtoHandshake(our *protoHandshake) (*protoHandshake, error) {
|
|
||||||
pubkey := crypto.FromECDSAPub(c.rpub)[1:]
|
|
||||||
return &protoHandshake{ID: pubkey, Name: "test"}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *testTransport) close(err error) {
|
|
||||||
c.conn.Close()
|
|
||||||
c.closeErr = err
|
|
||||||
}
|
|
||||||
|
|
||||||
func startTestServer(t *testing.T, remoteKey *ecdsa.PublicKey, pf func(*Peer)) *Server {
|
|
||||||
config := Config{
|
|
||||||
Name: "test",
|
|
||||||
MaxPeers: 10,
|
|
||||||
ListenAddr: "127.0.0.1:0",
|
|
||||||
NoDiscovery: true,
|
|
||||||
PrivateKey: newkey(),
|
|
||||||
Logger: testlog.Logger(t, log.LvlTrace),
|
|
||||||
}
|
|
||||||
server := &Server{
|
|
||||||
Config: config,
|
|
||||||
newPeerHook: pf,
|
|
||||||
newTransport: func(fd net.Conn, dialDest *ecdsa.PublicKey) transport {
|
|
||||||
return newTestTransport(remoteKey, fd, dialDest)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if err := server.Start(); err != nil {
|
|
||||||
t.Fatalf("Could not start server: %v", err)
|
|
||||||
}
|
|
||||||
return server
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServerListen(t *testing.T) {
|
|
||||||
// start the test server
|
|
||||||
connected := make(chan *Peer)
|
|
||||||
remid := &newkey().PublicKey
|
|
||||||
srv := startTestServer(t, remid, func(p *Peer) {
|
|
||||||
if p.ID() != enode.PubkeyToIDV4(remid) {
|
|
||||||
t.Error("peer func called with wrong node id")
|
|
||||||
}
|
|
||||||
connected <- p
|
|
||||||
})
|
|
||||||
defer close(connected)
|
|
||||||
defer srv.Stop()
|
|
||||||
|
|
||||||
// dial the test server
|
|
||||||
conn, err := net.DialTimeout("tcp", srv.ListenAddr, 5*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not dial: %v", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case peer := <-connected:
|
|
||||||
if peer.LocalAddr().String() != conn.RemoteAddr().String() {
|
|
||||||
t.Errorf("peer started with wrong conn: got %v, want %v",
|
|
||||||
peer.LocalAddr(), conn.RemoteAddr())
|
|
||||||
}
|
|
||||||
peers := srv.Peers()
|
|
||||||
if !reflect.DeepEqual(peers, []*Peer{peer}) {
|
|
||||||
t.Errorf("Peers mismatch: got %v, want %v", peers, []*Peer{peer})
|
|
||||||
}
|
|
||||||
case <-time.After(1 * time.Second):
|
|
||||||
t.Error("server did not accept within one second")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServerDial(t *testing.T) {
|
|
||||||
// run a one-shot TCP server to handle the connection.
|
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not setup listener: %v", err)
|
|
||||||
}
|
|
||||||
defer listener.Close()
|
|
||||||
accepted := make(chan net.Conn, 1)
|
|
||||||
go func() {
|
|
||||||
conn, err := listener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
accepted <- conn
|
|
||||||
}()
|
|
||||||
|
|
||||||
// start the server
|
|
||||||
connected := make(chan *Peer)
|
|
||||||
remid := &newkey().PublicKey
|
|
||||||
srv := startTestServer(t, remid, func(p *Peer) { connected <- p })
|
|
||||||
defer close(connected)
|
|
||||||
defer srv.Stop()
|
|
||||||
|
|
||||||
// tell the server to connect
|
|
||||||
tcpAddr := listener.Addr().(*net.TCPAddr)
|
|
||||||
node := enode.NewV4(remid, tcpAddr.IP, tcpAddr.Port, 0)
|
|
||||||
srv.AddPeer(node)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case conn := <-accepted:
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case peer := <-connected:
|
|
||||||
if peer.ID() != enode.PubkeyToIDV4(remid) {
|
|
||||||
t.Errorf("peer has wrong id")
|
|
||||||
}
|
|
||||||
if peer.Name() != "test" {
|
|
||||||
t.Errorf("peer has wrong name")
|
|
||||||
}
|
|
||||||
if peer.RemoteAddr().String() != conn.LocalAddr().String() {
|
|
||||||
t.Errorf("peer started with wrong conn: got %v, want %v",
|
|
||||||
peer.RemoteAddr(), conn.LocalAddr())
|
|
||||||
}
|
|
||||||
peers := srv.Peers()
|
|
||||||
if !reflect.DeepEqual(peers, []*Peer{peer}) {
|
|
||||||
t.Errorf("Peers mismatch: got %v, want %v", peers, []*Peer{peer})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test AddTrustedPeer/RemoveTrustedPeer and changing Trusted flags
|
|
||||||
// Particularly for race conditions on changing the flag state.
|
|
||||||
if peer := srv.Peers()[0]; peer.Info().Network.Trusted {
|
|
||||||
t.Errorf("peer is trusted prematurely: %v", peer)
|
|
||||||
}
|
|
||||||
done := make(chan bool)
|
|
||||||
go func() {
|
|
||||||
srv.AddTrustedPeer(node)
|
|
||||||
if peer := srv.Peers()[0]; !peer.Info().Network.Trusted {
|
|
||||||
t.Errorf("peer is not trusted after AddTrustedPeer: %v", peer)
|
|
||||||
}
|
|
||||||
srv.RemoveTrustedPeer(node)
|
|
||||||
if peer := srv.Peers()[0]; peer.Info().Network.Trusted {
|
|
||||||
t.Errorf("peer is trusted after RemoveTrustedPeer: %v", peer)
|
|
||||||
}
|
|
||||||
done <- true
|
|
||||||
}()
|
|
||||||
// Trigger potential race conditions
|
|
||||||
peer = srv.Peers()[0]
|
|
||||||
_ = peer.Inbound()
|
|
||||||
_ = peer.Info()
|
|
||||||
<-done
|
|
||||||
case <-time.After(1 * time.Second):
|
|
||||||
t.Error("server did not launch peer within one second")
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-time.After(1 * time.Second):
|
|
||||||
t.Error("server did not connect within one second")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that RemovePeer disconnects the peer if it is connected.
|
|
||||||
func TestServerRemovePeerDisconnect(t *testing.T) {
|
|
||||||
srv1 := &Server{Config: Config{
|
|
||||||
PrivateKey: newkey(),
|
|
||||||
MaxPeers: 1,
|
|
||||||
NoDiscovery: true,
|
|
||||||
Logger: testlog.Logger(t, log.LvlTrace).New("server", "1"),
|
|
||||||
}}
|
|
||||||
srv2 := &Server{Config: Config{
|
|
||||||
PrivateKey: newkey(),
|
|
||||||
MaxPeers: 1,
|
|
||||||
NoDiscovery: true,
|
|
||||||
NoDial: true,
|
|
||||||
ListenAddr: "127.0.0.1:0",
|
|
||||||
Logger: testlog.Logger(t, log.LvlTrace).New("server", "2"),
|
|
||||||
}}
|
|
||||||
srv1.Start()
|
|
||||||
defer srv1.Stop()
|
|
||||||
srv2.Start()
|
|
||||||
defer srv2.Stop()
|
|
||||||
|
|
||||||
s := strings.Split(srv2.ListenAddr, ":")
|
|
||||||
if len(s) != 2 {
|
|
||||||
t.Fatal("invalid ListenAddr")
|
|
||||||
}
|
|
||||||
if port, err := strconv.Atoi(s[1]); err == nil {
|
|
||||||
srv2.localnode.Set(enr.TCP(uint16(port)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !syncAddPeer(srv1, srv2.Self()) {
|
|
||||||
t.Fatal("peer not connected")
|
|
||||||
}
|
|
||||||
srv1.RemovePeer(srv2.Self())
|
|
||||||
if srv1.PeerCount() > 0 {
|
|
||||||
t.Fatal("removed peer still connected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that connections are disconnected just after the encryption handshake
|
|
||||||
// when the server is at capacity. Trusted connections should still be accepted.
|
|
||||||
func TestServerAtCap(t *testing.T) {
|
|
||||||
trustedNode := newkey()
|
|
||||||
trustedID := enode.PubkeyToIDV4(&trustedNode.PublicKey)
|
|
||||||
srv := &Server{
|
|
||||||
Config: Config{
|
|
||||||
PrivateKey: newkey(),
|
|
||||||
MaxPeers: 10,
|
|
||||||
NoDial: true,
|
|
||||||
NoDiscovery: true,
|
|
||||||
TrustedNodes: []*enode.Node{newNode(trustedID, "")},
|
|
||||||
Logger: testlog.Logger(t, log.LvlTrace),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if err := srv.Start(); err != nil {
|
|
||||||
t.Fatalf("could not start: %v", err)
|
|
||||||
}
|
|
||||||
defer srv.Stop()
|
|
||||||
|
|
||||||
newconn := func(id enode.ID) *conn {
|
|
||||||
fd, _ := net.Pipe()
|
|
||||||
tx := newTestTransport(&trustedNode.PublicKey, fd, nil)
|
|
||||||
node := enode.SignNull(new(enr.Record), id)
|
|
||||||
return &conn{fd: fd, transport: tx, flags: inboundConn, node: node, cont: make(chan error)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject a few connections to fill up the peer set.
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
c := newconn(randomID())
|
|
||||||
if err := srv.checkpoint(c, srv.checkpointAddPeer); err != nil {
|
|
||||||
t.Fatalf("could not add conn %d: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Try inserting a non-trusted connection.
|
|
||||||
anotherID := randomID()
|
|
||||||
c := newconn(anotherID)
|
|
||||||
if err := srv.checkpoint(c, srv.checkpointPostHandshake); err != DiscTooManyPeers {
|
|
||||||
t.Error("wrong error for insert:", err)
|
|
||||||
}
|
|
||||||
// Try inserting a trusted connection.
|
|
||||||
c = newconn(trustedID)
|
|
||||||
if err := srv.checkpoint(c, srv.checkpointPostHandshake); err != nil {
|
|
||||||
t.Error("unexpected error for trusted conn @posthandshake:", err)
|
|
||||||
}
|
|
||||||
if !c.is(trustedConn) {
|
|
||||||
t.Error("Server did not set trusted flag")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from trusted set and try again
|
|
||||||
srv.RemoveTrustedPeer(newNode(trustedID, ""))
|
|
||||||
c = newconn(trustedID)
|
|
||||||
if err := srv.checkpoint(c, srv.checkpointPostHandshake); err != DiscTooManyPeers {
|
|
||||||
t.Error("wrong error for insert:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add anotherID to trusted set and try again
|
|
||||||
srv.AddTrustedPeer(newNode(anotherID, ""))
|
|
||||||
c = newconn(anotherID)
|
|
||||||
if err := srv.checkpoint(c, srv.checkpointPostHandshake); err != nil {
|
|
||||||
t.Error("unexpected error for trusted conn @posthandshake:", err)
|
|
||||||
}
|
|
||||||
if !c.is(trustedConn) {
|
|
||||||
t.Error("Server did not set trusted flag")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServerPeerLimits(t *testing.T) {
|
|
||||||
srvkey := newkey()
|
|
||||||
clientkey := newkey()
|
|
||||||
clientnode := enode.NewV4(&clientkey.PublicKey, nil, 0, 0)
|
|
||||||
|
|
||||||
var tp = &setupTransport{
|
|
||||||
pubkey: &clientkey.PublicKey,
|
|
||||||
phs: protoHandshake{
|
|
||||||
ID: crypto.FromECDSAPub(&clientkey.PublicKey)[1:],
|
|
||||||
// Force "DiscUselessPeer" due to unmatching caps
|
|
||||||
// Caps: []Cap{discard.cap()},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
srv := &Server{
|
|
||||||
Config: Config{
|
|
||||||
PrivateKey: srvkey,
|
|
||||||
MaxPeers: 0,
|
|
||||||
NoDial: true,
|
|
||||||
NoDiscovery: true,
|
|
||||||
Protocols: []Protocol{discard},
|
|
||||||
Logger: testlog.Logger(t, log.LvlTrace),
|
|
||||||
},
|
|
||||||
newTransport: func(fd net.Conn, dialDest *ecdsa.PublicKey) transport { return tp },
|
|
||||||
}
|
|
||||||
if err := srv.Start(); err != nil {
|
|
||||||
t.Fatalf("couldn't start server: %v", err)
|
|
||||||
}
|
|
||||||
defer srv.Stop()
|
|
||||||
|
|
||||||
// Check that server is full (MaxPeers=0)
|
|
||||||
flags := dynDialedConn
|
|
||||||
dialDest := clientnode
|
|
||||||
conn, _ := net.Pipe()
|
|
||||||
srv.SetupConn(conn, flags, dialDest)
|
|
||||||
if tp.closeErr != DiscTooManyPeers {
|
|
||||||
t.Errorf("unexpected close error: %q", tp.closeErr)
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
|
|
||||||
srv.AddTrustedPeer(clientnode)
|
|
||||||
|
|
||||||
// Check that server allows a trusted peer despite being full.
|
|
||||||
conn, _ = net.Pipe()
|
|
||||||
srv.SetupConn(conn, flags, dialDest)
|
|
||||||
if tp.closeErr == DiscTooManyPeers {
|
|
||||||
t.Errorf("failed to bypass MaxPeers with trusted node: %q", tp.closeErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tp.closeErr != DiscUselessPeer {
|
|
||||||
t.Errorf("unexpected close error: %q", tp.closeErr)
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
|
|
||||||
srv.RemoveTrustedPeer(clientnode)
|
|
||||||
|
|
||||||
// Check that server is full again.
|
|
||||||
conn, _ = net.Pipe()
|
|
||||||
srv.SetupConn(conn, flags, dialDest)
|
|
||||||
if tp.closeErr != DiscTooManyPeers {
|
|
||||||
t.Errorf("unexpected close error: %q", tp.closeErr)
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServerSetupConn(t *testing.T) {
|
|
||||||
var (
|
|
||||||
clientkey, srvkey = newkey(), newkey()
|
|
||||||
clientpub = &clientkey.PublicKey
|
|
||||||
srvpub = &srvkey.PublicKey
|
|
||||||
)
|
|
||||||
tests := []struct {
|
|
||||||
dontstart bool
|
|
||||||
tt *setupTransport
|
|
||||||
flags connFlag
|
|
||||||
dialDest *enode.Node
|
|
||||||
|
|
||||||
wantCloseErr error
|
|
||||||
wantCalls string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
dontstart: true,
|
|
||||||
tt: &setupTransport{pubkey: clientpub},
|
|
||||||
wantCalls: "close,",
|
|
||||||
wantCloseErr: errServerStopped,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tt: &setupTransport{pubkey: clientpub, encHandshakeErr: errEncHandshakeError},
|
|
||||||
flags: inboundConn,
|
|
||||||
wantCalls: "doEncHandshake,close,",
|
|
||||||
wantCloseErr: errEncHandshakeError,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tt: &setupTransport{pubkey: clientpub, phs: protoHandshake{ID: randomID().Bytes()}},
|
|
||||||
dialDest: enode.NewV4(clientpub, nil, 0, 0),
|
|
||||||
flags: dynDialedConn,
|
|
||||||
wantCalls: "doEncHandshake,doProtoHandshake,close,",
|
|
||||||
wantCloseErr: DiscUnexpectedIdentity,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tt: &setupTransport{pubkey: clientpub, protoHandshakeErr: errProtoHandshakeError},
|
|
||||||
dialDest: enode.NewV4(clientpub, nil, 0, 0),
|
|
||||||
flags: dynDialedConn,
|
|
||||||
wantCalls: "doEncHandshake,doProtoHandshake,close,",
|
|
||||||
wantCloseErr: errProtoHandshakeError,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tt: &setupTransport{pubkey: srvpub, phs: protoHandshake{ID: crypto.FromECDSAPub(srvpub)[1:]}},
|
|
||||||
flags: inboundConn,
|
|
||||||
wantCalls: "doEncHandshake,close,",
|
|
||||||
wantCloseErr: DiscSelf,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tt: &setupTransport{pubkey: clientpub, phs: protoHandshake{ID: crypto.FromECDSAPub(clientpub)[1:]}},
|
|
||||||
flags: inboundConn,
|
|
||||||
wantCalls: "doEncHandshake,doProtoHandshake,close,",
|
|
||||||
wantCloseErr: DiscUselessPeer,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
t.Run(test.wantCalls, func(t *testing.T) {
|
|
||||||
cfg := Config{
|
|
||||||
PrivateKey: srvkey,
|
|
||||||
MaxPeers: 10,
|
|
||||||
NoDial: true,
|
|
||||||
NoDiscovery: true,
|
|
||||||
Protocols: []Protocol{discard},
|
|
||||||
Logger: testlog.Logger(t, log.LvlTrace),
|
|
||||||
}
|
|
||||||
srv := &Server{
|
|
||||||
Config: cfg,
|
|
||||||
newTransport: func(fd net.Conn, dialDest *ecdsa.PublicKey) transport { return test.tt },
|
|
||||||
log: cfg.Logger,
|
|
||||||
}
|
|
||||||
if !test.dontstart {
|
|
||||||
if err := srv.Start(); err != nil {
|
|
||||||
t.Fatalf("couldn't start server: %v", err)
|
|
||||||
}
|
|
||||||
defer srv.Stop()
|
|
||||||
}
|
|
||||||
p1, _ := net.Pipe()
|
|
||||||
srv.SetupConn(p1, test.flags, test.dialDest)
|
|
||||||
if !errors.Is(test.tt.closeErr, test.wantCloseErr) {
|
|
||||||
t.Errorf("test %d: close error mismatch: got %q, want %q", i, test.tt.closeErr, test.wantCloseErr)
|
|
||||||
}
|
|
||||||
if test.tt.calls != test.wantCalls {
|
|
||||||
t.Errorf("test %d: calls mismatch: got %q, want %q", i, test.tt.calls, test.wantCalls)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type setupTransport struct {
|
|
||||||
pubkey *ecdsa.PublicKey
|
|
||||||
encHandshakeErr error
|
|
||||||
phs protoHandshake
|
|
||||||
protoHandshakeErr error
|
|
||||||
|
|
||||||
calls string
|
|
||||||
closeErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *setupTransport) doEncHandshake(prv *ecdsa.PrivateKey) (*ecdsa.PublicKey, error) {
|
|
||||||
c.calls += "doEncHandshake,"
|
|
||||||
return c.pubkey, c.encHandshakeErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *setupTransport) doProtoHandshake(our *protoHandshake) (*protoHandshake, error) {
|
|
||||||
c.calls += "doProtoHandshake,"
|
|
||||||
if c.protoHandshakeErr != nil {
|
|
||||||
return nil, c.protoHandshakeErr
|
|
||||||
}
|
|
||||||
return &c.phs, nil
|
|
||||||
}
|
|
||||||
func (c *setupTransport) close(err error) {
|
|
||||||
c.calls += "close,"
|
|
||||||
c.closeErr = err
|
|
||||||
}
|
|
||||||
|
|
||||||
// setupConn shouldn't write to/read from the connection.
|
|
||||||
func (c *setupTransport) WriteMsg(Msg) error {
|
|
||||||
panic("WriteMsg called on setupTransport")
|
|
||||||
}
|
|
||||||
func (c *setupTransport) ReadMsg() (Msg, error) {
|
|
||||||
panic("ReadMsg called on setupTransport")
|
|
||||||
}
|
|
||||||
|
|
||||||
func newkey() *ecdsa.PrivateKey {
|
|
||||||
key, err := crypto.GenerateKey()
|
|
||||||
if err != nil {
|
|
||||||
panic("couldn't generate key: " + err.Error())
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
func randomID() (id enode.ID) {
|
|
||||||
for i := range id {
|
|
||||||
id[i] = byte(rand.Intn(255))
|
|
||||||
}
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
// This test checks that inbound connections are throttled by IP.
|
|
||||||
func TestServerInboundThrottle(t *testing.T) {
|
|
||||||
const timeout = 5 * time.Second
|
|
||||||
newTransportCalled := make(chan struct{})
|
|
||||||
srv := &Server{
|
|
||||||
Config: Config{
|
|
||||||
PrivateKey: newkey(),
|
|
||||||
ListenAddr: "127.0.0.1:0",
|
|
||||||
MaxPeers: 10,
|
|
||||||
NoDial: true,
|
|
||||||
NoDiscovery: true,
|
|
||||||
Protocols: []Protocol{discard},
|
|
||||||
Logger: testlog.Logger(t, log.LvlTrace),
|
|
||||||
},
|
|
||||||
newTransport: func(fd net.Conn, dialDest *ecdsa.PublicKey) transport {
|
|
||||||
newTransportCalled <- struct{}{}
|
|
||||||
return newRLPX(fd, dialDest)
|
|
||||||
},
|
|
||||||
listenFunc: func(network, laddr string) (net.Listener, error) {
|
|
||||||
fakeAddr := &net.TCPAddr{IP: net.IP{95, 33, 21, 2}, Port: 4444}
|
|
||||||
return listenFakeAddr(network, laddr, fakeAddr)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if err := srv.Start(); err != nil {
|
|
||||||
t.Fatal("can't start: ", err)
|
|
||||||
}
|
|
||||||
defer srv.Stop()
|
|
||||||
|
|
||||||
// Dial the test server.
|
|
||||||
conn, err := net.DialTimeout("tcp", srv.ListenAddr, timeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not dial: %v", err)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-newTransportCalled:
|
|
||||||
// OK
|
|
||||||
case <-time.After(timeout):
|
|
||||||
t.Error("newTransport not called")
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
|
|
||||||
// Dial again. This time the server should close the connection immediately.
|
|
||||||
connClosed := make(chan struct{}, 1)
|
|
||||||
conn, err = net.DialTimeout("tcp", srv.ListenAddr, timeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not dial: %v", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
go func() {
|
|
||||||
conn.SetDeadline(time.Now().Add(timeout))
|
|
||||||
buf := make([]byte, 10)
|
|
||||||
if n, err := conn.Read(buf); err != io.EOF || n != 0 {
|
|
||||||
t.Errorf("expected io.EOF and n == 0, got error %q and n == %d", err, n)
|
|
||||||
}
|
|
||||||
connClosed <- struct{}{}
|
|
||||||
}()
|
|
||||||
select {
|
|
||||||
case <-connClosed:
|
|
||||||
// OK
|
|
||||||
case <-newTransportCalled:
|
|
||||||
t.Error("newTransport called for second attempt")
|
|
||||||
case <-time.After(timeout):
|
|
||||||
t.Error("connection not closed within timeout")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func listenFakeAddr(network, laddr string, remoteAddr net.Addr) (net.Listener, error) {
|
|
||||||
l, err := net.Listen(network, laddr)
|
|
||||||
if err == nil {
|
|
||||||
l = &fakeAddrListener{l, remoteAddr}
|
|
||||||
}
|
|
||||||
return l, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// fakeAddrListener is a listener that creates connections with a mocked remote address.
|
|
||||||
type fakeAddrListener struct {
|
|
||||||
net.Listener
|
|
||||||
remoteAddr net.Addr
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeAddrConn struct {
|
|
||||||
net.Conn
|
|
||||||
remoteAddr net.Addr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *fakeAddrListener) Accept() (net.Conn, error) {
|
|
||||||
c, err := l.Listener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &fakeAddrConn{c, l.remoteAddr}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *fakeAddrConn) RemoteAddr() net.Addr {
|
|
||||||
return c.remoteAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncAddPeer(srv *Server, node *enode.Node) bool {
|
|
||||||
var (
|
|
||||||
ch = make(chan *PeerEvent)
|
|
||||||
sub = srv.SubscribeEvents(ch)
|
|
||||||
timeout = time.After(2 * time.Second)
|
|
||||||
)
|
|
||||||
defer sub.Unsubscribe()
|
|
||||||
srv.AddPeer(node)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case ev := <-ch:
|
|
||||||
if ev.Type == PeerEventTypeAdd && ev.Peer == node.ID() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
case <-timeout:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
# devp2p Simulations
|
|
||||||
|
|
||||||
The `p2p/simulations` package implements a simulation framework that supports
|
|
||||||
creating a collection of devp2p nodes, connecting them to form a
|
|
||||||
simulation network, performing simulation actions in that network and then
|
|
||||||
extracting useful information.
|
|
||||||
|
|
||||||
## Nodes
|
|
||||||
|
|
||||||
Each node in a simulation network runs multiple services by wrapping a collection
|
|
||||||
of objects which implement the `node.Service` interface meaning they:
|
|
||||||
|
|
||||||
* can be started and stopped
|
|
||||||
* run p2p protocols
|
|
||||||
* expose RPC APIs
|
|
||||||
|
|
||||||
This means that any object which implements the `node.Service` interface can be
|
|
||||||
used to run a node in the simulation.
|
|
||||||
|
|
||||||
## Services
|
|
||||||
|
|
||||||
Before running a simulation, a set of service initializers must be registered
|
|
||||||
which can then be used to run nodes in the network.
|
|
||||||
|
|
||||||
A service initializer is a function with the following signature:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func(ctx *adapters.ServiceContext) (node.Service, error)
|
|
||||||
```
|
|
||||||
|
|
||||||
These initializers should be registered by calling the `adapters.RegisterServices`
|
|
||||||
function in an `init()` hook:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func init() {
|
|
||||||
adapters.RegisterServices(adapters.Services{
|
|
||||||
"service1": initService1,
|
|
||||||
"service2": initService2,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Node Adapters
|
|
||||||
|
|
||||||
The simulation framework includes multiple "node adapters" which are
|
|
||||||
responsible for creating an environment in which a node runs.
|
|
||||||
|
|
||||||
### SimAdapter
|
|
||||||
|
|
||||||
The `SimAdapter` runs nodes in-memory, connecting them using an in-memory,
|
|
||||||
synchronous `net.Pipe` and connecting to their RPC server using an in-memory
|
|
||||||
`rpc.Client`.
|
|
||||||
|
|
||||||
### ExecAdapter
|
|
||||||
|
|
||||||
The `ExecAdapter` runs nodes as child processes of the running simulation.
|
|
||||||
|
|
||||||
It does this by executing the binary which is running the simulation but
|
|
||||||
setting `argv[0]` (i.e. the program name) to `p2p-node` which is then
|
|
||||||
detected by an init hook in the child process which runs the `node.Service`
|
|
||||||
using the devp2p node stack rather than executing `main()`.
|
|
||||||
|
|
||||||
The nodes listen for devp2p connections and WebSocket RPC clients on random
|
|
||||||
localhost ports.
|
|
||||||
|
|
||||||
## Network
|
|
||||||
|
|
||||||
A simulation network is created with an ID and default service. The default
|
|
||||||
service is used if a node is created without an explicit service. The
|
|
||||||
network has exposed methods for creating, starting, stopping, connecting
|
|
||||||
and disconnecting nodes. It also emits events when certain actions occur.
|
|
||||||
|
|
||||||
### Events
|
|
||||||
|
|
||||||
A simulation network emits the following events:
|
|
||||||
|
|
||||||
* node event - when nodes are created / started / stopped
|
|
||||||
* connection event - when nodes are connected / disconnected
|
|
||||||
* message event - when a protocol message is sent between two nodes
|
|
||||||
|
|
||||||
The events have a "control" flag which when set indicates that the event is the
|
|
||||||
outcome of a controlled simulation action (e.g. creating a node or explicitly
|
|
||||||
connecting two nodes).
|
|
||||||
|
|
||||||
This is in contrast to a non-control event, otherwise called a "live" event,
|
|
||||||
which is the outcome of something happening in the network as a result of a
|
|
||||||
control event (e.g. a node actually started up or a connection was actually
|
|
||||||
established between two nodes).
|
|
||||||
|
|
||||||
Live events are detected by the simulation network by subscribing to node peer
|
|
||||||
events via RPC when the nodes start up.
|
|
||||||
|
|
||||||
## Testing Framework
|
|
||||||
|
|
||||||
The `Simulation` type can be used in tests to perform actions in a simulation
|
|
||||||
network and then wait for expectations to be met.
|
|
||||||
|
|
||||||
With a running simulation network, the `Simulation.Run` method can be called
|
|
||||||
with a `Step` which has the following fields:
|
|
||||||
|
|
||||||
* `Action` - a function that performs some action in the network
|
|
||||||
|
|
||||||
* `Expect` - an expectation function which returns whether or not a
|
|
||||||
given node meets the expectation
|
|
||||||
|
|
||||||
* `Trigger` - a channel that receives node IDs which then trigger a check
|
|
||||||
of the expectation function to be performed against that node
|
|
||||||
|
|
||||||
As a concrete example, consider a simulated network of Ethereum nodes. An
|
|
||||||
`Action` could be the sending of a transaction, `Expect` it being included in
|
|
||||||
a block, and `Trigger` a check for every block that is mined.
|
|
||||||
|
|
||||||
On return, the `Simulation.Run` method returns a `StepResult` which can be used
|
|
||||||
to determine if all nodes met the expectation, how long it took them to meet
|
|
||||||
the expectation and what network events were emitted during the step run.
|
|
||||||
|
|
||||||
## HTTP API
|
|
||||||
|
|
||||||
The simulation framework includes a HTTP API that can be used to control the
|
|
||||||
simulation.
|
|
||||||
|
|
||||||
The API is initialised with a particular node adapter and has the following
|
|
||||||
endpoints:
|
|
||||||
|
|
||||||
```
|
|
||||||
GET / Get network information
|
|
||||||
POST /start Start all nodes in the network
|
|
||||||
POST /stop Stop all nodes in the network
|
|
||||||
GET /events Stream network events
|
|
||||||
GET /snapshot Take a network snapshot
|
|
||||||
POST /snapshot Load a network snapshot
|
|
||||||
POST /nodes Create a node
|
|
||||||
GET /nodes Get all nodes in the network
|
|
||||||
GET /nodes/:nodeid Get node information
|
|
||||||
POST /nodes/:nodeid/start Start a node
|
|
||||||
POST /nodes/:nodeid/stop Stop a node
|
|
||||||
POST /nodes/:nodeid/conn/:peerid Connect two nodes
|
|
||||||
DELETE /nodes/:nodeid/conn/:peerid Disconnect two nodes
|
|
||||||
GET /nodes/:nodeid/rpc Make RPC requests to a node via WebSocket
|
|
||||||
```
|
|
||||||
|
|
||||||
For convenience, `nodeid` in the URL can be the name of a node rather than its
|
|
||||||
ID.
|
|
||||||
|
|
||||||
## Command line client
|
|
||||||
|
|
||||||
`p2psim` is a command line client for the HTTP API, located in
|
|
||||||
`cmd/p2psim`.
|
|
||||||
|
|
||||||
It provides the following commands:
|
|
||||||
|
|
||||||
```
|
|
||||||
p2psim show
|
|
||||||
p2psim events [--current] [--filter=FILTER]
|
|
||||||
p2psim snapshot
|
|
||||||
p2psim load
|
|
||||||
p2psim node create [--name=NAME] [--services=SERVICES] [--key=KEY]
|
|
||||||
p2psim node list
|
|
||||||
p2psim node show <node>
|
|
||||||
p2psim node start <node>
|
|
||||||
p2psim node stop <node>
|
|
||||||
p2psim node connect <node> <peer>
|
|
||||||
p2psim node disconnect <node> <peer>
|
|
||||||
p2psim node rpc <node> <method> [<args>] [--subscribe]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
See [p2p/simulations/examples/README.md](examples/README.md).
|
|
||||||
|
|
@ -1,564 +0,0 @@
|
||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package adapters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/internal/reexec"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/node"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Register a reexec function to start a simulation node when the current binary is
|
|
||||||
// executed as "p2p-node" (rather than whatever the main() function would normally do).
|
|
||||||
reexec.Register("p2p-node", execP2PNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecAdapter is a NodeAdapter which runs simulation nodes by executing the current binary
|
|
||||||
// as a child process.
|
|
||||||
type ExecAdapter struct {
|
|
||||||
// BaseDir is the directory under which the data directories for each
|
|
||||||
// simulation node are created.
|
|
||||||
BaseDir string
|
|
||||||
|
|
||||||
nodes map[enode.ID]*ExecNode
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewExecAdapter returns an ExecAdapter which stores node data in
|
|
||||||
// subdirectories of the given base directory
|
|
||||||
func NewExecAdapter(baseDir string) *ExecAdapter {
|
|
||||||
return &ExecAdapter{
|
|
||||||
BaseDir: baseDir,
|
|
||||||
nodes: make(map[enode.ID]*ExecNode),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the name of the adapter for logging purposes
|
|
||||||
func (e *ExecAdapter) Name() string {
|
|
||||||
return "exec-adapter"
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewNode returns a new ExecNode using the given config
|
|
||||||
func (e *ExecAdapter) NewNode(config *NodeConfig) (Node, error) {
|
|
||||||
if len(config.Lifecycles) == 0 {
|
|
||||||
return nil, errors.New("node must have at least one service lifecycle")
|
|
||||||
}
|
|
||||||
for _, service := range config.Lifecycles {
|
|
||||||
if _, exists := lifecycleConstructorFuncs[service]; !exists {
|
|
||||||
return nil, fmt.Errorf("unknown node service %q", service)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create the node directory using the first 12 characters of the ID
|
|
||||||
// as Unix socket paths cannot be longer than 256 characters
|
|
||||||
dir := filepath.Join(e.BaseDir, config.ID.String()[:12])
|
|
||||||
if err := os.Mkdir(dir, 0755); err != nil {
|
|
||||||
return nil, fmt.Errorf("error creating node directory: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := config.initDummyEnode()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate the config
|
|
||||||
conf := &execNodeConfig{
|
|
||||||
Stack: node.DefaultConfig,
|
|
||||||
Node: config,
|
|
||||||
}
|
|
||||||
if config.DataDir != "" {
|
|
||||||
conf.Stack.DataDir = config.DataDir
|
|
||||||
} else {
|
|
||||||
conf.Stack.DataDir = filepath.Join(dir, "data")
|
|
||||||
}
|
|
||||||
|
|
||||||
// these parameters are crucial for execadapter node to run correctly
|
|
||||||
conf.Stack.WSHost = "127.0.0.1"
|
|
||||||
conf.Stack.WSPort = 0
|
|
||||||
conf.Stack.WSOrigins = []string{"*"}
|
|
||||||
conf.Stack.WSExposeAll = true
|
|
||||||
conf.Stack.P2P.EnableMsgEvents = config.EnableMsgEvents
|
|
||||||
conf.Stack.P2P.NoDiscovery = true
|
|
||||||
conf.Stack.P2P.NAT = nil
|
|
||||||
|
|
||||||
// Listen on a localhost port, which we set when we
|
|
||||||
// initialise NodeConfig (usually a random port)
|
|
||||||
conf.Stack.P2P.ListenAddr = fmt.Sprintf(":%d", config.Port)
|
|
||||||
|
|
||||||
node := &ExecNode{
|
|
||||||
ID: config.ID,
|
|
||||||
Dir: dir,
|
|
||||||
Config: conf,
|
|
||||||
adapter: e,
|
|
||||||
}
|
|
||||||
node.newCmd = node.execCommand
|
|
||||||
e.nodes[node.ID] = node
|
|
||||||
return node, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecNode starts a simulation node by exec'ing the current binary and
|
|
||||||
// running the configured services
|
|
||||||
type ExecNode struct {
|
|
||||||
ID enode.ID
|
|
||||||
Dir string
|
|
||||||
Config *execNodeConfig
|
|
||||||
Cmd *exec.Cmd
|
|
||||||
Info *p2p.NodeInfo
|
|
||||||
|
|
||||||
adapter *ExecAdapter
|
|
||||||
client *rpc.Client
|
|
||||||
wsAddr string
|
|
||||||
newCmd func() *exec.Cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Addr returns the node's enode URL
|
|
||||||
func (n *ExecNode) Addr() []byte {
|
|
||||||
if n.Info == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return []byte(n.Info.Enode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client returns an rpc.Client which can be used to communicate with the
|
|
||||||
// underlying services (it is set once the node has started)
|
|
||||||
func (n *ExecNode) Client() (*rpc.Client, error) {
|
|
||||||
return n.client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start exec's the node passing the ID and service as command line arguments
|
|
||||||
// and the node config encoded as JSON in an environment variable.
|
|
||||||
func (n *ExecNode) Start(snapshots map[string][]byte) (err error) {
|
|
||||||
if n.Cmd != nil {
|
|
||||||
return errors.New("already started")
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
n.Stop()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// encode a copy of the config containing the snapshot
|
|
||||||
confCopy := *n.Config
|
|
||||||
confCopy.Snapshots = snapshots
|
|
||||||
confCopy.PeerAddrs = make(map[string]string)
|
|
||||||
for id, node := range n.adapter.nodes {
|
|
||||||
confCopy.PeerAddrs[id.String()] = node.wsAddr
|
|
||||||
}
|
|
||||||
confData, err := json.Marshal(confCopy)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error generating node config: %s", err)
|
|
||||||
}
|
|
||||||
// expose the admin namespace via websocket if it's not enabled
|
|
||||||
exposed := confCopy.Stack.WSExposeAll
|
|
||||||
if !exposed {
|
|
||||||
for _, api := range confCopy.Stack.WSModules {
|
|
||||||
if api == "admin" {
|
|
||||||
exposed = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !exposed {
|
|
||||||
confCopy.Stack.WSModules = append(confCopy.Stack.WSModules, "admin")
|
|
||||||
}
|
|
||||||
// start the one-shot server that waits for startup information
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
statusURL, statusC := n.waitForStartupJSON(ctx)
|
|
||||||
|
|
||||||
// start the node
|
|
||||||
cmd := n.newCmd()
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
cmd.Env = append(os.Environ(),
|
|
||||||
envStatusURL+"="+statusURL,
|
|
||||||
envNodeConfig+"="+string(confData),
|
|
||||||
)
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return fmt.Errorf("error starting node: %s", err)
|
|
||||||
}
|
|
||||||
n.Cmd = cmd
|
|
||||||
|
|
||||||
// Wait for the node to start.
|
|
||||||
status := <-statusC
|
|
||||||
if status.Err != "" {
|
|
||||||
return errors.New(status.Err)
|
|
||||||
}
|
|
||||||
client, err := rpc.DialWebsocket(ctx, status.WSEndpoint, "")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't connect to RPC server: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node ready :)
|
|
||||||
n.client = client
|
|
||||||
n.wsAddr = status.WSEndpoint
|
|
||||||
n.Info = status.NodeInfo
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// waitForStartupJSON runs a one-shot HTTP server to receive a startup report.
|
|
||||||
func (n *ExecNode) waitForStartupJSON(ctx context.Context) (string, chan nodeStartupJSON) {
|
|
||||||
var (
|
|
||||||
ch = make(chan nodeStartupJSON, 1)
|
|
||||||
quitOnce sync.Once
|
|
||||||
srv http.Server
|
|
||||||
)
|
|
||||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
ch <- nodeStartupJSON{Err: err.Error()}
|
|
||||||
return "", ch
|
|
||||||
}
|
|
||||||
quit := func(status nodeStartupJSON) {
|
|
||||||
quitOnce.Do(func() {
|
|
||||||
l.Close()
|
|
||||||
ch <- status
|
|
||||||
})
|
|
||||||
}
|
|
||||||
srv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var status nodeStartupJSON
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&status); err != nil {
|
|
||||||
status.Err = fmt.Sprintf("can't decode startup report: %v", err)
|
|
||||||
}
|
|
||||||
quit(status)
|
|
||||||
})
|
|
||||||
// Run the HTTP server, but don't wait forever and shut it down
|
|
||||||
// if the context is canceled.
|
|
||||||
go srv.Serve(l)
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
quit(nodeStartupJSON{Err: "didn't get startup report"})
|
|
||||||
}()
|
|
||||||
|
|
||||||
url := "http://" + l.Addr().String()
|
|
||||||
return url, ch
|
|
||||||
}
|
|
||||||
|
|
||||||
// execCommand returns a command which runs the node locally by exec'ing
|
|
||||||
// the current binary but setting argv[0] to "p2p-node" so that the child
|
|
||||||
// runs execP2PNode
|
|
||||||
func (n *ExecNode) execCommand() *exec.Cmd {
|
|
||||||
return &exec.Cmd{
|
|
||||||
Path: reexec.Self(),
|
|
||||||
Args: []string{"p2p-node", strings.Join(n.Config.Node.Lifecycles, ","), n.ID.String()},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop stops the node by first sending SIGTERM and then SIGKILL if the node
|
|
||||||
// doesn't stop within 5s
|
|
||||||
func (n *ExecNode) Stop() error {
|
|
||||||
if n.Cmd == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
n.Cmd = nil
|
|
||||||
}()
|
|
||||||
|
|
||||||
if n.client != nil {
|
|
||||||
n.client.Close()
|
|
||||||
n.client = nil
|
|
||||||
n.wsAddr = ""
|
|
||||||
n.Info = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := n.Cmd.Process.Signal(syscall.SIGTERM); err != nil {
|
|
||||||
return n.Cmd.Process.Kill()
|
|
||||||
}
|
|
||||||
waitErr := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
waitErr <- n.Cmd.Wait()
|
|
||||||
}()
|
|
||||||
select {
|
|
||||||
case err := <-waitErr:
|
|
||||||
return err
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
return n.Cmd.Process.Kill()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeInfo returns information about the node
|
|
||||||
func (n *ExecNode) NodeInfo() *p2p.NodeInfo {
|
|
||||||
info := &p2p.NodeInfo{
|
|
||||||
ID: n.ID.String(),
|
|
||||||
}
|
|
||||||
if n.client != nil {
|
|
||||||
n.client.Call(&info, "admin_nodeInfo")
|
|
||||||
}
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeRPC serves RPC requests over the given connection by dialling the
|
|
||||||
// node's WebSocket address and joining the two connections
|
|
||||||
func (n *ExecNode) ServeRPC(clientConn *websocket.Conn) error {
|
|
||||||
conn, _, err := websocket.DefaultDialer.Dial(n.wsAddr, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(2)
|
|
||||||
go wsCopy(&wg, conn, clientConn)
|
|
||||||
go wsCopy(&wg, clientConn, conn)
|
|
||||||
wg.Wait()
|
|
||||||
conn.Close()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func wsCopy(wg *sync.WaitGroup, src, dst *websocket.Conn) {
|
|
||||||
defer wg.Done()
|
|
||||||
for {
|
|
||||||
msgType, r, err := src.NextReader()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w, err := dst.NextWriter(msgType)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err = io.Copy(w, r); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapshots creates snapshots of the services by calling the
|
|
||||||
// simulation_snapshot RPC method
|
|
||||||
func (n *ExecNode) Snapshots() (map[string][]byte, error) {
|
|
||||||
if n.client == nil {
|
|
||||||
return nil, errors.New("RPC not started")
|
|
||||||
}
|
|
||||||
var snapshots map[string][]byte
|
|
||||||
return snapshots, n.client.Call(&snapshots, "simulation_snapshot")
|
|
||||||
}
|
|
||||||
|
|
||||||
// execNodeConfig is used to serialize the node configuration so it can be
|
|
||||||
// passed to the child process as a JSON encoded environment variable
|
|
||||||
type execNodeConfig struct {
|
|
||||||
Stack node.Config `json:"stack"`
|
|
||||||
Node *NodeConfig `json:"node"`
|
|
||||||
Snapshots map[string][]byte `json:"snapshots,omitempty"`
|
|
||||||
PeerAddrs map[string]string `json:"peer_addrs,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func initLogging() {
|
|
||||||
// Initialize the logging by default first.
|
|
||||||
var innerHandler slog.Handler
|
|
||||||
innerHandler = slog.NewTextHandler(os.Stderr, nil)
|
|
||||||
glogger := log.NewGlogHandler(innerHandler)
|
|
||||||
glogger.Verbosity(log.LevelInfo)
|
|
||||||
log.SetDefault(log.NewLogger(glogger))
|
|
||||||
|
|
||||||
confEnv := os.Getenv(envNodeConfig)
|
|
||||||
if confEnv == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var conf execNodeConfig
|
|
||||||
if err := json.Unmarshal([]byte(confEnv), &conf); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var writer = os.Stderr
|
|
||||||
if conf.Node.LogFile != "" {
|
|
||||||
logWriter, err := os.Create(conf.Node.LogFile)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writer = logWriter
|
|
||||||
}
|
|
||||||
var verbosity = log.LevelInfo
|
|
||||||
if conf.Node.LogVerbosity <= log.LevelTrace && conf.Node.LogVerbosity >= log.LevelCrit {
|
|
||||||
verbosity = log.FromLegacyLevel(int(conf.Node.LogVerbosity))
|
|
||||||
}
|
|
||||||
// Reinitialize the logger
|
|
||||||
innerHandler = log.NewTerminalHandler(writer, true)
|
|
||||||
glogger = log.NewGlogHandler(innerHandler)
|
|
||||||
glogger.Verbosity(verbosity)
|
|
||||||
log.SetDefault(log.NewLogger(glogger))
|
|
||||||
}
|
|
||||||
|
|
||||||
// execP2PNode starts a simulation node when the current binary is executed with
|
|
||||||
// argv[0] being "p2p-node", reading the service / ID from argv[1] / argv[2]
|
|
||||||
// and the node config from an environment variable.
|
|
||||||
func execP2PNode() {
|
|
||||||
initLogging()
|
|
||||||
|
|
||||||
statusURL := os.Getenv(envStatusURL)
|
|
||||||
if statusURL == "" {
|
|
||||||
log.Crit("missing " + envStatusURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the node and gather startup report.
|
|
||||||
var status nodeStartupJSON
|
|
||||||
stack, stackErr := startExecNodeStack()
|
|
||||||
if stackErr != nil {
|
|
||||||
status.Err = stackErr.Error()
|
|
||||||
} else {
|
|
||||||
status.WSEndpoint = stack.WSEndpoint()
|
|
||||||
status.NodeInfo = stack.Server().NodeInfo()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send status to the host.
|
|
||||||
statusJSON, _ := json.Marshal(status)
|
|
||||||
resp, err := http.Post(statusURL, "application/json", bytes.NewReader(statusJSON))
|
|
||||||
if err != nil {
|
|
||||||
log.Crit("Can't post startup info", "url", statusURL, "err", err)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
if stackErr != nil {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop the stack if we get a SIGTERM signal.
|
|
||||||
go func() {
|
|
||||||
sigc := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigc, syscall.SIGTERM)
|
|
||||||
defer signal.Stop(sigc)
|
|
||||||
<-sigc
|
|
||||||
log.Info("Received SIGTERM, shutting down...")
|
|
||||||
stack.Close()
|
|
||||||
}()
|
|
||||||
stack.Wait() // Wait for the stack to exit.
|
|
||||||
}
|
|
||||||
|
|
||||||
func startExecNodeStack() (*node.Node, error) {
|
|
||||||
// read the services from argv
|
|
||||||
serviceNames := strings.Split(os.Args[1], ",")
|
|
||||||
|
|
||||||
// decode the config
|
|
||||||
confEnv := os.Getenv(envNodeConfig)
|
|
||||||
if confEnv == "" {
|
|
||||||
return nil, fmt.Errorf("missing " + envNodeConfig)
|
|
||||||
}
|
|
||||||
var conf execNodeConfig
|
|
||||||
if err := json.Unmarshal([]byte(confEnv), &conf); err != nil {
|
|
||||||
return nil, fmt.Errorf("error decoding %s: %v", envNodeConfig, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create enode record
|
|
||||||
nodeTcpConn, _ := net.ResolveTCPAddr("tcp", conf.Stack.P2P.ListenAddr)
|
|
||||||
if nodeTcpConn.IP == nil {
|
|
||||||
nodeTcpConn.IP = net.IPv4(127, 0, 0, 1)
|
|
||||||
}
|
|
||||||
conf.Node.initEnode(nodeTcpConn.IP, nodeTcpConn.Port, nodeTcpConn.Port)
|
|
||||||
conf.Stack.P2P.PrivateKey = conf.Node.PrivateKey
|
|
||||||
conf.Stack.Logger = log.New("node.id", conf.Node.ID.String())
|
|
||||||
|
|
||||||
// initialize the devp2p stack
|
|
||||||
stack, err := node.New(&conf.Stack)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error creating node stack: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the services, collecting them into a map so they can
|
|
||||||
// be accessed by the snapshot API.
|
|
||||||
services := make(map[string]node.Lifecycle, len(serviceNames))
|
|
||||||
for _, name := range serviceNames {
|
|
||||||
lifecycleFunc, exists := lifecycleConstructorFuncs[name]
|
|
||||||
if !exists {
|
|
||||||
return nil, fmt.Errorf("unknown node service %q", err)
|
|
||||||
}
|
|
||||||
ctx := &ServiceContext{
|
|
||||||
RPCDialer: &wsRPCDialer{addrs: conf.PeerAddrs},
|
|
||||||
Config: conf.Node,
|
|
||||||
}
|
|
||||||
if conf.Snapshots != nil {
|
|
||||||
ctx.Snapshot = conf.Snapshots[name]
|
|
||||||
}
|
|
||||||
service, err := lifecycleFunc(ctx, stack)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
services[name] = service
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the snapshot API.
|
|
||||||
stack.RegisterAPIs([]rpc.API{{
|
|
||||||
Namespace: "simulation",
|
|
||||||
Service: SnapshotAPI{services},
|
|
||||||
}})
|
|
||||||
|
|
||||||
if err = stack.Start(); err != nil {
|
|
||||||
err = fmt.Errorf("error starting stack: %v", err)
|
|
||||||
}
|
|
||||||
return stack, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
envStatusURL = "_P2P_STATUS_URL"
|
|
||||||
envNodeConfig = "_P2P_NODE_CONFIG"
|
|
||||||
)
|
|
||||||
|
|
||||||
// nodeStartupJSON is sent to the simulation host after startup.
|
|
||||||
type nodeStartupJSON struct {
|
|
||||||
Err string
|
|
||||||
WSEndpoint string
|
|
||||||
NodeInfo *p2p.NodeInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
// SnapshotAPI provides an RPC method to create snapshots of services
|
|
||||||
type SnapshotAPI struct {
|
|
||||||
services map[string]node.Lifecycle
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api SnapshotAPI) Snapshot() (map[string][]byte, error) {
|
|
||||||
snapshots := make(map[string][]byte)
|
|
||||||
for name, service := range api.services {
|
|
||||||
if s, ok := service.(interface {
|
|
||||||
Snapshot() ([]byte, error)
|
|
||||||
}); ok {
|
|
||||||
snap, err := s.Snapshot()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
snapshots[name] = snap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return snapshots, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type wsRPCDialer struct {
|
|
||||||
addrs map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialRPC implements the RPCDialer interface by creating a WebSocket RPC
|
|
||||||
// client of the given node
|
|
||||||
func (w *wsRPCDialer) DialRPC(id enode.ID) (*rpc.Client, error) {
|
|
||||||
addr, ok := w.addrs[id.String()]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("unknown node: %s", id)
|
|
||||||
}
|
|
||||||
return rpc.DialWebsocket(context.Background(), addr, "http://localhost")
|
|
||||||
}
|
|
||||||
|
|
@ -1,350 +0,0 @@
|
||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package adapters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/event"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/node"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/pipes"
|
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SimAdapter is a NodeAdapter which creates in-memory simulation nodes and
|
|
||||||
// connects them using net.Pipe
|
|
||||||
type SimAdapter struct {
|
|
||||||
pipe func() (net.Conn, net.Conn, error)
|
|
||||||
mtx sync.RWMutex
|
|
||||||
nodes map[enode.ID]*SimNode
|
|
||||||
lifecycles LifecycleConstructors
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSimAdapter creates a SimAdapter which is capable of running in-memory
|
|
||||||
// simulation nodes running any of the given services (the services to run on a
|
|
||||||
// particular node are passed to the NewNode function in the NodeConfig)
|
|
||||||
// the adapter uses a net.Pipe for in-memory simulated network connections
|
|
||||||
func NewSimAdapter(services LifecycleConstructors) *SimAdapter {
|
|
||||||
return &SimAdapter{
|
|
||||||
pipe: pipes.NetPipe,
|
|
||||||
nodes: make(map[enode.ID]*SimNode),
|
|
||||||
lifecycles: services,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the name of the adapter for logging purposes
|
|
||||||
func (s *SimAdapter) Name() string {
|
|
||||||
return "sim-adapter"
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewNode returns a new SimNode using the given config
|
|
||||||
func (s *SimAdapter) NewNode(config *NodeConfig) (Node, error) {
|
|
||||||
s.mtx.Lock()
|
|
||||||
defer s.mtx.Unlock()
|
|
||||||
|
|
||||||
id := config.ID
|
|
||||||
// verify that the node has a private key in the config
|
|
||||||
if config.PrivateKey == nil {
|
|
||||||
return nil, fmt.Errorf("node is missing private key: %s", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check a node with the ID doesn't already exist
|
|
||||||
if _, exists := s.nodes[id]; exists {
|
|
||||||
return nil, fmt.Errorf("node already exists: %s", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the services are valid
|
|
||||||
if len(config.Lifecycles) == 0 {
|
|
||||||
return nil, errors.New("node must have at least one service")
|
|
||||||
}
|
|
||||||
for _, service := range config.Lifecycles {
|
|
||||||
if _, exists := s.lifecycles[service]; !exists {
|
|
||||||
return nil, fmt.Errorf("unknown node service %q", service)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := config.initDummyEnode()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := node.New(&node.Config{
|
|
||||||
P2P: p2p.Config{
|
|
||||||
PrivateKey: config.PrivateKey,
|
|
||||||
MaxPeers: math.MaxInt32,
|
|
||||||
NoDiscovery: true,
|
|
||||||
Dialer: s,
|
|
||||||
EnableMsgEvents: config.EnableMsgEvents,
|
|
||||||
},
|
|
||||||
ExternalSigner: config.ExternalSigner,
|
|
||||||
Logger: log.New("node.id", id.String()),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
simNode := &SimNode{
|
|
||||||
ID: id,
|
|
||||||
config: config,
|
|
||||||
node: n,
|
|
||||||
adapter: s,
|
|
||||||
running: make(map[string]node.Lifecycle),
|
|
||||||
}
|
|
||||||
s.nodes[id] = simNode
|
|
||||||
return simNode, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial implements the p2p.NodeDialer interface by connecting to the node using
|
|
||||||
// an in-memory net.Pipe
|
|
||||||
func (s *SimAdapter) Dial(ctx context.Context, dest *enode.Node) (conn net.Conn, err error) {
|
|
||||||
node, ok := s.GetNode(dest.ID())
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("unknown node: %s", dest.ID())
|
|
||||||
}
|
|
||||||
srv := node.Server()
|
|
||||||
if srv == nil {
|
|
||||||
return nil, fmt.Errorf("node not running: %s", dest.ID())
|
|
||||||
}
|
|
||||||
// SimAdapter.pipe is net.Pipe (NewSimAdapter)
|
|
||||||
pipe1, pipe2, err := s.pipe()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// this is simulated 'listening'
|
|
||||||
// asynchronously call the dialed destination node's p2p server
|
|
||||||
// to set up connection on the 'listening' side
|
|
||||||
go srv.SetupConn(pipe1, 0, nil)
|
|
||||||
return pipe2, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DialRPC implements the RPCDialer interface by creating an in-memory RPC
|
|
||||||
// client of the given node
|
|
||||||
func (s *SimAdapter) DialRPC(id enode.ID) (*rpc.Client, error) {
|
|
||||||
node, ok := s.GetNode(id)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("unknown node: %s", id)
|
|
||||||
}
|
|
||||||
return node.node.Attach(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNode returns the node with the given ID if it exists
|
|
||||||
func (s *SimAdapter) GetNode(id enode.ID) (*SimNode, bool) {
|
|
||||||
s.mtx.RLock()
|
|
||||||
defer s.mtx.RUnlock()
|
|
||||||
node, ok := s.nodes[id]
|
|
||||||
return node, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// SimNode is an in-memory simulation node which connects to other nodes using
|
|
||||||
// net.Pipe (see SimAdapter.Dial), running devp2p protocols directly over that
|
|
||||||
// pipe
|
|
||||||
type SimNode struct {
|
|
||||||
lock sync.RWMutex
|
|
||||||
ID enode.ID
|
|
||||||
config *NodeConfig
|
|
||||||
adapter *SimAdapter
|
|
||||||
node *node.Node
|
|
||||||
running map[string]node.Lifecycle
|
|
||||||
client *rpc.Client
|
|
||||||
registerOnce sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the underlaying node.Node to release
|
|
||||||
// acquired resources.
|
|
||||||
func (sn *SimNode) Close() error {
|
|
||||||
return sn.node.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Addr returns the node's discovery address
|
|
||||||
func (sn *SimNode) Addr() []byte {
|
|
||||||
return []byte(sn.Node().String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node returns a node descriptor representing the SimNode
|
|
||||||
func (sn *SimNode) Node() *enode.Node {
|
|
||||||
return sn.config.Node()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client returns an rpc.Client which can be used to communicate with the
|
|
||||||
// underlying services (it is set once the node has started)
|
|
||||||
func (sn *SimNode) Client() (*rpc.Client, error) {
|
|
||||||
sn.lock.RLock()
|
|
||||||
defer sn.lock.RUnlock()
|
|
||||||
if sn.client == nil {
|
|
||||||
return nil, errors.New("node not started")
|
|
||||||
}
|
|
||||||
return sn.client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeRPC serves RPC requests over the given connection by creating an
|
|
||||||
// in-memory client to the node's RPC server.
|
|
||||||
func (sn *SimNode) ServeRPC(conn *websocket.Conn) error {
|
|
||||||
handler, err := sn.node.RPCHandler()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
codec := rpc.NewFuncCodec(conn, func(v any, _ bool) error { return conn.WriteJSON(v) }, conn.ReadJSON)
|
|
||||||
handler.ServeCodec(codec, 0)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapshots creates snapshots of the services by calling the
|
|
||||||
// simulation_snapshot RPC method
|
|
||||||
func (sn *SimNode) Snapshots() (map[string][]byte, error) {
|
|
||||||
sn.lock.RLock()
|
|
||||||
services := make(map[string]node.Lifecycle, len(sn.running))
|
|
||||||
for name, service := range sn.running {
|
|
||||||
services[name] = service
|
|
||||||
}
|
|
||||||
sn.lock.RUnlock()
|
|
||||||
if len(services) == 0 {
|
|
||||||
return nil, errors.New("no running services")
|
|
||||||
}
|
|
||||||
snapshots := make(map[string][]byte)
|
|
||||||
for name, service := range services {
|
|
||||||
if s, ok := service.(interface {
|
|
||||||
Snapshot() ([]byte, error)
|
|
||||||
}); ok {
|
|
||||||
snap, err := s.Snapshot()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
snapshots[name] = snap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return snapshots, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start registers the services and starts the underlying devp2p node
|
|
||||||
func (sn *SimNode) Start(snapshots map[string][]byte) error {
|
|
||||||
// ensure we only register the services once in the case of the node
|
|
||||||
// being stopped and then started again
|
|
||||||
var regErr error
|
|
||||||
sn.registerOnce.Do(func() {
|
|
||||||
for _, name := range sn.config.Lifecycles {
|
|
||||||
ctx := &ServiceContext{
|
|
||||||
RPCDialer: sn.adapter,
|
|
||||||
Config: sn.config,
|
|
||||||
}
|
|
||||||
if snapshots != nil {
|
|
||||||
ctx.Snapshot = snapshots[name]
|
|
||||||
}
|
|
||||||
serviceFunc := sn.adapter.lifecycles[name]
|
|
||||||
service, err := serviceFunc(ctx, sn.node)
|
|
||||||
if err != nil {
|
|
||||||
regErr = err
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// if the service has already been registered, don't register it again.
|
|
||||||
if _, ok := sn.running[name]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sn.running[name] = service
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if regErr != nil {
|
|
||||||
return regErr
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sn.node.Start(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// create an in-process RPC client
|
|
||||||
client := sn.node.Attach()
|
|
||||||
sn.lock.Lock()
|
|
||||||
sn.client = client
|
|
||||||
sn.lock.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop closes the RPC client and stops the underlying devp2p node
|
|
||||||
func (sn *SimNode) Stop() error {
|
|
||||||
sn.lock.Lock()
|
|
||||||
if sn.client != nil {
|
|
||||||
sn.client.Close()
|
|
||||||
sn.client = nil
|
|
||||||
}
|
|
||||||
sn.lock.Unlock()
|
|
||||||
return sn.node.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service returns a running service by name
|
|
||||||
func (sn *SimNode) Service(name string) node.Lifecycle {
|
|
||||||
sn.lock.RLock()
|
|
||||||
defer sn.lock.RUnlock()
|
|
||||||
return sn.running[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Services returns a copy of the underlying services
|
|
||||||
func (sn *SimNode) Services() []node.Lifecycle {
|
|
||||||
sn.lock.RLock()
|
|
||||||
defer sn.lock.RUnlock()
|
|
||||||
services := make([]node.Lifecycle, 0, len(sn.running))
|
|
||||||
for _, service := range sn.running {
|
|
||||||
services = append(services, service)
|
|
||||||
}
|
|
||||||
return services
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceMap returns a map by names of the underlying services
|
|
||||||
func (sn *SimNode) ServiceMap() map[string]node.Lifecycle {
|
|
||||||
sn.lock.RLock()
|
|
||||||
defer sn.lock.RUnlock()
|
|
||||||
services := make(map[string]node.Lifecycle, len(sn.running))
|
|
||||||
for name, service := range sn.running {
|
|
||||||
services[name] = service
|
|
||||||
}
|
|
||||||
return services
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server returns the underlying p2p.Server
|
|
||||||
func (sn *SimNode) Server() *p2p.Server {
|
|
||||||
return sn.node.Server()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeEvents subscribes the given channel to peer events from the
|
|
||||||
// underlying p2p.Server
|
|
||||||
func (sn *SimNode) SubscribeEvents(ch chan *p2p.PeerEvent) event.Subscription {
|
|
||||||
srv := sn.Server()
|
|
||||||
if srv == nil {
|
|
||||||
panic("node not running")
|
|
||||||
}
|
|
||||||
return srv.SubscribeEvents(ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeInfo returns information about the node
|
|
||||||
func (sn *SimNode) NodeInfo() *p2p.NodeInfo {
|
|
||||||
server := sn.Server()
|
|
||||||
if server == nil {
|
|
||||||
return &p2p.NodeInfo{
|
|
||||||
ID: sn.ID.String(),
|
|
||||||
Enode: sn.Node().String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return server.NodeInfo()
|
|
||||||
}
|
|
||||||
|
|
@ -1,202 +0,0 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package adapters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/pipes"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTCPPipe(t *testing.T) {
|
|
||||||
c1, c2, err := pipes.TCPPipe()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
msgs := 50
|
|
||||||
size := 1024
|
|
||||||
for i := 0; i < msgs; i++ {
|
|
||||||
msg := make([]byte, size)
|
|
||||||
binary.PutUvarint(msg, uint64(i))
|
|
||||||
if _, err := c1.Write(msg); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < msgs; i++ {
|
|
||||||
msg := make([]byte, size)
|
|
||||||
binary.PutUvarint(msg, uint64(i))
|
|
||||||
out := make([]byte, size)
|
|
||||||
if _, err := c2.Read(out); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(msg, out) {
|
|
||||||
t.Fatalf("expected %#v, got %#v", msg, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTCPPipeBidirections(t *testing.T) {
|
|
||||||
c1, c2, err := pipes.TCPPipe()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
msgs := 50
|
|
||||||
size := 7
|
|
||||||
for i := 0; i < msgs; i++ {
|
|
||||||
msg := []byte(fmt.Sprintf("ping %02d", i))
|
|
||||||
if _, err := c1.Write(msg); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < msgs; i++ {
|
|
||||||
expected := []byte(fmt.Sprintf("ping %02d", i))
|
|
||||||
out := make([]byte, size)
|
|
||||||
if _, err := c2.Read(out); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(expected, out) {
|
|
||||||
t.Fatalf("expected %#v, got %#v", out, expected)
|
|
||||||
} else {
|
|
||||||
msg := []byte(fmt.Sprintf("pong %02d", i))
|
|
||||||
if _, err := c2.Write(msg); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < msgs; i++ {
|
|
||||||
expected := []byte(fmt.Sprintf("pong %02d", i))
|
|
||||||
out := make([]byte, size)
|
|
||||||
if _, err := c1.Read(out); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(expected, out) {
|
|
||||||
t.Fatalf("expected %#v, got %#v", out, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNetPipe(t *testing.T) {
|
|
||||||
c1, c2, err := pipes.NetPipe()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
msgs := 50
|
|
||||||
size := 1024
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
defer wg.Wait()
|
|
||||||
|
|
||||||
// netPipe is blocking, so writes are emitted asynchronously
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
for i := 0; i < msgs; i++ {
|
|
||||||
msg := make([]byte, size)
|
|
||||||
binary.PutUvarint(msg, uint64(i))
|
|
||||||
if _, err := c1.Write(msg); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for i := 0; i < msgs; i++ {
|
|
||||||
msg := make([]byte, size)
|
|
||||||
binary.PutUvarint(msg, uint64(i))
|
|
||||||
out := make([]byte, size)
|
|
||||||
if _, err := c2.Read(out); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(msg, out) {
|
|
||||||
t.Errorf("expected %#v, got %#v", msg, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNetPipeBidirections(t *testing.T) {
|
|
||||||
c1, c2, err := pipes.NetPipe()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
msgs := 1000
|
|
||||||
size := 8
|
|
||||||
pingTemplate := "ping %03d"
|
|
||||||
pongTemplate := "pong %03d"
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
defer wg.Wait()
|
|
||||||
|
|
||||||
// netPipe is blocking, so writes are emitted asynchronously
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
for i := 0; i < msgs; i++ {
|
|
||||||
msg := []byte(fmt.Sprintf(pingTemplate, i))
|
|
||||||
if _, err := c1.Write(msg); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// netPipe is blocking, so reads for pong are emitted asynchronously
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
for i := 0; i < msgs; i++ {
|
|
||||||
expected := []byte(fmt.Sprintf(pongTemplate, i))
|
|
||||||
out := make([]byte, size)
|
|
||||||
if _, err := c1.Read(out); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(expected, out) {
|
|
||||||
t.Errorf("expected %#v, got %#v", expected, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// expect to read pings, and respond with pongs to the alternate connection
|
|
||||||
for i := 0; i < msgs; i++ {
|
|
||||||
expected := []byte(fmt.Sprintf(pingTemplate, i))
|
|
||||||
|
|
||||||
out := make([]byte, size)
|
|
||||||
_, err := c2.Read(out)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !bytes.Equal(expected, out) {
|
|
||||||
t.Errorf("expected %#v, got %#v", expected, out)
|
|
||||||
} else {
|
|
||||||
msg := []byte(fmt.Sprintf(pongTemplate, i))
|
|
||||||
if _, err := c2.Write(msg); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,326 +0,0 @@
|
||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package adapters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/reexec"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/node"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enr"
|
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Node represents a node in a simulation network which is created by a
|
|
||||||
// NodeAdapter, for example:
|
|
||||||
//
|
|
||||||
// - SimNode, an in-memory node in the same process
|
|
||||||
// - ExecNode, a child process node
|
|
||||||
// - DockerNode, a node running in a Docker container
|
|
||||||
type Node interface {
|
|
||||||
// Addr returns the node's address (e.g. an Enode URL)
|
|
||||||
Addr() []byte
|
|
||||||
|
|
||||||
// Client returns the RPC client which is created once the node is
|
|
||||||
// up and running
|
|
||||||
Client() (*rpc.Client, error)
|
|
||||||
|
|
||||||
// ServeRPC serves RPC requests over the given connection
|
|
||||||
ServeRPC(*websocket.Conn) error
|
|
||||||
|
|
||||||
// Start starts the node with the given snapshots
|
|
||||||
Start(snapshots map[string][]byte) error
|
|
||||||
|
|
||||||
// Stop stops the node
|
|
||||||
Stop() error
|
|
||||||
|
|
||||||
// NodeInfo returns information about the node
|
|
||||||
NodeInfo() *p2p.NodeInfo
|
|
||||||
|
|
||||||
// Snapshots creates snapshots of the running services
|
|
||||||
Snapshots() (map[string][]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeAdapter is used to create Nodes in a simulation network
|
|
||||||
type NodeAdapter interface {
|
|
||||||
// Name returns the name of the adapter for logging purposes
|
|
||||||
Name() string
|
|
||||||
|
|
||||||
// NewNode creates a new node with the given configuration
|
|
||||||
NewNode(config *NodeConfig) (Node, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeConfig is the configuration used to start a node in a simulation
|
|
||||||
// network
|
|
||||||
type NodeConfig struct {
|
|
||||||
// ID is the node's ID which is used to identify the node in the
|
|
||||||
// simulation network
|
|
||||||
ID enode.ID
|
|
||||||
|
|
||||||
// PrivateKey is the node's private key which is used by the devp2p
|
|
||||||
// stack to encrypt communications
|
|
||||||
PrivateKey *ecdsa.PrivateKey
|
|
||||||
|
|
||||||
// Enable peer events for Msgs
|
|
||||||
EnableMsgEvents bool
|
|
||||||
|
|
||||||
// Name is a human friendly name for the node like "node01"
|
|
||||||
Name string
|
|
||||||
|
|
||||||
// Use an existing database instead of a temporary one if non-empty
|
|
||||||
DataDir string
|
|
||||||
|
|
||||||
// Lifecycles are the names of the service lifecycles which should be run when
|
|
||||||
// starting the node (for SimNodes it should be the names of service lifecycles
|
|
||||||
// contained in SimAdapter.lifecycles, for other nodes it should be
|
|
||||||
// service lifecycles registered by calling the RegisterLifecycle function)
|
|
||||||
Lifecycles []string
|
|
||||||
|
|
||||||
// Properties are the names of the properties this node should hold
|
|
||||||
// within running services (e.g. "bootnode", "lightnode" or any custom values)
|
|
||||||
// These values need to be checked and acted upon by node Services
|
|
||||||
Properties []string
|
|
||||||
|
|
||||||
// ExternalSigner specifies an external URI for a clef-type signer
|
|
||||||
ExternalSigner string
|
|
||||||
|
|
||||||
// Enode
|
|
||||||
node *enode.Node
|
|
||||||
|
|
||||||
// ENR Record with entries to overwrite
|
|
||||||
Record enr.Record
|
|
||||||
|
|
||||||
// function to sanction or prevent suggesting a peer
|
|
||||||
Reachable func(id enode.ID) bool
|
|
||||||
|
|
||||||
Port uint16
|
|
||||||
|
|
||||||
// LogFile is the log file name of the p2p node at runtime.
|
|
||||||
//
|
|
||||||
// The default value is empty so that the default log writer
|
|
||||||
// is the system standard output.
|
|
||||||
LogFile string
|
|
||||||
|
|
||||||
// LogVerbosity is the log verbosity of the p2p node at runtime.
|
|
||||||
//
|
|
||||||
// The default verbosity is INFO.
|
|
||||||
LogVerbosity slog.Level
|
|
||||||
}
|
|
||||||
|
|
||||||
// nodeConfigJSON is used to encode and decode NodeConfig as JSON by encoding
|
|
||||||
// all fields as strings
|
|
||||||
type nodeConfigJSON struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
PrivateKey string `json:"private_key"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Lifecycles []string `json:"lifecycles"`
|
|
||||||
Properties []string `json:"properties"`
|
|
||||||
EnableMsgEvents bool `json:"enable_msg_events"`
|
|
||||||
Port uint16 `json:"port"`
|
|
||||||
LogFile string `json:"logfile"`
|
|
||||||
LogVerbosity int `json:"log_verbosity"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON implements the json.Marshaler interface by encoding the config
|
|
||||||
// fields as strings
|
|
||||||
func (n *NodeConfig) MarshalJSON() ([]byte, error) {
|
|
||||||
confJSON := nodeConfigJSON{
|
|
||||||
ID: n.ID.String(),
|
|
||||||
Name: n.Name,
|
|
||||||
Lifecycles: n.Lifecycles,
|
|
||||||
Properties: n.Properties,
|
|
||||||
Port: n.Port,
|
|
||||||
EnableMsgEvents: n.EnableMsgEvents,
|
|
||||||
LogFile: n.LogFile,
|
|
||||||
LogVerbosity: int(n.LogVerbosity),
|
|
||||||
}
|
|
||||||
if n.PrivateKey != nil {
|
|
||||||
confJSON.PrivateKey = hex.EncodeToString(crypto.FromECDSA(n.PrivateKey))
|
|
||||||
}
|
|
||||||
return json.Marshal(confJSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON implements the json.Unmarshaler interface by decoding the json
|
|
||||||
// string values into the config fields
|
|
||||||
func (n *NodeConfig) UnmarshalJSON(data []byte) error {
|
|
||||||
var confJSON nodeConfigJSON
|
|
||||||
if err := json.Unmarshal(data, &confJSON); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if confJSON.ID != "" {
|
|
||||||
if err := n.ID.UnmarshalText([]byte(confJSON.ID)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if confJSON.PrivateKey != "" {
|
|
||||||
key, err := hex.DecodeString(confJSON.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
privKey, err := crypto.ToECDSA(key)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
n.PrivateKey = privKey
|
|
||||||
}
|
|
||||||
|
|
||||||
n.Name = confJSON.Name
|
|
||||||
n.Lifecycles = confJSON.Lifecycles
|
|
||||||
n.Properties = confJSON.Properties
|
|
||||||
n.Port = confJSON.Port
|
|
||||||
n.EnableMsgEvents = confJSON.EnableMsgEvents
|
|
||||||
n.LogFile = confJSON.LogFile
|
|
||||||
n.LogVerbosity = slog.Level(confJSON.LogVerbosity)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Node returns the node descriptor represented by the config.
|
|
||||||
func (n *NodeConfig) Node() *enode.Node {
|
|
||||||
return n.node
|
|
||||||
}
|
|
||||||
|
|
||||||
// RandomNodeConfig returns node configuration with a randomly generated ID and
|
|
||||||
// PrivateKey
|
|
||||||
func RandomNodeConfig() *NodeConfig {
|
|
||||||
prvkey, err := crypto.GenerateKey()
|
|
||||||
if err != nil {
|
|
||||||
panic("unable to generate key")
|
|
||||||
}
|
|
||||||
|
|
||||||
port, err := assignTCPPort()
|
|
||||||
if err != nil {
|
|
||||||
panic("unable to assign tcp port")
|
|
||||||
}
|
|
||||||
|
|
||||||
enodId := enode.PubkeyToIDV4(&prvkey.PublicKey)
|
|
||||||
return &NodeConfig{
|
|
||||||
PrivateKey: prvkey,
|
|
||||||
ID: enodId,
|
|
||||||
Name: fmt.Sprintf("node_%s", enodId.String()),
|
|
||||||
Port: port,
|
|
||||||
EnableMsgEvents: true,
|
|
||||||
LogVerbosity: log.LvlInfo,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assignTCPPort() (uint16, error) {
|
|
||||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
l.Close()
|
|
||||||
_, port, err := net.SplitHostPort(l.Addr().String())
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
p, err := strconv.ParseUint(port, 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return uint16(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceContext is a collection of options and methods which can be utilised
|
|
||||||
// when starting services
|
|
||||||
type ServiceContext struct {
|
|
||||||
RPCDialer
|
|
||||||
|
|
||||||
Config *NodeConfig
|
|
||||||
Snapshot []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// RPCDialer is used when initialising services which need to connect to
|
|
||||||
// other nodes in the network (for example a simulated Swarm node which needs
|
|
||||||
// to connect to a Geth node to resolve ENS names)
|
|
||||||
type RPCDialer interface {
|
|
||||||
DialRPC(id enode.ID) (*rpc.Client, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LifecycleConstructor allows a Lifecycle to be constructed during node start-up.
|
|
||||||
// While the service-specific package usually takes care of Lifecycle creation and registration,
|
|
||||||
// for testing purposes, it is useful to be able to construct a Lifecycle on spot.
|
|
||||||
type LifecycleConstructor func(ctx *ServiceContext, stack *node.Node) (node.Lifecycle, error)
|
|
||||||
|
|
||||||
// LifecycleConstructors stores LifecycleConstructor functions to call during node start-up.
|
|
||||||
type LifecycleConstructors map[string]LifecycleConstructor
|
|
||||||
|
|
||||||
// lifecycleConstructorFuncs is a map of registered services which are used to boot devp2p
|
|
||||||
// nodes
|
|
||||||
var lifecycleConstructorFuncs = make(LifecycleConstructors)
|
|
||||||
|
|
||||||
// RegisterLifecycles registers the given Services which can then be used to
|
|
||||||
// start devp2p nodes using either the Exec or Docker adapters.
|
|
||||||
//
|
|
||||||
// It should be called in an init function so that it has the opportunity to
|
|
||||||
// execute the services before main() is called.
|
|
||||||
func RegisterLifecycles(lifecycles LifecycleConstructors) {
|
|
||||||
for name, f := range lifecycles {
|
|
||||||
if _, exists := lifecycleConstructorFuncs[name]; exists {
|
|
||||||
panic(fmt.Sprintf("node service already exists: %q", name))
|
|
||||||
}
|
|
||||||
lifecycleConstructorFuncs[name] = f
|
|
||||||
}
|
|
||||||
|
|
||||||
// now we have registered the services, run reexec.Init() which will
|
|
||||||
// potentially start one of the services if the current binary has
|
|
||||||
// been exec'd with argv[0] set to "p2p-node"
|
|
||||||
if reexec.Init() {
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// adds the host part to the configuration's ENR, signs it
|
|
||||||
// creates and the corresponding enode object to the configuration
|
|
||||||
func (n *NodeConfig) initEnode(ip net.IP, tcpport int, udpport int) error {
|
|
||||||
enrIp := enr.IP(ip)
|
|
||||||
n.Record.Set(&enrIp)
|
|
||||||
enrTcpPort := enr.TCP(tcpport)
|
|
||||||
n.Record.Set(&enrTcpPort)
|
|
||||||
enrUdpPort := enr.UDP(udpport)
|
|
||||||
n.Record.Set(&enrUdpPort)
|
|
||||||
|
|
||||||
err := enode.SignV4(&n.Record, n.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to generate ENR: %v", err)
|
|
||||||
}
|
|
||||||
nod, err := enode.New(enode.V4ID{}, &n.Record)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to create enode: %v", err)
|
|
||||||
}
|
|
||||||
log.Trace("simnode new", "record", n.Record)
|
|
||||||
n.node = nod
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NodeConfig) initDummyEnode() error {
|
|
||||||
return n.initEnode(net.IPv4(127, 0, 0, 1), int(n.Port), 0)
|
|
||||||
}
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package simulations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNodeNotFound = errors.New("node not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
// ConnectToLastNode connects the node with provided NodeID
|
|
||||||
// to the last node that is up, and avoiding connection to self.
|
|
||||||
// It is useful when constructing a chain network topology
|
|
||||||
// when Network adds and removes nodes dynamically.
|
|
||||||
func (net *Network) ConnectToLastNode(id enode.ID) (err error) {
|
|
||||||
net.lock.Lock()
|
|
||||||
defer net.lock.Unlock()
|
|
||||||
|
|
||||||
ids := net.getUpNodeIDs()
|
|
||||||
l := len(ids)
|
|
||||||
if l < 2 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
last := ids[l-1]
|
|
||||||
if last == id {
|
|
||||||
last = ids[l-2]
|
|
||||||
}
|
|
||||||
return net.connectNotConnected(last, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConnectToRandomNode connects the node with provided NodeID
|
|
||||||
// to a random node that is up.
|
|
||||||
func (net *Network) ConnectToRandomNode(id enode.ID) (err error) {
|
|
||||||
net.lock.Lock()
|
|
||||||
defer net.lock.Unlock()
|
|
||||||
|
|
||||||
selected := net.getRandomUpNode(id)
|
|
||||||
if selected == nil {
|
|
||||||
return ErrNodeNotFound
|
|
||||||
}
|
|
||||||
return net.connectNotConnected(selected.ID(), id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConnectNodesFull connects all nodes one to another.
|
|
||||||
// It provides a complete connectivity in the network
|
|
||||||
// which should be rarely needed.
|
|
||||||
func (net *Network) ConnectNodesFull(ids []enode.ID) (err error) {
|
|
||||||
net.lock.Lock()
|
|
||||||
defer net.lock.Unlock()
|
|
||||||
|
|
||||||
if ids == nil {
|
|
||||||
ids = net.getUpNodeIDs()
|
|
||||||
}
|
|
||||||
for i, lid := range ids {
|
|
||||||
for _, rid := range ids[i+1:] {
|
|
||||||
if err = net.connectNotConnected(lid, rid); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConnectNodesChain connects all nodes in a chain topology.
|
|
||||||
// If ids argument is nil, all nodes that are up will be connected.
|
|
||||||
func (net *Network) ConnectNodesChain(ids []enode.ID) (err error) {
|
|
||||||
net.lock.Lock()
|
|
||||||
defer net.lock.Unlock()
|
|
||||||
|
|
||||||
return net.connectNodesChain(ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (net *Network) connectNodesChain(ids []enode.ID) (err error) {
|
|
||||||
if ids == nil {
|
|
||||||
ids = net.getUpNodeIDs()
|
|
||||||
}
|
|
||||||
l := len(ids)
|
|
||||||
for i := 0; i < l-1; i++ {
|
|
||||||
if err := net.connectNotConnected(ids[i], ids[i+1]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConnectNodesRing connects all nodes in a ring topology.
|
|
||||||
// If ids argument is nil, all nodes that are up will be connected.
|
|
||||||
func (net *Network) ConnectNodesRing(ids []enode.ID) (err error) {
|
|
||||||
net.lock.Lock()
|
|
||||||
defer net.lock.Unlock()
|
|
||||||
|
|
||||||
if ids == nil {
|
|
||||||
ids = net.getUpNodeIDs()
|
|
||||||
}
|
|
||||||
l := len(ids)
|
|
||||||
if l < 2 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := net.connectNodesChain(ids); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return net.connectNotConnected(ids[l-1], ids[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConnectNodesStar connects all nodes into a star topology
|
|
||||||
// If ids argument is nil, all nodes that are up will be connected.
|
|
||||||
func (net *Network) ConnectNodesStar(ids []enode.ID, center enode.ID) (err error) {
|
|
||||||
net.lock.Lock()
|
|
||||||
defer net.lock.Unlock()
|
|
||||||
|
|
||||||
if ids == nil {
|
|
||||||
ids = net.getUpNodeIDs()
|
|
||||||
}
|
|
||||||
for _, id := range ids {
|
|
||||||
if center == id {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := net.connectNotConnected(center, id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (net *Network) connectNotConnected(oneID, otherID enode.ID) error {
|
|
||||||
return ignoreAlreadyConnectedErr(net.connect(oneID, otherID))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ignoreAlreadyConnectedErr(err error) error {
|
|
||||||
if err == nil || strings.Contains(err.Error(), "already connected") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
// Copyright 2018 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package simulations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/node"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newTestNetwork(t *testing.T, nodeCount int) (*Network, []enode.ID) {
|
|
||||||
t.Helper()
|
|
||||||
adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{
|
|
||||||
"noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) {
|
|
||||||
return NewNoopService(nil), nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// create network
|
|
||||||
network := NewNetwork(adapter, &NetworkConfig{
|
|
||||||
DefaultService: "noopwoop",
|
|
||||||
})
|
|
||||||
|
|
||||||
// create and start nodes
|
|
||||||
ids := make([]enode.ID, nodeCount)
|
|
||||||
for i := range ids {
|
|
||||||
conf := adapters.RandomNodeConfig()
|
|
||||||
node, err := network.NewNodeWithConfig(conf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error creating node: %s", err)
|
|
||||||
}
|
|
||||||
if err := network.Start(node.ID()); err != nil {
|
|
||||||
t.Fatalf("error starting node: %s", err)
|
|
||||||
}
|
|
||||||
ids[i] = node.ID()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(network.Conns) > 0 {
|
|
||||||
t.Fatal("no connections should exist after just adding nodes")
|
|
||||||
}
|
|
||||||
|
|
||||||
return network, ids
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConnectToLastNode(t *testing.T) {
|
|
||||||
net, ids := newTestNetwork(t, 10)
|
|
||||||
defer net.Shutdown()
|
|
||||||
|
|
||||||
first := ids[0]
|
|
||||||
if err := net.ConnectToLastNode(first); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
last := ids[len(ids)-1]
|
|
||||||
for i, id := range ids {
|
|
||||||
if id == first || id == last {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if net.GetConn(first, id) != nil {
|
|
||||||
t.Errorf("connection must not exist with node(ind: %v, id: %v)", i, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if net.GetConn(first, last) == nil {
|
|
||||||
t.Error("first and last node must be connected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConnectToRandomNode(t *testing.T) {
|
|
||||||
net, ids := newTestNetwork(t, 10)
|
|
||||||
defer net.Shutdown()
|
|
||||||
|
|
||||||
err := net.ConnectToRandomNode(ids[0])
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var cc int
|
|
||||||
for i, a := range ids {
|
|
||||||
for _, b := range ids[i:] {
|
|
||||||
if net.GetConn(a, b) != nil {
|
|
||||||
cc++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cc != 1 {
|
|
||||||
t.Errorf("expected one connection, got %v", cc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConnectNodesFull(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
nodeCount int
|
|
||||||
}{
|
|
||||||
{name: "no node", nodeCount: 0},
|
|
||||||
{name: "single node", nodeCount: 1},
|
|
||||||
{name: "2 nodes", nodeCount: 2},
|
|
||||||
{name: "3 nodes", nodeCount: 3},
|
|
||||||
{name: "even number of nodes", nodeCount: 12},
|
|
||||||
{name: "odd number of nodes", nodeCount: 13},
|
|
||||||
}
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
net, ids := newTestNetwork(t, test.nodeCount)
|
|
||||||
defer net.Shutdown()
|
|
||||||
|
|
||||||
err := net.ConnectNodesFull(ids)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
VerifyFull(t, net, ids)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConnectNodesChain(t *testing.T) {
|
|
||||||
net, ids := newTestNetwork(t, 10)
|
|
||||||
defer net.Shutdown()
|
|
||||||
|
|
||||||
err := net.ConnectNodesChain(ids)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
VerifyChain(t, net, ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConnectNodesRing(t *testing.T) {
|
|
||||||
net, ids := newTestNetwork(t, 10)
|
|
||||||
defer net.Shutdown()
|
|
||||||
|
|
||||||
err := net.ConnectNodesRing(ids)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
VerifyRing(t, net, ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConnectNodesStar(t *testing.T) {
|
|
||||||
net, ids := newTestNetwork(t, 10)
|
|
||||||
defer net.Shutdown()
|
|
||||||
|
|
||||||
pivotIndex := 2
|
|
||||||
|
|
||||||
err := net.ConnectNodesStar(ids, ids[pivotIndex])
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
VerifyStar(t, net, ids, pivotIndex)
|
|
||||||
}
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package simulations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EventType is the type of event emitted by a simulation network
|
|
||||||
type EventType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// EventTypeNode is the type of event emitted when a node is either
|
|
||||||
// created, started or stopped
|
|
||||||
EventTypeNode EventType = "node"
|
|
||||||
|
|
||||||
// EventTypeConn is the type of event emitted when a connection is
|
|
||||||
// is either established or dropped between two nodes
|
|
||||||
EventTypeConn EventType = "conn"
|
|
||||||
|
|
||||||
// EventTypeMsg is the type of event emitted when a p2p message it
|
|
||||||
// sent between two nodes
|
|
||||||
EventTypeMsg EventType = "msg"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Event is an event emitted by a simulation network
|
|
||||||
type Event struct {
|
|
||||||
// Type is the type of the event
|
|
||||||
Type EventType `json:"type"`
|
|
||||||
|
|
||||||
// Time is the time the event happened
|
|
||||||
Time time.Time `json:"time"`
|
|
||||||
|
|
||||||
// Control indicates whether the event is the result of a controlled
|
|
||||||
// action in the network
|
|
||||||
Control bool `json:"control"`
|
|
||||||
|
|
||||||
// Node is set if the type is EventTypeNode
|
|
||||||
Node *Node `json:"node,omitempty"`
|
|
||||||
|
|
||||||
// Conn is set if the type is EventTypeConn
|
|
||||||
Conn *Conn `json:"conn,omitempty"`
|
|
||||||
|
|
||||||
// Msg is set if the type is EventTypeMsg
|
|
||||||
Msg *Msg `json:"msg,omitempty"`
|
|
||||||
|
|
||||||
//Optionally provide data (currently for simulation frontends only)
|
|
||||||
Data interface{} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEvent creates a new event for the given object which should be either a
|
|
||||||
// Node, Conn or Msg.
|
|
||||||
//
|
|
||||||
// The object is copied so that the event represents the state of the object
|
|
||||||
// when NewEvent is called.
|
|
||||||
func NewEvent(v interface{}) *Event {
|
|
||||||
event := &Event{Time: time.Now()}
|
|
||||||
switch v := v.(type) {
|
|
||||||
case *Node:
|
|
||||||
event.Type = EventTypeNode
|
|
||||||
event.Node = v.copy()
|
|
||||||
case *Conn:
|
|
||||||
event.Type = EventTypeConn
|
|
||||||
conn := *v
|
|
||||||
event.Conn = &conn
|
|
||||||
case *Msg:
|
|
||||||
event.Type = EventTypeMsg
|
|
||||||
msg := *v
|
|
||||||
event.Msg = &msg
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("invalid event type: %T", v))
|
|
||||||
}
|
|
||||||
return event
|
|
||||||
}
|
|
||||||
|
|
||||||
// ControlEvent creates a new control event
|
|
||||||
func ControlEvent(v interface{}) *Event {
|
|
||||||
event := NewEvent(v)
|
|
||||||
event.Control = true
|
|
||||||
return event
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the string representation of the event
|
|
||||||
func (e *Event) String() string {
|
|
||||||
switch e.Type {
|
|
||||||
case EventTypeNode:
|
|
||||||
return fmt.Sprintf("<node-event> id: %s up: %t", e.Node.ID().TerminalString(), e.Node.Up())
|
|
||||||
case EventTypeConn:
|
|
||||||
return fmt.Sprintf("<conn-event> nodes: %s->%s up: %t", e.Conn.One.TerminalString(), e.Conn.Other.TerminalString(), e.Conn.Up)
|
|
||||||
case EventTypeMsg:
|
|
||||||
return fmt.Sprintf("<msg-event> nodes: %s->%s proto: %s, code: %d, received: %t", e.Msg.One.TerminalString(), e.Msg.Other.TerminalString(), e.Msg.Protocol, e.Msg.Code, e.Msg.Received)
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
# devp2p simulation examples
|
|
||||||
|
|
||||||
## ping-pong
|
|
||||||
|
|
||||||
`ping-pong.go` implements a simulation network which contains nodes running a
|
|
||||||
simple "ping-pong" protocol where nodes send a ping message to all their
|
|
||||||
connected peers every 10s and receive pong messages in return.
|
|
||||||
|
|
||||||
To run the simulation, run `go run ping-pong.go` in one terminal to start the
|
|
||||||
simulation API and `./ping-pong.sh` in another to start and connect the nodes:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ go run ping-pong.go
|
|
||||||
INFO [08-15|13:53:49] using sim adapter
|
|
||||||
INFO [08-15|13:53:49] starting simulation server on 0.0.0.0:8888...
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
$ ./ping-pong.sh
|
|
||||||
---> 13:58:12 creating 10 nodes
|
|
||||||
Created node01
|
|
||||||
Started node01
|
|
||||||
...
|
|
||||||
Created node10
|
|
||||||
Started node10
|
|
||||||
---> 13:58:13 connecting node01 to all other nodes
|
|
||||||
Connected node01 to node02
|
|
||||||
...
|
|
||||||
Connected node01 to node10
|
|
||||||
---> 13:58:14 done
|
|
||||||
```
|
|
||||||
|
|
||||||
Use the `--adapter` flag to choose the adapter type:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ go run ping-pong.go --adapter exec
|
|
||||||
INFO [08-15|14:01:14] using exec adapter tmpdir=/var/folders/k6/wpsgfg4n23ddbc6f5cnw5qg00000gn/T/p2p-example992833779
|
|
||||||
INFO [08-15|14:01:14] starting simulation server on 0.0.0.0:8888...
|
|
||||||
```
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/node"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
|
||||||
)
|
|
||||||
|
|
||||||
var adapterType = flag.String("adapter", "sim", `node adapter to use (one of "sim", "exec" or "docker")`)
|
|
||||||
|
|
||||||
// main() starts a simulation network which contains nodes running a simple
|
|
||||||
// ping-pong protocol
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
// set the log level to Trace
|
|
||||||
log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelTrace, false)))
|
|
||||||
|
|
||||||
// register a single ping-pong service
|
|
||||||
services := map[string]adapters.LifecycleConstructor{
|
|
||||||
"ping-pong": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) {
|
|
||||||
pps := newPingPongService(ctx.Config.ID)
|
|
||||||
stack.RegisterProtocols(pps.Protocols())
|
|
||||||
return pps, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
adapters.RegisterLifecycles(services)
|
|
||||||
|
|
||||||
// create the NodeAdapter
|
|
||||||
var adapter adapters.NodeAdapter
|
|
||||||
|
|
||||||
switch *adapterType {
|
|
||||||
|
|
||||||
case "sim":
|
|
||||||
log.Info("using sim adapter")
|
|
||||||
adapter = adapters.NewSimAdapter(services)
|
|
||||||
|
|
||||||
case "exec":
|
|
||||||
tmpdir, err := os.MkdirTemp("", "p2p-example")
|
|
||||||
if err != nil {
|
|
||||||
log.Crit("error creating temp dir", "err", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tmpdir)
|
|
||||||
log.Info("using exec adapter", "tmpdir", tmpdir)
|
|
||||||
adapter = adapters.NewExecAdapter(tmpdir)
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Crit(fmt.Sprintf("unknown node adapter %q", *adapterType))
|
|
||||||
}
|
|
||||||
|
|
||||||
// start the HTTP API
|
|
||||||
log.Info("starting simulation server on 0.0.0.0:8888...")
|
|
||||||
network := simulations.NewNetwork(adapter, &simulations.NetworkConfig{
|
|
||||||
DefaultService: "ping-pong",
|
|
||||||
})
|
|
||||||
if err := http.ListenAndServe(":8888", simulations.NewServer(network)); err != nil {
|
|
||||||
log.Crit("error starting simulation server", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pingPongService runs a ping-pong protocol between nodes where each node
|
|
||||||
// sends a ping to all its connected peers every 10s and receives a pong in
|
|
||||||
// return
|
|
||||||
type pingPongService struct {
|
|
||||||
id enode.ID
|
|
||||||
log log.Logger
|
|
||||||
received atomic.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPingPongService(id enode.ID) *pingPongService {
|
|
||||||
return &pingPongService{
|
|
||||||
id: id,
|
|
||||||
log: log.New("node.id", id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pingPongService) Protocols() []p2p.Protocol {
|
|
||||||
return []p2p.Protocol{{
|
|
||||||
Name: "ping-pong",
|
|
||||||
Version: 1,
|
|
||||||
Length: 2,
|
|
||||||
Run: p.Run,
|
|
||||||
NodeInfo: p.Info,
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pingPongService) Start() error {
|
|
||||||
p.log.Info("ping-pong service starting")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pingPongService) Stop() error {
|
|
||||||
p.log.Info("ping-pong service stopping")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pingPongService) Info() interface{} {
|
|
||||||
return struct {
|
|
||||||
Received int64 `json:"received"`
|
|
||||||
}{
|
|
||||||
p.received.Load(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
pingMsgCode = iota
|
|
||||||
pongMsgCode
|
|
||||||
)
|
|
||||||
|
|
||||||
// Run implements the ping-pong protocol which sends ping messages to the peer
|
|
||||||
// at 10s intervals, and responds to pings with pong messages.
|
|
||||||
func (p *pingPongService) Run(peer *p2p.Peer, rw p2p.MsgReadWriter) error {
|
|
||||||
log := p.log.New("peer.id", peer.ID())
|
|
||||||
|
|
||||||
errC := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
for range time.Tick(10 * time.Second) {
|
|
||||||
log.Info("sending ping")
|
|
||||||
if err := p2p.Send(rw, pingMsgCode, "PING"); err != nil {
|
|
||||||
errC <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
msg, err := rw.ReadMsg()
|
|
||||||
if err != nil {
|
|
||||||
errC <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
payload, err := io.ReadAll(msg.Payload)
|
|
||||||
if err != nil {
|
|
||||||
errC <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Info("received message", "msg.code", msg.Code, "msg.payload", string(payload))
|
|
||||||
p.received.Add(1)
|
|
||||||
if msg.Code == pingMsgCode {
|
|
||||||
log.Info("sending pong")
|
|
||||||
go p2p.Send(rw, pongMsgCode, "PONG")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return <-errC
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# Boot a ping-pong network simulation using the HTTP API started by ping-pong.go
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
main() {
|
|
||||||
if ! which p2psim &>/dev/null; then
|
|
||||||
fail "missing p2psim binary (you need to build cmd/p2psim and put it in \$PATH)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
info "creating 10 nodes"
|
|
||||||
for i in $(seq 1 10); do
|
|
||||||
p2psim node create --name "$(node_name $i)"
|
|
||||||
p2psim node start "$(node_name $i)"
|
|
||||||
done
|
|
||||||
|
|
||||||
info "connecting node01 to all other nodes"
|
|
||||||
for i in $(seq 2 10); do
|
|
||||||
p2psim node connect "node01" "$(node_name $i)"
|
|
||||||
done
|
|
||||||
|
|
||||||
info "done"
|
|
||||||
}
|
|
||||||
|
|
||||||
node_name() {
|
|
||||||
local num=$1
|
|
||||||
echo "node$(printf '%02d' $num)"
|
|
||||||
}
|
|
||||||
|
|
||||||
info() {
|
|
||||||
echo -e "\033[1;32m---> $(date +%H:%M:%S) ${@}\033[0m"
|
|
||||||
}
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
echo -e "\033[1;31mERROR: ${@}\033[0m" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
|
|
@ -1,743 +0,0 @@
|
||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package simulations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"html"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/event"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DefaultClient is the default simulation API client which expects the API
|
|
||||||
// to be running at http://localhost:8888
|
|
||||||
var DefaultClient = NewClient("http://localhost:8888")
|
|
||||||
|
|
||||||
// Client is a client for the simulation HTTP API which supports creating
|
|
||||||
// and managing simulation networks
|
|
||||||
type Client struct {
|
|
||||||
URL string
|
|
||||||
|
|
||||||
client *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient returns a new simulation API client
|
|
||||||
func NewClient(url string) *Client {
|
|
||||||
return &Client{
|
|
||||||
URL: url,
|
|
||||||
client: http.DefaultClient,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNetwork returns details of the network
|
|
||||||
func (c *Client) GetNetwork() (*Network, error) {
|
|
||||||
network := &Network{}
|
|
||||||
return network, c.Get("/", network)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartNetwork starts all existing nodes in the simulation network
|
|
||||||
func (c *Client) StartNetwork() error {
|
|
||||||
return c.Post("/start", nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopNetwork stops all existing nodes in a simulation network
|
|
||||||
func (c *Client) StopNetwork() error {
|
|
||||||
return c.Post("/stop", nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateSnapshot creates a network snapshot
|
|
||||||
func (c *Client) CreateSnapshot() (*Snapshot, error) {
|
|
||||||
snap := &Snapshot{}
|
|
||||||
return snap, c.Get("/snapshot", snap)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadSnapshot loads a snapshot into the network
|
|
||||||
func (c *Client) LoadSnapshot(snap *Snapshot) error {
|
|
||||||
return c.Post("/snapshot", snap, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeOpts is a collection of options to use when subscribing to network
|
|
||||||
// events
|
|
||||||
type SubscribeOpts struct {
|
|
||||||
// Current instructs the server to send events for existing nodes and
|
|
||||||
// connections first
|
|
||||||
Current bool
|
|
||||||
|
|
||||||
// Filter instructs the server to only send a subset of message events
|
|
||||||
Filter string
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeNetwork subscribes to network events which are sent from the server
|
|
||||||
// as a server-sent-events stream, optionally receiving events for existing
|
|
||||||
// nodes and connections and filtering message events
|
|
||||||
func (c *Client) SubscribeNetwork(events chan *Event, opts SubscribeOpts) (event.Subscription, error) {
|
|
||||||
url := fmt.Sprintf("%s/events?current=%t&filter=%s", c.URL, opts.Current, opts.Filter)
|
|
||||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Set("Accept", "text/event-stream")
|
|
||||||
res, err := c.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
response, _ := io.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
return nil, fmt.Errorf("unexpected HTTP status: %s: %s", res.Status, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// define a producer function to pass to event.Subscription
|
|
||||||
// which reads server-sent events from res.Body and sends
|
|
||||||
// them to the events channel
|
|
||||||
producer := func(stop <-chan struct{}) error {
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
// read lines from res.Body in a goroutine so that we are
|
|
||||||
// always reading from the stop channel
|
|
||||||
lines := make(chan string)
|
|
||||||
errC := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
s := bufio.NewScanner(res.Body)
|
|
||||||
for s.Scan() {
|
|
||||||
select {
|
|
||||||
case lines <- s.Text():
|
|
||||||
case <-stop:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
errC <- s.Err()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// detect any lines which start with "data:", decode the data
|
|
||||||
// into an event and send it to the events channel
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case line := <-lines:
|
|
||||||
if !strings.HasPrefix(line, "data:") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
|
|
||||||
event := &Event{}
|
|
||||||
if err := json.Unmarshal([]byte(data), event); err != nil {
|
|
||||||
return fmt.Errorf("error decoding SSE event: %s", err)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case events <- event:
|
|
||||||
case <-stop:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
case err := <-errC:
|
|
||||||
return err
|
|
||||||
case <-stop:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return event.NewSubscription(producer), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNodes returns all nodes which exist in the network
|
|
||||||
func (c *Client) GetNodes() ([]*p2p.NodeInfo, error) {
|
|
||||||
var nodes []*p2p.NodeInfo
|
|
||||||
return nodes, c.Get("/nodes", &nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateNode creates a node in the network using the given configuration
|
|
||||||
func (c *Client) CreateNode(config *adapters.NodeConfig) (*p2p.NodeInfo, error) {
|
|
||||||
node := &p2p.NodeInfo{}
|
|
||||||
return node, c.Post("/nodes", config, node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNode returns details of a node
|
|
||||||
func (c *Client) GetNode(nodeID string) (*p2p.NodeInfo, error) {
|
|
||||||
node := &p2p.NodeInfo{}
|
|
||||||
return node, c.Get(fmt.Sprintf("/nodes/%s", nodeID), node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartNode starts a node
|
|
||||||
func (c *Client) StartNode(nodeID string) error {
|
|
||||||
return c.Post(fmt.Sprintf("/nodes/%s/start", nodeID), nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopNode stops a node
|
|
||||||
func (c *Client) StopNode(nodeID string) error {
|
|
||||||
return c.Post(fmt.Sprintf("/nodes/%s/stop", nodeID), nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConnectNode connects a node to a peer node
|
|
||||||
func (c *Client) ConnectNode(nodeID, peerID string) error {
|
|
||||||
return c.Post(fmt.Sprintf("/nodes/%s/conn/%s", nodeID, peerID), nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DisconnectNode disconnects a node from a peer node
|
|
||||||
func (c *Client) DisconnectNode(nodeID, peerID string) error {
|
|
||||||
return c.Delete(fmt.Sprintf("/nodes/%s/conn/%s", nodeID, peerID))
|
|
||||||
}
|
|
||||||
|
|
||||||
// RPCClient returns an RPC client connected to a node
|
|
||||||
func (c *Client) RPCClient(ctx context.Context, nodeID string) (*rpc.Client, error) {
|
|
||||||
baseURL := strings.Replace(c.URL, "http", "ws", 1)
|
|
||||||
return rpc.DialWebsocket(ctx, fmt.Sprintf("%s/nodes/%s/rpc", baseURL, nodeID), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get performs a HTTP GET request decoding the resulting JSON response
|
|
||||||
// into "out"
|
|
||||||
func (c *Client) Get(path string, out interface{}) error {
|
|
||||||
return c.Send(http.MethodGet, path, nil, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post performs a HTTP POST request sending "in" as the JSON body and
|
|
||||||
// decoding the resulting JSON response into "out"
|
|
||||||
func (c *Client) Post(path string, in, out interface{}) error {
|
|
||||||
return c.Send(http.MethodPost, path, in, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete performs a HTTP DELETE request
|
|
||||||
func (c *Client) Delete(path string) error {
|
|
||||||
return c.Send(http.MethodDelete, path, nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send performs a HTTP request, sending "in" as the JSON request body and
|
|
||||||
// decoding the JSON response into "out"
|
|
||||||
func (c *Client) Send(method, path string, in, out interface{}) error {
|
|
||||||
var body []byte
|
|
||||||
if in != nil {
|
|
||||||
var err error
|
|
||||||
body, err = json.Marshal(in)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest(method, c.URL+path, bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
res, err := c.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusCreated {
|
|
||||||
response, _ := io.ReadAll(res.Body)
|
|
||||||
return fmt.Errorf("unexpected HTTP status: %s: %s", res.Status, response)
|
|
||||||
}
|
|
||||||
if out != nil {
|
|
||||||
if err := json.NewDecoder(res.Body).Decode(out); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server is an HTTP server providing an API to manage a simulation network
|
|
||||||
type Server struct {
|
|
||||||
router *httprouter.Router
|
|
||||||
network *Network
|
|
||||||
mockerStop chan struct{} // when set, stops the current mocker
|
|
||||||
mockerMtx sync.Mutex // synchronises access to the mockerStop field
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewServer returns a new simulation API server
|
|
||||||
func NewServer(network *Network) *Server {
|
|
||||||
s := &Server{
|
|
||||||
router: httprouter.New(),
|
|
||||||
network: network,
|
|
||||||
}
|
|
||||||
|
|
||||||
s.OPTIONS("/", s.Options)
|
|
||||||
s.GET("/", s.GetNetwork)
|
|
||||||
s.POST("/start", s.StartNetwork)
|
|
||||||
s.POST("/stop", s.StopNetwork)
|
|
||||||
s.POST("/mocker/start", s.StartMocker)
|
|
||||||
s.POST("/mocker/stop", s.StopMocker)
|
|
||||||
s.GET("/mocker", s.GetMockers)
|
|
||||||
s.POST("/reset", s.ResetNetwork)
|
|
||||||
s.GET("/events", s.StreamNetworkEvents)
|
|
||||||
s.GET("/snapshot", s.CreateSnapshot)
|
|
||||||
s.POST("/snapshot", s.LoadSnapshot)
|
|
||||||
s.POST("/nodes", s.CreateNode)
|
|
||||||
s.GET("/nodes", s.GetNodes)
|
|
||||||
s.GET("/nodes/:nodeid", s.GetNode)
|
|
||||||
s.POST("/nodes/:nodeid/start", s.StartNode)
|
|
||||||
s.POST("/nodes/:nodeid/stop", s.StopNode)
|
|
||||||
s.POST("/nodes/:nodeid/conn/:peerid", s.ConnectNode)
|
|
||||||
s.DELETE("/nodes/:nodeid/conn/:peerid", s.DisconnectNode)
|
|
||||||
s.GET("/nodes/:nodeid/rpc", s.NodeRPC)
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNetwork returns details of the network
|
|
||||||
func (s *Server) GetNetwork(w http.ResponseWriter, req *http.Request) {
|
|
||||||
s.JSON(w, http.StatusOK, s.network)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartNetwork starts all nodes in the network
|
|
||||||
func (s *Server) StartNetwork(w http.ResponseWriter, req *http.Request) {
|
|
||||||
if err := s.network.StartAll(); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopNetwork stops all nodes in the network
|
|
||||||
func (s *Server) StopNetwork(w http.ResponseWriter, req *http.Request) {
|
|
||||||
if err := s.network.StopAll(); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartMocker starts the mocker node simulation
|
|
||||||
func (s *Server) StartMocker(w http.ResponseWriter, req *http.Request) {
|
|
||||||
s.mockerMtx.Lock()
|
|
||||||
defer s.mockerMtx.Unlock()
|
|
||||||
if s.mockerStop != nil {
|
|
||||||
http.Error(w, "mocker already running", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mockerType := req.FormValue("mocker-type")
|
|
||||||
mockerFn := LookupMocker(mockerType)
|
|
||||||
if mockerFn == nil {
|
|
||||||
http.Error(w, fmt.Sprintf("unknown mocker type %q", html.EscapeString(mockerType)), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
nodeCount, err := strconv.Atoi(req.FormValue("node-count"))
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "invalid node-count provided", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.mockerStop = make(chan struct{})
|
|
||||||
go mockerFn(s.network, s.mockerStop, nodeCount)
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopMocker stops the mocker node simulation
|
|
||||||
func (s *Server) StopMocker(w http.ResponseWriter, req *http.Request) {
|
|
||||||
s.mockerMtx.Lock()
|
|
||||||
defer s.mockerMtx.Unlock()
|
|
||||||
if s.mockerStop == nil {
|
|
||||||
http.Error(w, "stop channel not initialized", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
close(s.mockerStop)
|
|
||||||
s.mockerStop = nil
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMockers returns a list of available mockers
|
|
||||||
func (s *Server) GetMockers(w http.ResponseWriter, req *http.Request) {
|
|
||||||
list := GetMockerList()
|
|
||||||
s.JSON(w, http.StatusOK, list)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetNetwork resets all properties of a network to its initial (empty) state
|
|
||||||
func (s *Server) ResetNetwork(w http.ResponseWriter, req *http.Request) {
|
|
||||||
s.network.Reset()
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StreamNetworkEvents streams network events as a server-sent-events stream
|
|
||||||
func (s *Server) StreamNetworkEvents(w http.ResponseWriter, req *http.Request) {
|
|
||||||
events := make(chan *Event)
|
|
||||||
sub := s.network.events.Subscribe(events)
|
|
||||||
defer sub.Unsubscribe()
|
|
||||||
|
|
||||||
// write writes the given event and data to the stream like:
|
|
||||||
//
|
|
||||||
// event: <event>
|
|
||||||
// data: <data>
|
|
||||||
//
|
|
||||||
write := func(event, data string) {
|
|
||||||
fmt.Fprintf(w, "event: %s\n", event)
|
|
||||||
fmt.Fprintf(w, "data: %s\n\n", data)
|
|
||||||
if fw, ok := w.(http.Flusher); ok {
|
|
||||||
fw.Flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writeEvent := func(event *Event) error {
|
|
||||||
data, err := json.Marshal(event)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
write("network", string(data))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
writeErr := func(err error) {
|
|
||||||
write("error", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if filtering has been requested
|
|
||||||
var filters MsgFilters
|
|
||||||
if filterParam := req.URL.Query().Get("filter"); filterParam != "" {
|
|
||||||
var err error
|
|
||||||
filters, err = NewMsgFilters(filterParam)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
fmt.Fprintf(w, "\n\n")
|
|
||||||
if fw, ok := w.(http.Flusher); ok {
|
|
||||||
fw.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
// optionally send the existing nodes and connections
|
|
||||||
if req.URL.Query().Get("current") == "true" {
|
|
||||||
snap, err := s.network.Snapshot()
|
|
||||||
if err != nil {
|
|
||||||
writeErr(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, node := range snap.Nodes {
|
|
||||||
event := NewEvent(&node.Node)
|
|
||||||
if err := writeEvent(event); err != nil {
|
|
||||||
writeErr(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, conn := range snap.Conns {
|
|
||||||
conn := conn
|
|
||||||
event := NewEvent(&conn)
|
|
||||||
if err := writeEvent(event); err != nil {
|
|
||||||
writeErr(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clientGone := req.Context().Done()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event := <-events:
|
|
||||||
// only send message events which match the filters
|
|
||||||
if event.Msg != nil && !filters.Match(event.Msg) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := writeEvent(event); err != nil {
|
|
||||||
writeErr(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-clientGone:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMsgFilters constructs a collection of message filters from a URL query
|
|
||||||
// parameter.
|
|
||||||
//
|
|
||||||
// The parameter is expected to be a dash-separated list of individual filters,
|
|
||||||
// each having the format '<proto>:<codes>', where <proto> is the name of a
|
|
||||||
// protocol and <codes> is a comma-separated list of message codes.
|
|
||||||
//
|
|
||||||
// A message code of '*' or '-1' is considered a wildcard and matches any code.
|
|
||||||
func NewMsgFilters(filterParam string) (MsgFilters, error) {
|
|
||||||
filters := make(MsgFilters)
|
|
||||||
for _, filter := range strings.Split(filterParam, "-") {
|
|
||||||
proto, codes, found := strings.Cut(filter, ":")
|
|
||||||
if !found || proto == "" || codes == "" {
|
|
||||||
return nil, fmt.Errorf("invalid message filter: %s", filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, code := range strings.Split(codes, ",") {
|
|
||||||
if code == "*" || code == "-1" {
|
|
||||||
filters[MsgFilter{Proto: proto, Code: -1}] = struct{}{}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
n, err := strconv.ParseUint(code, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid message code: %s", code)
|
|
||||||
}
|
|
||||||
filters[MsgFilter{Proto: proto, Code: int64(n)}] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filters, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MsgFilters is a collection of filters which are used to filter message
|
|
||||||
// events
|
|
||||||
type MsgFilters map[MsgFilter]struct{}
|
|
||||||
|
|
||||||
// Match checks if the given message matches any of the filters
|
|
||||||
func (m MsgFilters) Match(msg *Msg) bool {
|
|
||||||
// check if there is a wildcard filter for the message's protocol
|
|
||||||
if _, ok := m[MsgFilter{Proto: msg.Protocol, Code: -1}]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if there is a filter for the message's protocol and code
|
|
||||||
if _, ok := m[MsgFilter{Proto: msg.Protocol, Code: int64(msg.Code)}]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// MsgFilter is used to filter message events based on protocol and message
|
|
||||||
// code
|
|
||||||
type MsgFilter struct {
|
|
||||||
// Proto is matched against a message's protocol
|
|
||||||
Proto string
|
|
||||||
|
|
||||||
// Code is matched against a message's code, with -1 matching all codes
|
|
||||||
Code int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateSnapshot creates a network snapshot
|
|
||||||
func (s *Server) CreateSnapshot(w http.ResponseWriter, req *http.Request) {
|
|
||||||
snap, err := s.network.Snapshot()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.JSON(w, http.StatusOK, snap)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadSnapshot loads a snapshot into the network
|
|
||||||
func (s *Server) LoadSnapshot(w http.ResponseWriter, req *http.Request) {
|
|
||||||
snap := &Snapshot{}
|
|
||||||
if err := json.NewDecoder(req.Body).Decode(snap); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.network.Load(snap); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.JSON(w, http.StatusOK, s.network)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateNode creates a node in the network using the given configuration
|
|
||||||
func (s *Server) CreateNode(w http.ResponseWriter, req *http.Request) {
|
|
||||||
config := &adapters.NodeConfig{}
|
|
||||||
|
|
||||||
err := json.NewDecoder(req.Body).Decode(config)
|
|
||||||
if err != nil && !errors.Is(err, io.EOF) {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
node, err := s.network.NewNodeWithConfig(config)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.JSON(w, http.StatusCreated, node.NodeInfo())
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNodes returns all nodes which exist in the network
|
|
||||||
func (s *Server) GetNodes(w http.ResponseWriter, req *http.Request) {
|
|
||||||
nodes := s.network.GetNodes()
|
|
||||||
|
|
||||||
infos := make([]*p2p.NodeInfo, len(nodes))
|
|
||||||
for i, node := range nodes {
|
|
||||||
infos[i] = node.NodeInfo()
|
|
||||||
}
|
|
||||||
|
|
||||||
s.JSON(w, http.StatusOK, infos)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNode returns details of a node
|
|
||||||
func (s *Server) GetNode(w http.ResponseWriter, req *http.Request) {
|
|
||||||
node := req.Context().Value("node").(*Node)
|
|
||||||
|
|
||||||
s.JSON(w, http.StatusOK, node.NodeInfo())
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartNode starts a node
|
|
||||||
func (s *Server) StartNode(w http.ResponseWriter, req *http.Request) {
|
|
||||||
node := req.Context().Value("node").(*Node)
|
|
||||||
|
|
||||||
if err := s.network.Start(node.ID()); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.JSON(w, http.StatusOK, node.NodeInfo())
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopNode stops a node
|
|
||||||
func (s *Server) StopNode(w http.ResponseWriter, req *http.Request) {
|
|
||||||
node := req.Context().Value("node").(*Node)
|
|
||||||
|
|
||||||
if err := s.network.Stop(node.ID()); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.JSON(w, http.StatusOK, node.NodeInfo())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConnectNode connects a node to a peer node
|
|
||||||
func (s *Server) ConnectNode(w http.ResponseWriter, req *http.Request) {
|
|
||||||
node := req.Context().Value("node").(*Node)
|
|
||||||
peer := req.Context().Value("peer").(*Node)
|
|
||||||
|
|
||||||
if err := s.network.Connect(node.ID(), peer.ID()); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.JSON(w, http.StatusOK, node.NodeInfo())
|
|
||||||
}
|
|
||||||
|
|
||||||
// DisconnectNode disconnects a node from a peer node
|
|
||||||
func (s *Server) DisconnectNode(w http.ResponseWriter, req *http.Request) {
|
|
||||||
node := req.Context().Value("node").(*Node)
|
|
||||||
peer := req.Context().Value("peer").(*Node)
|
|
||||||
|
|
||||||
if err := s.network.Disconnect(node.ID(), peer.ID()); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.JSON(w, http.StatusOK, node.NodeInfo())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Options responds to the OPTIONS HTTP method by returning a 200 OK response
|
|
||||||
// with the "Access-Control-Allow-Headers" header set to "Content-Type"
|
|
||||||
func (s *Server) Options(w http.ResponseWriter, req *http.Request) {
|
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
var wsUpgrade = websocket.Upgrader{
|
|
||||||
CheckOrigin: func(*http.Request) bool { return true },
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeRPC forwards RPC requests to a node in the network via a WebSocket
|
|
||||||
// connection
|
|
||||||
func (s *Server) NodeRPC(w http.ResponseWriter, req *http.Request) {
|
|
||||||
conn, err := wsUpgrade.Upgrade(w, req, nil)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
node := req.Context().Value("node").(*Node)
|
|
||||||
node.ServeRPC(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeHTTP implements the http.Handler interface by delegating to the
|
|
||||||
// underlying httprouter.Router
|
|
||||||
func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
||||||
s.router.ServeHTTP(w, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET registers a handler for GET requests to a particular path
|
|
||||||
func (s *Server) GET(path string, handle http.HandlerFunc) {
|
|
||||||
s.router.GET(path, s.wrapHandler(handle))
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST registers a handler for POST requests to a particular path
|
|
||||||
func (s *Server) POST(path string, handle http.HandlerFunc) {
|
|
||||||
s.router.POST(path, s.wrapHandler(handle))
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE registers a handler for DELETE requests to a particular path
|
|
||||||
func (s *Server) DELETE(path string, handle http.HandlerFunc) {
|
|
||||||
s.router.DELETE(path, s.wrapHandler(handle))
|
|
||||||
}
|
|
||||||
|
|
||||||
// OPTIONS registers a handler for OPTIONS requests to a particular path
|
|
||||||
func (s *Server) OPTIONS(path string, handle http.HandlerFunc) {
|
|
||||||
s.router.OPTIONS("/*path", s.wrapHandler(handle))
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON sends "data" as a JSON HTTP response
|
|
||||||
func (s *Server) JSON(w http.ResponseWriter, status int, data interface{}) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(status)
|
|
||||||
json.NewEncoder(w).Encode(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// wrapHandler returns an httprouter.Handle which wraps an http.HandlerFunc by
|
|
||||||
// populating request.Context with any objects from the URL params
|
|
||||||
func (s *Server) wrapHandler(handler http.HandlerFunc) httprouter.Handle {
|
|
||||||
return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
||||||
|
|
||||||
ctx := req.Context()
|
|
||||||
|
|
||||||
if id := params.ByName("nodeid"); id != "" {
|
|
||||||
var nodeID enode.ID
|
|
||||||
var node *Node
|
|
||||||
if nodeID.UnmarshalText([]byte(id)) == nil {
|
|
||||||
node = s.network.GetNode(nodeID)
|
|
||||||
} else {
|
|
||||||
node = s.network.GetNodeByName(id)
|
|
||||||
}
|
|
||||||
if node == nil {
|
|
||||||
http.NotFound(w, req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx = context.WithValue(ctx, "node", node)
|
|
||||||
}
|
|
||||||
|
|
||||||
if id := params.ByName("peerid"); id != "" {
|
|
||||||
var peerID enode.ID
|
|
||||||
var peer *Node
|
|
||||||
if peerID.UnmarshalText([]byte(id)) == nil {
|
|
||||||
peer = s.network.GetNode(peerID)
|
|
||||||
} else {
|
|
||||||
peer = s.network.GetNodeByName(id)
|
|
||||||
}
|
|
||||||
if peer == nil {
|
|
||||||
http.NotFound(w, req)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx = context.WithValue(ctx, "peer", peer)
|
|
||||||
}
|
|
||||||
|
|
||||||
handler(w, req.WithContext(ctx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,871 +0,0 @@
|
||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package simulations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/event"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/node"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
|
||||||
"github.com/mattn/go-colorable"
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
loglevel := flag.Int("loglevel", 2, "verbosity of logs")
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(colorable.NewColorableStderr(), slog.Level(*loglevel), true)))
|
|
||||||
os.Exit(m.Run())
|
|
||||||
}
|
|
||||||
|
|
||||||
// testService implements the node.Service interface and provides protocols
|
|
||||||
// and APIs which are useful for testing nodes in a simulation network
|
|
||||||
type testService struct {
|
|
||||||
id enode.ID
|
|
||||||
|
|
||||||
// peerCount is incremented once a peer handshake has been performed
|
|
||||||
peerCount int64
|
|
||||||
|
|
||||||
peers map[enode.ID]*testPeer
|
|
||||||
peersMtx sync.Mutex
|
|
||||||
|
|
||||||
// state stores []byte which is used to test creating and loading
|
|
||||||
// snapshots
|
|
||||||
state atomic.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestService(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) {
|
|
||||||
svc := &testService{
|
|
||||||
id: ctx.Config.ID,
|
|
||||||
peers: make(map[enode.ID]*testPeer),
|
|
||||||
}
|
|
||||||
svc.state.Store(ctx.Snapshot)
|
|
||||||
|
|
||||||
stack.RegisterProtocols(svc.Protocols())
|
|
||||||
stack.RegisterAPIs(svc.APIs())
|
|
||||||
return svc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type testPeer struct {
|
|
||||||
testReady chan struct{}
|
|
||||||
dumReady chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testService) peer(id enode.ID) *testPeer {
|
|
||||||
t.peersMtx.Lock()
|
|
||||||
defer t.peersMtx.Unlock()
|
|
||||||
if peer, ok := t.peers[id]; ok {
|
|
||||||
return peer
|
|
||||||
}
|
|
||||||
peer := &testPeer{
|
|
||||||
testReady: make(chan struct{}),
|
|
||||||
dumReady: make(chan struct{}),
|
|
||||||
}
|
|
||||||
t.peers[id] = peer
|
|
||||||
return peer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testService) Protocols() []p2p.Protocol {
|
|
||||||
return []p2p.Protocol{
|
|
||||||
{
|
|
||||||
Name: "test",
|
|
||||||
Version: 1,
|
|
||||||
Length: 3,
|
|
||||||
Run: t.RunTest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "dum",
|
|
||||||
Version: 1,
|
|
||||||
Length: 1,
|
|
||||||
Run: t.RunDum,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "prb",
|
|
||||||
Version: 1,
|
|
||||||
Length: 1,
|
|
||||||
Run: t.RunPrb,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testService) APIs() []rpc.API {
|
|
||||||
return []rpc.API{{
|
|
||||||
Namespace: "test",
|
|
||||||
Version: "1.0",
|
|
||||||
Service: &TestAPI{
|
|
||||||
state: &t.state,
|
|
||||||
peerCount: &t.peerCount,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testService) Start() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testService) Stop() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handshake performs a peer handshake by sending and expecting an empty
|
|
||||||
// message with the given code
|
|
||||||
func (t *testService) handshake(rw p2p.MsgReadWriter, code uint64) error {
|
|
||||||
errc := make(chan error, 2)
|
|
||||||
go func() { errc <- p2p.SendItems(rw, code) }()
|
|
||||||
go func() { errc <- p2p.ExpectMsg(rw, code, struct{}{}) }()
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
if err := <-errc; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testService) RunTest(p *p2p.Peer, rw p2p.MsgReadWriter) error {
|
|
||||||
peer := t.peer(p.ID())
|
|
||||||
|
|
||||||
// perform three handshakes with three different message codes,
|
|
||||||
// used to test message sending and filtering
|
|
||||||
if err := t.handshake(rw, 2); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := t.handshake(rw, 1); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := t.handshake(rw, 0); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// close the testReady channel so that other protocols can run
|
|
||||||
close(peer.testReady)
|
|
||||||
|
|
||||||
// track the peer
|
|
||||||
atomic.AddInt64(&t.peerCount, 1)
|
|
||||||
defer atomic.AddInt64(&t.peerCount, -1)
|
|
||||||
|
|
||||||
// block until the peer is dropped
|
|
||||||
for {
|
|
||||||
_, err := rw.ReadMsg()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testService) RunDum(p *p2p.Peer, rw p2p.MsgReadWriter) error {
|
|
||||||
peer := t.peer(p.ID())
|
|
||||||
|
|
||||||
// wait for the test protocol to perform its handshake
|
|
||||||
<-peer.testReady
|
|
||||||
|
|
||||||
// perform a handshake
|
|
||||||
if err := t.handshake(rw, 0); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// close the dumReady channel so that other protocols can run
|
|
||||||
close(peer.dumReady)
|
|
||||||
|
|
||||||
// block until the peer is dropped
|
|
||||||
for {
|
|
||||||
_, err := rw.ReadMsg()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (t *testService) RunPrb(p *p2p.Peer, rw p2p.MsgReadWriter) error {
|
|
||||||
peer := t.peer(p.ID())
|
|
||||||
|
|
||||||
// wait for the dum protocol to perform its handshake
|
|
||||||
<-peer.dumReady
|
|
||||||
|
|
||||||
// perform a handshake
|
|
||||||
if err := t.handshake(rw, 0); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// block until the peer is dropped
|
|
||||||
for {
|
|
||||||
_, err := rw.ReadMsg()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testService) Snapshot() ([]byte, error) {
|
|
||||||
return t.state.Load().([]byte), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestAPI provides a test API to:
|
|
||||||
// * get the peer count
|
|
||||||
// * get and set an arbitrary state byte slice
|
|
||||||
// * get and increment a counter
|
|
||||||
// * subscribe to counter increment events
|
|
||||||
type TestAPI struct {
|
|
||||||
state *atomic.Value
|
|
||||||
peerCount *int64
|
|
||||||
counter int64
|
|
||||||
feed event.Feed
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TestAPI) PeerCount() int64 {
|
|
||||||
return atomic.LoadInt64(t.peerCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TestAPI) Get() int64 {
|
|
||||||
return atomic.LoadInt64(&t.counter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TestAPI) Add(delta int64) {
|
|
||||||
atomic.AddInt64(&t.counter, delta)
|
|
||||||
t.feed.Send(delta)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TestAPI) GetState() []byte {
|
|
||||||
return t.state.Load().([]byte)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TestAPI) SetState(state []byte) {
|
|
||||||
t.state.Store(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TestAPI) Events(ctx context.Context) (*rpc.Subscription, error) {
|
|
||||||
notifier, supported := rpc.NotifierFromContext(ctx)
|
|
||||||
if !supported {
|
|
||||||
return nil, rpc.ErrNotificationsUnsupported
|
|
||||||
}
|
|
||||||
|
|
||||||
rpcSub := notifier.CreateSubscription()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
events := make(chan int64)
|
|
||||||
sub := t.feed.Subscribe(events)
|
|
||||||
defer sub.Unsubscribe()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event := <-events:
|
|
||||||
notifier.Notify(rpcSub.ID, event)
|
|
||||||
case <-sub.Err():
|
|
||||||
return
|
|
||||||
case <-rpcSub.Err():
|
|
||||||
return
|
|
||||||
case <-notifier.Closed():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return rpcSub, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var testServices = adapters.LifecycleConstructors{
|
|
||||||
"test": newTestService,
|
|
||||||
}
|
|
||||||
|
|
||||||
func testHTTPServer(t *testing.T) (*Network, *httptest.Server) {
|
|
||||||
t.Helper()
|
|
||||||
adapter := adapters.NewSimAdapter(testServices)
|
|
||||||
network := NewNetwork(adapter, &NetworkConfig{
|
|
||||||
DefaultService: "test",
|
|
||||||
})
|
|
||||||
return network, httptest.NewServer(NewServer(network))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHTTPNetwork tests interacting with a simulation network using the HTTP
|
|
||||||
// API
|
|
||||||
func TestHTTPNetwork(t *testing.T) {
|
|
||||||
// start the server
|
|
||||||
network, s := testHTTPServer(t)
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
// subscribe to events so we can check them later
|
|
||||||
client := NewClient(s.URL)
|
|
||||||
events := make(chan *Event, 100)
|
|
||||||
var opts SubscribeOpts
|
|
||||||
sub, err := client.SubscribeNetwork(events, opts)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error subscribing to network events: %s", err)
|
|
||||||
}
|
|
||||||
defer sub.Unsubscribe()
|
|
||||||
|
|
||||||
// check we can retrieve details about the network
|
|
||||||
gotNetwork, err := client.GetNetwork()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error getting network: %s", err)
|
|
||||||
}
|
|
||||||
if gotNetwork.ID != network.ID {
|
|
||||||
t.Fatalf("expected network to have ID %q, got %q", network.ID, gotNetwork.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// start a simulation network
|
|
||||||
nodeIDs := startTestNetwork(t, client)
|
|
||||||
|
|
||||||
// check we got all the events
|
|
||||||
x := &expectEvents{t, events, sub}
|
|
||||||
x.expect(
|
|
||||||
x.nodeEvent(nodeIDs[0], false),
|
|
||||||
x.nodeEvent(nodeIDs[1], false),
|
|
||||||
x.nodeEvent(nodeIDs[0], true),
|
|
||||||
x.nodeEvent(nodeIDs[1], true),
|
|
||||||
x.connEvent(nodeIDs[0], nodeIDs[1], false),
|
|
||||||
x.connEvent(nodeIDs[0], nodeIDs[1], true),
|
|
||||||
)
|
|
||||||
|
|
||||||
// reconnect the stream and check we get the current nodes and conns
|
|
||||||
events = make(chan *Event, 100)
|
|
||||||
opts.Current = true
|
|
||||||
sub, err = client.SubscribeNetwork(events, opts)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error subscribing to network events: %s", err)
|
|
||||||
}
|
|
||||||
defer sub.Unsubscribe()
|
|
||||||
x = &expectEvents{t, events, sub}
|
|
||||||
x.expect(
|
|
||||||
x.nodeEvent(nodeIDs[0], true),
|
|
||||||
x.nodeEvent(nodeIDs[1], true),
|
|
||||||
x.connEvent(nodeIDs[0], nodeIDs[1], true),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func startTestNetwork(t *testing.T, client *Client) []string {
|
|
||||||
// create two nodes
|
|
||||||
nodeCount := 2
|
|
||||||
nodeIDs := make([]string, nodeCount)
|
|
||||||
for i := 0; i < nodeCount; i++ {
|
|
||||||
config := adapters.RandomNodeConfig()
|
|
||||||
node, err := client.CreateNode(config)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error creating node: %s", err)
|
|
||||||
}
|
|
||||||
nodeIDs[i] = node.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// check both nodes exist
|
|
||||||
nodes, err := client.GetNodes()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error getting nodes: %s", err)
|
|
||||||
}
|
|
||||||
if len(nodes) != nodeCount {
|
|
||||||
t.Fatalf("expected %d nodes, got %d", nodeCount, len(nodes))
|
|
||||||
}
|
|
||||||
for i, nodeID := range nodeIDs {
|
|
||||||
if nodes[i].ID != nodeID {
|
|
||||||
t.Fatalf("expected node %d to have ID %q, got %q", i, nodeID, nodes[i].ID)
|
|
||||||
}
|
|
||||||
node, err := client.GetNode(nodeID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error getting node %d: %s", i, err)
|
|
||||||
}
|
|
||||||
if node.ID != nodeID {
|
|
||||||
t.Fatalf("expected node %d to have ID %q, got %q", i, nodeID, node.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// start both nodes
|
|
||||||
for _, nodeID := range nodeIDs {
|
|
||||||
if err := client.StartNode(nodeID); err != nil {
|
|
||||||
t.Fatalf("error starting node %q: %s", nodeID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// connect the nodes
|
|
||||||
for i := 0; i < nodeCount-1; i++ {
|
|
||||||
peerId := i + 1
|
|
||||||
if i == nodeCount-1 {
|
|
||||||
peerId = 0
|
|
||||||
}
|
|
||||||
if err := client.ConnectNode(nodeIDs[i], nodeIDs[peerId]); err != nil {
|
|
||||||
t.Fatalf("error connecting nodes: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodeIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
type expectEvents struct {
|
|
||||||
*testing.T
|
|
||||||
|
|
||||||
events chan *Event
|
|
||||||
sub event.Subscription
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *expectEvents) nodeEvent(id string, up bool) *Event {
|
|
||||||
config := &adapters.NodeConfig{ID: enode.HexID(id)}
|
|
||||||
return &Event{Type: EventTypeNode, Node: newNode(nil, config, up)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *expectEvents) connEvent(one, other string, up bool) *Event {
|
|
||||||
return &Event{
|
|
||||||
Type: EventTypeConn,
|
|
||||||
Conn: &Conn{
|
|
||||||
One: enode.HexID(one),
|
|
||||||
Other: enode.HexID(other),
|
|
||||||
Up: up,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *expectEvents) expectMsgs(expected map[MsgFilter]int) {
|
|
||||||
actual := make(map[MsgFilter]int)
|
|
||||||
timeout := time.After(10 * time.Second)
|
|
||||||
loop:
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event := <-t.events:
|
|
||||||
t.Logf("received %s event: %v", event.Type, event)
|
|
||||||
|
|
||||||
if event.Type != EventTypeMsg || event.Msg.Received {
|
|
||||||
continue loop
|
|
||||||
}
|
|
||||||
if event.Msg == nil {
|
|
||||||
t.Fatal("expected event.Msg to be set")
|
|
||||||
}
|
|
||||||
filter := MsgFilter{
|
|
||||||
Proto: event.Msg.Protocol,
|
|
||||||
Code: int64(event.Msg.Code),
|
|
||||||
}
|
|
||||||
actual[filter]++
|
|
||||||
if actual[filter] > expected[filter] {
|
|
||||||
t.Fatalf("received too many msgs for filter: %v", filter)
|
|
||||||
}
|
|
||||||
if reflect.DeepEqual(actual, expected) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
case err := <-t.sub.Err():
|
|
||||||
t.Fatalf("network stream closed unexpectedly: %s", err)
|
|
||||||
|
|
||||||
case <-timeout:
|
|
||||||
t.Fatal("timed out waiting for expected events")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *expectEvents) expect(events ...*Event) {
|
|
||||||
t.Helper()
|
|
||||||
timeout := time.After(10 * time.Second)
|
|
||||||
i := 0
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event := <-t.events:
|
|
||||||
t.Logf("received %s event: %v", event.Type, event)
|
|
||||||
|
|
||||||
expected := events[i]
|
|
||||||
if event.Type != expected.Type {
|
|
||||||
t.Fatalf("expected event %d to have type %q, got %q", i, expected.Type, event.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch expected.Type {
|
|
||||||
case EventTypeNode:
|
|
||||||
if event.Node == nil {
|
|
||||||
t.Fatal("expected event.Node to be set")
|
|
||||||
}
|
|
||||||
if event.Node.ID() != expected.Node.ID() {
|
|
||||||
t.Fatalf("expected node event %d to have id %q, got %q", i, expected.Node.ID().TerminalString(), event.Node.ID().TerminalString())
|
|
||||||
}
|
|
||||||
if event.Node.Up() != expected.Node.Up() {
|
|
||||||
t.Fatalf("expected node event %d to have up=%t, got up=%t", i, expected.Node.Up(), event.Node.Up())
|
|
||||||
}
|
|
||||||
|
|
||||||
case EventTypeConn:
|
|
||||||
if event.Conn == nil {
|
|
||||||
t.Fatal("expected event.Conn to be set")
|
|
||||||
}
|
|
||||||
if event.Conn.One != expected.Conn.One {
|
|
||||||
t.Fatalf("expected conn event %d to have one=%q, got one=%q", i, expected.Conn.One.TerminalString(), event.Conn.One.TerminalString())
|
|
||||||
}
|
|
||||||
if event.Conn.Other != expected.Conn.Other {
|
|
||||||
t.Fatalf("expected conn event %d to have other=%q, got other=%q", i, expected.Conn.Other.TerminalString(), event.Conn.Other.TerminalString())
|
|
||||||
}
|
|
||||||
if event.Conn.Up != expected.Conn.Up {
|
|
||||||
t.Fatalf("expected conn event %d to have up=%t, got up=%t", i, expected.Conn.Up, event.Conn.Up)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
i++
|
|
||||||
if i == len(events) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
case err := <-t.sub.Err():
|
|
||||||
t.Fatalf("network stream closed unexpectedly: %s", err)
|
|
||||||
|
|
||||||
case <-timeout:
|
|
||||||
t.Fatal("timed out waiting for expected events")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHTTPNodeRPC tests calling RPC methods on nodes via the HTTP API
|
|
||||||
func TestHTTPNodeRPC(t *testing.T) {
|
|
||||||
// start the server
|
|
||||||
_, s := testHTTPServer(t)
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
// start a node in the network
|
|
||||||
client := NewClient(s.URL)
|
|
||||||
|
|
||||||
config := adapters.RandomNodeConfig()
|
|
||||||
node, err := client.CreateNode(config)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error creating node: %s", err)
|
|
||||||
}
|
|
||||||
if err := client.StartNode(node.ID); err != nil {
|
|
||||||
t.Fatalf("error starting node: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create two RPC clients
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
rpcClient1, err := client.RPCClient(ctx, node.ID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error getting node RPC client: %s", err)
|
|
||||||
}
|
|
||||||
rpcClient2, err := client.RPCClient(ctx, node.ID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error getting node RPC client: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// subscribe to events using client 1
|
|
||||||
events := make(chan int64, 1)
|
|
||||||
sub, err := rpcClient1.Subscribe(ctx, "test", events, "events")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error subscribing to events: %s", err)
|
|
||||||
}
|
|
||||||
defer sub.Unsubscribe()
|
|
||||||
|
|
||||||
// call some RPC methods using client 2
|
|
||||||
if err := rpcClient2.CallContext(ctx, nil, "test_add", 10); err != nil {
|
|
||||||
t.Fatalf("error calling RPC method: %s", err)
|
|
||||||
}
|
|
||||||
var result int64
|
|
||||||
if err := rpcClient2.CallContext(ctx, &result, "test_get"); err != nil {
|
|
||||||
t.Fatalf("error calling RPC method: %s", err)
|
|
||||||
}
|
|
||||||
if result != 10 {
|
|
||||||
t.Fatalf("expected result to be 10, got %d", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check we got an event from client 1
|
|
||||||
select {
|
|
||||||
case event := <-events:
|
|
||||||
if event != 10 {
|
|
||||||
t.Fatalf("expected event to be 10, got %d", event)
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
t.Fatal(ctx.Err())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHTTPSnapshot tests creating and loading network snapshots
|
|
||||||
func TestHTTPSnapshot(t *testing.T) {
|
|
||||||
// start the server
|
|
||||||
network, s := testHTTPServer(t)
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
var eventsDone = make(chan struct{}, 1)
|
|
||||||
count := 1
|
|
||||||
eventsDoneChan := make(chan *Event)
|
|
||||||
eventSub := network.Events().Subscribe(eventsDoneChan)
|
|
||||||
go func() {
|
|
||||||
defer eventSub.Unsubscribe()
|
|
||||||
for event := range eventsDoneChan {
|
|
||||||
if event.Type == EventTypeConn && !event.Control {
|
|
||||||
count--
|
|
||||||
if count == 0 {
|
|
||||||
eventsDone <- struct{}{}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// create a two-node network
|
|
||||||
client := NewClient(s.URL)
|
|
||||||
nodeCount := 2
|
|
||||||
nodes := make([]*p2p.NodeInfo, nodeCount)
|
|
||||||
for i := 0; i < nodeCount; i++ {
|
|
||||||
config := adapters.RandomNodeConfig()
|
|
||||||
node, err := client.CreateNode(config)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error creating node: %s", err)
|
|
||||||
}
|
|
||||||
if err := client.StartNode(node.ID); err != nil {
|
|
||||||
t.Fatalf("error starting node: %s", err)
|
|
||||||
}
|
|
||||||
nodes[i] = node
|
|
||||||
}
|
|
||||||
if err := client.ConnectNode(nodes[0].ID, nodes[1].ID); err != nil {
|
|
||||||
t.Fatalf("error connecting nodes: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// store some state in the test services
|
|
||||||
states := make([]string, nodeCount)
|
|
||||||
for i, node := range nodes {
|
|
||||||
rpc, err := client.RPCClient(context.Background(), node.ID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error getting RPC client: %s", err)
|
|
||||||
}
|
|
||||||
defer rpc.Close()
|
|
||||||
state := fmt.Sprintf("%x", rand.Int())
|
|
||||||
if err := rpc.Call(nil, "test_setState", []byte(state)); err != nil {
|
|
||||||
t.Fatalf("error setting service state: %s", err)
|
|
||||||
}
|
|
||||||
states[i] = state
|
|
||||||
}
|
|
||||||
<-eventsDone
|
|
||||||
// create a snapshot
|
|
||||||
snap, err := client.CreateSnapshot()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error creating snapshot: %s", err)
|
|
||||||
}
|
|
||||||
for i, state := range states {
|
|
||||||
gotState := snap.Nodes[i].Snapshots["test"]
|
|
||||||
if string(gotState) != state {
|
|
||||||
t.Fatalf("expected snapshot state %q, got %q", state, gotState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create another network
|
|
||||||
network2, s := testHTTPServer(t)
|
|
||||||
defer s.Close()
|
|
||||||
client = NewClient(s.URL)
|
|
||||||
count = 1
|
|
||||||
eventSub = network2.Events().Subscribe(eventsDoneChan)
|
|
||||||
go func() {
|
|
||||||
defer eventSub.Unsubscribe()
|
|
||||||
for event := range eventsDoneChan {
|
|
||||||
if event.Type == EventTypeConn && !event.Control {
|
|
||||||
count--
|
|
||||||
if count == 0 {
|
|
||||||
eventsDone <- struct{}{}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// subscribe to events so we can check them later
|
|
||||||
events := make(chan *Event, 100)
|
|
||||||
var opts SubscribeOpts
|
|
||||||
sub, err := client.SubscribeNetwork(events, opts)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error subscribing to network events: %s", err)
|
|
||||||
}
|
|
||||||
defer sub.Unsubscribe()
|
|
||||||
|
|
||||||
// load the snapshot
|
|
||||||
if err := client.LoadSnapshot(snap); err != nil {
|
|
||||||
t.Fatalf("error loading snapshot: %s", err)
|
|
||||||
}
|
|
||||||
<-eventsDone
|
|
||||||
|
|
||||||
// check the nodes and connection exists
|
|
||||||
net, err := client.GetNetwork()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error getting network: %s", err)
|
|
||||||
}
|
|
||||||
if len(net.Nodes) != nodeCount {
|
|
||||||
t.Fatalf("expected network to have %d nodes, got %d", nodeCount, len(net.Nodes))
|
|
||||||
}
|
|
||||||
for i, node := range nodes {
|
|
||||||
id := net.Nodes[i].ID().String()
|
|
||||||
if id != node.ID {
|
|
||||||
t.Fatalf("expected node %d to have ID %s, got %s", i, node.ID, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(net.Conns) != 1 {
|
|
||||||
t.Fatalf("expected network to have 1 connection, got %d", len(net.Conns))
|
|
||||||
}
|
|
||||||
conn := net.Conns[0]
|
|
||||||
if conn.One.String() != nodes[0].ID {
|
|
||||||
t.Fatalf("expected connection to have one=%q, got one=%q", nodes[0].ID, conn.One)
|
|
||||||
}
|
|
||||||
if conn.Other.String() != nodes[1].ID {
|
|
||||||
t.Fatalf("expected connection to have other=%q, got other=%q", nodes[1].ID, conn.Other)
|
|
||||||
}
|
|
||||||
if !conn.Up {
|
|
||||||
t.Fatal("should be up")
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the node states were restored
|
|
||||||
for i, node := range nodes {
|
|
||||||
rpc, err := client.RPCClient(context.Background(), node.ID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error getting RPC client: %s", err)
|
|
||||||
}
|
|
||||||
defer rpc.Close()
|
|
||||||
var state []byte
|
|
||||||
if err := rpc.Call(&state, "test_getState"); err != nil {
|
|
||||||
t.Fatalf("error getting service state: %s", err)
|
|
||||||
}
|
|
||||||
if string(state) != states[i] {
|
|
||||||
t.Fatalf("expected snapshot state %q, got %q", states[i], state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check we got all the events
|
|
||||||
x := &expectEvents{t, events, sub}
|
|
||||||
x.expect(
|
|
||||||
x.nodeEvent(nodes[0].ID, false),
|
|
||||||
x.nodeEvent(nodes[0].ID, true),
|
|
||||||
x.nodeEvent(nodes[1].ID, false),
|
|
||||||
x.nodeEvent(nodes[1].ID, true),
|
|
||||||
x.connEvent(nodes[0].ID, nodes[1].ID, false),
|
|
||||||
x.connEvent(nodes[0].ID, nodes[1].ID, true),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMsgFilterPassMultiple tests streaming message events using a filter
|
|
||||||
// with multiple protocols
|
|
||||||
func TestMsgFilterPassMultiple(t *testing.T) {
|
|
||||||
// start the server
|
|
||||||
_, s := testHTTPServer(t)
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
// subscribe to events with a message filter
|
|
||||||
client := NewClient(s.URL)
|
|
||||||
events := make(chan *Event, 10)
|
|
||||||
opts := SubscribeOpts{
|
|
||||||
Filter: "prb:0-test:0",
|
|
||||||
}
|
|
||||||
sub, err := client.SubscribeNetwork(events, opts)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error subscribing to network events: %s", err)
|
|
||||||
}
|
|
||||||
defer sub.Unsubscribe()
|
|
||||||
|
|
||||||
// start a simulation network
|
|
||||||
startTestNetwork(t, client)
|
|
||||||
|
|
||||||
// check we got the expected events
|
|
||||||
x := &expectEvents{t, events, sub}
|
|
||||||
x.expectMsgs(map[MsgFilter]int{
|
|
||||||
{"test", 0}: 2,
|
|
||||||
{"prb", 0}: 2,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMsgFilterPassWildcard tests streaming message events using a filter
|
|
||||||
// with a code wildcard
|
|
||||||
func TestMsgFilterPassWildcard(t *testing.T) {
|
|
||||||
// start the server
|
|
||||||
_, s := testHTTPServer(t)
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
// subscribe to events with a message filter
|
|
||||||
client := NewClient(s.URL)
|
|
||||||
events := make(chan *Event, 10)
|
|
||||||
opts := SubscribeOpts{
|
|
||||||
Filter: "prb:0,2-test:*",
|
|
||||||
}
|
|
||||||
sub, err := client.SubscribeNetwork(events, opts)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error subscribing to network events: %s", err)
|
|
||||||
}
|
|
||||||
defer sub.Unsubscribe()
|
|
||||||
|
|
||||||
// start a simulation network
|
|
||||||
startTestNetwork(t, client)
|
|
||||||
|
|
||||||
// check we got the expected events
|
|
||||||
x := &expectEvents{t, events, sub}
|
|
||||||
x.expectMsgs(map[MsgFilter]int{
|
|
||||||
{"test", 2}: 2,
|
|
||||||
{"test", 1}: 2,
|
|
||||||
{"test", 0}: 2,
|
|
||||||
{"prb", 0}: 2,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMsgFilterPassSingle tests streaming message events using a filter
|
|
||||||
// with a single protocol and code
|
|
||||||
func TestMsgFilterPassSingle(t *testing.T) {
|
|
||||||
// start the server
|
|
||||||
_, s := testHTTPServer(t)
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
// subscribe to events with a message filter
|
|
||||||
client := NewClient(s.URL)
|
|
||||||
events := make(chan *Event, 10)
|
|
||||||
opts := SubscribeOpts{
|
|
||||||
Filter: "dum:0",
|
|
||||||
}
|
|
||||||
sub, err := client.SubscribeNetwork(events, opts)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error subscribing to network events: %s", err)
|
|
||||||
}
|
|
||||||
defer sub.Unsubscribe()
|
|
||||||
|
|
||||||
// start a simulation network
|
|
||||||
startTestNetwork(t, client)
|
|
||||||
|
|
||||||
// check we got the expected events
|
|
||||||
x := &expectEvents{t, events, sub}
|
|
||||||
x.expectMsgs(map[MsgFilter]int{
|
|
||||||
{"dum", 0}: 2,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMsgFilterPassSingle tests streaming message events using an invalid
|
|
||||||
// filter
|
|
||||||
func TestMsgFilterFailBadParams(t *testing.T) {
|
|
||||||
// start the server
|
|
||||||
_, s := testHTTPServer(t)
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
client := NewClient(s.URL)
|
|
||||||
events := make(chan *Event, 10)
|
|
||||||
opts := SubscribeOpts{
|
|
||||||
Filter: "foo:",
|
|
||||||
}
|
|
||||||
_, err := client.SubscribeNetwork(events, opts)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected event subscription to fail but succeeded!")
|
|
||||||
}
|
|
||||||
|
|
||||||
opts.Filter = "bzz:aa"
|
|
||||||
_, err = client.SubscribeNetwork(events, opts)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected event subscription to fail but succeeded!")
|
|
||||||
}
|
|
||||||
|
|
||||||
opts.Filter = "invalid"
|
|
||||||
_, err = client.SubscribeNetwork(events, opts)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected event subscription to fail but succeeded!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,191 +0,0 @@
|
||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Package simulations simulates p2p networks.
|
|
||||||
// A mocker simulates starting and stopping real nodes in a network.
|
|
||||||
package simulations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
|
||||||
)
|
|
||||||
|
|
||||||
// a map of mocker names to its function
|
|
||||||
var mockerList = map[string]func(net *Network, quit chan struct{}, nodeCount int){
|
|
||||||
"startStop": startStop,
|
|
||||||
"probabilistic": probabilistic,
|
|
||||||
"boot": boot,
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookupMocker looks a mocker by its name, returns the mockerFn
|
|
||||||
func LookupMocker(mockerType string) func(net *Network, quit chan struct{}, nodeCount int) {
|
|
||||||
return mockerList[mockerType]
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMockerList returns a list of mockers (keys of the map)
|
|
||||||
// Useful for frontend to build available mocker selection
|
|
||||||
func GetMockerList() []string {
|
|
||||||
list := make([]string, 0, len(mockerList))
|
|
||||||
for k := range mockerList {
|
|
||||||
list = append(list, k)
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
// The boot mockerFn only connects the node in a ring and doesn't do anything else
|
|
||||||
func boot(net *Network, quit chan struct{}, nodeCount int) {
|
|
||||||
_, err := connectNodesInRing(net, nodeCount)
|
|
||||||
if err != nil {
|
|
||||||
panic("Could not startup node network for mocker")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The startStop mockerFn stops and starts nodes in a defined period (ticker)
|
|
||||||
func startStop(net *Network, quit chan struct{}, nodeCount int) {
|
|
||||||
nodes, err := connectNodesInRing(net, nodeCount)
|
|
||||||
if err != nil {
|
|
||||||
panic("Could not startup node network for mocker")
|
|
||||||
}
|
|
||||||
tick := time.NewTicker(10 * time.Second)
|
|
||||||
defer tick.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-quit:
|
|
||||||
log.Info("Terminating simulation loop")
|
|
||||||
return
|
|
||||||
case <-tick.C:
|
|
||||||
id := nodes[rand.Intn(len(nodes))]
|
|
||||||
log.Info("stopping node", "id", id)
|
|
||||||
if err := net.Stop(id); err != nil {
|
|
||||||
log.Error("error stopping node", "id", id, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-quit:
|
|
||||||
log.Info("Terminating simulation loop")
|
|
||||||
return
|
|
||||||
case <-time.After(3 * time.Second):
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("starting node", "id", id)
|
|
||||||
if err := net.Start(id); err != nil {
|
|
||||||
log.Error("error starting node", "id", id, "err", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The probabilistic mocker func has a more probabilistic pattern
|
|
||||||
// (the implementation could probably be improved):
|
|
||||||
// nodes are connected in a ring, then a varying number of random nodes is selected,
|
|
||||||
// mocker then stops and starts them in random intervals, and continues the loop
|
|
||||||
func probabilistic(net *Network, quit chan struct{}, nodeCount int) {
|
|
||||||
nodes, err := connectNodesInRing(net, nodeCount)
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-quit:
|
|
||||||
//error may be due to abortion of mocking; so the quit channel is closed
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
panic("Could not startup node network for mocker")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-quit:
|
|
||||||
log.Info("Terminating simulation loop")
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
var lowid, highid int
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
randWait := time.Duration(rand.Intn(5000)+1000) * time.Millisecond
|
|
||||||
rand1 := rand.Intn(nodeCount - 1)
|
|
||||||
rand2 := rand.Intn(nodeCount - 1)
|
|
||||||
if rand1 <= rand2 {
|
|
||||||
lowid = rand1
|
|
||||||
highid = rand2
|
|
||||||
} else if rand1 > rand2 {
|
|
||||||
highid = rand1
|
|
||||||
lowid = rand2
|
|
||||||
}
|
|
||||||
var steps = highid - lowid
|
|
||||||
wg.Add(steps)
|
|
||||||
for i := lowid; i < highid; i++ {
|
|
||||||
select {
|
|
||||||
case <-quit:
|
|
||||||
log.Info("Terminating simulation loop")
|
|
||||||
return
|
|
||||||
case <-time.After(randWait):
|
|
||||||
}
|
|
||||||
log.Debug(fmt.Sprintf("node %v shutting down", nodes[i]))
|
|
||||||
err := net.Stop(nodes[i])
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Error stopping node", "node", nodes[i])
|
|
||||||
wg.Done()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
go func(id enode.ID) {
|
|
||||||
time.Sleep(randWait)
|
|
||||||
err := net.Start(id)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Error starting node", "node", id)
|
|
||||||
}
|
|
||||||
wg.Done()
|
|
||||||
}(nodes[i])
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// connect nodeCount number of nodes in a ring
|
|
||||||
func connectNodesInRing(net *Network, nodeCount int) ([]enode.ID, error) {
|
|
||||||
ids := make([]enode.ID, nodeCount)
|
|
||||||
for i := 0; i < nodeCount; i++ {
|
|
||||||
conf := adapters.RandomNodeConfig()
|
|
||||||
node, err := net.NewNodeWithConfig(conf)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Error creating a node!", "err", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ids[i] = node.ID()
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, id := range ids {
|
|
||||||
if err := net.Start(id); err != nil {
|
|
||||||
log.Error("Error starting a node!", "err", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Debug(fmt.Sprintf("node %v starting up", id))
|
|
||||||
}
|
|
||||||
for i, id := range ids {
|
|
||||||
peerID := ids[(i+1)%len(ids)]
|
|
||||||
if err := net.Connect(id, peerID); err != nil {
|
|
||||||
log.Error("Error connecting a node to a peer!", "err", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,174 +0,0 @@
|
||||||
// Copyright 2017 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
// Package simulations simulates p2p networks.
|
|
||||||
// A mocker simulates starting and stopping real nodes in a network.
|
|
||||||
package simulations
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMocker(t *testing.T) {
|
|
||||||
//start the simulation HTTP server
|
|
||||||
_, s := testHTTPServer(t)
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
//create a client
|
|
||||||
client := NewClient(s.URL)
|
|
||||||
|
|
||||||
//start the network
|
|
||||||
err := client.StartNetwork()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not start test network: %s", err)
|
|
||||||
}
|
|
||||||
//stop the network to terminate
|
|
||||||
defer func() {
|
|
||||||
err = client.StopNetwork()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not stop test network: %s", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
//get the list of available mocker types
|
|
||||||
resp, err := http.Get(s.URL + "/mocker")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not get mocker list: %s", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
t.Fatalf("Invalid Status Code received, expected 200, got %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
//check the list is at least 1 in size
|
|
||||||
var mockerlist []string
|
|
||||||
err = json.NewDecoder(resp.Body).Decode(&mockerlist)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error decoding JSON mockerlist: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(mockerlist) < 1 {
|
|
||||||
t.Fatalf("No mockers available")
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeCount := 10
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
events := make(chan *Event, 10)
|
|
||||||
var opts SubscribeOpts
|
|
||||||
sub, err := client.SubscribeNetwork(events, opts)
|
|
||||||
defer sub.Unsubscribe()
|
|
||||||
|
|
||||||
// wait until all nodes are started and connected
|
|
||||||
// store every node up event in a map (value is irrelevant, mimic Set datatype)
|
|
||||||
nodemap := make(map[enode.ID]bool)
|
|
||||||
nodesComplete := false
|
|
||||||
connCount := 0
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
for connCount < (nodeCount-1)*2 {
|
|
||||||
select {
|
|
||||||
case event := <-events:
|
|
||||||
if isNodeUp(event) {
|
|
||||||
//add the correspondent node ID to the map
|
|
||||||
nodemap[event.Node.Config.ID] = true
|
|
||||||
//this means all nodes got a nodeUp event, so we can continue the test
|
|
||||||
if len(nodemap) == nodeCount {
|
|
||||||
nodesComplete = true
|
|
||||||
}
|
|
||||||
} else if event.Conn != nil && nodesComplete {
|
|
||||||
connCount += 1
|
|
||||||
}
|
|
||||||
case <-time.After(30 * time.Second):
|
|
||||||
t.Errorf("Timeout waiting for nodes being started up!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
//take the last element of the mockerlist as the default mocker-type to ensure one is enabled
|
|
||||||
mockertype := mockerlist[len(mockerlist)-1]
|
|
||||||
//still, use hardcoded "probabilistic" one if available ;)
|
|
||||||
for _, m := range mockerlist {
|
|
||||||
if m == "probabilistic" {
|
|
||||||
mockertype = m
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//start the mocker with nodeCount number of nodes
|
|
||||||
resp, err = http.PostForm(s.URL+"/mocker/start", url.Values{"mocker-type": {mockertype}, "node-count": {strconv.Itoa(nodeCount)}})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not start mocker: %s", err)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
t.Fatalf("Invalid Status Code received for starting mocker, expected 200, got %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
//check there are nodeCount number of nodes in the network
|
|
||||||
nodesInfo, err := client.GetNodes()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not get nodes list: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(nodesInfo) != nodeCount {
|
|
||||||
t.Fatalf("Expected %d number of nodes, got: %d", nodeCount, len(nodesInfo))
|
|
||||||
}
|
|
||||||
|
|
||||||
//stop the mocker
|
|
||||||
resp, err = http.Post(s.URL+"/mocker/stop", "", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not stop mocker: %s", err)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
t.Fatalf("Invalid Status Code received for stopping mocker, expected 200, got %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
//reset the network
|
|
||||||
resp, err = http.Post(s.URL+"/reset", "", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not reset network: %s", err)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
//now the number of nodes in the network should be zero
|
|
||||||
nodesInfo, err = client.GetNodes()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Could not get nodes list: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(nodesInfo) != 0 {
|
|
||||||
t.Fatalf("Expected empty list of nodes, got: %d", len(nodesInfo))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isNodeUp(event *Event) bool {
|
|
||||||
return event.Node != nil && event.Node.Up()
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue