From b9d1320f45159b93d58395d2f5ca62165f8854d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isabel=20Sch=C3=B6ps=20Thiel?= <155141998+IST-Github@users.noreply.github.com> Date: Mon, 1 Jan 2024 00:33:51 +0100 Subject: [PATCH] Delete p2p directory --- p2p/dial.go | 544 -------- p2p/dial_test.go | 672 ---------- p2p/discover/common.go | 101 -- p2p/discover/lookup.go | 227 ---- p2p/discover/metrics.go | 73 -- p2p/discover/node.go | 97 -- p2p/discover/ntp.go | 111 -- p2p/discover/table.go | 754 ----------- p2p/discover/table_test.go | 446 ------- p2p/discover/table_util_test.go | 258 ---- p2p/discover/v4_lookup_test.go | 347 ----- p2p/discover/v4_udp.go | 787 ------------ p2p/discover/v4_udp_test.go | 661 ---------- p2p/discover/v4wire/v4wire.go | 296 ----- p2p/discover/v4wire/v4wire_test.go | 132 -- p2p/discover/v5_talk.go | 113 -- p2p/discover/v5_udp.go | 910 ------------- p2p/discover/v5_udp_test.go | 859 ------------- p2p/discover/v5wire/crypto.go | 180 --- p2p/discover/v5wire/crypto_test.go | 124 -- p2p/discover/v5wire/encoding.go | 672 ---------- p2p/discover/v5wire/encoding_test.go | 639 ---------- p2p/discover/v5wire/msg.go | 229 ---- p2p/discover/v5wire/session.go | 135 -- .../testdata/v5.1-ping-handshake-enr.txt | 27 - .../v5wire/testdata/v5.1-ping-handshake.txt | 23 - .../v5wire/testdata/v5.1-ping-message.txt | 10 - .../v5wire/testdata/v5.1-whoareyou.txt | 9 - p2p/dnsdisc/client.go | 389 ------ p2p/dnsdisc/client_test.go | 478 ------- p2p/dnsdisc/doc.go | 18 - p2p/dnsdisc/error.go | 63 - p2p/dnsdisc/sync.go | 329 ----- p2p/dnsdisc/sync_test.go | 83 -- p2p/dnsdisc/tree.go | 423 ------ p2p/dnsdisc/tree_test.go | 151 --- p2p/enode/idscheme.go | 161 --- p2p/enode/idscheme_test.go | 74 -- p2p/enode/iter.go | 295 ----- p2p/enode/iter_test.go | 291 ----- p2p/enode/localnode.go | 332 ----- p2p/enode/localnode_test.go | 129 -- p2p/enode/node.go | 279 ---- p2p/enode/node_test.go | 145 --- p2p/enode/nodedb.go | 501 -------- p2p/enode/nodedb_test.go | 469 ------- p2p/enode/urlv4.go | 203 --- p2p/enode/urlv4_test.go | 200 --- p2p/enr/enr.go | 335 ----- p2p/enr/enr_test.go | 348 ----- p2p/enr/entries.go | 196 --- p2p/message.go | 325 ----- p2p/message_test.go | 141 -- p2p/metrics.go | 132 -- p2p/msgrate/msgrate.go | 465 ------- p2p/msgrate/msgrate_test.go | 28 - p2p/nat/nat.go | 240 ---- p2p/nat/nat_test.go | 63 - p2p/nat/natpmp.go | 130 -- p2p/nat/natupnp.go | 250 ---- p2p/nat/natupnp_test.go | 249 ---- p2p/netutil/addrutil.go | 33 - p2p/netutil/error.go | 33 - p2p/netutil/error_test.go | 72 -- p2p/netutil/iptrack.go | 130 -- p2p/netutil/iptrack_test.go | 138 -- p2p/netutil/net.go | 322 ----- p2p/netutil/net_test.go | 262 ---- p2p/netutil/toobig_notwindows.go | 27 - p2p/netutil/toobig_windows.go | 41 - p2p/nodestate/nodestate.go | 1023 --------------- p2p/nodestate/nodestate_test.go | 407 ------ p2p/peer.go | 548 -------- p2p/peer_error.go | 119 -- p2p/peer_test.go | 362 ------ p2p/protocol.go | 93 -- p2p/rlpx/buffer.go | 127 -- p2p/rlpx/buffer_test.go | 51 - p2p/rlpx/rlpx.go | 676 ---------- p2p/rlpx/rlpx_test.go | 453 ------- p2p/server.go | 1134 ----------------- p2p/server_nat.go | 187 --- p2p/server_nat_test.go | 102 -- p2p/server_test.go | 631 --------- p2p/simulations/README.md | 169 --- p2p/simulations/adapters/exec.go | 564 -------- p2p/simulations/adapters/inproc.go | 350 ----- p2p/simulations/adapters/inproc_test.go | 202 --- p2p/simulations/adapters/types.go | 326 ----- p2p/simulations/connect.go | 153 --- p2p/simulations/connect_test.go | 172 --- p2p/simulations/events.go | 110 -- p2p/simulations/examples/README.md | 39 - p2p/simulations/examples/ping-pong.go | 173 --- p2p/simulations/examples/ping-pong.sh | 40 - p2p/simulations/http.go | 743 ----------- p2p/simulations/http_test.go | 871 ------------- p2p/simulations/mocker.go | 191 --- p2p/simulations/mocker_test.go | 174 --- p2p/simulations/network.go | 1090 ---------------- p2p/simulations/network_test.go | 872 ------------- p2p/simulations/pipes/pipes.go | 55 - p2p/simulations/simulation.go | 157 --- p2p/simulations/test.go | 150 --- p2p/tracker/tracker.go | 205 --- p2p/transport.go | 182 --- p2p/transport_test.go | 148 --- p2p/util.go | 76 -- p2p/util_test.go | 56 - 109 files changed, 31590 deletions(-) delete mode 100644 p2p/dial.go delete mode 100644 p2p/dial_test.go delete mode 100644 p2p/discover/common.go delete mode 100644 p2p/discover/lookup.go delete mode 100644 p2p/discover/metrics.go delete mode 100644 p2p/discover/node.go delete mode 100644 p2p/discover/ntp.go delete mode 100644 p2p/discover/table.go delete mode 100644 p2p/discover/table_test.go delete mode 100644 p2p/discover/table_util_test.go delete mode 100644 p2p/discover/v4_lookup_test.go delete mode 100644 p2p/discover/v4_udp.go delete mode 100644 p2p/discover/v4_udp_test.go delete mode 100644 p2p/discover/v4wire/v4wire.go delete mode 100644 p2p/discover/v4wire/v4wire_test.go delete mode 100644 p2p/discover/v5_talk.go delete mode 100644 p2p/discover/v5_udp.go delete mode 100644 p2p/discover/v5_udp_test.go delete mode 100644 p2p/discover/v5wire/crypto.go delete mode 100644 p2p/discover/v5wire/crypto_test.go delete mode 100644 p2p/discover/v5wire/encoding.go delete mode 100644 p2p/discover/v5wire/encoding_test.go delete mode 100644 p2p/discover/v5wire/msg.go delete mode 100644 p2p/discover/v5wire/session.go delete mode 100644 p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt delete mode 100644 p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt delete mode 100644 p2p/discover/v5wire/testdata/v5.1-ping-message.txt delete mode 100644 p2p/discover/v5wire/testdata/v5.1-whoareyou.txt delete mode 100644 p2p/dnsdisc/client.go delete mode 100644 p2p/dnsdisc/client_test.go delete mode 100644 p2p/dnsdisc/doc.go delete mode 100644 p2p/dnsdisc/error.go delete mode 100644 p2p/dnsdisc/sync.go delete mode 100644 p2p/dnsdisc/sync_test.go delete mode 100644 p2p/dnsdisc/tree.go delete mode 100644 p2p/dnsdisc/tree_test.go delete mode 100644 p2p/enode/idscheme.go delete mode 100644 p2p/enode/idscheme_test.go delete mode 100644 p2p/enode/iter.go delete mode 100644 p2p/enode/iter_test.go delete mode 100644 p2p/enode/localnode.go delete mode 100644 p2p/enode/localnode_test.go delete mode 100644 p2p/enode/node.go delete mode 100644 p2p/enode/node_test.go delete mode 100644 p2p/enode/nodedb.go delete mode 100644 p2p/enode/nodedb_test.go delete mode 100644 p2p/enode/urlv4.go delete mode 100644 p2p/enode/urlv4_test.go delete mode 100644 p2p/enr/enr.go delete mode 100644 p2p/enr/enr_test.go delete mode 100644 p2p/enr/entries.go delete mode 100644 p2p/message.go delete mode 100644 p2p/message_test.go delete mode 100644 p2p/metrics.go delete mode 100644 p2p/msgrate/msgrate.go delete mode 100644 p2p/msgrate/msgrate_test.go delete mode 100644 p2p/nat/nat.go delete mode 100644 p2p/nat/nat_test.go delete mode 100644 p2p/nat/natpmp.go delete mode 100644 p2p/nat/natupnp.go delete mode 100644 p2p/nat/natupnp_test.go delete mode 100644 p2p/netutil/addrutil.go delete mode 100644 p2p/netutil/error.go delete mode 100644 p2p/netutil/error_test.go delete mode 100644 p2p/netutil/iptrack.go delete mode 100644 p2p/netutil/iptrack_test.go delete mode 100644 p2p/netutil/net.go delete mode 100644 p2p/netutil/net_test.go delete mode 100644 p2p/netutil/toobig_notwindows.go delete mode 100644 p2p/netutil/toobig_windows.go delete mode 100644 p2p/nodestate/nodestate.go delete mode 100644 p2p/nodestate/nodestate_test.go delete mode 100644 p2p/peer.go delete mode 100644 p2p/peer_error.go delete mode 100644 p2p/peer_test.go delete mode 100644 p2p/protocol.go delete mode 100644 p2p/rlpx/buffer.go delete mode 100644 p2p/rlpx/buffer_test.go delete mode 100644 p2p/rlpx/rlpx.go delete mode 100644 p2p/rlpx/rlpx_test.go delete mode 100644 p2p/server.go delete mode 100644 p2p/server_nat.go delete mode 100644 p2p/server_nat_test.go delete mode 100644 p2p/server_test.go delete mode 100644 p2p/simulations/README.md delete mode 100644 p2p/simulations/adapters/exec.go delete mode 100644 p2p/simulations/adapters/inproc.go delete mode 100644 p2p/simulations/adapters/inproc_test.go delete mode 100644 p2p/simulations/adapters/types.go delete mode 100644 p2p/simulations/connect.go delete mode 100644 p2p/simulations/connect_test.go delete mode 100644 p2p/simulations/events.go delete mode 100644 p2p/simulations/examples/README.md delete mode 100644 p2p/simulations/examples/ping-pong.go delete mode 100755 p2p/simulations/examples/ping-pong.sh delete mode 100644 p2p/simulations/http.go delete mode 100644 p2p/simulations/http_test.go delete mode 100644 p2p/simulations/mocker.go delete mode 100644 p2p/simulations/mocker_test.go delete mode 100644 p2p/simulations/network.go delete mode 100644 p2p/simulations/network_test.go delete mode 100644 p2p/simulations/pipes/pipes.go delete mode 100644 p2p/simulations/simulation.go delete mode 100644 p2p/simulations/test.go delete mode 100644 p2p/tracker/tracker.go delete mode 100644 p2p/transport.go delete mode 100644 p2p/transport_test.go delete mode 100644 p2p/util.go delete mode 100644 p2p/util_test.go diff --git a/p2p/dial.go b/p2p/dial.go deleted file mode 100644 index 5e4ab1d50d..0000000000 --- a/p2p/dial.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/dial_test.go b/p2p/dial_test.go deleted file mode 100644 index 13908f11ea..0000000000 --- a/p2p/dial_test.go +++ /dev/null @@ -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 . - -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()] -} diff --git a/p2p/discover/common.go b/p2p/discover/common.go deleted file mode 100644 index c9f0477def..0000000000 --- a/p2p/discover/common.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/discover/lookup.go b/p2p/discover/lookup.go deleted file mode 100644 index b8d97b44e1..0000000000 --- a/p2p/discover/lookup.go +++ /dev/null @@ -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 . - -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() -} diff --git a/p2p/discover/metrics.go b/p2p/discover/metrics.go deleted file mode 100644 index da8e9cb817..0000000000 --- a/p2p/discover/metrics.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/discover/node.go b/p2p/discover/node.go deleted file mode 100644 index 9ffe101ccf..0000000000 --- a/p2p/discover/node.go +++ /dev/null @@ -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 . - -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() -} diff --git a/p2p/discover/ntp.go b/p2p/discover/ntp.go deleted file mode 100644 index 3f9157808f..0000000000 --- a/p2p/discover/ntp.go +++ /dev/null @@ -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 . - -// 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 -} diff --git a/p2p/discover/table.go b/p2p/discover/table.go deleted file mode 100644 index 2b7a28708b..0000000000 --- a/p2p/discover/table.go +++ /dev/null @@ -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 . - -// 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 - } -} diff --git a/p2p/discover/table_test.go b/p2p/discover/table_test.go deleted file mode 100644 index 3ba3422251..0000000000 --- a/p2p/discover/table_test.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/discover/table_util_test.go b/p2p/discover/table_util_test.go deleted file mode 100644 index d6309dfd6c..0000000000 --- a/p2p/discover/table_util_test.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/discover/v4_lookup_test.go b/p2p/discover/v4_lookup_test.go deleted file mode 100644 index 8867a5a8ac..0000000000 --- a/p2p/discover/v4_lookup_test.go +++ /dev/null @@ -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 . - -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") -} diff --git a/p2p/discover/v4_udp.go b/p2p/discover/v4_udp.go deleted file mode 100644 index 988f16b01d..0000000000 --- a/p2p/discover/v4_udp.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/discover/v4_udp_test.go b/p2p/discover/v4_udp_test.go deleted file mode 100644 index 361e379626..0000000000 --- a/p2p/discover/v4_udp_test.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/discover/v4wire/v4wire.go b/p2p/discover/v4wire/v4wire.go deleted file mode 100644 index 9c59359fb2..0000000000 --- a/p2p/discover/v4wire/v4wire.go +++ /dev/null @@ -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 . - -// 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 -} diff --git a/p2p/discover/v4wire/v4wire_test.go b/p2p/discover/v4wire/v4wire_test.go deleted file mode 100644 index 38820f3b48..0000000000 --- a/p2p/discover/v4wire/v4wire_test.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/discover/v5_talk.go b/p2p/discover/v5_talk.go deleted file mode 100644 index c1f6787940..0000000000 --- a/p2p/discover/v5_talk.go +++ /dev/null @@ -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 . - -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 - } -} diff --git a/p2p/discover/v5_udp.go b/p2p/discover/v5_udp.go deleted file mode 100644 index 8b3e33d37c..0000000000 --- a/p2p/discover/v5_udp.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/discover/v5_udp_test.go b/p2p/discover/v5_udp_test.go deleted file mode 100644 index eaa969ea8b..0000000000 --- a/p2p/discover/v5_udp_test.go +++ /dev/null @@ -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 . - -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)) - } -} diff --git a/p2p/discover/v5wire/crypto.go b/p2p/discover/v5wire/crypto.go deleted file mode 100644 index fc0a0edef5..0000000000 --- a/p2p/discover/v5wire/crypto.go +++ /dev/null @@ -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 . - -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) -} diff --git a/p2p/discover/v5wire/crypto_test.go b/p2p/discover/v5wire/crypto_test.go deleted file mode 100644 index 72169b4314..0000000000 --- a/p2p/discover/v5wire/crypto_test.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go deleted file mode 100644 index 5108910620..0000000000 --- a/p2p/discover/v5wire/encoding.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/discover/v5wire/encoding_test.go b/p2p/discover/v5wire/encoding_test.go deleted file mode 100644 index a5387311a5..0000000000 --- a/p2p/discover/v5wire/encoding_test.go +++ /dev/null @@ -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 . - -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) - } -} diff --git a/p2p/discover/v5wire/msg.go b/p2p/discover/v5wire/msg.go deleted file mode 100644 index 401db2f6c5..0000000000 --- a/p2p/discover/v5wire/msg.go +++ /dev/null @@ -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 . - -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)) -} diff --git a/p2p/discover/v5wire/session.go b/p2p/discover/v5wire/session.go deleted file mode 100644 index 862c21fcee..0000000000 --- a/p2p/discover/v5wire/session.go +++ /dev/null @@ -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 . - -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) - } - } -} diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt deleted file mode 100644 index 477f9e15a8..0000000000 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake-enr.txt +++ /dev/null @@ -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 diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt b/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt deleted file mode 100644 index b3f304766c..0000000000 --- a/p2p/discover/v5wire/testdata/v5.1-ping-handshake.txt +++ /dev/null @@ -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 diff --git a/p2p/discover/v5wire/testdata/v5.1-ping-message.txt b/p2p/discover/v5wire/testdata/v5.1-ping-message.txt deleted file mode 100644 index f82b99c3bc..0000000000 --- a/p2p/discover/v5wire/testdata/v5.1-ping-message.txt +++ /dev/null @@ -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 diff --git a/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt b/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt deleted file mode 100644 index 1a75f525ee..0000000000 --- a/p2p/discover/v5wire/testdata/v5.1-whoareyou.txt +++ /dev/null @@ -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 diff --git a/p2p/dnsdisc/client.go b/p2p/dnsdisc/client.go deleted file mode 100644 index 8f1c221b80..0000000000 --- a/p2p/dnsdisc/client.go +++ /dev/null @@ -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 . - -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) - } - } -} diff --git a/p2p/dnsdisc/client_test.go b/p2p/dnsdisc/client_test.go deleted file mode 100644 index abc35ddbd3..0000000000 --- a/p2p/dnsdisc/client_test.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/dnsdisc/doc.go b/p2p/dnsdisc/doc.go deleted file mode 100644 index cfd81cc187..0000000000 --- a/p2p/dnsdisc/doc.go +++ /dev/null @@ -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 . - -// Package dnsdisc implements node discovery via DNS (EIP-1459). -package dnsdisc diff --git a/p2p/dnsdisc/error.go b/p2p/dnsdisc/error.go deleted file mode 100644 index 39955cabff..0000000000 --- a/p2p/dnsdisc/error.go +++ /dev/null @@ -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 . - -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) -} diff --git a/p2p/dnsdisc/sync.go b/p2p/dnsdisc/sync.go deleted file mode 100644 index 073547c90d..0000000000 --- a/p2p/dnsdisc/sync.go +++ /dev/null @@ -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 . - -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) - } - } - } -} diff --git a/p2p/dnsdisc/sync_test.go b/p2p/dnsdisc/sync_test.go deleted file mode 100644 index ce1cb90bae..0000000000 --- a/p2p/dnsdisc/sync_test.go +++ /dev/null @@ -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 . - -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") - } -} diff --git a/p2p/dnsdisc/tree.go b/p2p/dnsdisc/tree.go deleted file mode 100644 index 06b7681f18..0000000000 --- a/p2p/dnsdisc/tree.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/dnsdisc/tree_test.go b/p2p/dnsdisc/tree_test.go deleted file mode 100644 index 9ed17aa4b3..0000000000 --- a/p2p/dnsdisc/tree_test.go +++ /dev/null @@ -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 . - -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") - } -} diff --git a/p2p/enode/idscheme.go b/p2p/enode/idscheme.go deleted file mode 100644 index fd5d868b76..0000000000 --- a/p2p/enode/idscheme.go +++ /dev/null @@ -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 . - -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} -} diff --git a/p2p/enode/idscheme_test.go b/p2p/enode/idscheme_test.go deleted file mode 100644 index 0910e6e83f..0000000000 --- a/p2p/enode/idscheme_test.go +++ /dev/null @@ -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 . - -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) -} diff --git a/p2p/enode/iter.go b/p2p/enode/iter.go deleted file mode 100644 index b8ab4a758a..0000000000 --- a/p2p/enode/iter.go +++ /dev/null @@ -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 . - -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 - } - } -} diff --git a/p2p/enode/iter_test.go b/p2p/enode/iter_test.go deleted file mode 100644 index b736ed450a..0000000000 --- a/p2p/enode/iter_test.go +++ /dev/null @@ -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 . - -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() -} diff --git a/p2p/enode/localnode.go b/p2p/enode/localnode.go deleted file mode 100644 index a18204e752..0000000000 --- a/p2p/enode/localnode.go +++ /dev/null @@ -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 . - -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) -} diff --git a/p2p/enode/localnode_test.go b/p2p/enode/localnode_test.go deleted file mode 100644 index 7f97ad392f..0000000000 --- a/p2p/enode/localnode_test.go +++ /dev/null @@ -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 . - -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()) -} diff --git a/p2p/enode/node.go b/p2p/enode/node.go deleted file mode 100644 index d7a1a9a156..0000000000 --- a/p2p/enode/node.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/enode/node_test.go b/p2p/enode/node_test.go deleted file mode 100644 index d15859c477..0000000000 --- a/p2p/enode/node_test.go +++ /dev/null @@ -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 . - -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") - } -} diff --git a/p2p/enode/nodedb.go b/p2p/enode/nodedb.go deleted file mode 100644 index 7e7fb69b29..0000000000 --- a/p2p/enode/nodedb.go +++ /dev/null @@ -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 . - -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::v4::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::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() -} diff --git a/p2p/enode/nodedb_test.go b/p2p/enode/nodedb_test.go deleted file mode 100644 index 38764f31b1..0000000000 --- a/p2p/enode/nodedb_test.go +++ /dev/null @@ -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 . - -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() -} diff --git a/p2p/enode/urlv4.go b/p2p/enode/urlv4.go deleted file mode 100644 index 0272eee987..0000000000 --- a/p2p/enode/urlv4.go +++ /dev/null @@ -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 . - -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:// -// -// -// 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://@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)) -} diff --git a/p2p/enode/urlv4_test.go b/p2p/enode/urlv4_test.go deleted file mode 100644 index 33de96cc57..0000000000 --- a/p2p/enode/urlv4_test.go +++ /dev/null @@ -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 . - -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) - } - } - } -} diff --git a/p2p/enr/enr.go b/p2p/enr/enr.go deleted file mode 100644 index 2b093b2f1a..0000000000 --- a/p2p/enr/enr.go +++ /dev/null @@ -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 . - -// 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 -} diff --git a/p2p/enr/enr_test.go b/p2p/enr/enr_test.go deleted file mode 100644 index b85ee209d5..0000000000 --- a/p2p/enr/enr_test.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/enr/entries.go b/p2p/enr/entries.go deleted file mode 100644 index 9945a436c9..0000000000 --- a/p2p/enr/entries.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/message.go b/p2p/message.go deleted file mode 100644 index 3ab56ee350..0000000000 --- a/p2p/message.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/message_test.go b/p2p/message_test.go deleted file mode 100644 index e575c5d96e..0000000000 --- a/p2p/message_test.go +++ /dev/null @@ -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 . - -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: - } -} diff --git a/p2p/metrics.go b/p2p/metrics.go deleted file mode 100644 index a6e36b91a8..0000000000 --- a/p2p/metrics.go +++ /dev/null @@ -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 . - -// 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 -} diff --git a/p2p/msgrate/msgrate.go b/p2p/msgrate/msgrate.go deleted file mode 100644 index de1a3177db..0000000000 --- a/p2p/msgrate/msgrate.go +++ /dev/null @@ -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 . - -// 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) - } -} diff --git a/p2p/msgrate/msgrate_test.go b/p2p/msgrate/msgrate_test.go deleted file mode 100644 index a5c8dd0518..0000000000 --- a/p2p/msgrate/msgrate_test.go +++ /dev/null @@ -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 . - -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)) - } -} diff --git a/p2p/nat/nat.go b/p2p/nat/nat.go deleted file mode 100644 index 2aa1f85585..0000000000 --- a/p2p/nat/nat.go +++ /dev/null @@ -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 . - -// 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 -} diff --git a/p2p/nat/nat_test.go b/p2p/nat/nat_test.go deleted file mode 100644 index 814e6d9e14..0000000000 --- a/p2p/nat/nat_test.go +++ /dev/null @@ -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 . - -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) - } - } - } -} diff --git a/p2p/nat/natpmp.go b/p2p/nat/natpmp.go deleted file mode 100644 index 97601c99dc..0000000000 --- a/p2p/nat/natpmp.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/nat/natupnp.go b/p2p/nat/natupnp.go deleted file mode 100644 index c90c4f3de8..0000000000 --- a/p2p/nat/natupnp.go +++ /dev/null @@ -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 . - -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 - } -} diff --git a/p2p/nat/natupnp_test.go b/p2p/nat/natupnp_test.go deleted file mode 100644 index 9072451d50..0000000000 --- a/p2p/nat/natupnp_test.go +++ /dev/null @@ -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 . - -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": ` - - - - 1 - 0 - - - urn:schemas-upnp-org:device:InternetGatewayDevice:1 - DD-WRT - http://www.dd-wrt.com - Gateway - Asus RT-N16:DD-WRT - Asus RT-N16 - V24 - 0000001 - http://www.dd-wrt.com - uuid:A13AB4C3-3A14-E386-DE6A-EFEA923A06FE - - - urn:schemas-upnp-org:service:Layer3Forwarding:1 - urn:upnp-org:serviceId:L3Forwarding1 - /x_layer3forwarding.xml - /control?Layer3Forwarding - /event?Layer3Forwarding - - - - - urn:schemas-upnp-org:device:WANDevice:1 - WANDevice - DD-WRT - http://www.dd-wrt.com - Gateway - router - http://www.dd-wrt.com - uuid:48FD569B-F9A9-96AE-4EE6-EB403D3DB91A - - - urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 - urn:upnp-org:serviceId:WANCommonIFC1 - /x_wancommoninterfaceconfig.xml - /control?WANCommonInterfaceConfig - /event?WANCommonInterfaceConfig - - - - - urn:schemas-upnp-org:device:WANConnectionDevice:1 - WAN Connection Device - DD-WRT - http://www.dd-wrt.com - Gateway - router - http://www.dd-wrt.com - uuid:CB2471CC-CF2E-9795-8D9C-E87B34C16800 - - - urn:schemas-upnp-org:service:WANIPConnection:1 - urn:upnp-org:serviceId:WANIPConn1 - /x_wanipconnection.xml - /control?WANIPConnection - /event?WANIPConnection - - - - - - - urn:schemas-upnp-org:device:LANDevice:1 - LANDevice - DD-WRT - http://www.dd-wrt.com - Gateway - router - http://www.dd-wrt.com - uuid:04021998-3B35-2BDB-7B3C-99DA4435DA09 - - - urn:schemas-upnp-org:service:LANHostConfigManagement:1 - urn:upnp-org:serviceId:LANHostCfg1 - /x_lanhostconfigmanagement.xml - /control?LANHostConfigManagement - /event?LANHostConfigManagement - - - - - http://{{listenAddr}} - - - `, - // 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": ` - - - - 0 - 1 - - - - `, - }, - } - 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() -} diff --git a/p2p/netutil/addrutil.go b/p2p/netutil/addrutil.go deleted file mode 100644 index fb6d8d2731..0000000000 --- a/p2p/netutil/addrutil.go +++ /dev/null @@ -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 . - -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 - } -} diff --git a/p2p/netutil/error.go b/p2p/netutil/error.go deleted file mode 100644 index 5d3d9bfd65..0000000000 --- a/p2p/netutil/error.go +++ /dev/null @@ -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 . - -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() -} diff --git a/p2p/netutil/error_test.go b/p2p/netutil/error_test.go deleted file mode 100644 index 84d5c2c206..0000000000 --- a/p2p/netutil/error_test.go +++ /dev/null @@ -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 . - -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") - } - } - } -} diff --git a/p2p/netutil/iptrack.go b/p2p/netutil/iptrack.go deleted file mode 100644 index a070499e19..0000000000 --- a/p2p/netutil/iptrack.go +++ /dev/null @@ -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 . - -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) - } - } -} diff --git a/p2p/netutil/iptrack_test.go b/p2p/netutil/iptrack_test.go deleted file mode 100644 index ee3bba861e..0000000000 --- a/p2p/netutil/iptrack_test.go +++ /dev/null @@ -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 . - -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)) - } -} diff --git a/p2p/netutil/net.go b/p2p/netutil/net.go deleted file mode 100644 index d5da3c694f..0000000000 --- a/p2p/netutil/net.go +++ /dev/null @@ -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 . - -// 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() -} diff --git a/p2p/netutil/net_test.go b/p2p/netutil/net_test.go deleted file mode 100644 index 3a6aa081f2..0000000000 --- a/p2p/netutil/net_test.go +++ /dev/null @@ -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 . - -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) - } -} diff --git a/p2p/netutil/toobig_notwindows.go b/p2p/netutil/toobig_notwindows.go deleted file mode 100644 index f9f936ae56..0000000000 --- a/p2p/netutil/toobig_notwindows.go +++ /dev/null @@ -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 . - -//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 -} diff --git a/p2p/netutil/toobig_windows.go b/p2p/netutil/toobig_windows.go deleted file mode 100644 index 652903e83c..0000000000 --- a/p2p/netutil/toobig_windows.go +++ /dev/null @@ -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 . - -//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 -} diff --git a/p2p/nodestate/nodestate.go b/p2p/nodestate/nodestate.go deleted file mode 100644 index 1e1757559c..0000000000 --- a/p2p/nodestate/nodestate.go +++ /dev/null @@ -1,1023 +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 . - -package nodestate - -import ( - "errors" - "reflect" - "sync" - "time" - "unsafe" - - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/ethdb" - "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" -) - -var ( - ErrInvalidField = errors.New("invalid field type") - ErrClosed = errors.New("already closed") -) - -type ( - // NodeStateMachine implements a network node-related event subscription system. - // It can assign binary state flags and fields of arbitrary type to each node and allows - // subscriptions to flag/field changes which can also modify further flags and fields, - // potentially triggering further subscriptions. An operation includes an initial change - // and all resulting subsequent changes and always ends in a consistent global state. - // It is initiated by a "top level" SetState/SetField call that blocks (also blocking other - // top-level functions) until the operation is finished. Callbacks making further changes - // should use the non-blocking SetStateSub/SetFieldSub functions. The tree of events - // resulting from the initial changes is traversed in a breadth-first order, ensuring for - // each subscription callback that all other callbacks caused by the same change triggering - // the current callback are processed before anything is triggered by the changes made in the - // current callback. In practice this logic ensures that all subscriptions "see" events in - // the logical order, callbacks are never called concurrently and "back and forth" effects - // are also possible. The state machine design should ensure that infinite event cycles - // cannot happen. - // The caller can also add timeouts assigned to a certain node and a subset of state flags. - // If the timeout elapses, the flags are reset. If all relevant flags are reset then the timer - // is dropped. State flags with no timeout are persisted in the database if the flag - // descriptor enables saving. If a node has no state flags set at any moment then it is discarded. - // Note: in order to avoid mutex deadlocks the callbacks should never lock a mutex that - // might be locked when the top level SetState/SetField functions are called. If a function - // potentially performs state/field changes then it is recommended to mention this fact in the - // function description, along with whether it should run inside an operation callback. - NodeStateMachine struct { - started, closed bool - lock sync.Mutex - clock mclock.Clock - db ethdb.KeyValueStore - dbNodeKey []byte - nodes map[enode.ID]*nodeInfo - offlineCallbackList []offlineCallback - opFlag bool // an operation has started - opWait *sync.Cond // signaled when the operation ends - opPending []func() // pending callback list of the current operation - - // Registered state flags or fields. Modifications are allowed - // only when the node state machine has not been started. - setup *Setup - fields []*fieldInfo - saveFlags bitMask - - // Installed callbacks. Modifications are allowed only when the - // node state machine has not been started. - stateSubs []stateSub - - // Testing hooks, only for testing purposes. - saveNodeHook func(*nodeInfo) - } - - // Flags represents a set of flags from a certain setup - Flags struct { - mask bitMask - setup *Setup - } - - // Field represents a field from a certain setup - Field struct { - index int - setup *Setup - } - - // flagDefinition describes a node state flag. Each registered instance is automatically - // mapped to a bit of the 64 bit node states. - // If persistent is true then the node is saved when state machine is shutdown. - flagDefinition struct { - name string - persistent bool - } - - // fieldDefinition describes an optional node field of the given type. The contents - // of the field are only retained for each node as long as at least one of the - // state flags is set. - fieldDefinition struct { - name string - ftype reflect.Type - encode func(interface{}) ([]byte, error) - decode func([]byte) (interface{}, error) - } - - // Setup contains the list of flags and fields used by the application - Setup struct { - Version uint - flags []flagDefinition - fields []fieldDefinition - } - - // bitMask describes a node state or state mask. It represents a subset - // of node flags with each bit assigned to a flag index (LSB represents flag 0). - bitMask uint64 - - // StateCallback is a subscription callback which is called when one of the - // state flags that is included in the subscription state mask is changed. - // Note: oldState and newState are also masked with the subscription mask so only - // the relevant bits are included. - StateCallback func(n *enode.Node, oldState, newState Flags) - - // FieldCallback is a subscription callback which is called when the value of - // a specific field is changed. - FieldCallback func(n *enode.Node, state Flags, oldValue, newValue interface{}) - - // nodeInfo contains node state, fields and state timeouts - nodeInfo struct { - node *enode.Node - state bitMask - timeouts []*nodeStateTimeout - fields []interface{} - fieldCount int - db, dirty bool - } - - nodeInfoEnc struct { - Enr enr.Record - Version uint - State bitMask - Fields [][]byte - } - - stateSub struct { - mask bitMask - callback StateCallback - } - - nodeStateTimeout struct { - mask bitMask - timer mclock.Timer - } - - fieldInfo struct { - fieldDefinition - subs []FieldCallback - } - - offlineCallback struct { - node *nodeInfo - state bitMask - fields []interface{} - } -) - -// offlineState is a special state that is assumed to be set before a node is loaded from -// the database and after it is shut down. -const offlineState = bitMask(1) - -// NewFlag creates a new node state flag -func (s *Setup) NewFlag(name string) Flags { - if s.flags == nil { - s.flags = []flagDefinition{{name: "offline"}} - } - f := Flags{mask: bitMask(1) << uint(len(s.flags)), setup: s} - s.flags = append(s.flags, flagDefinition{name: name}) - return f -} - -// NewPersistentFlag creates a new persistent node state flag -func (s *Setup) NewPersistentFlag(name string) Flags { - if s.flags == nil { - s.flags = []flagDefinition{{name: "offline"}} - } - f := Flags{mask: bitMask(1) << uint(len(s.flags)), setup: s} - s.flags = append(s.flags, flagDefinition{name: name, persistent: true}) - return f -} - -// OfflineFlag returns the system-defined offline flag belonging to the given setup -func (s *Setup) OfflineFlag() Flags { - return Flags{mask: offlineState, setup: s} -} - -// NewField creates a new node state field -func (s *Setup) NewField(name string, ftype reflect.Type) Field { - f := Field{index: len(s.fields), setup: s} - s.fields = append(s.fields, fieldDefinition{ - name: name, - ftype: ftype, - }) - return f -} - -// NewPersistentField creates a new persistent node field -func (s *Setup) NewPersistentField(name string, ftype reflect.Type, encode func(interface{}) ([]byte, error), decode func([]byte) (interface{}, error)) Field { - f := Field{index: len(s.fields), setup: s} - s.fields = append(s.fields, fieldDefinition{ - name: name, - ftype: ftype, - encode: encode, - decode: decode, - }) - return f -} - -// flagOp implements binary flag operations and also checks whether the operands belong to the same setup -func flagOp(a, b Flags, trueIfA, trueIfB, trueIfBoth bool) Flags { - if a.setup == nil { - if a.mask != 0 { - panic("Node state flags have no setup reference") - } - a.setup = b.setup - } - if b.setup == nil { - if b.mask != 0 { - panic("Node state flags have no setup reference") - } - b.setup = a.setup - } - if a.setup != b.setup { - panic("Node state flags belong to a different setup") - } - res := Flags{setup: a.setup} - if trueIfA { - res.mask |= a.mask & ^b.mask - } - if trueIfB { - res.mask |= b.mask & ^a.mask - } - if trueIfBoth { - res.mask |= a.mask & b.mask - } - return res -} - -// And returns the set of flags present in both a and b -func (a Flags) And(b Flags) Flags { return flagOp(a, b, false, false, true) } - -// AndNot returns the set of flags present in a but not in b -func (a Flags) AndNot(b Flags) Flags { return flagOp(a, b, true, false, false) } - -// Or returns the set of flags present in either a or b -func (a Flags) Or(b Flags) Flags { return flagOp(a, b, true, true, true) } - -// Xor returns the set of flags present in either a or b but not both -func (a Flags) Xor(b Flags) Flags { return flagOp(a, b, true, true, false) } - -// HasAll returns true if b is a subset of a -func (a Flags) HasAll(b Flags) bool { return flagOp(a, b, false, true, false).mask == 0 } - -// HasNone returns true if a and b have no shared flags -func (a Flags) HasNone(b Flags) bool { return flagOp(a, b, false, false, true).mask == 0 } - -// Equals returns true if a and b have the same flags set -func (a Flags) Equals(b Flags) bool { return flagOp(a, b, true, true, false).mask == 0 } - -// IsEmpty returns true if a has no flags set -func (a Flags) IsEmpty() bool { return a.mask == 0 } - -// MergeFlags merges multiple sets of state flags -func MergeFlags(list ...Flags) Flags { - if len(list) == 0 { - return Flags{} - } - res := list[0] - for i := 1; i < len(list); i++ { - res = res.Or(list[i]) - } - return res -} - -// String returns a list of the names of the flags specified in the bit mask -func (f Flags) String() string { - if f.mask == 0 { - return "[]" - } - s := "[" - comma := false - for index, flag := range f.setup.flags { - if f.mask&(bitMask(1)< 8*int(unsafe.Sizeof(bitMask(0))) { - panic("Too many node state flags") - } - ns := &NodeStateMachine{ - db: db, - dbNodeKey: dbKey, - clock: clock, - setup: setup, - nodes: make(map[enode.ID]*nodeInfo), - fields: make([]*fieldInfo, len(setup.fields)), - } - ns.opWait = sync.NewCond(&ns.lock) - stateNameMap := make(map[string]int, len(setup.flags)) - for index, flag := range setup.flags { - if _, ok := stateNameMap[flag.name]; ok { - panic("Node state flag name collision: " + flag.name) - } - stateNameMap[flag.name] = index - if flag.persistent { - ns.saveFlags |= bitMask(1) << uint(index) - } - } - fieldNameMap := make(map[string]int, len(setup.fields)) - for index, field := range setup.fields { - if _, ok := fieldNameMap[field.name]; ok { - panic("Node field name collision: " + field.name) - } - ns.fields[index] = &fieldInfo{fieldDefinition: field} - fieldNameMap[field.name] = index - } - return ns -} - -// stateMask checks whether the set of flags belongs to the same setup and returns its internal bit mask -func (ns *NodeStateMachine) stateMask(flags Flags) bitMask { - if flags.setup != ns.setup && flags.mask != 0 { - panic("Node state flags belong to a different setup") - } - return flags.mask -} - -// fieldIndex checks whether the field belongs to the same setup and returns its internal index -func (ns *NodeStateMachine) fieldIndex(field Field) int { - if field.setup != ns.setup { - panic("Node field belongs to a different setup") - } - return field.index -} - -// SubscribeState adds a node state subscription. The callback is called while the state -// machine mutex is not held and it is allowed to make further state updates using the -// non-blocking SetStateSub/SetFieldSub functions. All callbacks of an operation are running -// from the thread/goroutine of the initial caller and parallel operations are not permitted. -// Therefore the callback is never called concurrently. It is the responsibility of the -// implemented state logic to avoid deadlocks and to reach a stable state in a finite amount -// of steps. -// State subscriptions should be installed before loading the node database or making the -// first state update. -func (ns *NodeStateMachine) SubscribeState(flags Flags, callback StateCallback) { - ns.lock.Lock() - defer ns.lock.Unlock() - - if ns.started { - panic("state machine already started") - } - ns.stateSubs = append(ns.stateSubs, stateSub{ns.stateMask(flags), callback}) -} - -// SubscribeField adds a node field subscription. Same rules apply as for SubscribeState. -func (ns *NodeStateMachine) SubscribeField(field Field, callback FieldCallback) { - ns.lock.Lock() - defer ns.lock.Unlock() - - if ns.started { - panic("state machine already started") - } - f := ns.fields[ns.fieldIndex(field)] - f.subs = append(f.subs, callback) -} - -// newNode creates a new nodeInfo -func (ns *NodeStateMachine) newNode(n *enode.Node) *nodeInfo { - return &nodeInfo{node: n, fields: make([]interface{}, len(ns.fields))} -} - -// checkStarted checks whether the state machine has already been started and panics otherwise. -func (ns *NodeStateMachine) checkStarted() { - if !ns.started { - panic("state machine not started yet") - } -} - -// Start starts the state machine, enabling state and field operations and disabling -// further subscriptions. -func (ns *NodeStateMachine) Start() { - ns.lock.Lock() - if ns.started { - panic("state machine already started") - } - ns.started = true - if ns.db != nil { - ns.loadFromDb() - } - - ns.opStart() - ns.offlineCallbacks(true) - ns.opFinish() - ns.lock.Unlock() -} - -// Stop stops the state machine and saves its state if a database was supplied -func (ns *NodeStateMachine) Stop() { - ns.lock.Lock() - defer ns.lock.Unlock() - - ns.checkStarted() - if !ns.opStart() { - panic("already closed") - } - for _, node := range ns.nodes { - fields := make([]interface{}, len(node.fields)) - copy(fields, node.fields) - ns.offlineCallbackList = append(ns.offlineCallbackList, offlineCallback{node, node.state, fields}) - } - if ns.db != nil { - ns.saveToDb() - } - ns.offlineCallbacks(false) - ns.closed = true - ns.opFinish() -} - -// loadFromDb loads persisted node states from the database -func (ns *NodeStateMachine) loadFromDb() { - it := ns.db.NewIterator(ns.dbNodeKey, nil) - for it.Next() { - var id enode.ID - if len(it.Key()) != len(ns.dbNodeKey)+len(id) { - log.Error("Node state db entry with invalid length", "found", len(it.Key()), "expected", len(ns.dbNodeKey)+len(id)) - continue - } - copy(id[:], it.Key()[len(ns.dbNodeKey):]) - ns.decodeNode(id, it.Value()) - } -} - -type dummyIdentity enode.ID - -func (id dummyIdentity) Verify(r *enr.Record, sig []byte) error { return nil } -func (id dummyIdentity) NodeAddr(r *enr.Record) []byte { return id[:] } - -// decodeNode decodes a node database entry and adds it to the node set if successful -func (ns *NodeStateMachine) decodeNode(id enode.ID, data []byte) { - var enc nodeInfoEnc - if err := rlp.DecodeBytes(data, &enc); err != nil { - log.Error("Failed to decode node info", "id", id, "error", err) - return - } - n, _ := enode.New(dummyIdentity(id), &enc.Enr) - node := ns.newNode(n) - node.db = true - - if enc.Version != ns.setup.Version { - log.Debug("Removing stored node with unknown version", "current", ns.setup.Version, "stored", enc.Version) - ns.deleteNode(id) - return - } - if len(enc.Fields) > len(ns.setup.fields) { - log.Error("Invalid node field count", "id", id, "stored", len(enc.Fields)) - return - } - // Resolve persisted node fields - for i, encField := range enc.Fields { - if len(encField) == 0 { - continue - } - if decode := ns.fields[i].decode; decode != nil { - if field, err := decode(encField); err == nil { - node.fields[i] = field - node.fieldCount++ - } else { - log.Error("Failed to decode node field", "id", id, "field name", ns.fields[i].name, "error", err) - return - } - } else { - log.Error("Cannot decode node field", "id", id, "field name", ns.fields[i].name) - return - } - } - // It's a compatible node record, add it to set. - ns.nodes[id] = node - node.state = enc.State - fields := make([]interface{}, len(node.fields)) - copy(fields, node.fields) - ns.offlineCallbackList = append(ns.offlineCallbackList, offlineCallback{node, node.state, fields}) - log.Debug("Loaded node state", "id", id, "state", Flags{mask: enc.State, setup: ns.setup}) -} - -// saveNode saves the given node info to the database -func (ns *NodeStateMachine) saveNode(id enode.ID, node *nodeInfo) error { - if ns.db == nil { - return nil - } - - storedState := node.state & ns.saveFlags - for _, t := range node.timeouts { - storedState &= ^t.mask - } - enc := nodeInfoEnc{ - Enr: *node.node.Record(), - Version: ns.setup.Version, - State: storedState, - Fields: make([][]byte, len(ns.fields)), - } - log.Debug("Saved node state", "id", id, "state", Flags{mask: enc.State, setup: ns.setup}) - lastIndex := -1 - for i, f := range node.fields { - if f == nil { - continue - } - encode := ns.fields[i].encode - if encode == nil { - continue - } - blob, err := encode(f) - if err != nil { - return err - } - enc.Fields[i] = blob - lastIndex = i - } - if storedState == 0 && lastIndex == -1 { - if node.db { - node.db = false - ns.deleteNode(id) - } - node.dirty = false - return nil - } - enc.Fields = enc.Fields[:lastIndex+1] - data, err := rlp.EncodeToBytes(&enc) - if err != nil { - return err - } - if err := ns.db.Put(append(ns.dbNodeKey, id[:]...), data); err != nil { - return err - } - node.dirty, node.db = false, true - - if ns.saveNodeHook != nil { - ns.saveNodeHook(node) - } - return nil -} - -// deleteNode removes a node info from the database -func (ns *NodeStateMachine) deleteNode(id enode.ID) { - ns.db.Delete(append(ns.dbNodeKey, id[:]...)) -} - -// saveToDb saves the persistent flags and fields of all nodes that have been changed -func (ns *NodeStateMachine) saveToDb() { - for id, node := range ns.nodes { - if node.dirty { - err := ns.saveNode(id, node) - if err != nil { - log.Error("Failed to save node", "id", id, "error", err) - } - } - } -} - -// updateEnode updates the enode entry belonging to the given node if it already exists -func (ns *NodeStateMachine) updateEnode(n *enode.Node) (enode.ID, *nodeInfo) { - id := n.ID() - node := ns.nodes[id] - if node != nil && n.Seq() > node.node.Seq() { - node.node = n - node.dirty = true - } - return id, node -} - -// Persist saves the persistent state and fields of the given node immediately -func (ns *NodeStateMachine) Persist(n *enode.Node) error { - ns.lock.Lock() - defer ns.lock.Unlock() - - ns.checkStarted() - if id, node := ns.updateEnode(n); node != nil && node.dirty { - err := ns.saveNode(id, node) - if err != nil { - log.Error("Failed to save node", "id", id, "error", err) - } - return err - } - return nil -} - -// SetState updates the given node state flags and blocks until the operation is finished. -// If a flag with a timeout is set again, the operation removes or replaces the existing timeout. -func (ns *NodeStateMachine) SetState(n *enode.Node, setFlags, resetFlags Flags, timeout time.Duration) error { - ns.lock.Lock() - defer ns.lock.Unlock() - - if !ns.opStart() { - return ErrClosed - } - ns.setState(n, setFlags, resetFlags, timeout) - ns.opFinish() - return nil -} - -// SetStateSub updates the given node state flags without blocking (should be called -// from a subscription/operation callback). -func (ns *NodeStateMachine) SetStateSub(n *enode.Node, setFlags, resetFlags Flags, timeout time.Duration) { - ns.lock.Lock() - defer ns.lock.Unlock() - - ns.opCheck() - ns.setState(n, setFlags, resetFlags, timeout) -} - -func (ns *NodeStateMachine) setState(n *enode.Node, setFlags, resetFlags Flags, timeout time.Duration) { - ns.checkStarted() - set, reset := ns.stateMask(setFlags), ns.stateMask(resetFlags) - id, node := ns.updateEnode(n) - if node == nil { - if set == 0 { - return - } - node = ns.newNode(n) - ns.nodes[id] = node - } - oldState := node.state - newState := (node.state & (^reset)) | set - changed := oldState ^ newState - node.state = newState - - // Remove the timeout callbacks for all reset and set flags, - // even they are not existent(it's noop). - ns.removeTimeouts(node, set|reset) - - // Register the timeout callback if required - if timeout != 0 && set != 0 { - ns.addTimeout(n, set, timeout) - } - if newState == oldState { - return - } - if newState == 0 && node.fieldCount == 0 { - delete(ns.nodes, id) - if node.db { - ns.deleteNode(id) - } - } else { - if changed&ns.saveFlags != 0 { - node.dirty = true - } - } - callback := func() { - for _, sub := range ns.stateSubs { - if changed&sub.mask != 0 { - sub.callback(n, Flags{mask: oldState & sub.mask, setup: ns.setup}, Flags{mask: newState & sub.mask, setup: ns.setup}) - } - } - } - ns.opPending = append(ns.opPending, callback) -} - -// opCheck checks whether an operation is active -func (ns *NodeStateMachine) opCheck() { - if !ns.opFlag { - panic("Operation has not started") - } -} - -// opStart waits until other operations are finished and starts a new one -func (ns *NodeStateMachine) opStart() bool { - for ns.opFlag { - ns.opWait.Wait() - } - if ns.closed { - return false - } - ns.opFlag = true - return true -} - -// opFinish finishes the current operation by running all pending callbacks. -// Callbacks resulting from a state/field change performed in a previous callback are always -// put at the end of the pending list and therefore processed after all callbacks resulting -// from the previous state/field change. -func (ns *NodeStateMachine) opFinish() { - for len(ns.opPending) != 0 { - list := ns.opPending - ns.lock.Unlock() - for _, cb := range list { - cb() - } - ns.lock.Lock() - ns.opPending = ns.opPending[len(list):] - } - ns.opPending = nil - ns.opFlag = false - ns.opWait.Broadcast() -} - -// Operation calls the given function as an operation callback. This allows the caller -// to start an operation with multiple initial changes. The same rules apply as for -// subscription callbacks. -func (ns *NodeStateMachine) Operation(fn func()) error { - ns.lock.Lock() - started := ns.opStart() - ns.lock.Unlock() - if !started { - return ErrClosed - } - fn() - ns.lock.Lock() - ns.opFinish() - ns.lock.Unlock() - return nil -} - -// offlineCallbacks calls state update callbacks at startup or shutdown -func (ns *NodeStateMachine) offlineCallbacks(start bool) { - for _, cb := range ns.offlineCallbackList { - cb := cb - callback := func() { - for _, sub := range ns.stateSubs { - offState := offlineState & sub.mask - onState := cb.state & sub.mask - if offState == onState { - continue - } - if start { - sub.callback(cb.node.node, Flags{mask: offState, setup: ns.setup}, Flags{mask: onState, setup: ns.setup}) - } else { - sub.callback(cb.node.node, Flags{mask: onState, setup: ns.setup}, Flags{mask: offState, setup: ns.setup}) - } - } - for i, f := range cb.fields { - if f == nil || ns.fields[i].subs == nil { - continue - } - for _, fsub := range ns.fields[i].subs { - if start { - fsub(cb.node.node, Flags{mask: offlineState, setup: ns.setup}, nil, f) - } else { - fsub(cb.node.node, Flags{mask: offlineState, setup: ns.setup}, f, nil) - } - } - } - } - ns.opPending = append(ns.opPending, callback) - } - ns.offlineCallbackList = nil -} - -// AddTimeout adds a node state timeout associated to the given state flag(s). -// After the specified time interval, the relevant states will be reset. -func (ns *NodeStateMachine) AddTimeout(n *enode.Node, flags Flags, timeout time.Duration) error { - ns.lock.Lock() - defer ns.lock.Unlock() - - ns.checkStarted() - if ns.closed { - return ErrClosed - } - ns.addTimeout(n, ns.stateMask(flags), timeout) - return nil -} - -// addTimeout adds a node state timeout associated to the given state flag(s). -func (ns *NodeStateMachine) addTimeout(n *enode.Node, mask bitMask, timeout time.Duration) { - _, node := ns.updateEnode(n) - if node == nil { - return - } - mask &= node.state - if mask == 0 { - return - } - ns.removeTimeouts(node, mask) - t := &nodeStateTimeout{mask: mask} - t.timer = ns.clock.AfterFunc(timeout, func() { - ns.lock.Lock() - defer ns.lock.Unlock() - - if !ns.opStart() { - return - } - ns.setState(n, Flags{}, Flags{mask: t.mask, setup: ns.setup}, 0) - ns.opFinish() - }) - node.timeouts = append(node.timeouts, t) - if mask&ns.saveFlags != 0 { - node.dirty = true - } -} - -// removeTimeout removes node state timeouts associated to the given state flag(s). -// If a timeout was associated to multiple flags which are not all included in the -// specified remove mask then only the included flags are de-associated and the timer -// stays active. -func (ns *NodeStateMachine) removeTimeouts(node *nodeInfo, mask bitMask) { - for i := 0; i < len(node.timeouts); i++ { - t := node.timeouts[i] - match := t.mask & mask - if match == 0 { - continue - } - t.mask -= match - if t.mask != 0 { - continue - } - t.timer.Stop() - node.timeouts[i] = node.timeouts[len(node.timeouts)-1] - node.timeouts = node.timeouts[:len(node.timeouts)-1] - i-- - if match&ns.saveFlags != 0 { - node.dirty = true - } - } -} - -// GetField retrieves the given field of the given node. Note that when used in a -// subscription callback the result can be out of sync with the state change represented -// by the callback parameters so extra safety checks might be necessary. -func (ns *NodeStateMachine) GetField(n *enode.Node, field Field) interface{} { - ns.lock.Lock() - defer ns.lock.Unlock() - - ns.checkStarted() - if ns.closed { - return nil - } - if _, node := ns.updateEnode(n); node != nil { - return node.fields[ns.fieldIndex(field)] - } - return nil -} - -// GetState retrieves the current state of the given node. Note that when used in a -// subscription callback the result can be out of sync with the state change represented -// by the callback parameters so extra safety checks might be necessary. -func (ns *NodeStateMachine) GetState(n *enode.Node) Flags { - ns.lock.Lock() - defer ns.lock.Unlock() - - ns.checkStarted() - if ns.closed { - return Flags{} - } - if _, node := ns.updateEnode(n); node != nil { - return Flags{mask: node.state, setup: ns.setup} - } - return Flags{} -} - -// SetField sets the given field of the given node and blocks until the operation is finished -func (ns *NodeStateMachine) SetField(n *enode.Node, field Field, value interface{}) error { - ns.lock.Lock() - defer ns.lock.Unlock() - - if !ns.opStart() { - return ErrClosed - } - err := ns.setField(n, field, value) - ns.opFinish() - return err -} - -// SetFieldSub sets the given field of the given node without blocking (should be called -// from a subscription/operation callback). -func (ns *NodeStateMachine) SetFieldSub(n *enode.Node, field Field, value interface{}) error { - ns.lock.Lock() - defer ns.lock.Unlock() - - ns.opCheck() - return ns.setField(n, field, value) -} - -func (ns *NodeStateMachine) setField(n *enode.Node, field Field, value interface{}) error { - ns.checkStarted() - id, node := ns.updateEnode(n) - if node == nil { - if value == nil { - return nil - } - node = ns.newNode(n) - ns.nodes[id] = node - } - fieldIndex := ns.fieldIndex(field) - f := ns.fields[fieldIndex] - if value != nil && reflect.TypeOf(value) != f.ftype { - log.Error("Invalid field type", "type", reflect.TypeOf(value), "required", f.ftype) - return ErrInvalidField - } - oldValue := node.fields[fieldIndex] - if value == oldValue { - return nil - } - if oldValue != nil { - node.fieldCount-- - } - if value != nil { - node.fieldCount++ - } - node.fields[fieldIndex] = value - if node.state == 0 && node.fieldCount == 0 { - delete(ns.nodes, id) - if node.db { - ns.deleteNode(id) - } - } else { - if f.encode != nil { - node.dirty = true - } - } - state := node.state - callback := func() { - for _, cb := range f.subs { - cb(n, Flags{mask: state, setup: ns.setup}, oldValue, value) - } - } - ns.opPending = append(ns.opPending, callback) - return nil -} - -// ForEach calls the callback for each node having all of the required and none of the -// disabled flags set. -// Note that this callback is not an operation callback but ForEach can be called from an -// Operation callback or Operation can also be called from a ForEach callback if necessary. -func (ns *NodeStateMachine) ForEach(requireFlags, disableFlags Flags, cb func(n *enode.Node, state Flags)) { - ns.lock.Lock() - ns.checkStarted() - type callback struct { - node *enode.Node - state bitMask - } - require, disable := ns.stateMask(requireFlags), ns.stateMask(disableFlags) - var callbacks []callback - for _, node := range ns.nodes { - if node.state&require == require && node.state&disable == 0 { - callbacks = append(callbacks, callback{node.node, node.state & (require | disable)}) - } - } - ns.lock.Unlock() - for _, c := range callbacks { - cb(c.node, Flags{mask: c.state, setup: ns.setup}) - } -} - -// GetNode returns the enode currently associated with the given ID -func (ns *NodeStateMachine) GetNode(id enode.ID) *enode.Node { - ns.lock.Lock() - defer ns.lock.Unlock() - - ns.checkStarted() - if node := ns.nodes[id]; node != nil { - return node.node - } - return nil -} - -// AddLogMetrics adds logging and/or metrics for nodes entering, exiting and currently -// being in a given set specified by required and disabled state flags -func (ns *NodeStateMachine) AddLogMetrics(requireFlags, disableFlags Flags, name string, inMeter, outMeter metrics.Meter, gauge metrics.Gauge) { - var count int64 - ns.SubscribeState(requireFlags.Or(disableFlags), func(n *enode.Node, oldState, newState Flags) { - oldMatch := oldState.HasAll(requireFlags) && oldState.HasNone(disableFlags) - newMatch := newState.HasAll(requireFlags) && newState.HasNone(disableFlags) - if newMatch == oldMatch { - return - } - - if newMatch { - count++ - if name != "" { - log.Debug("Node entered", "set", name, "id", n.ID(), "count", count) - } - if inMeter != nil { - inMeter.Mark(1) - } - } else { - count-- - if name != "" { - log.Debug("Node left", "set", name, "id", n.ID(), "count", count) - } - if outMeter != nil { - outMeter.Mark(1) - } - } - if gauge != nil { - gauge.Update(count) - } - }) -} diff --git a/p2p/nodestate/nodestate_test.go b/p2p/nodestate/nodestate_test.go deleted file mode 100644 index d06ad755e2..0000000000 --- a/p2p/nodestate/nodestate_test.go +++ /dev/null @@ -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 . - -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) -} diff --git a/p2p/peer.go b/p2p/peer.go deleted file mode 100644 index 65a7903f58..0000000000 --- a/p2p/peer.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/peer_error.go b/p2p/peer_error.go deleted file mode 100644 index ebc59de251..0000000000 --- a/p2p/peer_error.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/peer_test.go b/p2p/peer_test.go deleted file mode 100644 index 4308bbd2eb..0000000000 --- a/p2p/peer_test.go +++ /dev/null @@ -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 . - -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 - } - } - } -} diff --git a/p2p/protocol.go b/p2p/protocol.go deleted file mode 100644 index 9bb6785a22..0000000000 --- a/p2p/protocol.go +++ /dev/null @@ -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 . - -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) -} diff --git a/p2p/rlpx/buffer.go b/p2p/rlpx/buffer.go deleted file mode 100644 index bb38e10577..0000000000 --- a/p2p/rlpx/buffer.go +++ /dev/null @@ -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 . - -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) -} diff --git a/p2p/rlpx/buffer_test.go b/p2p/rlpx/buffer_test.go deleted file mode 100644 index 9fee4172bd..0000000000 --- a/p2p/rlpx/buffer_test.go +++ /dev/null @@ -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 . - -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) -} diff --git a/p2p/rlpx/rlpx.go b/p2p/rlpx/rlpx.go deleted file mode 100644 index 8bd6f64b9b..0000000000 --- a/p2p/rlpx/rlpx.go +++ /dev/null @@ -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 . - -// 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 -} diff --git a/p2p/rlpx/rlpx_test.go b/p2p/rlpx/rlpx_test.go deleted file mode 100644 index 136cb1b5bf..0000000000 --- a/p2p/rlpx/rlpx_test.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/server.go b/p2p/server.go deleted file mode 100644 index 8f42765a8c..0000000000 --- a/p2p/server.go +++ /dev/null @@ -1,1134 +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 . - -// Package p2p implements the Ethereum p2p network protocols. -package p2p - -import ( - "bytes" - "crypto/ecdsa" - "encoding/hex" - "errors" - "fmt" - "net" - "sync" - "sync/atomic" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p/discover" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/enr" - "github.com/ethereum/go-ethereum/p2p/nat" - "github.com/ethereum/go-ethereum/p2p/netutil" - "golang.org/x/exp/slices" -) - -const ( - defaultDialTimeout = 15 * time.Second - - // This is the fairness knob for the discovery mixer. When looking for peers, we'll - // wait this long for a single source of candidates before moving on and trying other - // sources. - discmixTimeout = 5 * time.Second - - // Connectivity defaults. - defaultMaxPendingPeers = 50 - defaultDialRatio = 3 - - // This time limits inbound connection attempts per source IP. - inboundThrottleTime = 30 * time.Second - - // Maximum time allowed for reading a complete message. - // This is effectively the amount of time a connection can be idle. - frameReadTimeout = 30 * time.Second - - // Maximum amount of time allowed for writing a complete message. - frameWriteTimeout = 20 * time.Second -) - -var ( - errServerStopped = errors.New("server stopped") - errEncHandshakeError = errors.New("rlpx enc error") - errProtoHandshakeError = errors.New("rlpx proto error") -) - -// Config holds Server options. -type Config struct { - // This field must be set to a valid secp256k1 private key. - PrivateKey *ecdsa.PrivateKey `toml:"-"` - - // MaxPeers is the maximum number of peers that can be - // connected. It must be greater than zero. - MaxPeers int - - // MaxPendingPeers is the maximum number of peers that can be pending in the - // handshake phase, counted separately for inbound and outbound connections. - // Zero defaults to preset values. - MaxPendingPeers int `toml:",omitempty"` - - // DialRatio controls the ratio of inbound to dialed connections. - // Example: a DialRatio of 2 allows 1/2 of connections to be dialed. - // Setting DialRatio to zero defaults it to 3. - DialRatio int `toml:",omitempty"` - - // NoDiscovery can be used to disable the peer discovery mechanism. - // Disabling is useful for protocol debugging (manual topology). - NoDiscovery bool - - // DiscoveryV4 specifies whether V4 discovery should be started. - DiscoveryV4 bool `toml:",omitempty"` - - // DiscoveryV5 specifies whether the new topic-discovery based V5 discovery - // protocol should be started or not. - DiscoveryV5 bool `toml:",omitempty"` - - // Name sets the node name of this server. - Name string `toml:"-"` - - // BootstrapNodes are used to establish connectivity - // with the rest of the network. - BootstrapNodes []*enode.Node - - // BootstrapNodesV5 are used to establish connectivity - // with the rest of the network using the V5 discovery - // protocol. - BootstrapNodesV5 []*enode.Node `toml:",omitempty"` - - // Static nodes are used as pre-configured connections which are always - // maintained and re-connected on disconnects. - StaticNodes []*enode.Node - - // Trusted nodes are used as pre-configured connections which are always - // allowed to connect, even above the peer limit. - TrustedNodes []*enode.Node - - // Connectivity can be restricted to certain IP networks. - // If this option is set to a non-nil value, only hosts which match one of the - // IP networks contained in the list are considered. - NetRestrict *netutil.Netlist `toml:",omitempty"` - - // NodeDatabase is the path to the database containing the previously seen - // live nodes in the network. - NodeDatabase string `toml:",omitempty"` - - // Protocols should contain the protocols supported - // by the server. Matching protocols are launched for - // each peer. - Protocols []Protocol `toml:"-" json:"-"` - - // If ListenAddr is set to a non-nil address, the server - // will listen for incoming connections. - // - // If the port is zero, the operating system will pick a port. The - // ListenAddr field will be updated with the actual address when - // the server is started. - ListenAddr string - - // If DiscAddr is set to a non-nil value, the server will use ListenAddr - // for TCP and DiscAddr for the UDP discovery protocol. - DiscAddr string - - // If set to a non-nil value, the given NAT port mapper - // is used to make the listening port available to the - // Internet. - NAT nat.Interface `toml:",omitempty"` - - // If Dialer is set to a non-nil value, the given Dialer - // is used to dial outbound peer connections. - Dialer NodeDialer `toml:"-"` - - // If NoDial is true, the server will not dial any peers. - NoDial bool `toml:",omitempty"` - - // If EnableMsgEvents is set then the server will emit PeerEvents - // whenever a message is sent to or received from a peer - EnableMsgEvents bool - - // Logger is a custom logger to use with the p2p.Server. - Logger log.Logger `toml:",omitempty"` - - clock mclock.Clock -} - -// Server manages all peer connections. -type Server struct { - // Config fields may not be modified while the server is running. - Config - - // Hooks for testing. These are useful because we can inhibit - // the whole protocol stack. - newTransport func(net.Conn, *ecdsa.PublicKey) transport - newPeerHook func(*Peer) - listenFunc func(network, addr string) (net.Listener, error) - - lock sync.Mutex // protects running - running bool - - listener net.Listener - ourHandshake *protoHandshake - loopWG sync.WaitGroup // loop, listenLoop - peerFeed event.Feed - log log.Logger - - nodedb *enode.DB - localnode *enode.LocalNode - ntab *discover.UDPv4 - DiscV5 *discover.UDPv5 - discmix *enode.FairMix - dialsched *dialScheduler - - // This is read by the NAT port mapping loop. - portMappingRegister chan *portMapping - - // Channels into the run loop. - quit chan struct{} - addtrusted chan *enode.Node - removetrusted chan *enode.Node - peerOp chan peerOpFunc - peerOpDone chan struct{} - delpeer chan peerDrop - checkpointPostHandshake chan *conn - checkpointAddPeer chan *conn - - // State of run loop and listenLoop. - inboundHistory expHeap -} - -type peerOpFunc func(map[enode.ID]*Peer) - -type peerDrop struct { - *Peer - err error - requested bool // true if signaled by the peer -} - -type connFlag int32 - -const ( - dynDialedConn connFlag = 1 << iota - staticDialedConn - inboundConn - trustedConn -) - -// conn wraps a network connection with information gathered -// during the two handshakes. -type conn struct { - fd net.Conn - transport - node *enode.Node - flags connFlag - cont chan error // The run loop uses cont to signal errors to SetupConn. - caps []Cap // valid after the protocol handshake - name string // valid after the protocol handshake -} - -type transport interface { - // The two handshakes. - doEncHandshake(prv *ecdsa.PrivateKey) (*ecdsa.PublicKey, error) - doProtoHandshake(our *protoHandshake) (*protoHandshake, error) - // The MsgReadWriter can only be used after the encryption - // handshake has completed. The code uses conn.id to track this - // by setting it to a non-nil value after the encryption handshake. - MsgReadWriter - // transports must provide Close because we use MsgPipe in some of - // the tests. Closing the actual network connection doesn't do - // anything in those tests because MsgPipe doesn't use it. - close(err error) -} - -func (c *conn) String() string { - s := c.flags.String() - if (c.node.ID() != enode.ID{}) { - s += " " + c.node.ID().String() - } - s += " " + c.fd.RemoteAddr().String() - return s -} - -func (f connFlag) String() string { - s := "" - if f&trustedConn != 0 { - s += "-trusted" - } - if f&dynDialedConn != 0 { - s += "-dyndial" - } - if f&staticDialedConn != 0 { - s += "-staticdial" - } - if f&inboundConn != 0 { - s += "-inbound" - } - if s != "" { - s = s[1:] - } - return s -} - -func (c *conn) is(f connFlag) bool { - flags := connFlag(atomic.LoadInt32((*int32)(&c.flags))) - return flags&f != 0 -} - -func (c *conn) set(f connFlag, val bool) { - for { - oldFlags := connFlag(atomic.LoadInt32((*int32)(&c.flags))) - flags := oldFlags - if val { - flags |= f - } else { - flags &= ^f - } - if atomic.CompareAndSwapInt32((*int32)(&c.flags), int32(oldFlags), int32(flags)) { - return - } - } -} - -// LocalNode returns the local node record. -func (srv *Server) LocalNode() *enode.LocalNode { - return srv.localnode -} - -// Peers returns all connected peers. -func (srv *Server) Peers() []*Peer { - var ps []*Peer - srv.doPeerOp(func(peers map[enode.ID]*Peer) { - for _, p := range peers { - ps = append(ps, p) - } - }) - return ps -} - -// PeerCount returns the number of connected peers. -func (srv *Server) PeerCount() int { - var count int - srv.doPeerOp(func(ps map[enode.ID]*Peer) { - count = len(ps) - }) - return count -} - -// AddPeer adds the given node to the static node set. When there is room in the peer set, -// the server will connect to the node. If the connection fails for any reason, the server -// will attempt to reconnect the peer. -func (srv *Server) AddPeer(node *enode.Node) { - srv.dialsched.addStatic(node) -} - -// RemovePeer removes a node from the static node set. It also disconnects from the given -// node if it is currently connected as a peer. -// -// This method blocks until all protocols have exited and the peer is removed. Do not use -// RemovePeer in protocol implementations, call Disconnect on the Peer instead. -func (srv *Server) RemovePeer(node *enode.Node) { - var ( - ch chan *PeerEvent - sub event.Subscription - ) - // Disconnect the peer on the main loop. - srv.doPeerOp(func(peers map[enode.ID]*Peer) { - srv.dialsched.removeStatic(node) - if peer := peers[node.ID()]; peer != nil { - ch = make(chan *PeerEvent, 1) - sub = srv.peerFeed.Subscribe(ch) - peer.Disconnect(DiscRequested) - } - }) - // Wait for the peer connection to end. - if ch != nil { - defer sub.Unsubscribe() - for ev := range ch { - if ev.Peer == node.ID() && ev.Type == PeerEventTypeDrop { - return - } - } - } -} - -// AddTrustedPeer adds the given node to a reserved trusted list which allows the -// node to always connect, even if the slot are full. -func (srv *Server) AddTrustedPeer(node *enode.Node) { - select { - case srv.addtrusted <- node: - case <-srv.quit: - } -} - -// RemoveTrustedPeer removes the given node from the trusted peer set. -func (srv *Server) RemoveTrustedPeer(node *enode.Node) { - select { - case srv.removetrusted <- node: - case <-srv.quit: - } -} - -// SubscribeEvents subscribes the given channel to peer events -func (srv *Server) SubscribeEvents(ch chan *PeerEvent) event.Subscription { - return srv.peerFeed.Subscribe(ch) -} - -// Self returns the local node's endpoint information. -func (srv *Server) Self() *enode.Node { - srv.lock.Lock() - ln := srv.localnode - srv.lock.Unlock() - - if ln == nil { - return enode.NewV4(&srv.PrivateKey.PublicKey, net.ParseIP("0.0.0.0"), 0, 0) - } - return ln.Node() -} - -// Stop terminates the server and all active peer connections. -// It blocks until all active connections have been closed. -func (srv *Server) Stop() { - srv.lock.Lock() - if !srv.running { - srv.lock.Unlock() - return - } - srv.running = false - if srv.listener != nil { - // this unblocks listener Accept - srv.listener.Close() - } - close(srv.quit) - srv.lock.Unlock() - srv.loopWG.Wait() -} - -// sharedUDPConn implements a shared connection. Write sends messages to the underlying connection while read returns -// messages that were found unprocessable and sent to the unhandled channel by the primary listener. -type sharedUDPConn struct { - *net.UDPConn - unhandled chan discover.ReadPacket -} - -// ReadFromUDP implements discover.UDPConn -func (s *sharedUDPConn) ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) { - packet, ok := <-s.unhandled - if !ok { - return 0, nil, errors.New("connection was closed") - } - l := len(packet.Data) - if l > len(b) { - l = len(b) - } - copy(b[:l], packet.Data[:l]) - return l, packet.Addr, nil -} - -// Close implements discover.UDPConn -func (s *sharedUDPConn) Close() error { - return nil -} - -// Start starts running the server. -// Servers can not be re-used after stopping. -func (srv *Server) Start() (err error) { - srv.lock.Lock() - defer srv.lock.Unlock() - if srv.running { - return errors.New("server already running") - } - srv.running = true - srv.log = srv.Logger - if srv.log == nil { - srv.log = log.Root() - } - if srv.clock == nil { - srv.clock = mclock.System{} - } - if srv.NoDial && srv.ListenAddr == "" { - srv.log.Warn("P2P server will be useless, neither dialing nor listening") - } - - // static fields - if srv.PrivateKey == nil { - return errors.New("Server.PrivateKey must be set to a non-nil key") - } - if srv.newTransport == nil { - srv.newTransport = newRLPX - } - if srv.listenFunc == nil { - srv.listenFunc = net.Listen - } - srv.quit = make(chan struct{}) - srv.delpeer = make(chan peerDrop) - srv.checkpointPostHandshake = make(chan *conn) - srv.checkpointAddPeer = make(chan *conn) - srv.addtrusted = make(chan *enode.Node) - srv.removetrusted = make(chan *enode.Node) - srv.peerOp = make(chan peerOpFunc) - srv.peerOpDone = make(chan struct{}) - - if err := srv.setupLocalNode(); err != nil { - return err - } - srv.setupPortMapping() - - if srv.ListenAddr != "" { - if err := srv.setupListening(); err != nil { - return err - } - } - if err := srv.setupDiscovery(); err != nil { - return err - } - srv.setupDialScheduler() - - srv.loopWG.Add(1) - go srv.run() - return nil -} - -func (srv *Server) setupLocalNode() error { - // Create the devp2p handshake. - pubkey := crypto.FromECDSAPub(&srv.PrivateKey.PublicKey) - srv.ourHandshake = &protoHandshake{Version: baseProtocolVersion, Name: srv.Name, ID: pubkey[1:]} - for _, p := range srv.Protocols { - srv.ourHandshake.Caps = append(srv.ourHandshake.Caps, p.cap()) - } - slices.SortFunc(srv.ourHandshake.Caps, Cap.Cmp) - - // Create the local node. - db, err := enode.OpenDB(srv.NodeDatabase) - if err != nil { - return err - } - srv.nodedb = db - srv.localnode = enode.NewLocalNode(db, srv.PrivateKey) - srv.localnode.SetFallbackIP(net.IP{127, 0, 0, 1}) - // TODO: check conflicts - for _, p := range srv.Protocols { - for _, e := range p.Attributes { - srv.localnode.Set(e) - } - } - return nil -} - -func (srv *Server) setupDiscovery() error { - srv.discmix = enode.NewFairMix(discmixTimeout) - - // Don't listen on UDP endpoint if DHT is disabled. - if srv.NoDiscovery { - return nil - } - conn, err := srv.setupUDPListening() - if err != nil { - return err - } - - var ( - sconn discover.UDPConn = conn - unhandled chan discover.ReadPacket - ) - // If both versions of discovery are running, setup a shared - // connection, so v5 can read unhandled messages from v4. - if srv.DiscoveryV4 && srv.DiscoveryV5 { - unhandled = make(chan discover.ReadPacket, 100) - sconn = &sharedUDPConn{conn, unhandled} - } - - // Start discovery services. - if srv.DiscoveryV4 { - cfg := discover.Config{ - PrivateKey: srv.PrivateKey, - NetRestrict: srv.NetRestrict, - Bootnodes: srv.BootstrapNodes, - Unhandled: unhandled, - Log: srv.log, - } - ntab, err := discover.ListenV4(conn, srv.localnode, cfg) - if err != nil { - return err - } - srv.ntab = ntab - srv.discmix.AddSource(ntab.RandomNodes()) - } - if srv.DiscoveryV5 { - cfg := discover.Config{ - PrivateKey: srv.PrivateKey, - NetRestrict: srv.NetRestrict, - Bootnodes: srv.BootstrapNodesV5, - Log: srv.log, - } - srv.DiscV5, err = discover.ListenV5(sconn, srv.localnode, cfg) - if err != nil { - return err - } - } - - // Add protocol-specific discovery sources. - added := make(map[string]bool) - for _, proto := range srv.Protocols { - if proto.DialCandidates != nil && !added[proto.Name] { - srv.discmix.AddSource(proto.DialCandidates) - added[proto.Name] = true - } - } - return nil -} - -func (srv *Server) setupDialScheduler() { - config := dialConfig{ - self: srv.localnode.ID(), - maxDialPeers: srv.maxDialedConns(), - maxActiveDials: srv.MaxPendingPeers, - log: srv.Logger, - netRestrict: srv.NetRestrict, - dialer: srv.Dialer, - clock: srv.clock, - } - if srv.ntab != nil { - config.resolver = srv.ntab - } - if config.dialer == nil { - config.dialer = tcpDialer{&net.Dialer{Timeout: defaultDialTimeout}} - } - srv.dialsched = newDialScheduler(config, srv.discmix, srv.SetupConn) - for _, n := range srv.StaticNodes { - srv.dialsched.addStatic(n) - } -} - -func (srv *Server) maxInboundConns() int { - return srv.MaxPeers - srv.maxDialedConns() -} - -func (srv *Server) maxDialedConns() (limit int) { - if srv.NoDial || srv.MaxPeers == 0 { - return 0 - } - if srv.DialRatio == 0 { - limit = srv.MaxPeers / defaultDialRatio - } else { - limit = srv.MaxPeers / srv.DialRatio - } - if limit == 0 { - limit = 1 - } - return limit -} - -func (srv *Server) setupListening() error { - // Launch the listener. - listener, err := srv.listenFunc("tcp", srv.ListenAddr) - if err != nil { - return err - } - srv.listener = listener - srv.ListenAddr = listener.Addr().String() - - // Update the local node record and map the TCP listening port if NAT is configured. - tcp, isTCP := listener.Addr().(*net.TCPAddr) - if isTCP { - srv.localnode.Set(enr.TCP(tcp.Port)) - if !tcp.IP.IsLoopback() && !tcp.IP.IsPrivate() { - srv.portMappingRegister <- &portMapping{ - protocol: "TCP", - name: "ethereum p2p", - port: tcp.Port, - } - } - } - - srv.loopWG.Add(1) - go srv.listenLoop() - return nil -} - -func (srv *Server) setupUDPListening() (*net.UDPConn, error) { - listenAddr := srv.ListenAddr - - // Use an alternate listening address for UDP if - // a custom discovery address is configured. - if srv.DiscAddr != "" { - listenAddr = srv.DiscAddr - } - addr, err := net.ResolveUDPAddr("udp", listenAddr) - if err != nil { - return nil, err - } - conn, err := net.ListenUDP("udp", addr) - if err != nil { - return nil, err - } - laddr := conn.LocalAddr().(*net.UDPAddr) - srv.localnode.SetFallbackUDP(laddr.Port) - srv.log.Debug("UDP listener up", "addr", laddr) - if !laddr.IP.IsLoopback() && !laddr.IP.IsPrivate() { - srv.portMappingRegister <- &portMapping{ - protocol: "UDP", - name: "ethereum peer discovery", - port: laddr.Port, - } - } - - return conn, nil -} - -// doPeerOp runs fn on the main loop. -func (srv *Server) doPeerOp(fn peerOpFunc) { - select { - case srv.peerOp <- fn: - <-srv.peerOpDone - case <-srv.quit: - } -} - -// run is the main loop of the server. -func (srv *Server) run() { - srv.log.Info("Started P2P networking", "self", srv.localnode.Node().URLv4()) - defer srv.loopWG.Done() - defer srv.nodedb.Close() - defer srv.discmix.Close() - defer srv.dialsched.stop() - - var ( - peers = make(map[enode.ID]*Peer) - inboundCount = 0 - trusted = make(map[enode.ID]bool, len(srv.TrustedNodes)) - ) - // Put trusted nodes into a map to speed up checks. - // Trusted peers are loaded on startup or added via AddTrustedPeer RPC. - for _, n := range srv.TrustedNodes { - trusted[n.ID()] = true - } - -running: - for { - select { - case <-srv.quit: - // The server was stopped. Run the cleanup logic. - break running - - case n := <-srv.addtrusted: - // This channel is used by AddTrustedPeer to add a node - // to the trusted node set. - srv.log.Trace("Adding trusted node", "node", n) - trusted[n.ID()] = true - if p, ok := peers[n.ID()]; ok { - p.rw.set(trustedConn, true) - } - - case n := <-srv.removetrusted: - // This channel is used by RemoveTrustedPeer to remove a node - // from the trusted node set. - srv.log.Trace("Removing trusted node", "node", n) - delete(trusted, n.ID()) - if p, ok := peers[n.ID()]; ok { - p.rw.set(trustedConn, false) - } - - case op := <-srv.peerOp: - // This channel is used by Peers and PeerCount. - op(peers) - srv.peerOpDone <- struct{}{} - - case c := <-srv.checkpointPostHandshake: - // A connection has passed the encryption handshake so - // the remote identity is known (but hasn't been verified yet). - if trusted[c.node.ID()] { - // Ensure that the trusted flag is set before checking against MaxPeers. - c.flags |= trustedConn - } - // TODO: track in-progress inbound node IDs (pre-Peer) to avoid dialing them. - c.cont <- srv.postHandshakeChecks(peers, inboundCount, c) - - case c := <-srv.checkpointAddPeer: - // At this point the connection is past the protocol handshake. - // Its capabilities are known and the remote identity is verified. - err := srv.addPeerChecks(peers, inboundCount, c) - if err == nil { - // The handshakes are done and it passed all checks. - p := srv.launchPeer(c) - peers[c.node.ID()] = p - srv.log.Debug("Adding p2p peer", "peercount", len(peers), "id", p.ID(), "conn", c.flags, "addr", p.RemoteAddr(), "name", p.Name()) - srv.dialsched.peerAdded(c) - if p.Inbound() { - inboundCount++ - serveSuccessMeter.Mark(1) - } else { - dialSuccessMeter.Mark(1) - } - activePeerGauge.Inc(1) - } - c.cont <- err - - case pd := <-srv.delpeer: - // A peer disconnected. - d := common.PrettyDuration(mclock.Now() - pd.created) - delete(peers, pd.ID()) - srv.log.Debug("Removing p2p peer", "peercount", len(peers), "id", pd.ID(), "duration", d, "req", pd.requested, "err", pd.err) - srv.dialsched.peerRemoved(pd.rw) - if pd.Inbound() { - inboundCount-- - } - activePeerGauge.Dec(1) - } - } - - srv.log.Trace("P2P networking is spinning down") - - // Terminate discovery. If there is a running lookup it will terminate soon. - if srv.ntab != nil { - srv.ntab.Close() - } - if srv.DiscV5 != nil { - srv.DiscV5.Close() - } - // Disconnect all peers. - for _, p := range peers { - p.Disconnect(DiscQuitting) - } - // Wait for peers to shut down. Pending connections and tasks are - // not handled here and will terminate soon-ish because srv.quit - // is closed. - for len(peers) > 0 { - p := <-srv.delpeer - p.log.Trace("<-delpeer (spindown)") - delete(peers, p.ID()) - } -} - -func (srv *Server) postHandshakeChecks(peers map[enode.ID]*Peer, inboundCount int, c *conn) error { - switch { - case !c.is(trustedConn) && len(peers) >= srv.MaxPeers: - return DiscTooManyPeers - case !c.is(trustedConn) && c.is(inboundConn) && inboundCount >= srv.maxInboundConns(): - return DiscTooManyPeers - case peers[c.node.ID()] != nil: - return DiscAlreadyConnected - case c.node.ID() == srv.localnode.ID(): - return DiscSelf - default: - return nil - } -} - -func (srv *Server) addPeerChecks(peers map[enode.ID]*Peer, inboundCount int, c *conn) error { - // Drop connections with no matching protocols. - if len(srv.Protocols) > 0 && countMatchingProtocols(srv.Protocols, c.caps) == 0 { - return DiscUselessPeer - } - // Repeat the post-handshake checks because the - // peer set might have changed since those checks were performed. - return srv.postHandshakeChecks(peers, inboundCount, c) -} - -// listenLoop runs in its own goroutine and accepts -// inbound connections. -func (srv *Server) listenLoop() { - srv.log.Debug("TCP listener up", "addr", srv.listener.Addr()) - - // The slots channel limits accepts of new connections. - tokens := defaultMaxPendingPeers - if srv.MaxPendingPeers > 0 { - tokens = srv.MaxPendingPeers - } - slots := make(chan struct{}, tokens) - for i := 0; i < tokens; i++ { - slots <- struct{}{} - } - - // Wait for slots to be returned on exit. This ensures all connection goroutines - // are down before listenLoop returns. - defer srv.loopWG.Done() - defer func() { - for i := 0; i < cap(slots); i++ { - <-slots - } - }() - - for { - // Wait for a free slot before accepting. - <-slots - - var ( - fd net.Conn - err error - lastLog time.Time - ) - for { - fd, err = srv.listener.Accept() - if netutil.IsTemporaryError(err) { - if time.Since(lastLog) > 1*time.Second { - srv.log.Debug("Temporary read error", "err", err) - lastLog = time.Now() - } - time.Sleep(time.Millisecond * 200) - continue - } else if err != nil { - srv.log.Debug("Read error", "err", err) - slots <- struct{}{} - return - } - break - } - - remoteIP := netutil.AddrIP(fd.RemoteAddr()) - if err := srv.checkInboundConn(remoteIP); err != nil { - srv.log.Debug("Rejected inbound connection", "addr", fd.RemoteAddr(), "err", err) - fd.Close() - slots <- struct{}{} - continue - } - if remoteIP != nil { - fd = newMeteredConn(fd) - serveMeter.Mark(1) - srv.log.Trace("Accepted connection", "addr", fd.RemoteAddr()) - } - go func() { - srv.SetupConn(fd, inboundConn, nil) - slots <- struct{}{} - }() - } -} - -func (srv *Server) checkInboundConn(remoteIP net.IP) error { - if remoteIP == nil { - return nil - } - // Reject connections that do not match NetRestrict. - if srv.NetRestrict != nil && !srv.NetRestrict.Contains(remoteIP) { - return fmt.Errorf("not in netrestrict list") - } - // Reject Internet peers that try too often. - now := srv.clock.Now() - srv.inboundHistory.expire(now, nil) - if !netutil.IsLAN(remoteIP) && srv.inboundHistory.contains(remoteIP.String()) { - return fmt.Errorf("too many attempts") - } - srv.inboundHistory.add(remoteIP.String(), now.Add(inboundThrottleTime)) - return nil -} - -// SetupConn runs the handshakes and attempts to add the connection -// as a peer. It returns when the connection has been added as a peer -// or the handshakes have failed. -func (srv *Server) SetupConn(fd net.Conn, flags connFlag, dialDest *enode.Node) error { - c := &conn{fd: fd, flags: flags, cont: make(chan error)} - if dialDest == nil { - c.transport = srv.newTransport(fd, nil) - } else { - c.transport = srv.newTransport(fd, dialDest.Pubkey()) - } - - err := srv.setupConn(c, flags, dialDest) - if err != nil { - if !c.is(inboundConn) { - markDialError(err) - } - c.close(err) - } - return err -} - -func (srv *Server) setupConn(c *conn, flags connFlag, dialDest *enode.Node) error { - // Prevent leftover pending conns from entering the handshake. - srv.lock.Lock() - running := srv.running - srv.lock.Unlock() - if !running { - return errServerStopped - } - - // If dialing, figure out the remote public key. - if dialDest != nil { - dialPubkey := new(ecdsa.PublicKey) - if err := dialDest.Load((*enode.Secp256k1)(dialPubkey)); err != nil { - err = fmt.Errorf("%w: dial destination doesn't have a secp256k1 public key", errEncHandshakeError) - srv.log.Trace("Setting up connection failed", "addr", c.fd.RemoteAddr(), "conn", c.flags, "err", err) - return err - } - } - - // Run the RLPx handshake. - remotePubkey, err := c.doEncHandshake(srv.PrivateKey) - if err != nil { - srv.log.Trace("Failed RLPx handshake", "addr", c.fd.RemoteAddr(), "conn", c.flags, "err", err) - return fmt.Errorf("%w: %v", errEncHandshakeError, err) - } - if dialDest != nil { - c.node = dialDest - } else { - c.node = nodeFromConn(remotePubkey, c.fd) - } - clog := srv.log.New("id", c.node.ID(), "addr", c.fd.RemoteAddr(), "conn", c.flags) - err = srv.checkpoint(c, srv.checkpointPostHandshake) - if err != nil { - clog.Trace("Rejected peer", "err", err) - return err - } - - // Run the capability negotiation handshake. - phs, err := c.doProtoHandshake(srv.ourHandshake) - if err != nil { - clog.Trace("Failed p2p handshake", "err", err) - return fmt.Errorf("%w: %v", errProtoHandshakeError, err) - } - if id := c.node.ID(); !bytes.Equal(crypto.Keccak256(phs.ID), id[:]) { - clog.Trace("Wrong devp2p handshake identity", "phsid", hex.EncodeToString(phs.ID)) - return DiscUnexpectedIdentity - } - c.caps, c.name = phs.Caps, phs.Name - err = srv.checkpoint(c, srv.checkpointAddPeer) - if err != nil { - clog.Trace("Rejected peer", "err", err) - return err - } - - return nil -} - -func nodeFromConn(pubkey *ecdsa.PublicKey, conn net.Conn) *enode.Node { - var ip net.IP - var port int - if tcp, ok := conn.RemoteAddr().(*net.TCPAddr); ok { - ip = tcp.IP - port = tcp.Port - } - return enode.NewV4(pubkey, ip, port, port) -} - -// checkpoint sends the conn to run, which performs the -// post-handshake checks for the stage (posthandshake, addpeer). -func (srv *Server) checkpoint(c *conn, stage chan<- *conn) error { - select { - case stage <- c: - case <-srv.quit: - return errServerStopped - } - return <-c.cont -} - -func (srv *Server) launchPeer(c *conn) *Peer { - p := newPeer(srv.log, c, srv.Protocols) - if srv.EnableMsgEvents { - // If message events are enabled, pass the peerFeed - // to the peer. - p.events = &srv.peerFeed - } - go srv.runPeer(p) - return p -} - -// runPeer runs in its own goroutine for each peer. -func (srv *Server) runPeer(p *Peer) { - if srv.newPeerHook != nil { - srv.newPeerHook(p) - } - srv.peerFeed.Send(&PeerEvent{ - Type: PeerEventTypeAdd, - Peer: p.ID(), - RemoteAddress: p.RemoteAddr().String(), - LocalAddress: p.LocalAddr().String(), - }) - - // Run the per-peer main loop. - remoteRequested, err := p.run() - - // Announce disconnect on the main loop to update the peer set. - // The main loop waits for existing peers to be sent on srv.delpeer - // before returning, so this send should not select on srv.quit. - srv.delpeer <- peerDrop{p, err, remoteRequested} - - // Broadcast peer drop to external subscribers. This needs to be - // after the send to delpeer so subscribers have a consistent view of - // the peer set (i.e. Server.Peers() doesn't include the peer when the - // event is received). - srv.peerFeed.Send(&PeerEvent{ - Type: PeerEventTypeDrop, - Peer: p.ID(), - Error: err.Error(), - RemoteAddress: p.RemoteAddr().String(), - LocalAddress: p.LocalAddr().String(), - }) -} - -// NodeInfo represents a short summary of the information known about the host. -type NodeInfo struct { - ID string `json:"id"` // Unique node identifier (also the encryption key) - Name string `json:"name"` // Name of the node, including client type, version, OS, custom data - Enode string `json:"enode"` // Enode URL for adding this peer from remote peers - ENR string `json:"enr"` // Ethereum Node Record - IP string `json:"ip"` // IP address of the node - Ports struct { - Discovery int `json:"discovery"` // UDP listening port for discovery protocol - Listener int `json:"listener"` // TCP listening port for RLPx - } `json:"ports"` - ListenAddr string `json:"listenAddr"` - Protocols map[string]interface{} `json:"protocols"` -} - -// NodeInfo gathers and returns a collection of metadata known about the host. -func (srv *Server) NodeInfo() *NodeInfo { - // Gather and assemble the generic node infos - node := srv.Self() - info := &NodeInfo{ - Name: srv.Name, - Enode: node.URLv4(), - ID: node.ID().String(), - IP: node.IP().String(), - ListenAddr: srv.ListenAddr, - Protocols: make(map[string]interface{}), - } - info.Ports.Discovery = node.UDP() - info.Ports.Listener = node.TCP() - info.ENR = node.String() - - // Gather all the running protocol infos (only once per protocol type) - for _, proto := range srv.Protocols { - if _, ok := info.Protocols[proto.Name]; !ok { - nodeInfo := interface{}("unknown") - if query := proto.NodeInfo; query != nil { - nodeInfo = proto.NodeInfo() - } - info.Protocols[proto.Name] = nodeInfo - } - } - return info -} - -// PeersInfo returns an array of metadata objects describing connected peers. -func (srv *Server) PeersInfo() []*PeerInfo { - // Gather all the generic and sub-protocol specific infos - infos := make([]*PeerInfo, 0, srv.PeerCount()) - for _, peer := range srv.Peers() { - if peer != nil { - infos = append(infos, peer.Info()) - } - } - // Sort the result array alphabetically by node identifier - for i := 0; i < len(infos); i++ { - for j := i + 1; j < len(infos); j++ { - if infos[i].ID > infos[j].ID { - infos[i], infos[j] = infos[j], infos[i] - } - } - } - return infos -} diff --git a/p2p/server_nat.go b/p2p/server_nat.go deleted file mode 100644 index 354597cc7a..0000000000 --- a/p2p/server_nat.go +++ /dev/null @@ -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 . - -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) - } - } - } - } -} diff --git a/p2p/server_nat_test.go b/p2p/server_nat_test.go deleted file mode 100644 index de935fcfc5..0000000000 --- a/p2p/server_nat_test.go +++ /dev/null @@ -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 . - -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" -} diff --git a/p2p/server_test.go b/p2p/server_test.go deleted file mode 100644 index a0491e984a..0000000000 --- a/p2p/server_test.go +++ /dev/null @@ -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 . - -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 - } - } -} diff --git a/p2p/simulations/README.md b/p2p/simulations/README.md deleted file mode 100644 index 023f73a098..0000000000 --- a/p2p/simulations/README.md +++ /dev/null @@ -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 -p2psim node start -p2psim node stop -p2psim node connect -p2psim node disconnect -p2psim node rpc [] [--subscribe] -``` - -## Example - -See [p2p/simulations/examples/README.md](examples/README.md). diff --git a/p2p/simulations/adapters/exec.go b/p2p/simulations/adapters/exec.go deleted file mode 100644 index 63cc4936c1..0000000000 --- a/p2p/simulations/adapters/exec.go +++ /dev/null @@ -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 . - -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") -} diff --git a/p2p/simulations/adapters/inproc.go b/p2p/simulations/adapters/inproc.go deleted file mode 100644 index c52917fd0a..0000000000 --- a/p2p/simulations/adapters/inproc.go +++ /dev/null @@ -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 . - -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() -} diff --git a/p2p/simulations/adapters/inproc_test.go b/p2p/simulations/adapters/inproc_test.go deleted file mode 100644 index 2a61508fe1..0000000000 --- a/p2p/simulations/adapters/inproc_test.go +++ /dev/null @@ -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 . - -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) - } - } - } -} diff --git a/p2p/simulations/adapters/types.go b/p2p/simulations/adapters/types.go deleted file mode 100644 index fb8463d221..0000000000 --- a/p2p/simulations/adapters/types.go +++ /dev/null @@ -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 . - -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) -} diff --git a/p2p/simulations/connect.go b/p2p/simulations/connect.go deleted file mode 100644 index ede96b34c1..0000000000 --- a/p2p/simulations/connect.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/simulations/connect_test.go b/p2p/simulations/connect_test.go deleted file mode 100644 index 0154a18b03..0000000000 --- a/p2p/simulations/connect_test.go +++ /dev/null @@ -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 . - -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) -} diff --git a/p2p/simulations/events.go b/p2p/simulations/events.go deleted file mode 100644 index d0d03794ed..0000000000 --- a/p2p/simulations/events.go +++ /dev/null @@ -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 . - -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(" id: %s up: %t", e.Node.ID().TerminalString(), e.Node.Up()) - case EventTypeConn: - return fmt.Sprintf(" nodes: %s->%s up: %t", e.Conn.One.TerminalString(), e.Conn.Other.TerminalString(), e.Conn.Up) - case EventTypeMsg: - return fmt.Sprintf(" 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 "" - } -} diff --git a/p2p/simulations/examples/README.md b/p2p/simulations/examples/README.md deleted file mode 100644 index 822a48dcb6..0000000000 --- a/p2p/simulations/examples/README.md +++ /dev/null @@ -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... -``` diff --git a/p2p/simulations/examples/ping-pong.go b/p2p/simulations/examples/ping-pong.go deleted file mode 100644 index 70b35ad777..0000000000 --- a/p2p/simulations/examples/ping-pong.go +++ /dev/null @@ -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 . - -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 -} diff --git a/p2p/simulations/examples/ping-pong.sh b/p2p/simulations/examples/ping-pong.sh deleted file mode 100755 index 47936bd9a0..0000000000 --- a/p2p/simulations/examples/ping-pong.sh +++ /dev/null @@ -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 "$@" diff --git a/p2p/simulations/http.go b/p2p/simulations/http.go deleted file mode 100644 index 34521b4778..0000000000 --- a/p2p/simulations/http.go +++ /dev/null @@ -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 . - -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: - // 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 ':', where is the name of a -// protocol and 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)) - } -} diff --git a/p2p/simulations/http_test.go b/p2p/simulations/http_test.go deleted file mode 100644 index c53a49797b..0000000000 --- a/p2p/simulations/http_test.go +++ /dev/null @@ -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 . - -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!") - } -} diff --git a/p2p/simulations/mocker.go b/p2p/simulations/mocker.go deleted file mode 100644 index 0dc04e65f9..0000000000 --- a/p2p/simulations/mocker.go +++ /dev/null @@ -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 . - -// 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 -} diff --git a/p2p/simulations/mocker_test.go b/p2p/simulations/mocker_test.go deleted file mode 100644 index 0112ee5cfd..0000000000 --- a/p2p/simulations/mocker_test.go +++ /dev/null @@ -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 . - -// 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() -} diff --git a/p2p/simulations/network.go b/p2p/simulations/network.go deleted file mode 100644 index 4735e5cfa6..0000000000 --- a/p2p/simulations/network.go +++ /dev/null @@ -1,1090 +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 . - -package simulations - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "math/rand" - "sync" - "time" - - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/simulations/adapters" -) - -var DialBanTimeout = 200 * time.Millisecond - -// NetworkConfig defines configuration options for starting a Network -type NetworkConfig struct { - ID string `json:"id"` - DefaultService string `json:"default_service,omitempty"` -} - -// Network models a p2p simulation network which consists of a collection of -// simulated nodes and the connections which exist between them. -// -// The Network has a single NodeAdapter which is responsible for actually -// starting nodes and connecting them together. -// -// The Network emits events when nodes are started and stopped, when they are -// connected and disconnected, and also when messages are sent between nodes. -type Network struct { - NetworkConfig - - Nodes []*Node `json:"nodes"` - nodeMap map[enode.ID]int - - // Maps a node property string to node indexes of all nodes that hold this property - propertyMap map[string][]int - - Conns []*Conn `json:"conns"` - connMap map[string]int - - nodeAdapter adapters.NodeAdapter - events event.Feed - lock sync.RWMutex - quitc chan struct{} -} - -// NewNetwork returns a Network which uses the given NodeAdapter and NetworkConfig -func NewNetwork(nodeAdapter adapters.NodeAdapter, conf *NetworkConfig) *Network { - return &Network{ - NetworkConfig: *conf, - nodeAdapter: nodeAdapter, - nodeMap: make(map[enode.ID]int), - propertyMap: make(map[string][]int), - connMap: make(map[string]int), - quitc: make(chan struct{}), - } -} - -// Events returns the output event feed of the Network. -func (net *Network) Events() *event.Feed { - return &net.events -} - -// NewNodeWithConfig adds a new node to the network with the given config, -// returning an error if a node with the same ID or name already exists -func (net *Network) NewNodeWithConfig(conf *adapters.NodeConfig) (*Node, error) { - net.lock.Lock() - defer net.lock.Unlock() - - if conf.Reachable == nil { - conf.Reachable = func(otherID enode.ID) bool { - _, err := net.InitConn(conf.ID, otherID) - if err != nil && bytes.Compare(conf.ID.Bytes(), otherID.Bytes()) < 0 { - return false - } - return true - } - } - - // check the node doesn't already exist - if node := net.getNode(conf.ID); node != nil { - return nil, fmt.Errorf("node with ID %q already exists", conf.ID) - } - if node := net.getNodeByName(conf.Name); node != nil { - return nil, fmt.Errorf("node with name %q already exists", conf.Name) - } - - // if no services are configured, use the default service - if len(conf.Lifecycles) == 0 { - conf.Lifecycles = []string{net.DefaultService} - } - - // use the NodeAdapter to create the node - adapterNode, err := net.nodeAdapter.NewNode(conf) - if err != nil { - return nil, err - } - node := newNode(adapterNode, conf, false) - log.Trace("Node created", "id", conf.ID) - - nodeIndex := len(net.Nodes) - net.nodeMap[conf.ID] = nodeIndex - net.Nodes = append(net.Nodes, node) - - // Register any node properties with the network-level propertyMap - for _, property := range conf.Properties { - net.propertyMap[property] = append(net.propertyMap[property], nodeIndex) - } - - // emit a "control" event - net.events.Send(ControlEvent(node)) - - return node, nil -} - -// Config returns the network configuration -func (net *Network) Config() *NetworkConfig { - return &net.NetworkConfig -} - -// StartAll starts all nodes in the network -func (net *Network) StartAll() error { - for _, node := range net.Nodes { - if node.Up() { - continue - } - if err := net.Start(node.ID()); err != nil { - return err - } - } - return nil -} - -// StopAll stops all nodes in the network -func (net *Network) StopAll() error { - for _, node := range net.Nodes { - if !node.Up() { - continue - } - if err := net.Stop(node.ID()); err != nil { - return err - } - } - return nil -} - -// Start starts the node with the given ID -func (net *Network) Start(id enode.ID) error { - return net.startWithSnapshots(id, nil) -} - -// startWithSnapshots starts the node with the given ID using the give -// snapshots -func (net *Network) startWithSnapshots(id enode.ID, snapshots map[string][]byte) error { - net.lock.Lock() - defer net.lock.Unlock() - - node := net.getNode(id) - if node == nil { - return fmt.Errorf("node %v does not exist", id) - } - if node.Up() { - return fmt.Errorf("node %v already up", id) - } - log.Trace("Starting node", "id", id, "adapter", net.nodeAdapter.Name()) - if err := node.Start(snapshots); err != nil { - log.Warn("Node startup failed", "id", id, "err", err) - return err - } - node.SetUp(true) - log.Info("Started node", "id", id) - ev := NewEvent(node) - net.events.Send(ev) - - // subscribe to peer events - client, err := node.Client() - if err != nil { - return fmt.Errorf("error getting rpc client for node %v: %s", id, err) - } - events := make(chan *p2p.PeerEvent) - sub, err := client.Subscribe(context.Background(), "admin", events, "peerEvents") - if err != nil { - return fmt.Errorf("error getting peer events for node %v: %s", id, err) - } - go net.watchPeerEvents(id, events, sub) - return nil -} - -// watchPeerEvents reads peer events from the given channel and emits -// corresponding network events -func (net *Network) watchPeerEvents(id enode.ID, events chan *p2p.PeerEvent, sub event.Subscription) { - defer func() { - sub.Unsubscribe() - - // assume the node is now down - net.lock.Lock() - defer net.lock.Unlock() - - node := net.getNode(id) - if node == nil { - return - } - node.SetUp(false) - ev := NewEvent(node) - net.events.Send(ev) - }() - for { - select { - case event, ok := <-events: - if !ok { - return - } - peer := event.Peer - switch event.Type { - case p2p.PeerEventTypeAdd: - net.DidConnect(id, peer) - - case p2p.PeerEventTypeDrop: - net.DidDisconnect(id, peer) - - case p2p.PeerEventTypeMsgSend: - net.DidSend(id, peer, event.Protocol, *event.MsgCode) - - case p2p.PeerEventTypeMsgRecv: - net.DidReceive(peer, id, event.Protocol, *event.MsgCode) - } - - case err := <-sub.Err(): - if err != nil { - log.Error("Error in peer event subscription", "id", id, "err", err) - } - return - } - } -} - -// Stop stops the node with the given ID -func (net *Network) Stop(id enode.ID) error { - // IMPORTANT: node.Stop() must NOT be called under net.lock as - // node.Reachable() closure has a reference to the network and - // calls net.InitConn() what also locks the network. => DEADLOCK - // That holds until the following ticket is not resolved: - - var err error - - node, err := func() (*Node, error) { - net.lock.Lock() - defer net.lock.Unlock() - - node := net.getNode(id) - if node == nil { - return nil, fmt.Errorf("node %v does not exist", id) - } - if !node.Up() { - return nil, fmt.Errorf("node %v already down", id) - } - node.SetUp(false) - return node, nil - }() - if err != nil { - return err - } - - err = node.Stop() // must be called without net.lock - - net.lock.Lock() - defer net.lock.Unlock() - - if err != nil { - node.SetUp(true) - return err - } - log.Info("Stopped node", "id", id, "err", err) - ev := ControlEvent(node) - net.events.Send(ev) - return nil -} - -// Connect connects two nodes together by calling the "admin_addPeer" RPC -// method on the "one" node so that it connects to the "other" node -func (net *Network) Connect(oneID, otherID enode.ID) error { - net.lock.Lock() - defer net.lock.Unlock() - return net.connect(oneID, otherID) -} - -func (net *Network) connect(oneID, otherID enode.ID) error { - log.Debug("Connecting nodes with addPeer", "id", oneID, "other", otherID) - conn, err := net.initConn(oneID, otherID) - if err != nil { - return err - } - client, err := conn.one.Client() - if err != nil { - return err - } - net.events.Send(ControlEvent(conn)) - return client.Call(nil, "admin_addPeer", string(conn.other.Addr())) -} - -// Disconnect disconnects two nodes by calling the "admin_removePeer" RPC -// method on the "one" node so that it disconnects from the "other" node -func (net *Network) Disconnect(oneID, otherID enode.ID) error { - conn := net.GetConn(oneID, otherID) - if conn == nil { - return fmt.Errorf("connection between %v and %v does not exist", oneID, otherID) - } - if !conn.Up { - return fmt.Errorf("%v and %v already disconnected", oneID, otherID) - } - client, err := conn.one.Client() - if err != nil { - return err - } - net.events.Send(ControlEvent(conn)) - return client.Call(nil, "admin_removePeer", string(conn.other.Addr())) -} - -// DidConnect tracks the fact that the "one" node connected to the "other" node -func (net *Network) DidConnect(one, other enode.ID) error { - net.lock.Lock() - defer net.lock.Unlock() - conn, err := net.getOrCreateConn(one, other) - if err != nil { - return fmt.Errorf("connection between %v and %v does not exist", one, other) - } - if conn.Up { - return fmt.Errorf("%v and %v already connected", one, other) - } - conn.Up = true - net.events.Send(NewEvent(conn)) - return nil -} - -// DidDisconnect tracks the fact that the "one" node disconnected from the -// "other" node -func (net *Network) DidDisconnect(one, other enode.ID) error { - net.lock.Lock() - defer net.lock.Unlock() - conn := net.getConn(one, other) - if conn == nil { - return fmt.Errorf("connection between %v and %v does not exist", one, other) - } - if !conn.Up { - return fmt.Errorf("%v and %v already disconnected", one, other) - } - conn.Up = false - conn.initiated = time.Now().Add(-DialBanTimeout) - net.events.Send(NewEvent(conn)) - return nil -} - -// DidSend tracks the fact that "sender" sent a message to "receiver" -func (net *Network) DidSend(sender, receiver enode.ID, proto string, code uint64) error { - msg := &Msg{ - One: sender, - Other: receiver, - Protocol: proto, - Code: code, - Received: false, - } - net.events.Send(NewEvent(msg)) - return nil -} - -// DidReceive tracks the fact that "receiver" received a message from "sender" -func (net *Network) DidReceive(sender, receiver enode.ID, proto string, code uint64) error { - msg := &Msg{ - One: sender, - Other: receiver, - Protocol: proto, - Code: code, - Received: true, - } - net.events.Send(NewEvent(msg)) - return nil -} - -// GetNode gets the node with the given ID, returning nil if the node does not -// exist -func (net *Network) GetNode(id enode.ID) *Node { - net.lock.RLock() - defer net.lock.RUnlock() - return net.getNode(id) -} - -func (net *Network) getNode(id enode.ID) *Node { - i, found := net.nodeMap[id] - if !found { - return nil - } - return net.Nodes[i] -} - -// GetNodeByName gets the node with the given name, returning nil if the node does -// not exist -func (net *Network) GetNodeByName(name string) *Node { - net.lock.RLock() - defer net.lock.RUnlock() - return net.getNodeByName(name) -} - -func (net *Network) getNodeByName(name string) *Node { - for _, node := range net.Nodes { - if node.Config.Name == name { - return node - } - } - return nil -} - -// GetNodeIDs returns the IDs of all existing nodes -// Nodes can optionally be excluded by specifying their enode.ID. -func (net *Network) GetNodeIDs(excludeIDs ...enode.ID) []enode.ID { - net.lock.RLock() - defer net.lock.RUnlock() - - return net.getNodeIDs(excludeIDs) -} - -func (net *Network) getNodeIDs(excludeIDs []enode.ID) []enode.ID { - // Get all current nodeIDs - nodeIDs := make([]enode.ID, 0, len(net.nodeMap)) - for id := range net.nodeMap { - nodeIDs = append(nodeIDs, id) - } - - if len(excludeIDs) > 0 { - // Return the difference of nodeIDs and excludeIDs - return filterIDs(nodeIDs, excludeIDs) - } - return nodeIDs -} - -// GetNodes returns the existing nodes. -// Nodes can optionally be excluded by specifying their enode.ID. -func (net *Network) GetNodes(excludeIDs ...enode.ID) []*Node { - net.lock.RLock() - defer net.lock.RUnlock() - - return net.getNodes(excludeIDs) -} - -func (net *Network) getNodes(excludeIDs []enode.ID) []*Node { - if len(excludeIDs) > 0 { - nodeIDs := net.getNodeIDs(excludeIDs) - return net.getNodesByID(nodeIDs) - } - return net.Nodes -} - -// GetNodesByID returns existing nodes with the given enode.IDs. -// If a node doesn't exist with a given enode.ID, it is ignored. -func (net *Network) GetNodesByID(nodeIDs []enode.ID) []*Node { - net.lock.RLock() - defer net.lock.RUnlock() - - return net.getNodesByID(nodeIDs) -} - -func (net *Network) getNodesByID(nodeIDs []enode.ID) []*Node { - nodes := make([]*Node, 0, len(nodeIDs)) - for _, id := range nodeIDs { - node := net.getNode(id) - if node != nil { - nodes = append(nodes, node) - } - } - - return nodes -} - -// GetNodesByProperty returns existing nodes that have the given property string registered in their NodeConfig -func (net *Network) GetNodesByProperty(property string) []*Node { - net.lock.RLock() - defer net.lock.RUnlock() - - return net.getNodesByProperty(property) -} - -func (net *Network) getNodesByProperty(property string) []*Node { - nodes := make([]*Node, 0, len(net.propertyMap[property])) - for _, nodeIndex := range net.propertyMap[property] { - nodes = append(nodes, net.Nodes[nodeIndex]) - } - - return nodes -} - -// GetNodeIDsByProperty returns existing node's enode IDs that have the given property string registered in the NodeConfig -func (net *Network) GetNodeIDsByProperty(property string) []enode.ID { - net.lock.RLock() - defer net.lock.RUnlock() - - return net.getNodeIDsByProperty(property) -} - -func (net *Network) getNodeIDsByProperty(property string) []enode.ID { - nodeIDs := make([]enode.ID, 0, len(net.propertyMap[property])) - for _, nodeIndex := range net.propertyMap[property] { - node := net.Nodes[nodeIndex] - nodeIDs = append(nodeIDs, node.ID()) - } - - return nodeIDs -} - -// GetRandomUpNode returns a random node on the network, which is running. -func (net *Network) GetRandomUpNode(excludeIDs ...enode.ID) *Node { - net.lock.RLock() - defer net.lock.RUnlock() - return net.getRandomUpNode(excludeIDs...) -} - -// GetRandomUpNode returns a random node on the network, which is running. -func (net *Network) getRandomUpNode(excludeIDs ...enode.ID) *Node { - return net.getRandomNode(net.getUpNodeIDs(), excludeIDs) -} - -func (net *Network) getUpNodeIDs() (ids []enode.ID) { - for _, node := range net.Nodes { - if node.Up() { - ids = append(ids, node.ID()) - } - } - return ids -} - -// GetRandomDownNode returns a random node on the network, which is stopped. -func (net *Network) GetRandomDownNode(excludeIDs ...enode.ID) *Node { - net.lock.RLock() - defer net.lock.RUnlock() - return net.getRandomNode(net.getDownNodeIDs(), excludeIDs) -} - -func (net *Network) getDownNodeIDs() (ids []enode.ID) { - for _, node := range net.Nodes { - if !node.Up() { - ids = append(ids, node.ID()) - } - } - return ids -} - -// GetRandomNode returns a random node on the network, regardless of whether it is running or not -func (net *Network) GetRandomNode(excludeIDs ...enode.ID) *Node { - net.lock.RLock() - defer net.lock.RUnlock() - return net.getRandomNode(net.getNodeIDs(nil), excludeIDs) // no need to exclude twice -} - -func (net *Network) getRandomNode(ids []enode.ID, excludeIDs []enode.ID) *Node { - filtered := filterIDs(ids, excludeIDs) - - l := len(filtered) - if l == 0 { - return nil - } - return net.getNode(filtered[rand.Intn(l)]) -} - -func filterIDs(ids []enode.ID, excludeIDs []enode.ID) []enode.ID { - exclude := make(map[enode.ID]bool) - for _, id := range excludeIDs { - exclude[id] = true - } - var filtered []enode.ID - for _, id := range ids { - if _, found := exclude[id]; !found { - filtered = append(filtered, id) - } - } - return filtered -} - -// GetConn returns the connection which exists between "one" and "other" -// regardless of which node initiated the connection -func (net *Network) GetConn(oneID, otherID enode.ID) *Conn { - net.lock.RLock() - defer net.lock.RUnlock() - return net.getConn(oneID, otherID) -} - -// GetOrCreateConn is like GetConn but creates the connection if it doesn't -// already exist -func (net *Network) GetOrCreateConn(oneID, otherID enode.ID) (*Conn, error) { - net.lock.Lock() - defer net.lock.Unlock() - return net.getOrCreateConn(oneID, otherID) -} - -func (net *Network) getOrCreateConn(oneID, otherID enode.ID) (*Conn, error) { - if conn := net.getConn(oneID, otherID); conn != nil { - return conn, nil - } - - one := net.getNode(oneID) - if one == nil { - return nil, fmt.Errorf("node %v does not exist", oneID) - } - other := net.getNode(otherID) - if other == nil { - return nil, fmt.Errorf("node %v does not exist", otherID) - } - conn := &Conn{ - One: oneID, - Other: otherID, - one: one, - other: other, - } - label := ConnLabel(oneID, otherID) - net.connMap[label] = len(net.Conns) - net.Conns = append(net.Conns, conn) - return conn, nil -} - -func (net *Network) getConn(oneID, otherID enode.ID) *Conn { - label := ConnLabel(oneID, otherID) - i, found := net.connMap[label] - if !found { - return nil - } - return net.Conns[i] -} - -// InitConn retrieves the connection model for the connection between -// peers 'oneID' and 'otherID', or creates a new one if it does not exist -// the order of nodes does not matter, i.e., Conn(i,j) == Conn(j, i) -// it checks if the connection is already up, and if the nodes are running -// NOTE: -// it also checks whether there has been recent attempt to connect the peers -// this is cheating as the simulation is used as an oracle and know about -// remote peers attempt to connect to a node which will then not initiate the connection -func (net *Network) InitConn(oneID, otherID enode.ID) (*Conn, error) { - net.lock.Lock() - defer net.lock.Unlock() - return net.initConn(oneID, otherID) -} - -func (net *Network) initConn(oneID, otherID enode.ID) (*Conn, error) { - if oneID == otherID { - return nil, fmt.Errorf("refusing to connect to self %v", oneID) - } - conn, err := net.getOrCreateConn(oneID, otherID) - if err != nil { - return nil, err - } - if conn.Up { - return nil, fmt.Errorf("%v and %v already connected", oneID, otherID) - } - if time.Since(conn.initiated) < DialBanTimeout { - return nil, fmt.Errorf("connection between %v and %v recently attempted", oneID, otherID) - } - - err = conn.nodesUp() - if err != nil { - log.Trace("Nodes not up", "err", err) - return nil, fmt.Errorf("nodes not up: %v", err) - } - log.Debug("Connection initiated", "id", oneID, "other", otherID) - conn.initiated = time.Now() - return conn, nil -} - -// Shutdown stops all nodes in the network and closes the quit channel -func (net *Network) Shutdown() { - for _, node := range net.Nodes { - log.Debug("Stopping node", "id", node.ID()) - if err := node.Stop(); err != nil { - log.Warn("Can't stop node", "id", node.ID(), "err", err) - } - } - close(net.quitc) -} - -// Reset resets all network properties: -// empties the nodes and the connection list -func (net *Network) Reset() { - net.lock.Lock() - defer net.lock.Unlock() - - //re-initialize the maps - net.connMap = make(map[string]int) - net.nodeMap = make(map[enode.ID]int) - net.propertyMap = make(map[string][]int) - - net.Nodes = nil - net.Conns = nil -} - -// Node is a wrapper around adapters.Node which is used to track the status -// of a node in the network -type Node struct { - adapters.Node `json:"-"` - - // Config if the config used to created the node - Config *adapters.NodeConfig `json:"config"` - - // up tracks whether or not the node is running - up bool - upMu *sync.RWMutex -} - -func newNode(an adapters.Node, ac *adapters.NodeConfig, up bool) *Node { - return &Node{Node: an, Config: ac, up: up, upMu: new(sync.RWMutex)} -} - -func (n *Node) copy() *Node { - configCpy := *n.Config - return newNode(n.Node, &configCpy, n.Up()) -} - -// Up returns whether the node is currently up (online) -func (n *Node) Up() bool { - n.upMu.RLock() - defer n.upMu.RUnlock() - return n.up -} - -// SetUp sets the up (online) status of the nodes with the given value -func (n *Node) SetUp(up bool) { - n.upMu.Lock() - defer n.upMu.Unlock() - n.up = up -} - -// ID returns the ID of the node -func (n *Node) ID() enode.ID { - return n.Config.ID -} - -// String returns a log-friendly string -func (n *Node) String() string { - return fmt.Sprintf("Node %v", n.ID().TerminalString()) -} - -// NodeInfo returns information about the node -func (n *Node) NodeInfo() *p2p.NodeInfo { - // avoid a panic if the node is not started yet - if n.Node == nil { - return nil - } - info := n.Node.NodeInfo() - info.Name = n.Config.Name - return info -} - -// MarshalJSON implements the json.Marshaler interface so that the encoded -// JSON includes the NodeInfo -func (n *Node) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - Info *p2p.NodeInfo `json:"info,omitempty"` - Config *adapters.NodeConfig `json:"config,omitempty"` - Up bool `json:"up"` - }{ - Info: n.NodeInfo(), - Config: n.Config, - Up: n.Up(), - }) -} - -// UnmarshalJSON implements json.Unmarshaler interface so that we don't lose Node.up -// status. IMPORTANT: The implementation is incomplete; we lose p2p.NodeInfo. -func (n *Node) UnmarshalJSON(raw []byte) error { - // TODO: How should we turn back NodeInfo into n.Node? - // Ticket: https://github.com/ethersphere/go-ethereum/issues/1177 - var node struct { - Config *adapters.NodeConfig `json:"config,omitempty"` - Up bool `json:"up"` - } - if err := json.Unmarshal(raw, &node); err != nil { - return err - } - *n = *newNode(nil, node.Config, node.Up) - return nil -} - -// Conn represents a connection between two nodes in the network -type Conn struct { - // One is the node which initiated the connection - One enode.ID `json:"one"` - - // Other is the node which the connection was made to - Other enode.ID `json:"other"` - - // Up tracks whether or not the connection is active - Up bool `json:"up"` - // Registers when the connection was grabbed to dial - initiated time.Time - - one *Node - other *Node -} - -// nodesUp returns whether both nodes are currently up -func (c *Conn) nodesUp() error { - if !c.one.Up() { - return fmt.Errorf("one %v is not up", c.One) - } - if !c.other.Up() { - return fmt.Errorf("other %v is not up", c.Other) - } - return nil -} - -// String returns a log-friendly string -func (c *Conn) String() string { - return fmt.Sprintf("Conn %v->%v", c.One.TerminalString(), c.Other.TerminalString()) -} - -// Msg represents a p2p message sent between two nodes in the network -type Msg struct { - One enode.ID `json:"one"` - Other enode.ID `json:"other"` - Protocol string `json:"protocol"` - Code uint64 `json:"code"` - Received bool `json:"received"` -} - -// String returns a log-friendly string -func (m *Msg) String() string { - return fmt.Sprintf("Msg(%d) %v->%v", m.Code, m.One.TerminalString(), m.Other.TerminalString()) -} - -// ConnLabel generates a deterministic string which represents a connection -// between two nodes, used to compare if two connections are between the same -// nodes -func ConnLabel(source, target enode.ID) string { - var first, second enode.ID - if bytes.Compare(source.Bytes(), target.Bytes()) > 0 { - first = target - second = source - } else { - first = source - second = target - } - return fmt.Sprintf("%v-%v", first, second) -} - -// Snapshot represents the state of a network at a single point in time and can -// be used to restore the state of a network -type Snapshot struct { - Nodes []NodeSnapshot `json:"nodes,omitempty"` - Conns []Conn `json:"conns,omitempty"` -} - -// NodeSnapshot represents the state of a node in the network -type NodeSnapshot struct { - Node Node `json:"node,omitempty"` - - // Snapshots is arbitrary data gathered from calling node.Snapshots() - Snapshots map[string][]byte `json:"snapshots,omitempty"` -} - -// Snapshot creates a network snapshot -func (net *Network) Snapshot() (*Snapshot, error) { - return net.snapshot(nil, nil) -} - -func (net *Network) SnapshotWithServices(addServices []string, removeServices []string) (*Snapshot, error) { - return net.snapshot(addServices, removeServices) -} - -func (net *Network) snapshot(addServices []string, removeServices []string) (*Snapshot, error) { - net.lock.Lock() - defer net.lock.Unlock() - snap := &Snapshot{ - Nodes: make([]NodeSnapshot, len(net.Nodes)), - } - for i, node := range net.Nodes { - snap.Nodes[i] = NodeSnapshot{Node: *node.copy()} - if !node.Up() { - continue - } - snapshots, err := node.Snapshots() - if err != nil { - return nil, err - } - snap.Nodes[i].Snapshots = snapshots - for _, addSvc := range addServices { - haveSvc := false - for _, svc := range snap.Nodes[i].Node.Config.Lifecycles { - if svc == addSvc { - haveSvc = true - break - } - } - if !haveSvc { - snap.Nodes[i].Node.Config.Lifecycles = append(snap.Nodes[i].Node.Config.Lifecycles, addSvc) - } - } - if len(removeServices) > 0 { - var cleanedServices []string - for _, svc := range snap.Nodes[i].Node.Config.Lifecycles { - haveSvc := false - for _, rmSvc := range removeServices { - if rmSvc == svc { - haveSvc = true - break - } - } - if !haveSvc { - cleanedServices = append(cleanedServices, svc) - } - } - snap.Nodes[i].Node.Config.Lifecycles = cleanedServices - } - } - for _, conn := range net.Conns { - if conn.Up { - snap.Conns = append(snap.Conns, *conn) - } - } - return snap, nil -} - -// longrunning tests may need a longer timeout -var snapshotLoadTimeout = 900 * time.Second - -// Load loads a network snapshot -func (net *Network) Load(snap *Snapshot) error { - // Start nodes. - for _, n := range snap.Nodes { - if _, err := net.NewNodeWithConfig(n.Node.Config); err != nil { - return err - } - if !n.Node.Up() { - continue - } - if err := net.startWithSnapshots(n.Node.Config.ID, n.Snapshots); err != nil { - return err - } - } - - // Prepare connection events counter. - allConnected := make(chan struct{}) // closed when all connections are established - done := make(chan struct{}) // ensures that the event loop goroutine is terminated - defer close(done) - - // Subscribe to event channel. - // It needs to be done outside of the event loop goroutine (created below) - // to ensure that the event channel is blocking before connect calls are made. - events := make(chan *Event) - sub := net.Events().Subscribe(events) - defer sub.Unsubscribe() - - go func() { - // Expected number of connections. - total := len(snap.Conns) - // Set of all established connections from the snapshot, not other connections. - // Key array element 0 is the connection One field value, and element 1 connection Other field. - connections := make(map[[2]enode.ID]struct{}, total) - - for { - select { - case e := <-events: - // Ignore control events as they do not represent - // connect or disconnect (Up) state change. - if e.Control { - continue - } - // Detect only connection events. - if e.Type != EventTypeConn { - continue - } - connection := [2]enode.ID{e.Conn.One, e.Conn.Other} - // Nodes are still not connected or have been disconnected. - if !e.Conn.Up { - // Delete the connection from the set of established connections. - // This will prevent false positive in case disconnections happen. - delete(connections, connection) - log.Warn("load snapshot: unexpected disconnection", "one", e.Conn.One, "other", e.Conn.Other) - continue - } - // Check that the connection is from the snapshot. - for _, conn := range snap.Conns { - if conn.One == e.Conn.One && conn.Other == e.Conn.Other { - // Add the connection to the set of established connections. - connections[connection] = struct{}{} - if len(connections) == total { - // Signal that all nodes are connected. - close(allConnected) - return - } - - break - } - } - case <-done: - // Load function returned, terminate this goroutine. - return - } - } - }() - - // Start connecting. - for _, conn := range snap.Conns { - if !net.GetNode(conn.One).Up() || !net.GetNode(conn.Other).Up() { - //in this case, at least one of the nodes of a connection is not up, - //so it would result in the snapshot `Load` to fail - continue - } - if err := net.Connect(conn.One, conn.Other); err != nil { - return err - } - } - - select { - // Wait until all connections from the snapshot are established. - case <-allConnected: - // Make sure that we do not wait forever. - case <-time.After(snapshotLoadTimeout): - return errors.New("snapshot connections not established") - } - return nil -} - -// Subscribe reads control events from a channel and executes them -func (net *Network) Subscribe(events chan *Event) { - for { - select { - case event, ok := <-events: - if !ok { - return - } - if event.Control { - net.executeControlEvent(event) - } - case <-net.quitc: - return - } - } -} - -func (net *Network) executeControlEvent(event *Event) { - log.Trace("Executing control event", "type", event.Type, "event", event) - switch event.Type { - case EventTypeNode: - if err := net.executeNodeEvent(event); err != nil { - log.Error("Error executing node event", "event", event, "err", err) - } - case EventTypeConn: - if err := net.executeConnEvent(event); err != nil { - log.Error("Error executing conn event", "event", event, "err", err) - } - case EventTypeMsg: - log.Warn("Ignoring control msg event") - } -} - -func (net *Network) executeNodeEvent(e *Event) error { - if !e.Node.Up() { - return net.Stop(e.Node.ID()) - } - - if _, err := net.NewNodeWithConfig(e.Node.Config); err != nil { - return err - } - return net.Start(e.Node.ID()) -} - -func (net *Network) executeConnEvent(e *Event) error { - if e.Conn.Up { - return net.Connect(e.Conn.One, e.Conn.Other) - } - return net.Disconnect(e.Conn.One, e.Conn.Other) -} diff --git a/p2p/simulations/network_test.go b/p2p/simulations/network_test.go deleted file mode 100644 index 4ed1e4e6c3..0000000000 --- a/p2p/simulations/network_test.go +++ /dev/null @@ -1,872 +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 . - -package simulations - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "reflect" - "strconv" - "strings" - "testing" - "time" - - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/node" - "github.com/ethereum/go-ethereum/p2p/enode" - "github.com/ethereum/go-ethereum/p2p/simulations/adapters" -) - -// Tests that a created snapshot with a minimal service only contains the expected connections -// and that a network when loaded with this snapshot only contains those same connections -func TestSnapshot(t *testing.T) { - // PART I - // create snapshot from ring network - - // this is a minimal service, whose protocol will take exactly one message OR close of connection before quitting - 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", - }) - // \todo consider making a member of network, set to true threadsafe when shutdown - runningOne := true - defer func() { - if runningOne { - network.Shutdown() - } - }() - - // create and start nodes - nodeCount := 20 - ids := make([]enode.ID, nodeCount) - for i := 0; i < nodeCount; i++ { - 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() - } - - // subscribe to peer events - evC := make(chan *Event) - sub := network.Events().Subscribe(evC) - defer sub.Unsubscribe() - - // connect nodes in a ring - // spawn separate thread to avoid deadlock in the event listeners - connectErr := make(chan error, 1) - go func() { - for i, id := range ids { - peerID := ids[(i+1)%len(ids)] - if err := network.Connect(id, peerID); err != nil { - connectErr <- err - return - } - } - }() - - // collect connection events up to expected number - ctx, cancel := context.WithTimeout(context.TODO(), time.Second) - defer cancel() - checkIds := make(map[enode.ID][]enode.ID) - connEventCount := nodeCount -OUTER: - for { - select { - case <-ctx.Done(): - t.Fatal(ctx.Err()) - case err := <-connectErr: - t.Fatal(err) - case ev := <-evC: - if ev.Type == EventTypeConn && !ev.Control { - // fail on any disconnect - if !ev.Conn.Up { - t.Fatalf("unexpected disconnect: %v -> %v", ev.Conn.One, ev.Conn.Other) - } - checkIds[ev.Conn.One] = append(checkIds[ev.Conn.One], ev.Conn.Other) - checkIds[ev.Conn.Other] = append(checkIds[ev.Conn.Other], ev.Conn.One) - connEventCount-- - log.Debug("ev", "count", connEventCount) - if connEventCount == 0 { - break OUTER - } - } - } - } - - // create snapshot of current network - snap, err := network.Snapshot() - if err != nil { - t.Fatal(err) - } - j, err := json.Marshal(snap) - if err != nil { - t.Fatal(err) - } - log.Debug("snapshot taken", "nodes", len(snap.Nodes), "conns", len(snap.Conns), "json", string(j)) - - // verify that the snap element numbers check out - if len(checkIds) != len(snap.Conns) || len(checkIds) != len(snap.Nodes) { - t.Fatalf("snapshot wrong node,conn counts %d,%d != %d", len(snap.Nodes), len(snap.Conns), len(checkIds)) - } - - // shut down sim network - runningOne = false - sub.Unsubscribe() - network.Shutdown() - - // check that we have all the expected connections in the snapshot - for nodid, nodConns := range checkIds { - for _, nodConn := range nodConns { - var match bool - for _, snapConn := range snap.Conns { - if snapConn.One == nodid && snapConn.Other == nodConn { - match = true - break - } else if snapConn.Other == nodid && snapConn.One == nodConn { - match = true - break - } - } - if !match { - t.Fatalf("snapshot missing conn %v -> %v", nodid, nodConn) - } - } - } - log.Info("snapshot checked") - - // PART II - // load snapshot and verify that exactly same connections are formed - - adapter = adapters.NewSimAdapter(adapters.LifecycleConstructors{ - "noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { - return NewNoopService(nil), nil - }, - }) - network = NewNetwork(adapter, &NetworkConfig{ - DefaultService: "noopwoop", - }) - defer func() { - network.Shutdown() - }() - - // subscribe to peer events - // every node up and conn up event will generate one additional control event - // therefore multiply the count by two - evC = make(chan *Event, (len(snap.Conns)*2)+(len(snap.Nodes)*2)) - sub = network.Events().Subscribe(evC) - defer sub.Unsubscribe() - - // load the snapshot - // spawn separate thread to avoid deadlock in the event listeners - err = network.Load(snap) - if err != nil { - t.Fatal(err) - } - - // collect connection events up to expected number - ctx, cancel = context.WithTimeout(context.TODO(), time.Second*3) - defer cancel() - - connEventCount = nodeCount - -OuterTwo: - for { - select { - case <-ctx.Done(): - t.Fatal(ctx.Err()) - case ev := <-evC: - if ev.Type == EventTypeConn && !ev.Control { - // fail on any disconnect - if !ev.Conn.Up { - t.Fatalf("unexpected disconnect: %v -> %v", ev.Conn.One, ev.Conn.Other) - } - log.Debug("conn", "on", ev.Conn.One, "other", ev.Conn.Other) - checkIds[ev.Conn.One] = append(checkIds[ev.Conn.One], ev.Conn.Other) - checkIds[ev.Conn.Other] = append(checkIds[ev.Conn.Other], ev.Conn.One) - connEventCount-- - log.Debug("ev", "count", connEventCount) - if connEventCount == 0 { - break OuterTwo - } - } - } - } - - // check that we have all expected connections in the network - for _, snapConn := range snap.Conns { - var match bool - for nodid, nodConns := range checkIds { - for _, nodConn := range nodConns { - if snapConn.One == nodid && snapConn.Other == nodConn { - match = true - break - } else if snapConn.Other == nodid && snapConn.One == nodConn { - match = true - break - } - } - } - if !match { - t.Fatalf("network missing conn %v -> %v", snapConn.One, snapConn.Other) - } - } - - // verify that network didn't generate any other additional connection events after the ones we have collected within a reasonable period of time - ctx, cancel = context.WithTimeout(context.TODO(), time.Second) - defer cancel() - select { - case <-ctx.Done(): - case ev := <-evC: - if ev.Type == EventTypeConn { - t.Fatalf("Superfluous conn found %v -> %v", ev.Conn.One, ev.Conn.Other) - } - } - - // This test validates if all connections from the snapshot - // are created in the network. - t.Run("conns after load", func(t *testing.T) { - // Create new network. - n := NewNetwork( - adapters.NewSimAdapter(adapters.LifecycleConstructors{ - "noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { - return NewNoopService(nil), nil - }, - }), - &NetworkConfig{ - DefaultService: "noopwoop", - }, - ) - defer n.Shutdown() - - // Load the same snapshot. - err := n.Load(snap) - if err != nil { - t.Fatal(err) - } - - // Check every connection from the snapshot - // if it is in the network, too. - for _, c := range snap.Conns { - if n.GetConn(c.One, c.Other) == nil { - t.Errorf("missing connection: %s -> %s", c.One, c.Other) - } - } - }) -} - -// TestNetworkSimulation creates a multi-node simulation network with each node -// connected in a ring topology, checks that all nodes successfully handshake -// with each other and that a snapshot fully represents the desired topology -func TestNetworkSimulation(t *testing.T) { - // create simulation network with 20 testService nodes - adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ - "test": newTestService, - }) - network := NewNetwork(adapter, &NetworkConfig{ - DefaultService: "test", - }) - defer network.Shutdown() - nodeCount := 20 - ids := make([]enode.ID, nodeCount) - for i := 0; i < nodeCount; i++ { - 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() - } - - // perform a check which connects the nodes in a ring (so each node is - // connected to exactly two peers) and then checks that all nodes - // performed two handshakes by checking their peerCount - action := func(_ context.Context) error { - for i, id := range ids { - peerID := ids[(i+1)%len(ids)] - if err := network.Connect(id, peerID); err != nil { - return err - } - } - return nil - } - check := func(ctx context.Context, id enode.ID) (bool, error) { - // check we haven't run out of time - select { - case <-ctx.Done(): - return false, ctx.Err() - default: - } - - // get the node - node := network.GetNode(id) - if node == nil { - return false, fmt.Errorf("unknown node: %s", id) - } - - // check it has exactly two peers - client, err := node.Client() - if err != nil { - return false, err - } - var peerCount int64 - if err := client.CallContext(ctx, &peerCount, "test_peerCount"); err != nil { - return false, err - } - switch { - case peerCount < 2: - return false, nil - case peerCount == 2: - return true, nil - default: - return false, fmt.Errorf("unexpected peerCount: %d", peerCount) - } - } - - timeout := 30 * time.Second - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - // trigger a check every 100ms - trigger := make(chan enode.ID) - go triggerChecks(ctx, ids, trigger, 100*time.Millisecond) - - result := NewSimulation(network).Run(ctx, &Step{ - Action: action, - Trigger: trigger, - Expect: &Expectation{ - Nodes: ids, - Check: check, - }, - }) - if result.Error != nil { - t.Fatalf("simulation failed: %s", result.Error) - } - - // take a network snapshot and check it contains the correct topology - snap, err := network.Snapshot() - if err != nil { - t.Fatal(err) - } - if len(snap.Nodes) != nodeCount { - t.Fatalf("expected snapshot to contain %d nodes, got %d", nodeCount, len(snap.Nodes)) - } - if len(snap.Conns) != nodeCount { - t.Fatalf("expected snapshot to contain %d connections, got %d", nodeCount, len(snap.Conns)) - } - for i, id := range ids { - conn := snap.Conns[i] - if conn.One != id { - t.Fatalf("expected conn[%d].One to be %s, got %s", i, id, conn.One) - } - peerID := ids[(i+1)%len(ids)] - if conn.Other != peerID { - t.Fatalf("expected conn[%d].Other to be %s, got %s", i, peerID, conn.Other) - } - } -} - -func createTestNodes(count int, network *Network) (nodes []*Node, err error) { - for i := 0; i < count; i++ { - nodeConf := adapters.RandomNodeConfig() - node, err := network.NewNodeWithConfig(nodeConf) - if err != nil { - return nil, err - } - if err := network.Start(node.ID()); err != nil { - return nil, err - } - - nodes = append(nodes, node) - } - - return nodes, nil -} - -func createTestNodesWithProperty(property string, count int, network *Network) (propertyNodes []*Node, err error) { - for i := 0; i < count; i++ { - nodeConf := adapters.RandomNodeConfig() - nodeConf.Properties = append(nodeConf.Properties, property) - - node, err := network.NewNodeWithConfig(nodeConf) - if err != nil { - return nil, err - } - if err := network.Start(node.ID()); err != nil { - return nil, err - } - - propertyNodes = append(propertyNodes, node) - } - - return propertyNodes, nil -} - -// TestGetNodeIDs creates a set of nodes and attempts to retrieve their IDs,. -// It then tests again whilst excluding a node ID from being returned. -// If a node ID is not returned, or more node IDs than expected are returned, the test fails. -func TestGetNodeIDs(t *testing.T) { - adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ - "test": newTestService, - }) - network := NewNetwork(adapter, &NetworkConfig{ - DefaultService: "test", - }) - defer network.Shutdown() - - numNodes := 5 - nodes, err := createTestNodes(numNodes, network) - if err != nil { - t.Fatalf("Could not create test nodes %v", err) - } - - gotNodeIDs := network.GetNodeIDs() - if len(gotNodeIDs) != numNodes { - t.Fatalf("Expected %d nodes, got %d", numNodes, len(gotNodeIDs)) - } - - for _, node1 := range nodes { - match := false - for _, node2ID := range gotNodeIDs { - if bytes.Equal(node1.ID().Bytes(), node2ID.Bytes()) { - match = true - break - } - } - - if !match { - t.Fatalf("A created node was not returned by GetNodes(), ID: %s", node1.ID().String()) - } - } - - excludeNodeID := nodes[3].ID() - gotNodeIDsExcl := network.GetNodeIDs(excludeNodeID) - if len(gotNodeIDsExcl) != numNodes-1 { - t.Fatalf("Expected one less node ID to be returned") - } - for _, nodeID := range gotNodeIDsExcl { - if bytes.Equal(excludeNodeID.Bytes(), nodeID.Bytes()) { - t.Fatalf("GetNodeIDs returned the node ID we excluded, ID: %s", nodeID.String()) - } - } -} - -// TestGetNodes creates a set of nodes and attempts to retrieve them again. -// It then tests again whilst excluding a node from being returned. -// If a node is not returned, or more nodes than expected are returned, the test fails. -func TestGetNodes(t *testing.T) { - adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ - "test": newTestService, - }) - network := NewNetwork(adapter, &NetworkConfig{ - DefaultService: "test", - }) - defer network.Shutdown() - - numNodes := 5 - nodes, err := createTestNodes(numNodes, network) - if err != nil { - t.Fatalf("Could not create test nodes %v", err) - } - - gotNodes := network.GetNodes() - if len(gotNodes) != numNodes { - t.Fatalf("Expected %d nodes, got %d", numNodes, len(gotNodes)) - } - - for _, node1 := range nodes { - match := false - for _, node2 := range gotNodes { - if bytes.Equal(node1.ID().Bytes(), node2.ID().Bytes()) { - match = true - break - } - } - - if !match { - t.Fatalf("A created node was not returned by GetNodes(), ID: %s", node1.ID().String()) - } - } - - excludeNodeID := nodes[3].ID() - gotNodesExcl := network.GetNodes(excludeNodeID) - if len(gotNodesExcl) != numNodes-1 { - t.Fatalf("Expected one less node to be returned") - } - for _, node := range gotNodesExcl { - if bytes.Equal(excludeNodeID.Bytes(), node.ID().Bytes()) { - t.Fatalf("GetNodes returned the node we excluded, ID: %s", node.ID().String()) - } - } -} - -// TestGetNodesByID creates a set of nodes and attempts to retrieve a subset of them by ID -// If a node is not returned, or more nodes than expected are returned, the test fails. -func TestGetNodesByID(t *testing.T) { - adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ - "test": newTestService, - }) - network := NewNetwork(adapter, &NetworkConfig{ - DefaultService: "test", - }) - defer network.Shutdown() - - numNodes := 5 - nodes, err := createTestNodes(numNodes, network) - if err != nil { - t.Fatalf("Could not create test nodes: %v", err) - } - - numSubsetNodes := 2 - subsetNodes := nodes[0:numSubsetNodes] - var subsetNodeIDs []enode.ID - for _, node := range subsetNodes { - subsetNodeIDs = append(subsetNodeIDs, node.ID()) - } - - gotNodesByID := network.GetNodesByID(subsetNodeIDs) - if len(gotNodesByID) != numSubsetNodes { - t.Fatalf("Expected %d nodes, got %d", numSubsetNodes, len(gotNodesByID)) - } - - for _, node1 := range subsetNodes { - match := false - for _, node2 := range gotNodesByID { - if bytes.Equal(node1.ID().Bytes(), node2.ID().Bytes()) { - match = true - break - } - } - - if !match { - t.Fatalf("A created node was not returned by GetNodesByID(), ID: %s", node1.ID().String()) - } - } -} - -// TestGetNodesByProperty creates a subset of nodes with a property assigned. -// GetNodesByProperty is then checked for correctness by comparing the nodes returned to those initially created. -// If a node with a property is not found, or more nodes than expected are returned, the test fails. -func TestGetNodesByProperty(t *testing.T) { - adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ - "test": newTestService, - }) - network := NewNetwork(adapter, &NetworkConfig{ - DefaultService: "test", - }) - defer network.Shutdown() - - numNodes := 3 - _, err := createTestNodes(numNodes, network) - if err != nil { - t.Fatalf("Failed to create nodes: %v", err) - } - - numPropertyNodes := 3 - propertyTest := "test" - propertyNodes, err := createTestNodesWithProperty(propertyTest, numPropertyNodes, network) - if err != nil { - t.Fatalf("Failed to create nodes with property: %v", err) - } - - gotNodesByProperty := network.GetNodesByProperty(propertyTest) - if len(gotNodesByProperty) != numPropertyNodes { - t.Fatalf("Expected %d nodes with a property, got %d", numPropertyNodes, len(gotNodesByProperty)) - } - - for _, node1 := range propertyNodes { - match := false - for _, node2 := range gotNodesByProperty { - if bytes.Equal(node1.ID().Bytes(), node2.ID().Bytes()) { - match = true - break - } - } - - if !match { - t.Fatalf("A created node with property was not returned by GetNodesByProperty(), ID: %s", node1.ID().String()) - } - } -} - -// TestGetNodeIDsByProperty creates a subset of nodes with a property assigned. -// GetNodeIDsByProperty is then checked for correctness by comparing the node IDs returned to those initially created. -// If a node ID with a property is not found, or more nodes IDs than expected are returned, the test fails. -func TestGetNodeIDsByProperty(t *testing.T) { - adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ - "test": newTestService, - }) - network := NewNetwork(adapter, &NetworkConfig{ - DefaultService: "test", - }) - defer network.Shutdown() - - numNodes := 3 - _, err := createTestNodes(numNodes, network) - if err != nil { - t.Fatalf("Failed to create nodes: %v", err) - } - - numPropertyNodes := 3 - propertyTest := "test" - propertyNodes, err := createTestNodesWithProperty(propertyTest, numPropertyNodes, network) - if err != nil { - t.Fatalf("Failed to created nodes with property: %v", err) - } - - gotNodeIDsByProperty := network.GetNodeIDsByProperty(propertyTest) - if len(gotNodeIDsByProperty) != numPropertyNodes { - t.Fatalf("Expected %d nodes with a property, got %d", numPropertyNodes, len(gotNodeIDsByProperty)) - } - - for _, node1 := range propertyNodes { - match := false - id1 := node1.ID() - for _, id2 := range gotNodeIDsByProperty { - if bytes.Equal(id1.Bytes(), id2.Bytes()) { - match = true - break - } - } - - if !match { - t.Fatalf("Not all nodes IDs were returned by GetNodeIDsByProperty(), ID: %s", id1.String()) - } - } -} - -func triggerChecks(ctx context.Context, ids []enode.ID, trigger chan enode.ID, interval time.Duration) { - tick := time.NewTicker(interval) - defer tick.Stop() - for { - select { - case <-tick.C: - for _, id := range ids { - select { - case trigger <- id: - case <-ctx.Done(): - return - } - } - case <-ctx.Done(): - return - } - } -} - -// \todo: refactor to implement snapshots -// and connect configuration methods once these are moved from -// swarm/network/simulations/connect.go -func BenchmarkMinimalService(b *testing.B) { - b.Run("ring/32", benchmarkMinimalServiceTmp) -} - -func benchmarkMinimalServiceTmp(b *testing.B) { - // stop timer to discard setup time pollution - args := strings.Split(b.Name(), "/") - nodeCount, err := strconv.ParseInt(args[2], 10, 16) - if err != nil { - b.Fatal(err) - } - - for i := 0; i < b.N; i++ { - // this is a minimal service, whose protocol will close a channel upon run of protocol - // making it possible to bench the time it takes for the service to start and protocol actually to be run - protoCMap := make(map[enode.ID]map[enode.ID]chan struct{}) - adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ - "noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { - protoCMap[ctx.Config.ID] = make(map[enode.ID]chan struct{}) - svc := NewNoopService(protoCMap[ctx.Config.ID]) - return svc, nil - }, - }) - - // create network - network := NewNetwork(adapter, &NetworkConfig{ - DefaultService: "noopwoop", - }) - defer network.Shutdown() - - // create and start nodes - ids := make([]enode.ID, nodeCount) - for i := 0; i < int(nodeCount); i++ { - conf := adapters.RandomNodeConfig() - node, err := network.NewNodeWithConfig(conf) - if err != nil { - b.Fatalf("error creating node: %s", err) - } - if err := network.Start(node.ID()); err != nil { - b.Fatalf("error starting node: %s", err) - } - ids[i] = node.ID() - } - - // ready, set, go - b.ResetTimer() - - // connect nodes in a ring - for i, id := range ids { - peerID := ids[(i+1)%len(ids)] - if err := network.Connect(id, peerID); err != nil { - b.Fatal(err) - } - } - - // wait for all protocols to signal to close down - ctx, cancel := context.WithTimeout(context.TODO(), time.Second) - defer cancel() - for nodid, peers := range protoCMap { - for peerid, peerC := range peers { - log.Debug("getting ", "node", nodid, "peer", peerid) - select { - case <-ctx.Done(): - b.Fatal(ctx.Err()) - case <-peerC: - } - } - } - } -} - -func TestNode_UnmarshalJSON(t *testing.T) { - t.Run("up_field", func(t *testing.T) { - runNodeUnmarshalJSON(t, casesNodeUnmarshalJSONUpField()) - }) - t.Run("config_field", func(t *testing.T) { - runNodeUnmarshalJSON(t, casesNodeUnmarshalJSONConfigField()) - }) -} - -func runNodeUnmarshalJSON(t *testing.T, tests []nodeUnmarshalTestCase) { - t.Helper() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var got *Node - if err := json.Unmarshal([]byte(tt.marshaled), &got); err != nil { - expectErrorMessageToContain(t, err, tt.wantErr) - got = nil - } - expectNodeEquality(t, got, tt.want) - }) - } -} - -type nodeUnmarshalTestCase struct { - name string - marshaled string - want *Node - wantErr string -} - -func expectErrorMessageToContain(t *testing.T, got error, want string) { - t.Helper() - if got == nil && want == "" { - return - } - - if got == nil && want != "" { - t.Errorf("error was expected, got: nil, want: %v", want) - return - } - - if !strings.Contains(got.Error(), want) { - t.Errorf( - "unexpected error message, got %v, want: %v", - want, - got, - ) - } -} - -func expectNodeEquality(t *testing.T, got, want *Node) { - t.Helper() - if !reflect.DeepEqual(got, want) { - t.Errorf("Node.UnmarshalJSON() = %v, want %v", got, want) - } -} - -func casesNodeUnmarshalJSONUpField() []nodeUnmarshalTestCase { - return []nodeUnmarshalTestCase{ - { - name: "empty json", - marshaled: "{}", - want: newNode(nil, nil, false), - }, - { - name: "a stopped node", - marshaled: "{\"up\": false}", - want: newNode(nil, nil, false), - }, - { - name: "a running node", - marshaled: "{\"up\": true}", - want: newNode(nil, nil, true), - }, - { - name: "invalid JSON value on valid key", - marshaled: "{\"up\": foo}", - wantErr: "invalid character", - }, - { - name: "invalid JSON key and value", - marshaled: "{foo: bar}", - wantErr: "invalid character", - }, - { - name: "bool value expected but got something else (string)", - marshaled: "{\"up\": \"true\"}", - wantErr: "cannot unmarshal string into Go struct", - }, - } -} - -func casesNodeUnmarshalJSONConfigField() []nodeUnmarshalTestCase { - // Don't do a big fuss around testing, as adapters.NodeConfig should - // handle it's own serialization. Just do a sanity check. - return []nodeUnmarshalTestCase{ - { - name: "Config field is omitted", - marshaled: "{}", - want: newNode(nil, nil, false), - }, - { - name: "Config field is nil", - marshaled: "{\"config\": null}", - want: newNode(nil, nil, false), - }, - { - name: "a non default Config field", - marshaled: "{\"config\":{\"name\":\"node_ecdd0\",\"port\":44665}}", - want: newNode(nil, &adapters.NodeConfig{Name: "node_ecdd0", Port: 44665}, false), - }, - } -} diff --git a/p2p/simulations/pipes/pipes.go b/p2p/simulations/pipes/pipes.go deleted file mode 100644 index ec277c0d14..0000000000 --- a/p2p/simulations/pipes/pipes.go +++ /dev/null @@ -1,55 +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 . - -package pipes - -import ( - "net" -) - -// NetPipe wraps net.Pipe in a signature returning an error -func NetPipe() (net.Conn, net.Conn, error) { - p1, p2 := net.Pipe() - return p1, p2, nil -} - -// TCPPipe creates an in process full duplex pipe based on a localhost TCP socket -func TCPPipe() (net.Conn, net.Conn, error) { - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - return nil, nil, err - } - defer l.Close() - - var aconn net.Conn - aerr := make(chan error, 1) - go func() { - var err error - aconn, err = l.Accept() - aerr <- err - }() - - dconn, err := net.Dial("tcp", l.Addr().String()) - if err != nil { - <-aerr - return nil, nil, err - } - if err := <-aerr; err != nil { - dconn.Close() - return nil, nil, err - } - return aconn, dconn, nil -} diff --git a/p2p/simulations/simulation.go b/p2p/simulations/simulation.go deleted file mode 100644 index ae62c42b9c..0000000000 --- a/p2p/simulations/simulation.go +++ /dev/null @@ -1,157 +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 . - -package simulations - -import ( - "context" - "time" - - "github.com/ethereum/go-ethereum/p2p/enode" -) - -// Simulation provides a framework for running actions in a simulated network -// and then waiting for expectations to be met -type Simulation struct { - network *Network -} - -// NewSimulation returns a new simulation which runs in the given network -func NewSimulation(network *Network) *Simulation { - return &Simulation{ - network: network, - } -} - -// Run performs a step of the simulation by performing the step's action and -// then waiting for the step's expectation to be met -func (s *Simulation) Run(ctx context.Context, step *Step) (result *StepResult) { - result = newStepResult() - - result.StartedAt = time.Now() - defer func() { result.FinishedAt = time.Now() }() - - // watch network events for the duration of the step - stop := s.watchNetwork(result) - defer stop() - - // perform the action - if err := step.Action(ctx); err != nil { - result.Error = err - return - } - - // wait for all node expectations to either pass, error or timeout - nodes := make(map[enode.ID]struct{}, len(step.Expect.Nodes)) - for _, id := range step.Expect.Nodes { - nodes[id] = struct{}{} - } - for len(result.Passes) < len(nodes) { - select { - case id := <-step.Trigger: - // skip if we aren't checking the node - if _, ok := nodes[id]; !ok { - continue - } - - // skip if the node has already passed - if _, ok := result.Passes[id]; ok { - continue - } - - // run the node expectation check - pass, err := step.Expect.Check(ctx, id) - if err != nil { - result.Error = err - return - } - if pass { - result.Passes[id] = time.Now() - } - case <-ctx.Done(): - result.Error = ctx.Err() - return - } - } - - return -} - -func (s *Simulation) watchNetwork(result *StepResult) func() { - stop := make(chan struct{}) - done := make(chan struct{}) - events := make(chan *Event) - sub := s.network.Events().Subscribe(events) - go func() { - defer close(done) - defer sub.Unsubscribe() - for { - select { - case event := <-events: - result.NetworkEvents = append(result.NetworkEvents, event) - case <-stop: - return - } - } - }() - return func() { - close(stop) - <-done - } -} - -type Step struct { - // Action is the action to perform for this step - Action func(context.Context) error - - // Trigger is a channel which receives node ids and triggers an - // expectation check for that node - Trigger chan enode.ID - - // Expect is the expectation to wait for when performing this step - Expect *Expectation -} - -type Expectation struct { - // Nodes is a list of nodes to check - Nodes []enode.ID - - // Check checks whether a given node meets the expectation - Check func(context.Context, enode.ID) (bool, error) -} - -func newStepResult() *StepResult { - return &StepResult{ - Passes: make(map[enode.ID]time.Time), - } -} - -type StepResult struct { - // Error is the error encountered whilst running the step - Error error - - // StartedAt is the time the step started - StartedAt time.Time - - // FinishedAt is the time the step finished - FinishedAt time.Time - - // Passes are the timestamps of the successful node expectations - Passes map[enode.ID]time.Time - - // NetworkEvents are the network events which occurred during the step - NetworkEvents []*Event -} diff --git a/p2p/simulations/test.go b/p2p/simulations/test.go deleted file mode 100644 index 0edb07b127..0000000000 --- a/p2p/simulations/test.go +++ /dev/null @@ -1,150 +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 . - -package simulations - -import ( - "testing" - - "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" -) - -// NoopService is the service that does not do anything -// but implements node.Service interface. -type NoopService struct { - c map[enode.ID]chan struct{} -} - -func NewNoopService(ackC map[enode.ID]chan struct{}) *NoopService { - return &NoopService{ - c: ackC, - } -} - -func (t *NoopService) Protocols() []p2p.Protocol { - return []p2p.Protocol{ - { - Name: "noop", - Version: 666, - Length: 0, - Run: func(peer *p2p.Peer, rw p2p.MsgReadWriter) error { - if t.c != nil { - t.c[peer.ID()] = make(chan struct{}) - close(t.c[peer.ID()]) - } - rw.ReadMsg() - return nil - }, - NodeInfo: func() interface{} { - return struct{}{} - }, - PeerInfo: func(id enode.ID) interface{} { - return struct{}{} - }, - Attributes: []enr.Entry{}, - }, - } -} - -func (t *NoopService) APIs() []rpc.API { - return []rpc.API{} -} - -func (t *NoopService) Start() error { - return nil -} - -func (t *NoopService) Stop() error { - return nil -} - -func VerifyRing(t *testing.T, net *Network, ids []enode.ID) { - t.Helper() - n := len(ids) - for i := 0; i < n; i++ { - for j := i + 1; j < n; j++ { - c := net.GetConn(ids[i], ids[j]) - if i == j-1 || (i == 0 && j == n-1) { - if c == nil { - t.Errorf("nodes %v and %v are not connected, but they should be", i, j) - } - } else { - if c != nil { - t.Errorf("nodes %v and %v are connected, but they should not be", i, j) - } - } - } - } -} - -func VerifyChain(t *testing.T, net *Network, ids []enode.ID) { - t.Helper() - n := len(ids) - for i := 0; i < n; i++ { - for j := i + 1; j < n; j++ { - c := net.GetConn(ids[i], ids[j]) - if i == j-1 { - if c == nil { - t.Errorf("nodes %v and %v are not connected, but they should be", i, j) - } - } else { - if c != nil { - t.Errorf("nodes %v and %v are connected, but they should not be", i, j) - } - } - } - } -} - -func VerifyFull(t *testing.T, net *Network, ids []enode.ID) { - t.Helper() - n := len(ids) - var connections int - for i, lid := range ids { - for _, rid := range ids[i+1:] { - if net.GetConn(lid, rid) != nil { - connections++ - } - } - } - - want := n * (n - 1) / 2 - if connections != want { - t.Errorf("wrong number of connections, got: %v, want: %v", connections, want) - } -} - -func VerifyStar(t *testing.T, net *Network, ids []enode.ID, centerIndex int) { - t.Helper() - n := len(ids) - for i := 0; i < n; i++ { - for j := i + 1; j < n; j++ { - c := net.GetConn(ids[i], ids[j]) - if i == centerIndex || j == centerIndex { - if c == nil { - t.Errorf("nodes %v and %v are not connected, but they should be", i, j) - } - } else { - if c != nil { - t.Errorf("nodes %v and %v are connected, but they should not be", i, j) - } - } - } - } -} diff --git a/p2p/tracker/tracker.go b/p2p/tracker/tracker.go deleted file mode 100644 index 6a733b9ba5..0000000000 --- a/p2p/tracker/tracker.go +++ /dev/null @@ -1,205 +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 . - -package tracker - -import ( - "container/list" - "fmt" - "sync" - "time" - - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" -) - -const ( - // trackedGaugeName is the prefix of the per-packet request tracking. - trackedGaugeName = "p2p/tracked" - - // lostMeterName is the prefix of the per-packet request expirations. - lostMeterName = "p2p/lost" - - // staleMeterName is the prefix of the per-packet stale responses. - staleMeterName = "p2p/stale" - - // waitHistName is the prefix of the per-packet (req only) waiting time histograms. - waitHistName = "p2p/wait" - - // maxTrackedPackets is a huge number to act as a failsafe on the number of - // pending requests the node will track. It should never be hit unless an - // attacker figures out a way to spin requests. - maxTrackedPackets = 100000 -) - -// request tracks sent network requests which have not yet received a response. -type request struct { - peer string - version uint // Protocol version - - reqCode uint64 // Protocol message code of the request - resCode uint64 // Protocol message code of the expected response - - time time.Time // Timestamp when the request was made - expire *list.Element // Expiration marker to untrack it -} - -// Tracker is a pending network request tracker to measure how much time it takes -// a remote peer to respond. -type Tracker struct { - protocol string // Protocol capability identifier for the metrics - timeout time.Duration // Global timeout after which to drop a tracked packet - - pending map[uint64]*request // Currently pending requests - expire *list.List // Linked list tracking the expiration order - wake *time.Timer // Timer tracking the expiration of the next item - - lock sync.Mutex // Lock protecting from concurrent updates -} - -// New creates a new network request tracker to monitor how much time it takes to -// fill certain requests and how individual peers perform. -func New(protocol string, timeout time.Duration) *Tracker { - return &Tracker{ - protocol: protocol, - timeout: timeout, - pending: make(map[uint64]*request), - expire: list.New(), - } -} - -// Track adds a network request to the tracker to wait for a response to arrive -// or until the request it cancelled or times out. -func (t *Tracker) Track(peer string, version uint, reqCode uint64, resCode uint64, id uint64) { - if !metrics.Enabled { - return - } - t.lock.Lock() - defer t.lock.Unlock() - - // If there's a duplicate request, we've just random-collided (or more probably, - // we have a bug), report it. We could also add a metric, but we're not really - // expecting ourselves to be buggy, so a noisy warning should be enough. - if _, ok := t.pending[id]; ok { - log.Error("Network request id collision", "protocol", t.protocol, "version", version, "code", reqCode, "id", id) - return - } - // If we have too many pending requests, bail out instead of leaking memory - if pending := len(t.pending); pending >= maxTrackedPackets { - log.Error("Request tracker exceeded allowance", "pending", pending, "peer", peer, "protocol", t.protocol, "version", version, "code", reqCode) - return - } - // Id doesn't exist yet, start tracking it - t.pending[id] = &request{ - peer: peer, - version: version, - reqCode: reqCode, - resCode: resCode, - time: time.Now(), - expire: t.expire.PushBack(id), - } - g := fmt.Sprintf("%s/%s/%d/%#02x", trackedGaugeName, t.protocol, version, reqCode) - metrics.GetOrRegisterGauge(g, nil).Inc(1) - - // If we've just inserted the first item, start the expiration timer - if t.wake == nil { - t.wake = time.AfterFunc(t.timeout, t.clean) - } -} - -// clean is called automatically when a preset time passes without a response -// being delivered for the first network request. -func (t *Tracker) clean() { - t.lock.Lock() - defer t.lock.Unlock() - - // Expire anything within a certain threshold (might be no items at all if - // we raced with the delivery) - for t.expire.Len() > 0 { - // Stop iterating if the next pending request is still alive - var ( - head = t.expire.Front() - id = head.Value.(uint64) - req = t.pending[id] - ) - if time.Since(req.time) < t.timeout+5*time.Millisecond { - break - } - // Nope, dead, drop it - t.expire.Remove(head) - delete(t.pending, id) - - g := fmt.Sprintf("%s/%s/%d/%#02x", trackedGaugeName, t.protocol, req.version, req.reqCode) - metrics.GetOrRegisterGauge(g, nil).Dec(1) - - m := fmt.Sprintf("%s/%s/%d/%#02x", lostMeterName, t.protocol, req.version, req.reqCode) - metrics.GetOrRegisterMeter(m, nil).Mark(1) - } - t.schedule() -} - -// schedule starts a timer to trigger on the expiration of the first network -// packet. -func (t *Tracker) schedule() { - if t.expire.Len() == 0 { - t.wake = nil - return - } - t.wake = time.AfterFunc(time.Until(t.pending[t.expire.Front().Value.(uint64)].time.Add(t.timeout)), t.clean) -} - -// Fulfil fills a pending request, if any is available, reporting on various metrics. -func (t *Tracker) Fulfil(peer string, version uint, code uint64, id uint64) { - if !metrics.Enabled { - return - } - t.lock.Lock() - defer t.lock.Unlock() - - // If it's a non existing request, track as stale response - req, ok := t.pending[id] - if !ok { - m := fmt.Sprintf("%s/%s/%d/%#02x", staleMeterName, t.protocol, version, code) - metrics.GetOrRegisterMeter(m, nil).Mark(1) - return - } - // If the response is funky, it might be some active attack - if req.peer != peer || req.version != version || req.resCode != code { - log.Warn("Network response id collision", - "have", fmt.Sprintf("%s:%s/%d:%d", peer, t.protocol, version, code), - "want", fmt.Sprintf("%s:%s/%d:%d", peer, t.protocol, req.version, req.resCode), - ) - return - } - // Everything matches, mark the request serviced and meter it - t.expire.Remove(req.expire) - delete(t.pending, id) - if req.expire.Prev() == nil { - if t.wake.Stop() { - t.schedule() - } - } - g := fmt.Sprintf("%s/%s/%d/%#02x", trackedGaugeName, t.protocol, req.version, req.reqCode) - metrics.GetOrRegisterGauge(g, nil).Dec(1) - - h := fmt.Sprintf("%s/%s/%d/%#02x", waitHistName, t.protocol, req.version, req.reqCode) - sampler := func() metrics.Sample { - return metrics.ResettingSample( - metrics.NewExpDecaySample(1028, 0.015), - ) - } - metrics.GetOrRegisterHistogramLazy(h, nil, sampler).Update(time.Since(req.time).Microseconds()) -} diff --git a/p2p/transport.go b/p2p/transport.go deleted file mode 100644 index 4f6bb569bf..0000000000 --- a/p2p/transport.go +++ /dev/null @@ -1,182 +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 . - -package p2p - -import ( - "bytes" - "crypto/ecdsa" - "fmt" - "io" - "net" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/bitutil" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/p2p/rlpx" - "github.com/ethereum/go-ethereum/rlp" -) - -const ( - // total timeout for encryption handshake and protocol - // handshake in both directions. - handshakeTimeout = 5 * time.Second - - // This is the timeout for sending the disconnect reason. - // This is shorter than the usual timeout because we don't want - // to wait if the connection is known to be bad anyway. - discWriteTimeout = 1 * time.Second -) - -// rlpxTransport is the transport used by actual (non-test) connections. -// It wraps an RLPx connection with locks and read/write deadlines. -type rlpxTransport struct { - rmu, wmu sync.Mutex - wbuf bytes.Buffer - conn *rlpx.Conn -} - -func newRLPX(conn net.Conn, dialDest *ecdsa.PublicKey) transport { - return &rlpxTransport{conn: rlpx.NewConn(conn, dialDest)} -} - -func (t *rlpxTransport) ReadMsg() (Msg, error) { - t.rmu.Lock() - defer t.rmu.Unlock() - - var msg Msg - t.conn.SetReadDeadline(time.Now().Add(frameReadTimeout)) - code, data, wireSize, err := t.conn.Read() - if err == nil { - // Protocol messages are dispatched to subprotocol handlers asynchronously, - // but package rlpx may reuse the returned 'data' buffer on the next call - // to Read. Copy the message data to avoid this being an issue. - data = common.CopyBytes(data) - msg = Msg{ - ReceivedAt: time.Now(), - Code: code, - Size: uint32(len(data)), - meterSize: uint32(wireSize), - Payload: bytes.NewReader(data), - } - } - return msg, err -} - -func (t *rlpxTransport) WriteMsg(msg Msg) error { - t.wmu.Lock() - defer t.wmu.Unlock() - - // Copy message data to write buffer. - t.wbuf.Reset() - if _, err := io.CopyN(&t.wbuf, msg.Payload, int64(msg.Size)); err != nil { - return err - } - - // Write the message. - t.conn.SetWriteDeadline(time.Now().Add(frameWriteTimeout)) - size, err := t.conn.Write(msg.Code, t.wbuf.Bytes()) - if err != nil { - return err - } - - // Set metrics. - msg.meterSize = size - if metrics.Enabled && msg.meterCap.Name != "" { // don't meter non-subprotocol messages - m := fmt.Sprintf("%s/%s/%d/%#02x", egressMeterName, msg.meterCap.Name, msg.meterCap.Version, msg.meterCode) - metrics.GetOrRegisterMeter(m, nil).Mark(int64(msg.meterSize)) - metrics.GetOrRegisterMeter(m+"/packets", nil).Mark(1) - } - return nil -} - -func (t *rlpxTransport) close(err error) { - t.wmu.Lock() - defer t.wmu.Unlock() - - // Tell the remote end why we're disconnecting if possible. - // We only bother doing this if the underlying connection supports - // setting a timeout tough. - if t.conn != nil { - if r, ok := err.(DiscReason); ok && r != DiscNetworkError { - deadline := time.Now().Add(discWriteTimeout) - if err := t.conn.SetWriteDeadline(deadline); err == nil { - // Connection supports write deadline. - t.wbuf.Reset() - rlp.Encode(&t.wbuf, []DiscReason{r}) - t.conn.Write(discMsg, t.wbuf.Bytes()) - } - } - } - t.conn.Close() -} - -func (t *rlpxTransport) doEncHandshake(prv *ecdsa.PrivateKey) (*ecdsa.PublicKey, error) { - t.conn.SetDeadline(time.Now().Add(handshakeTimeout)) - return t.conn.Handshake(prv) -} - -func (t *rlpxTransport) doProtoHandshake(our *protoHandshake) (their *protoHandshake, err error) { - // Writing our handshake happens concurrently, we prefer - // returning the handshake read error. If the remote side - // disconnects us early with a valid reason, we should return it - // as the error so it can be tracked elsewhere. - werr := make(chan error, 1) - go func() { werr <- Send(t, handshakeMsg, our) }() - if their, err = readProtocolHandshake(t); err != nil { - <-werr // make sure the write terminates too - return nil, err - } - if err := <-werr; err != nil { - return nil, fmt.Errorf("write error: %v", err) - } - // If the protocol version supports Snappy encoding, upgrade immediately - t.conn.SetSnappy(their.Version >= snappyProtocolVersion) - - return their, nil -} - -func readProtocolHandshake(rw MsgReader) (*protoHandshake, error) { - msg, err := rw.ReadMsg() - if err != nil { - return nil, err - } - if msg.Size > baseProtocolMaxMsgSize { - return nil, fmt.Errorf("message too big") - } - if msg.Code == discMsg { - // Disconnect before protocol handshake is valid according to the - // spec and we send it ourself if the post-handshake checks fail. - // We can't return the reason directly, though, because it is echoed - // back otherwise. Wrap it in a string instead. - var reason [1]DiscReason - rlp.Decode(msg.Payload, &reason) - return nil, reason[0] - } - if msg.Code != handshakeMsg { - return nil, fmt.Errorf("expected handshake, got %x", msg.Code) - } - var hs protoHandshake - if err := msg.Decode(&hs); err != nil { - return nil, err - } - if len(hs.ID) != 64 || !bitutil.TestBytes(hs.ID) { - return nil, DiscInvalidIdentity - } - return &hs, nil -} diff --git a/p2p/transport_test.go b/p2p/transport_test.go deleted file mode 100644 index 24e06c5a06..0000000000 --- a/p2p/transport_test.go +++ /dev/null @@ -1,148 +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 . - -package p2p - -import ( - "errors" - "reflect" - "sync" - "testing" - - "github.com/davecgh/go-spew/spew" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/p2p/simulations/pipes" -) - -func TestProtocolHandshake(t *testing.T) { - var ( - prv0, _ = crypto.GenerateKey() - pub0 = crypto.FromECDSAPub(&prv0.PublicKey)[1:] - hs0 = &protoHandshake{Version: 3, ID: pub0, Caps: []Cap{{"a", 0}, {"b", 2}}} - - prv1, _ = crypto.GenerateKey() - pub1 = crypto.FromECDSAPub(&prv1.PublicKey)[1:] - hs1 = &protoHandshake{Version: 3, ID: pub1, Caps: []Cap{{"c", 1}, {"d", 3}}} - - wg sync.WaitGroup - ) - - fd0, fd1, err := pipes.TCPPipe() - if err != nil { - t.Fatal(err) - } - - wg.Add(2) - go func() { - defer wg.Done() - defer fd0.Close() - frame := newRLPX(fd0, &prv1.PublicKey) - rpubkey, err := frame.doEncHandshake(prv0) - if err != nil { - t.Errorf("dial side enc handshake failed: %v", err) - return - } - if !reflect.DeepEqual(rpubkey, &prv1.PublicKey) { - t.Errorf("dial side remote pubkey mismatch: got %v, want %v", rpubkey, &prv1.PublicKey) - return - } - - phs, err := frame.doProtoHandshake(hs0) - if err != nil { - t.Errorf("dial side proto handshake error: %v", err) - return - } - phs.Rest = nil - if !reflect.DeepEqual(phs, hs1) { - t.Errorf("dial side proto handshake mismatch:\ngot: %s\nwant: %s\n", spew.Sdump(phs), spew.Sdump(hs1)) - return - } - frame.close(DiscQuitting) - }() - go func() { - defer wg.Done() - defer fd1.Close() - rlpx := newRLPX(fd1, nil) - rpubkey, err := rlpx.doEncHandshake(prv1) - if err != nil { - t.Errorf("listen side enc handshake failed: %v", err) - return - } - if !reflect.DeepEqual(rpubkey, &prv0.PublicKey) { - t.Errorf("listen side remote pubkey mismatch: got %v, want %v", rpubkey, &prv0.PublicKey) - return - } - - phs, err := rlpx.doProtoHandshake(hs1) - if err != nil { - t.Errorf("listen side proto handshake error: %v", err) - return - } - phs.Rest = nil - if !reflect.DeepEqual(phs, hs0) { - t.Errorf("listen side proto handshake mismatch:\ngot: %s\nwant: %s\n", spew.Sdump(phs), spew.Sdump(hs0)) - return - } - - if err := ExpectMsg(rlpx, discMsg, []DiscReason{DiscQuitting}); err != nil { - t.Errorf("error receiving disconnect: %v", err) - } - }() - wg.Wait() -} - -func TestProtocolHandshakeErrors(t *testing.T) { - tests := []struct { - code uint64 - msg interface{} - err error - }{ - { - code: discMsg, - msg: []DiscReason{DiscQuitting}, - err: DiscQuitting, - }, - { - code: 0x989898, - msg: []byte{1}, - err: errors.New("expected handshake, got 989898"), - }, - { - code: handshakeMsg, - msg: make([]byte, baseProtocolMaxMsgSize+2), - err: errors.New("message too big"), - }, - { - code: handshakeMsg, - msg: []byte{1, 2, 3}, - err: newPeerError(errInvalidMsg, "(code 0) (size 4) rlp: expected input list for p2p.protoHandshake"), - }, - { - code: handshakeMsg, - msg: &protoHandshake{Version: 3}, - err: DiscInvalidIdentity, - }, - } - - for i, test := range tests { - p1, p2 := MsgPipe() - go Send(p1, test.code, test.msg) - _, err := readProtocolHandshake(p2) - if !reflect.DeepEqual(err, test.err) { - t.Errorf("test %d: error mismatch: got %q, want %q", i, err, test.err) - } - } -} diff --git a/p2p/util.go b/p2p/util.go deleted file mode 100644 index 2c8f322a66..0000000000 --- a/p2p/util.go +++ /dev/null @@ -1,76 +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 . - -package p2p - -import ( - "container/heap" - - "github.com/ethereum/go-ethereum/common/mclock" -) - -// expHeap tracks strings and their expiry time. -type expHeap []expItem - -// expItem is an entry in addrHistory. -type expItem struct { - item string - exp mclock.AbsTime -} - -// nextExpiry returns the next expiry time. -func (h *expHeap) nextExpiry() mclock.AbsTime { - return (*h)[0].exp -} - -// add adds an item and sets its expiry time. -func (h *expHeap) add(item string, exp mclock.AbsTime) { - heap.Push(h, expItem{item, exp}) -} - -// contains checks whether an item is present. -func (h expHeap) contains(item string) bool { - for _, v := range h { - if v.item == item { - return true - } - } - return false -} - -// expire removes items with expiry time before 'now'. -func (h *expHeap) expire(now mclock.AbsTime, onExp func(string)) { - for h.Len() > 0 && h.nextExpiry() < now { - item := heap.Pop(h) - if onExp != nil { - onExp(item.(expItem).item) - } - } -} - -// heap.Interface boilerplate -func (h expHeap) Len() int { return len(h) } -func (h expHeap) Less(i, j int) bool { return h[i].exp < h[j].exp } -func (h expHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } -func (h *expHeap) Push(x interface{}) { *h = append(*h, x.(expItem)) } -func (h *expHeap) Pop() interface{} { - old := *h - n := len(old) - x := old[n-1] - old[n-1] = expItem{} - *h = old[0 : n-1] - return x -} diff --git a/p2p/util_test.go b/p2p/util_test.go deleted file mode 100644 index cc0d2b215f..0000000000 --- a/p2p/util_test.go +++ /dev/null @@ -1,56 +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 . - -package p2p - -import ( - "testing" - "time" - - "github.com/ethereum/go-ethereum/common/mclock" -) - -func TestExpHeap(t *testing.T) { - var h expHeap - - var ( - basetime = mclock.AbsTime(10) - exptimeA = basetime.Add(2 * time.Second) - exptimeB = basetime.Add(3 * time.Second) - exptimeC = basetime.Add(4 * time.Second) - ) - h.add("b", exptimeB) - h.add("a", exptimeA) - h.add("c", exptimeC) - - if h.nextExpiry() != exptimeA { - t.Fatal("wrong nextExpiry") - } - if !h.contains("a") || !h.contains("b") || !h.contains("c") { - t.Fatal("heap doesn't contain all live items") - } - - h.expire(exptimeA.Add(1), nil) - if h.nextExpiry() != exptimeB { - t.Fatal("wrong nextExpiry") - } - if h.contains("a") { - t.Fatal("heap contains a even though it has already expired") - } - if !h.contains("b") || !h.contains("c") { - t.Fatal("heap doesn't contain all live items") - } -}