Delete p2p directory

This commit is contained in:
Isabel Schöps Thiel 2024-01-01 00:33:51 +01:00 committed by GitHub
parent 3d0a141460
commit b9d1320f45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
109 changed files with 0 additions and 31590 deletions

View file

@ -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
}

View file

@ -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()]
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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")
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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))
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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))
}

View file

@ -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)
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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)
}

View file

@ -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)
}
}
}
}

View file

@ -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")
}
}

View file

@ -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
}

View file

@ -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")
}
}

View file

@ -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}
}

View file

@ -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)
}

View file

@ -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
}
}
}

View file

@ -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()
}

View file

@ -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)
}

View file

@ -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())
}

View file

@ -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
}

View file

@ -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")
}
}

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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))
}

View file

@ -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)
}
}
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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:
}
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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))
}
}

View file

@ -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
}

View file

@ -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)
}
}
}
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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()
}

View file

@ -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
}
}

View file

@ -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()
}

View file

@ -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")
}
}
}
}

View file

@ -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)
}
}
}

View file

@ -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))
}
}

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}
}
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

File diff suppressed because it is too large Load diff

View file

@ -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)
}
}
}
}
}

View file

@ -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"
}

View file

@ -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
}
}
}

View file

@ -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).

View file

@ -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")
}

View file

@ -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()
}

View file

@ -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)
}
}
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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 ""
}
}

View file

@ -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...
```

View file

@ -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
}

View file

@ -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 "$@"

View file

@ -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))
}
}

View file

@ -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!")
}
}

View file

@ -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
}

View file

@ -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