A few notes: - this is not all the deletion that we can do, but this is the most "simple" case: it leaves in shims, and there's some trivial additional cleanup to the transport that can happen but that requires writing more code, and I wanted this to be easy to review above all else. - This should land *after* we cut the branch for 0.35, but I'm anticipating that to happen soon, and I wanted to run this through CI.pull/7064/head
@ -1,74 +0,0 @@ | |||
package p2p | |||
import ( | |||
"github.com/tendermint/tendermint/internal/p2p/conn" | |||
"github.com/tendermint/tendermint/libs/service" | |||
) | |||
// Reactor is responsible for handling incoming messages on one or more | |||
// Channel. Switch calls GetChannels when reactor is added to it. When a new | |||
// peer joins our node, InitPeer and AddPeer are called. RemovePeer is called | |||
// when the peer is stopped. Receive is called when a message is received on a | |||
// channel associated with this reactor. | |||
// | |||
// Peer#Send or Peer#TrySend should be used to send the message to a peer. | |||
type Reactor interface { | |||
service.Service // Start, Stop | |||
// SetSwitch allows setting a switch. | |||
SetSwitch(*Switch) | |||
// GetChannels returns the list of MConnection.ChannelDescriptor. Make sure | |||
// that each ID is unique across all the reactors added to the switch. | |||
GetChannels() []*conn.ChannelDescriptor | |||
// InitPeer is called by the switch before the peer is started. Use it to | |||
// initialize data for the peer (e.g. peer state). | |||
// | |||
// NOTE: The switch won't call AddPeer nor RemovePeer if it fails to start | |||
// the peer. Do not store any data associated with the peer in the reactor | |||
// itself unless you don't want to have a state, which is never cleaned up. | |||
InitPeer(peer Peer) Peer | |||
// AddPeer is called by the switch after the peer is added and successfully | |||
// started. Use it to start goroutines communicating with the peer. | |||
AddPeer(peer Peer) | |||
// RemovePeer is called by the switch when the peer is stopped (due to error | |||
// or other reason). | |||
RemovePeer(peer Peer, reason interface{}) | |||
// Receive is called by the switch when msgBytes is received from the peer. | |||
// | |||
// NOTE reactor can not keep msgBytes around after Receive completes without | |||
// copying. | |||
// | |||
// CONTRACT: msgBytes are not nil. | |||
// | |||
// XXX: do not call any methods that can block or incur heavy processing. | |||
// https://github.com/tendermint/tendermint/issues/2888 | |||
Receive(chID byte, peer Peer, msgBytes []byte) | |||
} | |||
//-------------------------------------- | |||
type BaseReactor struct { | |||
service.BaseService // Provides Start, Stop, .Quit | |||
Switch *Switch | |||
} | |||
func NewBaseReactor(name string, impl Reactor) *BaseReactor { | |||
return &BaseReactor{ | |||
BaseService: *service.NewBaseService(nil, name, impl), | |||
Switch: nil, | |||
} | |||
} | |||
func (br *BaseReactor) SetSwitch(sw *Switch) { | |||
br.Switch = sw | |||
} | |||
func (*BaseReactor) GetChannels() []*conn.ChannelDescriptor { return nil } | |||
func (*BaseReactor) AddPeer(peer Peer) {} | |||
func (*BaseReactor) RemovePeer(peer Peer, reason interface{}) {} | |||
func (*BaseReactor) Receive(chID byte, peer Peer, msgBytes []byte) {} | |||
func (*BaseReactor) InitPeer(peer Peer) Peer { return peer } |
@ -1,23 +0,0 @@ | |||
package mock | |||
import ( | |||
"github.com/tendermint/tendermint/internal/p2p" | |||
"github.com/tendermint/tendermint/internal/p2p/conn" | |||
"github.com/tendermint/tendermint/libs/log" | |||
) | |||
type Reactor struct { | |||
p2p.BaseReactor | |||
} | |||
func NewReactor() *Reactor { | |||
r := &Reactor{} | |||
r.BaseReactor = *p2p.NewBaseReactor("Mock-PEX", r) | |||
r.SetLogger(log.TestingLogger()) | |||
return r | |||
} | |||
func (r *Reactor) GetChannels() []*conn.ChannelDescriptor { return []*conn.ChannelDescriptor{} } | |||
func (r *Reactor) AddPeer(peer p2p.Peer) {} | |||
func (r *Reactor) RemovePeer(peer p2p.Peer, reason interface{}) {} | |||
func (r *Reactor) Receive(chID byte, peer p2p.Peer, msgBytes []byte) {} |
@ -1,371 +0,0 @@ | |||
package p2p | |||
import ( | |||
"fmt" | |||
"io" | |||
"net" | |||
"runtime/debug" | |||
"time" | |||
tmconn "github.com/tendermint/tendermint/internal/p2p/conn" | |||
"github.com/tendermint/tendermint/libs/cmap" | |||
"github.com/tendermint/tendermint/libs/log" | |||
"github.com/tendermint/tendermint/libs/service" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
//go:generate ../../scripts/mockery_generate.sh Peer | |||
const metricsTickerDuration = 10 * time.Second | |||
// Peer is an interface representing a peer connected on a reactor. | |||
type Peer interface { | |||
service.Service | |||
FlushStop() | |||
ID() types.NodeID // peer's cryptographic ID | |||
RemoteIP() net.IP // remote IP of the connection | |||
RemoteAddr() net.Addr // remote address of the connection | |||
IsOutbound() bool // did we dial the peer | |||
IsPersistent() bool // do we redial this peer when we disconnect | |||
CloseConn() error // close original connection | |||
NodeInfo() types.NodeInfo // peer's info | |||
Status() tmconn.ConnectionStatus | |||
SocketAddr() *NetAddress // actual address of the socket | |||
Send(byte, []byte) bool | |||
TrySend(byte, []byte) bool | |||
Set(string, interface{}) | |||
Get(string) interface{} | |||
} | |||
//---------------------------------------------------------- | |||
// peerConn contains the raw connection and its config. | |||
type peerConn struct { | |||
outbound bool | |||
persistent bool | |||
conn Connection | |||
ip net.IP // cached RemoteIP() | |||
} | |||
func newPeerConn(outbound, persistent bool, conn Connection) peerConn { | |||
return peerConn{ | |||
outbound: outbound, | |||
persistent: persistent, | |||
conn: conn, | |||
} | |||
} | |||
// Return the IP from the connection RemoteAddr | |||
func (pc peerConn) RemoteIP() net.IP { | |||
if pc.ip == nil { | |||
pc.ip = pc.conn.RemoteEndpoint().IP | |||
} | |||
return pc.ip | |||
} | |||
// peer implements Peer. | |||
// | |||
// Before using a peer, you will need to perform a handshake on connection. | |||
type peer struct { | |||
service.BaseService | |||
// raw peerConn and the multiplex connection | |||
peerConn | |||
// peer's node info and the channel it knows about | |||
// channels = nodeInfo.Channels | |||
// cached to avoid copying nodeInfo in hasChannel | |||
nodeInfo types.NodeInfo | |||
channels []byte | |||
reactors map[byte]Reactor | |||
onPeerError func(Peer, interface{}) | |||
// User data | |||
Data *cmap.CMap | |||
metrics *Metrics | |||
metricsTicker *time.Ticker | |||
} | |||
type PeerOption func(*peer) | |||
func newPeer( | |||
nodeInfo types.NodeInfo, | |||
pc peerConn, | |||
reactorsByCh map[byte]Reactor, | |||
onPeerError func(Peer, interface{}), | |||
options ...PeerOption, | |||
) *peer { | |||
p := &peer{ | |||
peerConn: pc, | |||
nodeInfo: nodeInfo, | |||
channels: nodeInfo.Channels, | |||
reactors: reactorsByCh, | |||
onPeerError: onPeerError, | |||
Data: cmap.NewCMap(), | |||
metricsTicker: time.NewTicker(metricsTickerDuration), | |||
metrics: NopMetrics(), | |||
} | |||
p.BaseService = *service.NewBaseService(nil, "Peer", p) | |||
for _, option := range options { | |||
option(p) | |||
} | |||
return p | |||
} | |||
// onError calls the peer error callback. | |||
func (p *peer) onError(err interface{}) { | |||
p.onPeerError(p, err) | |||
} | |||
// String representation. | |||
func (p *peer) String() string { | |||
if p.outbound { | |||
return fmt.Sprintf("Peer{%v %v out}", p.conn, p.ID()) | |||
} | |||
return fmt.Sprintf("Peer{%v %v in}", p.conn, p.ID()) | |||
} | |||
//--------------------------------------------------- | |||
// Implements service.Service | |||
// SetLogger implements BaseService. | |||
func (p *peer) SetLogger(l log.Logger) { | |||
p.Logger = l | |||
} | |||
// OnStart implements BaseService. | |||
func (p *peer) OnStart() error { | |||
if err := p.BaseService.OnStart(); err != nil { | |||
return err | |||
} | |||
go p.processMessages() | |||
go p.metricsReporter() | |||
return nil | |||
} | |||
// processMessages processes messages received from the connection. | |||
func (p *peer) processMessages() { | |||
defer func() { | |||
if r := recover(); r != nil { | |||
p.Logger.Error("peer message processing panic", "err", r, "stack", string(debug.Stack())) | |||
p.onError(fmt.Errorf("panic during peer message processing: %v", r)) | |||
} | |||
}() | |||
for { | |||
chID, msg, err := p.conn.ReceiveMessage() | |||
if err != nil { | |||
p.onError(err) | |||
return | |||
} | |||
reactor, ok := p.reactors[byte(chID)] | |||
if !ok { | |||
p.onError(fmt.Errorf("unknown channel %v", chID)) | |||
return | |||
} | |||
reactor.Receive(byte(chID), p, msg) | |||
} | |||
} | |||
// FlushStop mimics OnStop but additionally ensures that all successful | |||
// .Send() calls will get flushed before closing the connection. | |||
// NOTE: it is not safe to call this method more than once. | |||
func (p *peer) FlushStop() { | |||
p.metricsTicker.Stop() | |||
p.BaseService.OnStop() | |||
if err := p.conn.FlushClose(); err != nil { | |||
p.Logger.Debug("error while stopping peer", "err", err) | |||
} | |||
} | |||
// OnStop implements BaseService. | |||
func (p *peer) OnStop() { | |||
p.metricsTicker.Stop() | |||
p.BaseService.OnStop() | |||
if err := p.conn.Close(); err != nil { | |||
p.Logger.Debug("error while stopping peer", "err", err) | |||
} | |||
} | |||
//--------------------------------------------------- | |||
// Implements Peer | |||
// ID returns the peer's ID - the hex encoded hash of its pubkey. | |||
func (p *peer) ID() types.NodeID { | |||
return p.nodeInfo.ID() | |||
} | |||
// IsOutbound returns true if the connection is outbound, false otherwise. | |||
func (p *peer) IsOutbound() bool { | |||
return p.peerConn.outbound | |||
} | |||
// IsPersistent returns true if the peer is persitent, false otherwise. | |||
func (p *peer) IsPersistent() bool { | |||
return p.peerConn.persistent | |||
} | |||
// NodeInfo returns a copy of the peer's NodeInfo. | |||
func (p *peer) NodeInfo() types.NodeInfo { | |||
return p.nodeInfo | |||
} | |||
// SocketAddr returns the address of the socket. | |||
// For outbound peers, it's the address dialed (after DNS resolution). | |||
// For inbound peers, it's the address returned by the underlying connection | |||
// (not what's reported in the peer's NodeInfo). | |||
func (p *peer) SocketAddr() *NetAddress { | |||
endpoint := p.peerConn.conn.RemoteEndpoint() | |||
return &NetAddress{ | |||
ID: p.ID(), | |||
IP: endpoint.IP, | |||
Port: endpoint.Port, | |||
} | |||
} | |||
// Status returns the peer's ConnectionStatus. | |||
func (p *peer) Status() tmconn.ConnectionStatus { | |||
return p.conn.Status() | |||
} | |||
// Send msg bytes to the channel identified by chID byte. Returns false if the | |||
// send queue is full after timeout, specified by MConnection. | |||
func (p *peer) Send(chID byte, msgBytes []byte) bool { | |||
if !p.IsRunning() { | |||
// see Switch#Broadcast, where we fetch the list of peers and loop over | |||
// them - while we're looping, one peer may be removed and stopped. | |||
return false | |||
} else if !p.hasChannel(chID) { | |||
return false | |||
} | |||
res, err := p.conn.SendMessage(ChannelID(chID), msgBytes) | |||
if err == io.EOF { | |||
return false | |||
} else if err != nil { | |||
p.onError(err) | |||
return false | |||
} | |||
if res { | |||
labels := []string{ | |||
"peer_id", string(p.ID()), | |||
"chID", fmt.Sprintf("%#x", chID), | |||
} | |||
p.metrics.PeerSendBytesTotal.With(labels...).Add(float64(len(msgBytes))) | |||
} | |||
return res | |||
} | |||
// TrySend msg bytes to the channel identified by chID byte. Immediately returns | |||
// false if the send queue is full. | |||
func (p *peer) TrySend(chID byte, msgBytes []byte) bool { | |||
if !p.IsRunning() { | |||
return false | |||
} else if !p.hasChannel(chID) { | |||
return false | |||
} | |||
res, err := p.conn.TrySendMessage(ChannelID(chID), msgBytes) | |||
if err == io.EOF { | |||
return false | |||
} else if err != nil { | |||
p.onError(err) | |||
return false | |||
} | |||
if res { | |||
labels := []string{ | |||
"peer_id", string(p.ID()), | |||
"chID", fmt.Sprintf("%#x", chID), | |||
} | |||
p.metrics.PeerSendBytesTotal.With(labels...).Add(float64(len(msgBytes))) | |||
} | |||
return res | |||
} | |||
// Get the data for a given key. | |||
func (p *peer) Get(key string) interface{} { | |||
return p.Data.Get(key) | |||
} | |||
// Set sets the data for the given key. | |||
func (p *peer) Set(key string, data interface{}) { | |||
p.Data.Set(key, data) | |||
} | |||
// hasChannel returns true if the peer reported | |||
// knowing about the given chID. | |||
func (p *peer) hasChannel(chID byte) bool { | |||
for _, ch := range p.channels { | |||
if ch == chID { | |||
return true | |||
} | |||
} | |||
// NOTE: probably will want to remove this | |||
// but could be helpful while the feature is new | |||
p.Logger.Debug( | |||
"Unknown channel for peer", | |||
"channel", | |||
chID, | |||
"channels", | |||
p.channels, | |||
) | |||
return false | |||
} | |||
// CloseConn closes original connection. Used for cleaning up in cases where the peer had not been started at all. | |||
func (p *peer) CloseConn() error { | |||
return p.peerConn.conn.Close() | |||
} | |||
//--------------------------------------------------- | |||
// methods only used for testing | |||
// TODO: can we remove these? | |||
// CloseConn closes the underlying connection | |||
func (pc *peerConn) CloseConn() { | |||
pc.conn.Close() | |||
} | |||
// RemoteAddr returns peer's remote network address. | |||
func (p *peer) RemoteAddr() net.Addr { | |||
endpoint := p.conn.RemoteEndpoint() | |||
return &net.TCPAddr{ | |||
IP: endpoint.IP, | |||
Port: int(endpoint.Port), | |||
} | |||
} | |||
//--------------------------------------------------- | |||
func PeerMetrics(metrics *Metrics) PeerOption { | |||
return func(p *peer) { | |||
p.metrics = metrics | |||
} | |||
} | |||
func (p *peer) metricsReporter() { | |||
for { | |||
select { | |||
case <-p.metricsTicker.C: | |||
status := p.conn.Status() | |||
var sendQueueSize float64 | |||
for _, chStatus := range status.Channels { | |||
sendQueueSize += float64(chStatus.SendQueueSize) | |||
} | |||
p.metrics.PeerPendingSendBytes.With("peer_id", string(p.ID())).Set(sendQueueSize) | |||
case <-p.Quit(): | |||
return | |||
} | |||
} | |||
} |
@ -1,149 +0,0 @@ | |||
package p2p | |||
import ( | |||
"net" | |||
tmsync "github.com/tendermint/tendermint/internal/libs/sync" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// IPeerSet has a (immutable) subset of the methods of PeerSet. | |||
type IPeerSet interface { | |||
Has(key types.NodeID) bool | |||
HasIP(ip net.IP) bool | |||
Get(key types.NodeID) Peer | |||
List() []Peer | |||
Size() int | |||
} | |||
//----------------------------------------------------------------------------- | |||
// PeerSet is a special structure for keeping a table of peers. | |||
// Iteration over the peers is super fast and thread-safe. | |||
type PeerSet struct { | |||
mtx tmsync.Mutex | |||
lookup map[types.NodeID]*peerSetItem | |||
list []Peer | |||
} | |||
type peerSetItem struct { | |||
peer Peer | |||
index int | |||
} | |||
// NewPeerSet creates a new peerSet with a list of initial capacity of 256 items. | |||
func NewPeerSet() *PeerSet { | |||
return &PeerSet{ | |||
lookup: make(map[types.NodeID]*peerSetItem), | |||
list: make([]Peer, 0, 256), | |||
} | |||
} | |||
// Add adds the peer to the PeerSet. | |||
// It returns an error carrying the reason, if the peer is already present. | |||
func (ps *PeerSet) Add(peer Peer) error { | |||
ps.mtx.Lock() | |||
defer ps.mtx.Unlock() | |||
if ps.lookup[peer.ID()] != nil { | |||
return ErrSwitchDuplicatePeerID{peer.ID()} | |||
} | |||
index := len(ps.list) | |||
// Appending is safe even with other goroutines | |||
// iterating over the ps.list slice. | |||
ps.list = append(ps.list, peer) | |||
ps.lookup[peer.ID()] = &peerSetItem{peer, index} | |||
return nil | |||
} | |||
// Has returns true if the set contains the peer referred to by this | |||
// peerKey, otherwise false. | |||
func (ps *PeerSet) Has(peerKey types.NodeID) bool { | |||
ps.mtx.Lock() | |||
_, ok := ps.lookup[peerKey] | |||
ps.mtx.Unlock() | |||
return ok | |||
} | |||
// HasIP returns true if the set contains the peer referred to by this IP | |||
// address, otherwise false. | |||
func (ps *PeerSet) HasIP(peerIP net.IP) bool { | |||
ps.mtx.Lock() | |||
defer ps.mtx.Unlock() | |||
return ps.hasIP(peerIP) | |||
} | |||
// hasIP does not acquire a lock so it can be used in public methods which | |||
// already lock. | |||
func (ps *PeerSet) hasIP(peerIP net.IP) bool { | |||
for _, item := range ps.lookup { | |||
if item.peer.RemoteIP().Equal(peerIP) { | |||
return true | |||
} | |||
} | |||
return false | |||
} | |||
// Get looks up a peer by the provided peerKey. Returns nil if peer is not | |||
// found. | |||
func (ps *PeerSet) Get(peerKey types.NodeID) Peer { | |||
ps.mtx.Lock() | |||
defer ps.mtx.Unlock() | |||
item, ok := ps.lookup[peerKey] | |||
if ok { | |||
return item.peer | |||
} | |||
return nil | |||
} | |||
// Remove discards peer by its Key, if the peer was previously memoized. | |||
// Returns true if the peer was removed, and false if it was not found. | |||
// in the set. | |||
func (ps *PeerSet) Remove(peer Peer) bool { | |||
ps.mtx.Lock() | |||
defer ps.mtx.Unlock() | |||
item := ps.lookup[peer.ID()] | |||
if item == nil { | |||
return false | |||
} | |||
index := item.index | |||
// Create a new copy of the list but with one less item. | |||
// (we must copy because we'll be mutating the list). | |||
newList := make([]Peer, len(ps.list)-1) | |||
copy(newList, ps.list) | |||
// If it's the last peer, that's an easy special case. | |||
if index == len(ps.list)-1 { | |||
ps.list = newList | |||
delete(ps.lookup, peer.ID()) | |||
return true | |||
} | |||
// Replace the popped item with the last item in the old list. | |||
lastPeer := ps.list[len(ps.list)-1] | |||
lastPeerKey := lastPeer.ID() | |||
lastPeerItem := ps.lookup[lastPeerKey] | |||
newList[index] = lastPeer | |||
lastPeerItem.index = index | |||
ps.list = newList | |||
delete(ps.lookup, peer.ID()) | |||
return true | |||
} | |||
// Size returns the number of unique items in the peerSet. | |||
func (ps *PeerSet) Size() int { | |||
ps.mtx.Lock() | |||
defer ps.mtx.Unlock() | |||
return len(ps.list) | |||
} | |||
// List returns the threadsafe list of peers. | |||
func (ps *PeerSet) List() []Peer { | |||
ps.mtx.Lock() | |||
defer ps.mtx.Unlock() | |||
return ps.list | |||
} |
@ -1,190 +0,0 @@ | |||
package p2p | |||
import ( | |||
"net" | |||
"sync" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/tendermint/tendermint/libs/service" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// mockPeer for testing the PeerSet | |||
type mockPeer struct { | |||
service.BaseService | |||
ip net.IP | |||
id types.NodeID | |||
} | |||
func (mp *mockPeer) FlushStop() { mp.Stop() } //nolint:errcheck // ignore error | |||
func (mp *mockPeer) TrySend(chID byte, msgBytes []byte) bool { return true } | |||
func (mp *mockPeer) Send(chID byte, msgBytes []byte) bool { return true } | |||
func (mp *mockPeer) NodeInfo() types.NodeInfo { return types.NodeInfo{} } | |||
func (mp *mockPeer) Status() ConnectionStatus { return ConnectionStatus{} } | |||
func (mp *mockPeer) ID() types.NodeID { return mp.id } | |||
func (mp *mockPeer) IsOutbound() bool { return false } | |||
func (mp *mockPeer) IsPersistent() bool { return true } | |||
func (mp *mockPeer) Get(s string) interface{} { return s } | |||
func (mp *mockPeer) Set(string, interface{}) {} | |||
func (mp *mockPeer) RemoteIP() net.IP { return mp.ip } | |||
func (mp *mockPeer) SocketAddr() *NetAddress { return nil } | |||
func (mp *mockPeer) RemoteAddr() net.Addr { return &net.TCPAddr{IP: mp.ip, Port: 8800} } | |||
func (mp *mockPeer) CloseConn() error { return nil } | |||
// Returns a mock peer | |||
func newMockPeer(ip net.IP) *mockPeer { | |||
if ip == nil { | |||
ip = net.IP{127, 0, 0, 1} | |||
} | |||
nodeKey := types.GenNodeKey() | |||
return &mockPeer{ | |||
ip: ip, | |||
id: nodeKey.ID, | |||
} | |||
} | |||
func TestPeerSetAddRemoveOne(t *testing.T) { | |||
t.Parallel() | |||
peerSet := NewPeerSet() | |||
var peerList []Peer | |||
for i := 0; i < 5; i++ { | |||
p := newMockPeer(net.IP{127, 0, 0, byte(i)}) | |||
if err := peerSet.Add(p); err != nil { | |||
t.Error(err) | |||
} | |||
peerList = append(peerList, p) | |||
} | |||
n := len(peerList) | |||
// 1. Test removing from the front | |||
for i, peerAtFront := range peerList { | |||
removed := peerSet.Remove(peerAtFront) | |||
assert.True(t, removed) | |||
wantSize := n - i - 1 | |||
for j := 0; j < 2; j++ { | |||
assert.Equal(t, false, peerSet.Has(peerAtFront.ID()), "#%d Run #%d: failed to remove peer", i, j) | |||
assert.Equal(t, wantSize, peerSet.Size(), "#%d Run #%d: failed to remove peer and decrement size", i, j) | |||
// Test the route of removing the now non-existent element | |||
removed := peerSet.Remove(peerAtFront) | |||
assert.False(t, removed) | |||
} | |||
} | |||
// 2. Next we are testing removing the peer at the end | |||
// a) Replenish the peerSet | |||
for _, peer := range peerList { | |||
if err := peerSet.Add(peer); err != nil { | |||
t.Error(err) | |||
} | |||
} | |||
// b) In reverse, remove each element | |||
for i := n - 1; i >= 0; i-- { | |||
peerAtEnd := peerList[i] | |||
removed := peerSet.Remove(peerAtEnd) | |||
assert.True(t, removed) | |||
assert.Equal(t, false, peerSet.Has(peerAtEnd.ID()), "#%d: failed to remove item at end", i) | |||
assert.Equal(t, i, peerSet.Size(), "#%d: differing sizes after peerSet.Remove(atEndPeer)", i) | |||
} | |||
} | |||
func TestPeerSetAddRemoveMany(t *testing.T) { | |||
t.Parallel() | |||
peerSet := NewPeerSet() | |||
peers := []Peer{} | |||
N := 100 | |||
for i := 0; i < N; i++ { | |||
peer := newMockPeer(net.IP{127, 0, 0, byte(i)}) | |||
if err := peerSet.Add(peer); err != nil { | |||
t.Errorf("failed to add new peer") | |||
} | |||
if peerSet.Size() != i+1 { | |||
t.Errorf("failed to add new peer and increment size") | |||
} | |||
peers = append(peers, peer) | |||
} | |||
for i, peer := range peers { | |||
removed := peerSet.Remove(peer) | |||
assert.True(t, removed) | |||
if peerSet.Has(peer.ID()) { | |||
t.Errorf("failed to remove peer") | |||
} | |||
if peerSet.Size() != len(peers)-i-1 { | |||
t.Errorf("failed to remove peer and decrement size") | |||
} | |||
} | |||
} | |||
func TestPeerSetAddDuplicate(t *testing.T) { | |||
t.Parallel() | |||
peerSet := NewPeerSet() | |||
peer := newMockPeer(nil) | |||
n := 20 | |||
errsChan := make(chan error) | |||
// Add the same asynchronously to test the | |||
// concurrent guarantees of our APIs, and | |||
// our expectation in the end is that only | |||
// one addition succeeded, but the rest are | |||
// instances of ErrSwitchDuplicatePeer. | |||
for i := 0; i < n; i++ { | |||
go func() { | |||
errsChan <- peerSet.Add(peer) | |||
}() | |||
} | |||
// Now collect and tally the results | |||
errsTally := make(map[string]int) | |||
for i := 0; i < n; i++ { | |||
err := <-errsChan | |||
switch err.(type) { | |||
case ErrSwitchDuplicatePeerID: | |||
errsTally["duplicateID"]++ | |||
default: | |||
errsTally["other"]++ | |||
} | |||
} | |||
// Our next procedure is to ensure that only one addition | |||
// succeeded and that the rest are each ErrSwitchDuplicatePeer. | |||
wantErrCount, gotErrCount := n-1, errsTally["duplicateID"] | |||
assert.Equal(t, wantErrCount, gotErrCount, "invalid ErrSwitchDuplicatePeer count") | |||
wantNilErrCount, gotNilErrCount := 1, errsTally["other"] | |||
assert.Equal(t, wantNilErrCount, gotNilErrCount, "invalid nil errCount") | |||
} | |||
func TestPeerSetGet(t *testing.T) { | |||
t.Parallel() | |||
var ( | |||
peerSet = NewPeerSet() | |||
peer = newMockPeer(nil) | |||
) | |||
assert.Nil(t, peerSet.Get(peer.ID()), "expecting a nil lookup, before .Add") | |||
if err := peerSet.Add(peer); err != nil { | |||
t.Fatalf("Failed to add new peer: %v", err) | |||
} | |||
var wg sync.WaitGroup | |||
for i := 0; i < 10; i++ { | |||
// Add them asynchronously to test the | |||
// concurrent guarantees of our APIs. | |||
wg.Add(1) | |||
go func(i int) { | |||
defer wg.Done() | |||
have, want := peerSet.Get(peer.ID()), peer | |||
assert.Equal(t, have, want, "%d: have %v, want %v", i, have, want) | |||
}(i) | |||
} | |||
wg.Wait() | |||
} |
@ -1,239 +0,0 @@ | |||
package p2p | |||
import ( | |||
"context" | |||
"fmt" | |||
golog "log" | |||
"net" | |||
"testing" | |||
"time" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
"github.com/tendermint/tendermint/crypto" | |||
"github.com/tendermint/tendermint/crypto/ed25519" | |||
"github.com/tendermint/tendermint/libs/bytes" | |||
"github.com/tendermint/tendermint/libs/log" | |||
"github.com/tendermint/tendermint/types" | |||
"github.com/tendermint/tendermint/config" | |||
tmconn "github.com/tendermint/tendermint/internal/p2p/conn" | |||
) | |||
func TestPeerBasic(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
// simulate remote peer | |||
rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} | |||
rp.Start() | |||
t.Cleanup(rp.Stop) | |||
p, err := createOutboundPeerAndPerformHandshake(rp.Addr(), cfg, tmconn.DefaultMConnConfig()) | |||
require.Nil(err) | |||
err = p.Start() | |||
require.Nil(err) | |||
t.Cleanup(func() { | |||
if err := p.Stop(); err != nil { | |||
t.Error(err) | |||
} | |||
}) | |||
assert.True(p.IsRunning()) | |||
assert.True(p.IsOutbound()) | |||
assert.False(p.IsPersistent()) | |||
p.persistent = true | |||
assert.True(p.IsPersistent()) | |||
assert.Equal(rp.Addr().DialString(), p.RemoteAddr().String()) | |||
assert.Equal(rp.ID(), p.ID()) | |||
} | |||
func TestPeerSend(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
config := cfg | |||
// simulate remote peer | |||
rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: config} | |||
rp.Start() | |||
t.Cleanup(rp.Stop) | |||
p, err := createOutboundPeerAndPerformHandshake(rp.Addr(), config, tmconn.DefaultMConnConfig()) | |||
require.Nil(err) | |||
err = p.Start() | |||
require.Nil(err) | |||
t.Cleanup(func() { | |||
if err := p.Stop(); err != nil { | |||
t.Error(err) | |||
} | |||
}) | |||
assert.True(p.Send(testCh, []byte("Asylum"))) | |||
} | |||
func createOutboundPeerAndPerformHandshake( | |||
addr *NetAddress, | |||
config *config.P2PConfig, | |||
mConfig tmconn.MConnConfig, | |||
) (*peer, error) { | |||
chDescs := []*tmconn.ChannelDescriptor{ | |||
{ID: testCh, Priority: 1}, | |||
} | |||
pk := ed25519.GenPrivKey() | |||
ourNodeInfo := testNodeInfo(types.NodeIDFromPubKey(pk.PubKey()), "host_peer") | |||
transport := NewMConnTransport(log.TestingLogger(), mConfig, chDescs, MConnTransportOptions{}) | |||
reactorsByCh := map[byte]Reactor{testCh: NewTestReactor(chDescs, true)} | |||
pc, err := testOutboundPeerConn(transport, addr, config, false, pk) | |||
if err != nil { | |||
return nil, err | |||
} | |||
peerInfo, _, err := pc.conn.Handshake(context.Background(), ourNodeInfo, pk) | |||
if err != nil { | |||
return nil, err | |||
} | |||
p := newPeer(peerInfo, pc, reactorsByCh, func(p Peer, r interface{}) {}) | |||
p.SetLogger(log.TestingLogger().With("peer", addr)) | |||
return p, nil | |||
} | |||
func testDial(addr *NetAddress, cfg *config.P2PConfig) (net.Conn, error) { | |||
if cfg.TestDialFail { | |||
return nil, fmt.Errorf("dial err (peerConfig.DialFail == true)") | |||
} | |||
conn, err := addr.DialTimeout(cfg.DialTimeout) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return conn, nil | |||
} | |||
func testOutboundPeerConn( | |||
transport *MConnTransport, | |||
addr *NetAddress, | |||
config *config.P2PConfig, | |||
persistent bool, | |||
ourNodePrivKey crypto.PrivKey, | |||
) (peerConn, error) { | |||
var pc peerConn | |||
conn, err := testDial(addr, config) | |||
if err != nil { | |||
return pc, fmt.Errorf("error creating peer: %w", err) | |||
} | |||
pc, err = testPeerConn(transport, conn, true, persistent) | |||
if err != nil { | |||
if cerr := conn.Close(); cerr != nil { | |||
return pc, fmt.Errorf("%v: %w", cerr.Error(), err) | |||
} | |||
return pc, err | |||
} | |||
return pc, nil | |||
} | |||
type remotePeer struct { | |||
PrivKey crypto.PrivKey | |||
Config *config.P2PConfig | |||
Network string | |||
addr *NetAddress | |||
channels bytes.HexBytes | |||
listenAddr string | |||
listener net.Listener | |||
} | |||
func (rp *remotePeer) Addr() *NetAddress { | |||
return rp.addr | |||
} | |||
func (rp *remotePeer) ID() types.NodeID { | |||
return types.NodeIDFromPubKey(rp.PrivKey.PubKey()) | |||
} | |||
func (rp *remotePeer) Start() { | |||
if rp.listenAddr == "" { | |||
rp.listenAddr = "127.0.0.1:0" | |||
} | |||
l, e := net.Listen("tcp", rp.listenAddr) // any available address | |||
if e != nil { | |||
golog.Fatalf("net.Listen tcp :0: %+v", e) | |||
} | |||
rp.listener = l | |||
rp.addr = types.NewNetAddress(types.NodeIDFromPubKey(rp.PrivKey.PubKey()), l.Addr()) | |||
if rp.channels == nil { | |||
rp.channels = []byte{testCh} | |||
} | |||
go rp.accept() | |||
} | |||
func (rp *remotePeer) Stop() { | |||
rp.listener.Close() | |||
} | |||
func (rp *remotePeer) Dial(addr *NetAddress) (net.Conn, error) { | |||
transport := NewMConnTransport(log.TestingLogger(), MConnConfig(rp.Config), | |||
[]*ChannelDescriptor{}, MConnTransportOptions{}) | |||
conn, err := addr.DialTimeout(1 * time.Second) | |||
if err != nil { | |||
return nil, err | |||
} | |||
pc, err := testInboundPeerConn(transport, conn) | |||
if err != nil { | |||
return nil, err | |||
} | |||
_, _, err = pc.conn.Handshake(context.Background(), rp.nodeInfo(), rp.PrivKey) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return conn, err | |||
} | |||
func (rp *remotePeer) accept() { | |||
transport := NewMConnTransport(log.TestingLogger(), MConnConfig(rp.Config), | |||
[]*ChannelDescriptor{}, MConnTransportOptions{}) | |||
conns := []net.Conn{} | |||
for { | |||
conn, err := rp.listener.Accept() | |||
if err != nil { | |||
golog.Printf("Failed to accept conn: %+v", err) | |||
for _, conn := range conns { | |||
_ = conn.Close() | |||
} | |||
return | |||
} | |||
pc, err := testInboundPeerConn(transport, conn) | |||
if err != nil { | |||
golog.Printf("Failed to create a peer: %+v", err) | |||
} | |||
_, _, err = pc.conn.Handshake(context.Background(), rp.nodeInfo(), rp.PrivKey) | |||
if err != nil { | |||
golog.Printf("Failed to handshake a peer: %+v", err) | |||
} | |||
conns = append(conns, conn) | |||
} | |||
} | |||
func (rp *remotePeer) nodeInfo() types.NodeInfo { | |||
ni := types.NodeInfo{ | |||
ProtocolVersion: defaultProtocolVersion, | |||
NodeID: rp.Addr().ID, | |||
ListenAddr: rp.listener.Addr().String(), | |||
Network: "testing", | |||
Version: "1.2.3-rc0-deadbeef", | |||
Channels: rp.channels, | |||
Moniker: "remote_peer", | |||
} | |||
if rp.Network != "" { | |||
ni.Network = rp.Network | |||
} | |||
return ni | |||
} |
@ -1,948 +0,0 @@ | |||
// Modified for Tendermint | |||
// Originally Copyright (c) 2013-2014 Conformal Systems LLC. | |||
// https://github.com/conformal/btcd/blob/master/LICENSE | |||
package pex | |||
import ( | |||
"encoding/binary" | |||
"fmt" | |||
"hash" | |||
"math" | |||
mrand "math/rand" | |||
"net" | |||
"sync" | |||
"time" | |||
"github.com/minio/highwayhash" | |||
"github.com/tendermint/tendermint/crypto" | |||
tmsync "github.com/tendermint/tendermint/internal/libs/sync" | |||
"github.com/tendermint/tendermint/internal/p2p" | |||
tmmath "github.com/tendermint/tendermint/libs/math" | |||
tmrand "github.com/tendermint/tendermint/libs/rand" | |||
"github.com/tendermint/tendermint/libs/service" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
const ( | |||
bucketTypeNew = 0x01 | |||
bucketTypeOld = 0x02 | |||
) | |||
// AddrBook is an address book used for tracking peers | |||
// so we can gossip about them to others and select | |||
// peers to dial. | |||
// TODO: break this up? | |||
type AddrBook interface { | |||
service.Service | |||
// Add our own addresses so we don't later add ourselves | |||
AddOurAddress(*p2p.NetAddress) | |||
// Check if it is our address | |||
OurAddress(*p2p.NetAddress) bool | |||
AddPrivateIDs([]string) | |||
// Add and remove an address | |||
AddAddress(addr *p2p.NetAddress, src *p2p.NetAddress) error | |||
RemoveAddress(*p2p.NetAddress) | |||
// Check if the address is in the book | |||
HasAddress(*p2p.NetAddress) bool | |||
// Do we need more peers? | |||
NeedMoreAddrs() bool | |||
// Is Address Book Empty? Answer should not depend on being in your own | |||
// address book, or private peers | |||
Empty() bool | |||
// Pick an address to dial | |||
PickAddress(biasTowardsNewAddrs int) *p2p.NetAddress | |||
// Mark address | |||
MarkGood(types.NodeID) | |||
MarkAttempt(*p2p.NetAddress) | |||
MarkBad(*p2p.NetAddress, time.Duration) // Move peer to bad peers list | |||
// Add bad peers back to addrBook | |||
ReinstateBadPeers() | |||
IsGood(*p2p.NetAddress) bool | |||
IsBanned(*p2p.NetAddress) bool | |||
// Send a selection of addresses to peers | |||
GetSelection() []*p2p.NetAddress | |||
// Send a selection of addresses with bias | |||
GetSelectionWithBias(biasTowardsNewAddrs int) []*p2p.NetAddress | |||
Size() int | |||
// Persist to disk | |||
Save() | |||
} | |||
var _ AddrBook = (*addrBook)(nil) | |||
// addrBook - concurrency safe peer address manager. | |||
// Implements AddrBook. | |||
type addrBook struct { | |||
service.BaseService | |||
// accessed concurrently | |||
mtx tmsync.Mutex | |||
ourAddrs map[string]struct{} | |||
privateIDs map[types.NodeID]struct{} | |||
addrLookup map[types.NodeID]*knownAddress // new & old | |||
badPeers map[types.NodeID]*knownAddress // blacklisted peers | |||
bucketsOld []map[string]*knownAddress | |||
bucketsNew []map[string]*knownAddress | |||
nOld int | |||
nNew int | |||
// immutable after creation | |||
filePath string | |||
key string // random prefix for bucket placement | |||
routabilityStrict bool | |||
hasher hash.Hash64 | |||
wg sync.WaitGroup | |||
} | |||
func mustNewHasher() hash.Hash64 { | |||
key := crypto.CRandBytes(highwayhash.Size) | |||
hasher, err := highwayhash.New64(key) | |||
if err != nil { | |||
panic(err) | |||
} | |||
return hasher | |||
} | |||
// NewAddrBook creates a new address book. | |||
// Use Start to begin processing asynchronous address updates. | |||
func NewAddrBook(filePath string, routabilityStrict bool) AddrBook { | |||
am := &addrBook{ | |||
ourAddrs: make(map[string]struct{}), | |||
privateIDs: make(map[types.NodeID]struct{}), | |||
addrLookup: make(map[types.NodeID]*knownAddress), | |||
badPeers: make(map[types.NodeID]*knownAddress), | |||
filePath: filePath, | |||
routabilityStrict: routabilityStrict, | |||
} | |||
am.init() | |||
am.BaseService = *service.NewBaseService(nil, "AddrBook", am) | |||
return am | |||
} | |||
// Initialize the buckets. | |||
// When modifying this, don't forget to update loadFromFile() | |||
func (a *addrBook) init() { | |||
a.key = crypto.CRandHex(24) // 24/2 * 8 = 96 bits | |||
// New addr buckets | |||
a.bucketsNew = make([]map[string]*knownAddress, newBucketCount) | |||
for i := range a.bucketsNew { | |||
a.bucketsNew[i] = make(map[string]*knownAddress) | |||
} | |||
// Old addr buckets | |||
a.bucketsOld = make([]map[string]*knownAddress, oldBucketCount) | |||
for i := range a.bucketsOld { | |||
a.bucketsOld[i] = make(map[string]*knownAddress) | |||
} | |||
a.hasher = mustNewHasher() | |||
} | |||
// OnStart implements Service. | |||
func (a *addrBook) OnStart() error { | |||
if err := a.BaseService.OnStart(); err != nil { | |||
return err | |||
} | |||
a.loadFromFile(a.filePath) | |||
// wg.Add to ensure that any invocation of .Wait() | |||
// later on will wait for saveRoutine to terminate. | |||
a.wg.Add(1) | |||
go a.saveRoutine() | |||
return nil | |||
} | |||
// OnStop implements Service. | |||
func (a *addrBook) OnStop() { | |||
a.BaseService.OnStop() | |||
} | |||
func (a *addrBook) Wait() { | |||
a.wg.Wait() | |||
} | |||
func (a *addrBook) FilePath() string { | |||
return a.filePath | |||
} | |||
//------------------------------------------------------- | |||
// AddOurAddress one of our addresses. | |||
func (a *addrBook) AddOurAddress(addr *p2p.NetAddress) { | |||
a.mtx.Lock() | |||
defer a.mtx.Unlock() | |||
a.Logger.Info("Add our address to book", "addr", addr) | |||
a.ourAddrs[addr.String()] = struct{}{} | |||
} | |||
// OurAddress returns true if it is our address. | |||
func (a *addrBook) OurAddress(addr *p2p.NetAddress) bool { | |||
a.mtx.Lock() | |||
defer a.mtx.Unlock() | |||
_, ok := a.ourAddrs[addr.String()] | |||
return ok | |||
} | |||
func (a *addrBook) AddPrivateIDs(ids []string) { | |||
a.mtx.Lock() | |||
defer a.mtx.Unlock() | |||
for _, id := range ids { | |||
a.privateIDs[types.NodeID(id)] = struct{}{} | |||
} | |||
} | |||
// AddAddress implements AddrBook | |||
// Add address to a "new" bucket. If it's already in one, only add it probabilistically. | |||
// Returns error if the addr is non-routable. Does not add self. | |||
// NOTE: addr must not be nil | |||
func (a *addrBook) AddAddress(addr *p2p.NetAddress, src *p2p.NetAddress) error { | |||
a.mtx.Lock() | |||
defer a.mtx.Unlock() | |||
return a.addAddress(addr, src) | |||
} | |||
// RemoveAddress implements AddrBook - removes the address from the book. | |||
func (a *addrBook) RemoveAddress(addr *p2p.NetAddress) { | |||
a.mtx.Lock() | |||
defer a.mtx.Unlock() | |||
a.removeAddress(addr) | |||
} | |||
// IsGood returns true if peer was ever marked as good and haven't | |||
// done anything wrong since then. | |||
func (a *addrBook) IsGood(addr *p2p.NetAddress) bool { | |||
a.mtx.Lock() | |||
defer a.mtx.Unlock() | |||
return a.addrLookup[addr.ID].isOld() | |||
} | |||
// IsBanned returns true if the peer is currently banned | |||
func (a *addrBook) IsBanned(addr *p2p.NetAddress) bool { | |||
a.mtx.Lock() | |||
_, ok := a.badPeers[addr.ID] | |||
a.mtx.Unlock() | |||
return ok | |||
} | |||
// HasAddress returns true if the address is in the book. | |||
func (a *addrBook) HasAddress(addr *p2p.NetAddress) bool { | |||
a.mtx.Lock() | |||
defer a.mtx.Unlock() | |||
ka := a.addrLookup[addr.ID] | |||
return ka != nil | |||
} | |||
// NeedMoreAddrs implements AddrBook - returns true if there are not have enough addresses in the book. | |||
func (a *addrBook) NeedMoreAddrs() bool { | |||
return a.Size() < needAddressThreshold | |||
} | |||
// Empty implements AddrBook - returns true if there are no addresses in the address book. | |||
// Does not count the peer appearing in its own address book, or private peers. | |||
func (a *addrBook) Empty() bool { | |||
return a.Size() == 0 | |||
} | |||
// PickAddress implements AddrBook. It picks an address to connect to. | |||
// The address is picked randomly from an old or new bucket according | |||
// to the biasTowardsNewAddrs argument, which must be between [0, 100] (or else is truncated to that range) | |||
// and determines how biased we are to pick an address from a new bucket. | |||
// PickAddress returns nil if the AddrBook is empty or if we try to pick | |||
// from an empty bucket. | |||
// nolint:gosec // G404: Use of weak random number generator | |||
func (a *addrBook) PickAddress(biasTowardsNewAddrs int) *p2p.NetAddress { | |||
a.mtx.Lock() | |||
defer a.mtx.Unlock() | |||
bookSize := a.size() | |||
if bookSize <= 0 { | |||
if bookSize < 0 { | |||
panic(fmt.Sprintf("Addrbook size %d (new: %d + old: %d) is less than 0", a.nNew+a.nOld, a.nNew, a.nOld)) | |||
} | |||
return nil | |||
} | |||
if biasTowardsNewAddrs > 100 { | |||
biasTowardsNewAddrs = 100 | |||
} | |||
if biasTowardsNewAddrs < 0 { | |||
biasTowardsNewAddrs = 0 | |||
} | |||
// Bias between new and old addresses. | |||
oldCorrelation := math.Sqrt(float64(a.nOld)) * (100.0 - float64(biasTowardsNewAddrs)) | |||
newCorrelation := math.Sqrt(float64(a.nNew)) * float64(biasTowardsNewAddrs) | |||
// pick a random peer from a random bucket | |||
var bucket map[string]*knownAddress | |||
pickFromOldBucket := (newCorrelation+oldCorrelation)*mrand.Float64() < oldCorrelation | |||
if (pickFromOldBucket && a.nOld == 0) || | |||
(!pickFromOldBucket && a.nNew == 0) { | |||
return nil | |||
} | |||
// loop until we pick a random non-empty bucket | |||
for len(bucket) == 0 { | |||
if pickFromOldBucket { | |||
bucket = a.bucketsOld[mrand.Intn(len(a.bucketsOld))] | |||
} else { | |||
bucket = a.bucketsNew[mrand.Intn(len(a.bucketsNew))] | |||
} | |||
} | |||
// pick a random index and loop over the map to return that index | |||
randIndex := mrand.Intn(len(bucket)) | |||
for _, ka := range bucket { | |||
if randIndex == 0 { | |||
return ka.Addr | |||
} | |||
randIndex-- | |||
} | |||
return nil | |||
} | |||
// MarkGood implements AddrBook - it marks the peer as good and | |||
// moves it into an "old" bucket. | |||
func (a *addrBook) MarkGood(id types.NodeID) { | |||
a.mtx.Lock() | |||
defer a.mtx.Unlock() | |||
ka := a.addrLookup[id] | |||
if ka == nil { | |||
return | |||
} | |||
ka.markGood() | |||
if ka.isNew() { | |||
if err := a.moveToOld(ka); err != nil { | |||
a.Logger.Error("Error moving address to old", "err", err) | |||
} | |||
} | |||
} | |||
// MarkAttempt implements AddrBook - it marks that an attempt was made to connect to the address. | |||
func (a *addrBook) MarkAttempt(addr *p2p.NetAddress) { | |||
a.mtx.Lock() | |||
defer a.mtx.Unlock() | |||
ka := a.addrLookup[addr.ID] | |||
if ka == nil { | |||
return | |||
} | |||
ka.markAttempt() | |||
} | |||
// MarkBad implements AddrBook. Kicks address out from book, places | |||
// the address in the badPeers pool. | |||
func (a *addrBook) MarkBad(addr *p2p.NetAddress, banTime time.Duration) { | |||
a.mtx.Lock() | |||
defer a.mtx.Unlock() | |||
if a.addBadPeer(addr, banTime) { | |||
a.removeAddress(addr) | |||
} | |||
} | |||
// ReinstateBadPeers removes bad peers from ban list and places them into a new | |||
// bucket. | |||
func (a *addrBook) ReinstateBadPeers() { | |||
a.mtx.Lock() | |||
defer a.mtx.Unlock() | |||
for _, ka := range a.badPeers { | |||
if ka.isBanned() { | |||
continue | |||
} | |||
bucket, err := a.calcNewBucket(ka.Addr, ka.Src) | |||
if err != nil { | |||
a.Logger.Error("Failed to calculate new bucket (bad peer won't be reinstantiated)", | |||
"addr", ka.Addr, "err", err) | |||
continue | |||
} | |||
if err := a.addToNewBucket(ka, bucket); err != nil { | |||
a.Logger.Error("Error adding peer to new bucket", "err", err) | |||
} | |||
delete(a.badPeers, ka.ID()) | |||
a.Logger.Info("Reinstated address", "addr", ka.Addr) | |||
} | |||
} | |||
// GetSelection implements AddrBook. | |||
// It randomly selects some addresses (old & new). Suitable for peer-exchange protocols. | |||
// Must never return a nil address. | |||
func (a *addrBook) GetSelection() []*p2p.NetAddress { | |||
a.mtx.Lock() | |||
defer a.mtx.Unlock() | |||
bookSize := a.size() | |||
if bookSize <= 0 { | |||
if bookSize < 0 { | |||
panic(fmt.Sprintf("Addrbook size %d (new: %d + old: %d) is less than 0", a.nNew+a.nOld, a.nNew, a.nOld)) | |||
} | |||
return nil | |||
} | |||
numAddresses := tmmath.MaxInt( | |||
tmmath.MinInt(minGetSelection, bookSize), | |||
bookSize*getSelectionPercent/100) | |||
numAddresses = tmmath.MinInt(maxGetSelection, numAddresses) | |||
// XXX: instead of making a list of all addresses, shuffling, and slicing a random chunk, | |||
// could we just select a random numAddresses of indexes? | |||
allAddr := make([]*p2p.NetAddress, bookSize) | |||
i := 0 | |||
for _, ka := range a.addrLookup { | |||
allAddr[i] = ka.Addr | |||
i++ | |||
} | |||
// Fisher-Yates shuffle the array. We only need to do the first | |||
// `numAddresses' since we are throwing the rest. | |||
for i := 0; i < numAddresses; i++ { | |||
// pick a number between current index and the end | |||
// nolint:gosec // G404: Use of weak random number generator | |||
j := mrand.Intn(len(allAddr)-i) + i | |||
allAddr[i], allAddr[j] = allAddr[j], allAddr[i] | |||
} | |||
// slice off the limit we are willing to share. | |||
return allAddr[:numAddresses] | |||
} | |||
func percentageOfNum(p, n int) int { | |||
return int(math.Round((float64(p) / float64(100)) * float64(n))) | |||
} | |||
// GetSelectionWithBias implements AddrBook. | |||
// It randomly selects some addresses (old & new). Suitable for peer-exchange protocols. | |||
// Must never return a nil address. | |||
// | |||
// Each address is picked randomly from an old or new bucket according to the | |||
// biasTowardsNewAddrs argument, which must be between [0, 100] (or else is truncated to | |||
// that range) and determines how biased we are to pick an address from a new | |||
// bucket. | |||
func (a *addrBook) GetSelectionWithBias(biasTowardsNewAddrs int) []*p2p.NetAddress { | |||
a.mtx.Lock() | |||
defer a.mtx.Unlock() | |||
bookSize := a.size() | |||
if bookSize <= 0 { | |||
if bookSize < 0 { | |||
panic(fmt.Sprintf("Addrbook size %d (new: %d + old: %d) is less than 0", a.nNew+a.nOld, a.nNew, a.nOld)) | |||
} | |||
return nil | |||
} | |||
if biasTowardsNewAddrs > 100 { | |||
biasTowardsNewAddrs = 100 | |||
} | |||
if biasTowardsNewAddrs < 0 { | |||
biasTowardsNewAddrs = 0 | |||
} | |||
numAddresses := tmmath.MaxInt( | |||
tmmath.MinInt(minGetSelection, bookSize), | |||
bookSize*getSelectionPercent/100) | |||
numAddresses = tmmath.MinInt(maxGetSelection, numAddresses) | |||
// number of new addresses that, if possible, should be in the beginning of the selection | |||
// if there are no enough old addrs, will choose new addr instead. | |||
numRequiredNewAdd := tmmath.MaxInt(percentageOfNum(biasTowardsNewAddrs, numAddresses), numAddresses-a.nOld) | |||
selection := a.randomPickAddresses(bucketTypeNew, numRequiredNewAdd) | |||
selection = append(selection, a.randomPickAddresses(bucketTypeOld, numAddresses-len(selection))...) | |||
return selection | |||
} | |||
//------------------------------------------------ | |||
// Size returns the number of addresses in the book. | |||
func (a *addrBook) Size() int { | |||
a.mtx.Lock() | |||
defer a.mtx.Unlock() | |||
return a.size() | |||
} | |||
func (a *addrBook) size() int { | |||
return a.nNew + a.nOld | |||
} | |||
//---------------------------------------------------------- | |||
// Save persists the address book to disk. | |||
func (a *addrBook) Save() { | |||
a.saveToFile(a.filePath) // thread safe | |||
} | |||
func (a *addrBook) saveRoutine() { | |||
defer a.wg.Done() | |||
saveFileTicker := time.NewTicker(dumpAddressInterval) | |||
out: | |||
for { | |||
select { | |||
case <-saveFileTicker.C: | |||
a.saveToFile(a.filePath) | |||
case <-a.Quit(): | |||
break out | |||
} | |||
} | |||
saveFileTicker.Stop() | |||
a.saveToFile(a.filePath) | |||
} | |||
//---------------------------------------------------------- | |||
func (a *addrBook) getBucket(bucketType byte, bucketIdx int) map[string]*knownAddress { | |||
switch bucketType { | |||
case bucketTypeNew: | |||
return a.bucketsNew[bucketIdx] | |||
case bucketTypeOld: | |||
return a.bucketsOld[bucketIdx] | |||
default: | |||
panic("Invalid bucket type") | |||
} | |||
} | |||
// Adds ka to new bucket. Returns false if it couldn't do it cuz buckets full. | |||
// NOTE: currently it always returns true. | |||
func (a *addrBook) addToNewBucket(ka *knownAddress, bucketIdx int) error { | |||
// Consistency check to ensure we don't add an already known address | |||
if ka.isOld() { | |||
return errAddrBookOldAddressNewBucket{ka.Addr, bucketIdx} | |||
} | |||
addrStr := ka.Addr.String() | |||
bucket := a.getBucket(bucketTypeNew, bucketIdx) | |||
// Already exists? | |||
if _, ok := bucket[addrStr]; ok { | |||
return nil | |||
} | |||
// Enforce max addresses. | |||
if len(bucket) > newBucketSize { | |||
a.Logger.Info("new bucket is full, expiring new") | |||
a.expireNew(bucketIdx) | |||
} | |||
// Add to bucket. | |||
bucket[addrStr] = ka | |||
// increment nNew if the peer doesnt already exist in a bucket | |||
if ka.addBucketRef(bucketIdx) == 1 { | |||
a.nNew++ | |||
} | |||
// Add it to addrLookup | |||
a.addrLookup[ka.ID()] = ka | |||
return nil | |||
} | |||
// Adds ka to old bucket. Returns false if it couldn't do it cuz buckets full. | |||
func (a *addrBook) addToOldBucket(ka *knownAddress, bucketIdx int) bool { | |||
// Sanity check | |||
if ka.isNew() { | |||
a.Logger.Error(fmt.Sprintf("Cannot add new address to old bucket: %v", ka)) | |||
return false | |||
} | |||
if len(ka.Buckets) != 0 { | |||
a.Logger.Error(fmt.Sprintf("Cannot add already old address to another old bucket: %v", ka)) | |||
return false | |||
} | |||
addrStr := ka.Addr.String() | |||
bucket := a.getBucket(bucketTypeOld, bucketIdx) | |||
// Already exists? | |||
if _, ok := bucket[addrStr]; ok { | |||
return true | |||
} | |||
// Enforce max addresses. | |||
if len(bucket) > oldBucketSize { | |||
return false | |||
} | |||
// Add to bucket. | |||
bucket[addrStr] = ka | |||
if ka.addBucketRef(bucketIdx) == 1 { | |||
a.nOld++ | |||
} | |||
// Ensure in addrLookup | |||
a.addrLookup[ka.ID()] = ka | |||
return true | |||
} | |||
func (a *addrBook) removeFromBucket(ka *knownAddress, bucketType byte, bucketIdx int) { | |||
if ka.BucketType != bucketType { | |||
a.Logger.Error(fmt.Sprintf("Bucket type mismatch: %v", ka)) | |||
return | |||
} | |||
bucket := a.getBucket(bucketType, bucketIdx) | |||
delete(bucket, ka.Addr.String()) | |||
if ka.removeBucketRef(bucketIdx) == 0 { | |||
if bucketType == bucketTypeNew { | |||
a.nNew-- | |||
} else { | |||
a.nOld-- | |||
} | |||
delete(a.addrLookup, ka.ID()) | |||
} | |||
} | |||
func (a *addrBook) removeFromAllBuckets(ka *knownAddress) { | |||
for _, bucketIdx := range ka.Buckets { | |||
bucket := a.getBucket(ka.BucketType, bucketIdx) | |||
delete(bucket, ka.Addr.String()) | |||
} | |||
ka.Buckets = nil | |||
if ka.BucketType == bucketTypeNew { | |||
a.nNew-- | |||
} else { | |||
a.nOld-- | |||
} | |||
delete(a.addrLookup, ka.ID()) | |||
} | |||
//---------------------------------------------------------- | |||
func (a *addrBook) pickOldest(bucketType byte, bucketIdx int) *knownAddress { | |||
bucket := a.getBucket(bucketType, bucketIdx) | |||
var oldest *knownAddress | |||
for _, ka := range bucket { | |||
if oldest == nil || ka.LastAttempt.Before(oldest.LastAttempt) { | |||
oldest = ka | |||
} | |||
} | |||
return oldest | |||
} | |||
// adds the address to a "new" bucket. if its already in one, | |||
// it only adds it probabilistically | |||
func (a *addrBook) addAddress(addr, src *p2p.NetAddress) error { | |||
if addr == nil || src == nil { | |||
return ErrAddrBookNilAddr{addr, src} | |||
} | |||
if err := addr.Valid(); err != nil { | |||
return ErrAddrBookInvalidAddr{Addr: addr, AddrErr: err} | |||
} | |||
if _, ok := a.badPeers[addr.ID]; ok { | |||
return ErrAddressBanned{addr} | |||
} | |||
if _, ok := a.privateIDs[addr.ID]; ok { | |||
return ErrAddrBookPrivate{addr} | |||
} | |||
if _, ok := a.privateIDs[src.ID]; ok { | |||
return ErrAddrBookPrivateSrc{src} | |||
} | |||
// TODO: we should track ourAddrs by ID and by IP:PORT and refuse both. | |||
if _, ok := a.ourAddrs[addr.String()]; ok { | |||
return ErrAddrBookSelf{addr} | |||
} | |||
if a.routabilityStrict && !addr.Routable() { | |||
return ErrAddrBookNonRoutable{addr} | |||
} | |||
ka := a.addrLookup[addr.ID] | |||
if ka != nil { | |||
// If its already old and the address ID's are the same, ignore it. | |||
// Thereby avoiding issues with a node on the network attempting to change | |||
// the IP of a known node ID. (Which could yield an eclipse attack on the node) | |||
if ka.isOld() && ka.Addr.ID == addr.ID { | |||
return nil | |||
} | |||
// Already in max new buckets. | |||
if len(ka.Buckets) == maxNewBucketsPerAddress { | |||
return nil | |||
} | |||
// The more entries we have, the less likely we are to add more. | |||
factor := int32(2 * len(ka.Buckets)) | |||
// nolint:gosec // G404: Use of weak random number generator | |||
if mrand.Int31n(factor) != 0 { | |||
return nil | |||
} | |||
} else { | |||
ka = newKnownAddress(addr, src) | |||
} | |||
bucket, err := a.calcNewBucket(addr, src) | |||
if err != nil { | |||
return err | |||
} | |||
return a.addToNewBucket(ka, bucket) | |||
} | |||
func (a *addrBook) randomPickAddresses(bucketType byte, num int) []*p2p.NetAddress { | |||
var buckets []map[string]*knownAddress | |||
switch bucketType { | |||
case bucketTypeNew: | |||
buckets = a.bucketsNew | |||
case bucketTypeOld: | |||
buckets = a.bucketsOld | |||
default: | |||
panic("unexpected bucketType") | |||
} | |||
total := 0 | |||
for _, bucket := range buckets { | |||
total += len(bucket) | |||
} | |||
addresses := make([]*knownAddress, 0, total) | |||
for _, bucket := range buckets { | |||
for _, ka := range bucket { | |||
addresses = append(addresses, ka) | |||
} | |||
} | |||
selection := make([]*p2p.NetAddress, 0, num) | |||
chosenSet := make(map[string]bool, num) | |||
rand := tmrand.NewRand() | |||
rand.Shuffle(total, func(i, j int) { | |||
addresses[i], addresses[j] = addresses[j], addresses[i] | |||
}) | |||
for _, addr := range addresses { | |||
if chosenSet[addr.Addr.String()] { | |||
continue | |||
} | |||
chosenSet[addr.Addr.String()] = true | |||
selection = append(selection, addr.Addr) | |||
if len(selection) >= num { | |||
return selection | |||
} | |||
} | |||
return selection | |||
} | |||
// Make space in the new buckets by expiring the really bad entries. | |||
// If no bad entries are available we remove the oldest. | |||
func (a *addrBook) expireNew(bucketIdx int) { | |||
for addrStr, ka := range a.bucketsNew[bucketIdx] { | |||
// If an entry is bad, throw it away | |||
if ka.isBad() { | |||
a.Logger.Info(fmt.Sprintf("expiring bad address %v", addrStr)) | |||
a.removeFromBucket(ka, bucketTypeNew, bucketIdx) | |||
return | |||
} | |||
} | |||
// If we haven't thrown out a bad entry, throw out the oldest entry | |||
oldest := a.pickOldest(bucketTypeNew, bucketIdx) | |||
a.removeFromBucket(oldest, bucketTypeNew, bucketIdx) | |||
} | |||
// Promotes an address from new to old. If the destination bucket is full, | |||
// demote the oldest one to a "new" bucket. | |||
// TODO: Demote more probabilistically? | |||
func (a *addrBook) moveToOld(ka *knownAddress) error { | |||
// Sanity check | |||
if ka.isOld() { | |||
a.Logger.Error(fmt.Sprintf("Cannot promote address that is already old %v", ka)) | |||
return nil | |||
} | |||
if len(ka.Buckets) == 0 { | |||
a.Logger.Error(fmt.Sprintf("Cannot promote address that isn't in any new buckets %v", ka)) | |||
return nil | |||
} | |||
// Remove from all (new) buckets. | |||
a.removeFromAllBuckets(ka) | |||
// It's officially old now. | |||
ka.BucketType = bucketTypeOld | |||
// Try to add it to its oldBucket destination. | |||
oldBucketIdx, err := a.calcOldBucket(ka.Addr) | |||
if err != nil { | |||
return err | |||
} | |||
added := a.addToOldBucket(ka, oldBucketIdx) | |||
if !added { | |||
// No room; move the oldest to a new bucket | |||
oldest := a.pickOldest(bucketTypeOld, oldBucketIdx) | |||
a.removeFromBucket(oldest, bucketTypeOld, oldBucketIdx) | |||
newBucketIdx, err := a.calcNewBucket(oldest.Addr, oldest.Src) | |||
if err != nil { | |||
return err | |||
} | |||
if err := a.addToNewBucket(oldest, newBucketIdx); err != nil { | |||
a.Logger.Error("Error adding peer to old bucket", "err", err) | |||
} | |||
// Finally, add our ka to old bucket again. | |||
added = a.addToOldBucket(ka, oldBucketIdx) | |||
if !added { | |||
a.Logger.Error(fmt.Sprintf("Could not re-add ka %v to oldBucketIdx %v", ka, oldBucketIdx)) | |||
} | |||
} | |||
return nil | |||
} | |||
func (a *addrBook) removeAddress(addr *p2p.NetAddress) { | |||
ka := a.addrLookup[addr.ID] | |||
if ka == nil { | |||
return | |||
} | |||
a.Logger.Info("Remove address from book", "addr", addr) | |||
a.removeFromAllBuckets(ka) | |||
} | |||
func (a *addrBook) addBadPeer(addr *p2p.NetAddress, banTime time.Duration) bool { | |||
// check it exists in addrbook | |||
ka := a.addrLookup[addr.ID] | |||
// check address is not already there | |||
if ka == nil { | |||
return false | |||
} | |||
if _, alreadyBadPeer := a.badPeers[addr.ID]; !alreadyBadPeer { | |||
// add to bad peer list | |||
ka.ban(banTime) | |||
a.badPeers[addr.ID] = ka | |||
a.Logger.Info("Add address to blacklist", "addr", addr) | |||
} | |||
return true | |||
} | |||
//--------------------------------------------------------------------- | |||
// calculate bucket placements | |||
// hash(key + sourcegroup + int64(hash(key + group + sourcegroup)) % bucket_per_group) % num_new_buckets | |||
func (a *addrBook) calcNewBucket(addr, src *p2p.NetAddress) (int, error) { | |||
data1 := []byte{} | |||
data1 = append(data1, []byte(a.key)...) | |||
data1 = append(data1, []byte(a.groupKey(addr))...) | |||
data1 = append(data1, []byte(a.groupKey(src))...) | |||
hash1, err := a.hash(data1) | |||
if err != nil { | |||
return 0, err | |||
} | |||
hash64 := binary.BigEndian.Uint64(hash1) | |||
hash64 %= newBucketsPerGroup | |||
var hashbuf [8]byte | |||
binary.BigEndian.PutUint64(hashbuf[:], hash64) | |||
data2 := []byte{} | |||
data2 = append(data2, []byte(a.key)...) | |||
data2 = append(data2, a.groupKey(src)...) | |||
data2 = append(data2, hashbuf[:]...) | |||
hash2, err := a.hash(data2) | |||
if err != nil { | |||
return 0, err | |||
} | |||
result := int(binary.BigEndian.Uint64(hash2) % newBucketCount) | |||
return result, nil | |||
} | |||
// hash(key + group + int64(hash(key + addr)) % buckets_per_group) % num_old_buckets | |||
func (a *addrBook) calcOldBucket(addr *p2p.NetAddress) (int, error) { | |||
data1 := []byte{} | |||
data1 = append(data1, []byte(a.key)...) | |||
data1 = append(data1, []byte(addr.String())...) | |||
hash1, err := a.hash(data1) | |||
if err != nil { | |||
return 0, err | |||
} | |||
hash64 := binary.BigEndian.Uint64(hash1) | |||
hash64 %= oldBucketsPerGroup | |||
var hashbuf [8]byte | |||
binary.BigEndian.PutUint64(hashbuf[:], hash64) | |||
data2 := []byte{} | |||
data2 = append(data2, []byte(a.key)...) | |||
data2 = append(data2, a.groupKey(addr)...) | |||
data2 = append(data2, hashbuf[:]...) | |||
hash2, err := a.hash(data2) | |||
if err != nil { | |||
return 0, err | |||
} | |||
result := int(binary.BigEndian.Uint64(hash2) % oldBucketCount) | |||
return result, nil | |||
} | |||
// Return a string representing the network group of this address. | |||
// This is the /16 for IPv4 (e.g. 1.2.0.0), the /32 (/36 for he.net) for IPv6, the string | |||
// "local" for a local address and the string "unroutable" for an unroutable | |||
// address. | |||
func (a *addrBook) groupKey(na *p2p.NetAddress) string { | |||
return groupKeyFor(na, a.routabilityStrict) | |||
} | |||
func groupKeyFor(na *p2p.NetAddress, routabilityStrict bool) string { | |||
if routabilityStrict && na.Local() { | |||
return "local" | |||
} | |||
if routabilityStrict && !na.Routable() { | |||
return "unroutable" | |||
} | |||
if ipv4 := na.IP.To4(); ipv4 != nil { | |||
return na.IP.Mask(net.CIDRMask(16, 32)).String() | |||
} | |||
if na.RFC6145() || na.RFC6052() { | |||
// last four bytes are the ip address | |||
ip := na.IP[12:16] | |||
return ip.Mask(net.CIDRMask(16, 32)).String() | |||
} | |||
if na.RFC3964() { | |||
ip := na.IP[2:6] | |||
return ip.Mask(net.CIDRMask(16, 32)).String() | |||
} | |||
if na.RFC4380() { | |||
// teredo tunnels have the last 4 bytes as the v4 address XOR | |||
// 0xff. | |||
ip := net.IP(make([]byte, 4)) | |||
for i, byte := range na.IP[12:16] { | |||
ip[i] = byte ^ 0xff | |||
} | |||
return ip.Mask(net.CIDRMask(16, 32)).String() | |||
} | |||
if na.OnionCatTor() { | |||
// group is keyed off the first 4 bits of the actual onion key. | |||
return fmt.Sprintf("tor:%d", na.IP[6]&((1<<4)-1)) | |||
} | |||
// OK, so now we know ourselves to be a IPv6 address. | |||
// bitcoind uses /32 for everything, except for Hurricane Electric's | |||
// (he.net) IP range, which it uses /36 for. | |||
bits := 32 | |||
heNet := &net.IPNet{IP: net.ParseIP("2001:470::"), Mask: net.CIDRMask(32, 128)} | |||
if heNet.Contains(na.IP) { | |||
bits = 36 | |||
} | |||
ipv6Mask := net.CIDRMask(bits, 128) | |||
return na.IP.Mask(ipv6Mask).String() | |||
} | |||
func (a *addrBook) hash(b []byte) ([]byte, error) { | |||
a.hasher.Reset() | |||
a.hasher.Write(b) | |||
return a.hasher.Sum(nil), nil | |||
} |
@ -1,777 +0,0 @@ | |||
package pex | |||
import ( | |||
"encoding/hex" | |||
"fmt" | |||
"io/ioutil" | |||
"math" | |||
mrand "math/rand" | |||
"net" | |||
"os" | |||
"testing" | |||
"time" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
"github.com/tendermint/tendermint/internal/p2p" | |||
"github.com/tendermint/tendermint/libs/log" | |||
tmmath "github.com/tendermint/tendermint/libs/math" | |||
tmrand "github.com/tendermint/tendermint/libs/rand" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// FIXME These tests should not rely on .(*addrBook) assertions | |||
func TestAddrBookPickAddress(t *testing.T) { | |||
fname := createTempFileName(t, "addrbook_test") | |||
// 0 addresses | |||
book := NewAddrBook(fname, true) | |||
book.SetLogger(log.TestingLogger()) | |||
assert.Zero(t, book.Size()) | |||
addr := book.PickAddress(50) | |||
assert.Nil(t, addr, "expected no address") | |||
randAddrs := randNetAddressPairs(t, 1) | |||
addrSrc := randAddrs[0] | |||
err := book.AddAddress(addrSrc.addr, addrSrc.src) | |||
require.NoError(t, err) | |||
// pick an address when we only have new address | |||
addr = book.PickAddress(0) | |||
assert.NotNil(t, addr, "expected an address") | |||
addr = book.PickAddress(50) | |||
assert.NotNil(t, addr, "expected an address") | |||
addr = book.PickAddress(100) | |||
assert.NotNil(t, addr, "expected an address") | |||
// pick an address when we only have old address | |||
book.MarkGood(addrSrc.addr.ID) | |||
addr = book.PickAddress(0) | |||
assert.NotNil(t, addr, "expected an address") | |||
addr = book.PickAddress(50) | |||
assert.NotNil(t, addr, "expected an address") | |||
// in this case, nNew==0 but we biased 100% to new, so we return nil | |||
addr = book.PickAddress(100) | |||
assert.Nil(t, addr, "did not expected an address") | |||
} | |||
func TestAddrBookSaveLoad(t *testing.T) { | |||
fname := createTempFileName(t, "addrbook_test") | |||
// 0 addresses | |||
book := NewAddrBook(fname, true) | |||
book.SetLogger(log.TestingLogger()) | |||
book.Save() | |||
book = NewAddrBook(fname, true) | |||
book.SetLogger(log.TestingLogger()) | |||
err := book.Start() | |||
require.NoError(t, err) | |||
assert.True(t, book.Empty()) | |||
// 100 addresses | |||
randAddrs := randNetAddressPairs(t, 100) | |||
for _, addrSrc := range randAddrs { | |||
err := book.AddAddress(addrSrc.addr, addrSrc.src) | |||
require.NoError(t, err) | |||
} | |||
assert.Equal(t, 100, book.Size()) | |||
book.Save() | |||
book = NewAddrBook(fname, true) | |||
book.SetLogger(log.TestingLogger()) | |||
err = book.Start() | |||
require.NoError(t, err) | |||
assert.Equal(t, 100, book.Size()) | |||
} | |||
func TestAddrBookLookup(t *testing.T) { | |||
fname := createTempFileName(t, "addrbook_test") | |||
randAddrs := randNetAddressPairs(t, 100) | |||
book := NewAddrBook(fname, true) | |||
book.SetLogger(log.TestingLogger()) | |||
for _, addrSrc := range randAddrs { | |||
addr := addrSrc.addr | |||
src := addrSrc.src | |||
err := book.AddAddress(addr, src) | |||
require.NoError(t, err) | |||
ka := book.HasAddress(addr) | |||
assert.True(t, ka, "Expected to find KnownAddress %v but wasn't there.", addr) | |||
} | |||
} | |||
func TestAddrBookPromoteToOld(t *testing.T) { | |||
fname := createTempFileName(t, "addrbook_test") | |||
randAddrs := randNetAddressPairs(t, 100) | |||
book := NewAddrBook(fname, true) | |||
book.SetLogger(log.TestingLogger()) | |||
for _, addrSrc := range randAddrs { | |||
err := book.AddAddress(addrSrc.addr, addrSrc.src) | |||
require.NoError(t, err) | |||
} | |||
// Attempt all addresses. | |||
for _, addrSrc := range randAddrs { | |||
book.MarkAttempt(addrSrc.addr) | |||
} | |||
// Promote half of them | |||
for i, addrSrc := range randAddrs { | |||
if i%2 == 0 { | |||
book.MarkGood(addrSrc.addr.ID) | |||
} | |||
} | |||
// TODO: do more testing :) | |||
selection := book.GetSelection() | |||
t.Logf("selection: %v", selection) | |||
if len(selection) > book.Size() { | |||
t.Errorf("selection could not be bigger than the book") | |||
} | |||
selection = book.GetSelectionWithBias(30) | |||
t.Logf("selection: %v", selection) | |||
if len(selection) > book.Size() { | |||
t.Errorf("selection with bias could not be bigger than the book") | |||
} | |||
assert.Equal(t, book.Size(), 100, "expecting book size to be 100") | |||
} | |||
func TestAddrBookHandlesDuplicates(t *testing.T) { | |||
fname := createTempFileName(t, "addrbook_test") | |||
book := NewAddrBook(fname, true) | |||
book.SetLogger(log.TestingLogger()) | |||
randAddrs := randNetAddressPairs(t, 100) | |||
differentSrc := randIPv4Address(t) | |||
for _, addrSrc := range randAddrs { | |||
err := book.AddAddress(addrSrc.addr, addrSrc.src) | |||
require.NoError(t, err) | |||
err = book.AddAddress(addrSrc.addr, addrSrc.src) // duplicate | |||
require.NoError(t, err) | |||
err = book.AddAddress(addrSrc.addr, differentSrc) // different src | |||
require.NoError(t, err) | |||
} | |||
assert.Equal(t, 100, book.Size()) | |||
} | |||
type netAddressPair struct { | |||
addr *p2p.NetAddress | |||
src *p2p.NetAddress | |||
} | |||
func randNetAddressPairs(t *testing.T, n int) []netAddressPair { | |||
randAddrs := make([]netAddressPair, n) | |||
for i := 0; i < n; i++ { | |||
randAddrs[i] = netAddressPair{addr: randIPv4Address(t), src: randIPv4Address(t)} | |||
} | |||
return randAddrs | |||
} | |||
func randIPv4Address(t *testing.T) *p2p.NetAddress { | |||
for { | |||
ip := fmt.Sprintf("%v.%v.%v.%v", | |||
mrand.Intn(254)+1, | |||
mrand.Intn(255), | |||
mrand.Intn(255), | |||
mrand.Intn(255), | |||
) | |||
port := mrand.Intn(65535-1) + 1 | |||
id := types.NodeID(hex.EncodeToString(tmrand.Bytes(types.NodeIDByteLength))) | |||
idAddr := id.AddressString(fmt.Sprintf("%v:%v", ip, port)) | |||
addr, err := types.NewNetAddressString(idAddr) | |||
assert.Nil(t, err, "error generating rand network address") | |||
if addr.Routable() { | |||
return addr | |||
} | |||
} | |||
} | |||
func TestAddrBookRemoveAddress(t *testing.T) { | |||
fname := createTempFileName(t, "addrbook_test") | |||
book := NewAddrBook(fname, true) | |||
book.SetLogger(log.TestingLogger()) | |||
addr := randIPv4Address(t) | |||
err := book.AddAddress(addr, addr) | |||
require.NoError(t, err) | |||
assert.Equal(t, 1, book.Size()) | |||
book.RemoveAddress(addr) | |||
assert.Equal(t, 0, book.Size()) | |||
nonExistingAddr := randIPv4Address(t) | |||
book.RemoveAddress(nonExistingAddr) | |||
assert.Equal(t, 0, book.Size()) | |||
} | |||
func TestAddrBookGetSelectionWithOneMarkedGood(t *testing.T) { | |||
// create a book with 10 addresses, 1 good/old and 9 new | |||
book, _ := createAddrBookWithMOldAndNNewAddrs(t, 1, 9) | |||
addrs := book.GetSelectionWithBias(biasToSelectNewPeers) | |||
assert.NotNil(t, addrs) | |||
assertMOldAndNNewAddrsInSelection(t, 1, 9, addrs, book) | |||
} | |||
func TestAddrBookGetSelectionWithOneNotMarkedGood(t *testing.T) { | |||
// create a book with 10 addresses, 9 good/old and 1 new | |||
book, _ := createAddrBookWithMOldAndNNewAddrs(t, 9, 1) | |||
addrs := book.GetSelectionWithBias(biasToSelectNewPeers) | |||
assert.NotNil(t, addrs) | |||
assertMOldAndNNewAddrsInSelection(t, 9, 1, addrs, book) | |||
} | |||
func TestAddrBookGetSelectionReturnsNilWhenAddrBookIsEmpty(t *testing.T) { | |||
book, _ := createAddrBookWithMOldAndNNewAddrs(t, 0, 0) | |||
addrs := book.GetSelectionWithBias(biasToSelectNewPeers) | |||
assert.Nil(t, addrs) | |||
} | |||
func TestAddrBookGetSelection(t *testing.T) { | |||
fname := createTempFileName(t, "addrbook_test") | |||
book := NewAddrBook(fname, true) | |||
book.SetLogger(log.TestingLogger()) | |||
// 1) empty book | |||
assert.Empty(t, book.GetSelection()) | |||
// 2) add one address | |||
addr := randIPv4Address(t) | |||
err := book.AddAddress(addr, addr) | |||
require.NoError(t, err) | |||
assert.Equal(t, 1, len(book.GetSelection())) | |||
assert.Equal(t, addr, book.GetSelection()[0]) | |||
// 3) add a bunch of addresses | |||
randAddrs := randNetAddressPairs(t, 100) | |||
for _, addrSrc := range randAddrs { | |||
err := book.AddAddress(addrSrc.addr, addrSrc.src) | |||
require.NoError(t, err) | |||
} | |||
// check there is no duplicates | |||
addrs := make(map[string]*p2p.NetAddress) | |||
selection := book.GetSelection() | |||
for _, addr := range selection { | |||
if dup, ok := addrs[addr.String()]; ok { | |||
t.Fatalf("selection %v contains duplicates %v", selection, dup) | |||
} | |||
addrs[addr.String()] = addr | |||
} | |||
if len(selection) > book.Size() { | |||
t.Errorf("selection %v could not be bigger than the book", selection) | |||
} | |||
} | |||
func TestAddrBookGetSelectionWithBias(t *testing.T) { | |||
const biasTowardsNewAddrs = 30 | |||
fname := createTempFileName(t, "addrbook_test") | |||
book := NewAddrBook(fname, true) | |||
book.SetLogger(log.TestingLogger()) | |||
// 1) empty book | |||
selection := book.GetSelectionWithBias(biasTowardsNewAddrs) | |||
assert.Empty(t, selection) | |||
// 2) add one address | |||
addr := randIPv4Address(t) | |||
err := book.AddAddress(addr, addr) | |||
require.NoError(t, err) | |||
selection = book.GetSelectionWithBias(biasTowardsNewAddrs) | |||
assert.Equal(t, 1, len(selection)) | |||
assert.Equal(t, addr, selection[0]) | |||
// 3) add a bunch of addresses | |||
randAddrs := randNetAddressPairs(t, 100) | |||
for _, addrSrc := range randAddrs { | |||
err := book.AddAddress(addrSrc.addr, addrSrc.src) | |||
require.NoError(t, err) | |||
} | |||
// check there is no duplicates | |||
addrs := make(map[string]*p2p.NetAddress) | |||
selection = book.GetSelectionWithBias(biasTowardsNewAddrs) | |||
for _, addr := range selection { | |||
if dup, ok := addrs[addr.String()]; ok { | |||
t.Fatalf("selection %v contains duplicates %v", selection, dup) | |||
} | |||
addrs[addr.String()] = addr | |||
} | |||
if len(selection) > book.Size() { | |||
t.Fatalf("selection %v could not be bigger than the book", selection) | |||
} | |||
// 4) mark 80% of the addresses as good | |||
randAddrsLen := len(randAddrs) | |||
for i, addrSrc := range randAddrs { | |||
if int((float64(i)/float64(randAddrsLen))*100) >= 20 { | |||
book.MarkGood(addrSrc.addr.ID) | |||
} | |||
} | |||
selection = book.GetSelectionWithBias(biasTowardsNewAddrs) | |||
// check that ~70% of addresses returned are good | |||
good := 0 | |||
for _, addr := range selection { | |||
if book.IsGood(addr) { | |||
good++ | |||
} | |||
} | |||
got, expected := int((float64(good)/float64(len(selection)))*100), 100-biasTowardsNewAddrs | |||
// compute some slack to protect against small differences due to rounding: | |||
slack := int(math.Round(float64(100) / float64(len(selection)))) | |||
if got > expected+slack { | |||
t.Fatalf( | |||
"got more good peers (%% got: %d, %% expected: %d, number of good addrs: %d, total: %d)", | |||
got, | |||
expected, | |||
good, | |||
len(selection), | |||
) | |||
} | |||
if got < expected-slack { | |||
t.Fatalf( | |||
"got fewer good peers (%% got: %d, %% expected: %d, number of good addrs: %d, total: %d)", | |||
got, | |||
expected, | |||
good, | |||
len(selection), | |||
) | |||
} | |||
} | |||
func TestAddrBookHasAddress(t *testing.T) { | |||
fname := createTempFileName(t, "addrbook_test") | |||
book := NewAddrBook(fname, true) | |||
book.SetLogger(log.TestingLogger()) | |||
addr := randIPv4Address(t) | |||
err := book.AddAddress(addr, addr) | |||
require.NoError(t, err) | |||
assert.True(t, book.HasAddress(addr)) | |||
book.RemoveAddress(addr) | |||
assert.False(t, book.HasAddress(addr)) | |||
} | |||
func testCreatePrivateAddrs(t *testing.T, numAddrs int) ([]*p2p.NetAddress, []string) { | |||
t.Helper() | |||
addrs := make([]*p2p.NetAddress, numAddrs) | |||
for i := 0; i < numAddrs; i++ { | |||
addrs[i] = randIPv4Address(t) | |||
} | |||
private := make([]string, numAddrs) | |||
for i, addr := range addrs { | |||
private[i] = string(addr.ID) | |||
} | |||
return addrs, private | |||
} | |||
func TestBanBadPeers(t *testing.T) { | |||
fname := createTempFileName(t, "addrbook_test") | |||
book := NewAddrBook(fname, true) | |||
book.SetLogger(log.TestingLogger()) | |||
addr := randIPv4Address(t) | |||
_ = book.AddAddress(addr, addr) | |||
book.MarkBad(addr, 1*time.Second) | |||
// addr should not reachable | |||
assert.False(t, book.HasAddress(addr)) | |||
assert.True(t, book.IsBanned(addr)) | |||
err := book.AddAddress(addr, addr) | |||
// book should not add address from the blacklist | |||
assert.Error(t, err) | |||
time.Sleep(1 * time.Second) | |||
book.ReinstateBadPeers() | |||
// address should be reinstated in the new bucket | |||
assert.EqualValues(t, 1, book.Size()) | |||
assert.True(t, book.HasAddress(addr)) | |||
assert.False(t, book.IsGood(addr)) | |||
} | |||
func TestAddrBookEmpty(t *testing.T) { | |||
fname := createTempFileName(t, "addrbook_test") | |||
book := NewAddrBook(fname, true) | |||
book.SetLogger(log.TestingLogger()) | |||
// Check that empty book is empty | |||
require.True(t, book.Empty()) | |||
// Check that book with our address is empty | |||
book.AddOurAddress(randIPv4Address(t)) | |||
require.True(t, book.Empty()) | |||
// Check that book with private addrs is empty | |||
_, privateIds := testCreatePrivateAddrs(t, 5) | |||
book.AddPrivateIDs(privateIds) | |||
require.True(t, book.Empty()) | |||
// Check that book with address is not empty | |||
err := book.AddAddress(randIPv4Address(t), randIPv4Address(t)) | |||
require.NoError(t, err) | |||
require.False(t, book.Empty()) | |||
} | |||
func TestPrivatePeers(t *testing.T) { | |||
fname := createTempFileName(t, "addrbook_test") | |||
book := NewAddrBook(fname, true) | |||
book.SetLogger(log.TestingLogger()) | |||
addrs, private := testCreatePrivateAddrs(t, 10) | |||
book.AddPrivateIDs(private) | |||
// private addrs must not be added | |||
for _, addr := range addrs { | |||
err := book.AddAddress(addr, addr) | |||
if assert.Error(t, err) { | |||
_, ok := err.(ErrAddrBookPrivate) | |||
assert.True(t, ok) | |||
} | |||
} | |||
// addrs coming from private peers must not be added | |||
err := book.AddAddress(randIPv4Address(t), addrs[0]) | |||
if assert.Error(t, err) { | |||
_, ok := err.(ErrAddrBookPrivateSrc) | |||
assert.True(t, ok) | |||
} | |||
} | |||
func testAddrBookAddressSelection(t *testing.T, bookSize int) { | |||
// generate all combinations of old (m) and new addresses | |||
for nBookOld := 0; nBookOld <= bookSize; nBookOld++ { | |||
nBookNew := bookSize - nBookOld | |||
dbgStr := fmt.Sprintf("book of size %d (new %d, old %d)", bookSize, nBookNew, nBookOld) | |||
// create book and get selection | |||
book, _ := createAddrBookWithMOldAndNNewAddrs(t, nBookOld, nBookNew) | |||
addrs := book.GetSelectionWithBias(biasToSelectNewPeers) | |||
assert.NotNil(t, addrs, "%s - expected a non-nil selection", dbgStr) | |||
nAddrs := len(addrs) | |||
assert.NotZero(t, nAddrs, "%s - expected at least one address in selection", dbgStr) | |||
// check there's no nil addresses | |||
for _, addr := range addrs { | |||
if addr == nil { | |||
t.Fatalf("%s - got nil address in selection %v", dbgStr, addrs) | |||
} | |||
} | |||
// XXX: shadowing | |||
nOld, nNew := countOldAndNewAddrsInSelection(addrs, book) | |||
// Given: | |||
// n - num new addrs, m - num old addrs | |||
// k - num new addrs expected in the beginning (based on bias %) | |||
// i=min(n, max(k,r-m)), aka expNew | |||
// j=min(m, r-i), aka expOld | |||
// | |||
// We expect this layout: | |||
// indices: 0...i-1 i...i+j-1 | |||
// addresses: N0..Ni-1 O0..Oj-1 | |||
// | |||
// There is at least one partition and at most three. | |||
var ( | |||
k = percentageOfNum(biasToSelectNewPeers, nAddrs) | |||
expNew = tmmath.MinInt(nNew, tmmath.MaxInt(k, nAddrs-nBookOld)) | |||
expOld = tmmath.MinInt(nOld, nAddrs-expNew) | |||
) | |||
// Verify that the number of old and new addresses are as expected | |||
if nNew != expNew { | |||
t.Fatalf("%s - expected new addrs %d, got %d", dbgStr, expNew, nNew) | |||
} | |||
if nOld != expOld { | |||
t.Fatalf("%s - expected old addrs %d, got %d", dbgStr, expOld, nOld) | |||
} | |||
// Verify that the order of addresses is as expected | |||
// Get the sequence types and lengths of the selection | |||
seqLens, seqTypes, err := analyseSelectionLayout(book, addrs) | |||
assert.NoError(t, err, "%s", dbgStr) | |||
// Build a list with the expected lengths of partitions and another with the expected types, e.g.: | |||
// expSeqLens = [10, 22], expSeqTypes = [1, 2] | |||
// means we expect 10 new (type 1) addresses followed by 22 old (type 2) addresses. | |||
var expSeqLens []int | |||
var expSeqTypes []int | |||
switch { | |||
case expOld == 0: // all new addresses | |||
expSeqLens = []int{nAddrs} | |||
expSeqTypes = []int{1} | |||
case expNew == 0: // all old addresses | |||
expSeqLens = []int{nAddrs} | |||
expSeqTypes = []int{2} | |||
case nAddrs-expNew-expOld == 0: // new addresses, old addresses | |||
expSeqLens = []int{expNew, expOld} | |||
expSeqTypes = []int{1, 2} | |||
} | |||
assert.Equal(t, expSeqLens, seqLens, | |||
"%s - expected sequence lengths of old/new %v, got %v", | |||
dbgStr, expSeqLens, seqLens) | |||
assert.Equal(t, expSeqTypes, seqTypes, | |||
"%s - expected sequence types (1-new, 2-old) was %v, got %v", | |||
dbgStr, expSeqTypes, seqTypes) | |||
} | |||
} | |||
func TestMultipleAddrBookAddressSelection(t *testing.T) { | |||
// test books with smaller size, < N | |||
const N = 32 | |||
for bookSize := 1; bookSize < N; bookSize++ { | |||
testAddrBookAddressSelection(t, bookSize) | |||
} | |||
// Test for two books with sizes from following ranges | |||
ranges := [...][]int{{33, 100}, {100, 175}} | |||
bookSizes := make([]int, 0, len(ranges)) | |||
for _, r := range ranges { | |||
bookSizes = append(bookSizes, mrand.Intn(r[1]-r[0])+r[0]) | |||
} | |||
t.Logf("Testing address selection for the following book sizes %v\n", bookSizes) | |||
for _, bookSize := range bookSizes { | |||
testAddrBookAddressSelection(t, bookSize) | |||
} | |||
} | |||
func TestAddrBookAddDoesNotOverwriteOldIP(t *testing.T) { | |||
fname := createTempFileName(t, "addrbook_test") | |||
// This test creates adds a peer to the address book and marks it good | |||
// It then attempts to override the peer's IP, by adding a peer with the same ID | |||
// but different IP. We distinguish the IP's by "RealIP" and "OverrideAttemptIP" | |||
peerID := "678503e6c8f50db7279c7da3cb9b072aac4bc0d5" | |||
peerRealIP := "1.1.1.1:26656" | |||
peerOverrideAttemptIP := "2.2.2.2:26656" | |||
SrcAddr := "b0dd378c3fbc4c156cd6d302a799f0d2e4227201@159.89.121.174:26656" | |||
// There is a chance that AddAddress will ignore the new peer its given. | |||
// So we repeat trying to override the peer several times, | |||
// to ensure we aren't in a case that got probabilistically ignored | |||
numOverrideAttempts := 10 | |||
peerRealAddr, err := types.NewNetAddressString(peerID + "@" + peerRealIP) | |||
require.Nil(t, err) | |||
peerOverrideAttemptAddr, err := types.NewNetAddressString(peerID + "@" + peerOverrideAttemptIP) | |||
require.Nil(t, err) | |||
src, err := types.NewNetAddressString(SrcAddr) | |||
require.Nil(t, err) | |||
book := NewAddrBook(fname, true) | |||
book.SetLogger(log.TestingLogger()) | |||
err = book.AddAddress(peerRealAddr, src) | |||
require.Nil(t, err) | |||
book.MarkAttempt(peerRealAddr) | |||
book.MarkGood(peerRealAddr.ID) | |||
// Double check that adding a peer again doesn't error | |||
err = book.AddAddress(peerRealAddr, src) | |||
require.Nil(t, err) | |||
// Try changing ip but keeping the same node id. (change 1.1.1.1 to 2.2.2.2) | |||
// This should just be ignored, and not error. | |||
for i := 0; i < numOverrideAttempts; i++ { | |||
err = book.AddAddress(peerOverrideAttemptAddr, src) | |||
require.Nil(t, err) | |||
} | |||
// Now check that the IP was not overridden. | |||
// This is done by sampling several peers from addr book | |||
// and ensuring they all have the correct IP. | |||
// In the expected functionality, this test should only have 1 Peer, hence will pass. | |||
for i := 0; i < numOverrideAttempts; i++ { | |||
selection := book.GetSelection() | |||
for _, addr := range selection { | |||
require.Equal(t, addr.IP, peerRealAddr.IP) | |||
} | |||
} | |||
} | |||
func TestAddrBookGroupKey(t *testing.T) { | |||
// non-strict routability | |||
testCases := []struct { | |||
name string | |||
ip string | |||
expKey string | |||
}{ | |||
// IPv4 normal. | |||
{"ipv4 normal class a", "12.1.2.3", "12.1.0.0"}, | |||
{"ipv4 normal class b", "173.1.2.3", "173.1.0.0"}, | |||
{"ipv4 normal class c", "196.1.2.3", "196.1.0.0"}, | |||
// IPv6/IPv4 translations. | |||
{"ipv6 rfc3964 with ipv4 encap", "2002:0c01:0203::", "12.1.0.0"}, | |||
{"ipv6 rfc4380 toredo ipv4", "2001:0:1234::f3fe:fdfc", "12.1.0.0"}, | |||
{"ipv6 rfc6052 well-known prefix with ipv4", "64:ff9b::0c01:0203", "12.1.0.0"}, | |||
{"ipv6 rfc6145 translated ipv4", "::ffff:0:0c01:0203", "12.1.0.0"}, | |||
// Tor. | |||
{"ipv6 tor onioncat", "fd87:d87e:eb43:1234::5678", "tor:2"}, | |||
{"ipv6 tor onioncat 2", "fd87:d87e:eb43:1245::6789", "tor:2"}, | |||
{"ipv6 tor onioncat 3", "fd87:d87e:eb43:1345::6789", "tor:3"}, | |||
// IPv6 normal. | |||
{"ipv6 normal", "2602:100::1", "2602:100::"}, | |||
{"ipv6 normal 2", "2602:0100::1234", "2602:100::"}, | |||
{"ipv6 hurricane electric", "2001:470:1f10:a1::2", "2001:470:1000::"}, | |||
{"ipv6 hurricane electric 2", "2001:0470:1f10:a1::2", "2001:470:1000::"}, | |||
} | |||
for i, tc := range testCases { | |||
nip := net.ParseIP(tc.ip) | |||
key := groupKeyFor(types.NewNetAddressIPPort(nip, 26656), false) | |||
assert.Equal(t, tc.expKey, key, "#%d", i) | |||
} | |||
// strict routability | |||
testCases = []struct { | |||
name string | |||
ip string | |||
expKey string | |||
}{ | |||
// Local addresses. | |||
{"ipv4 localhost", "127.0.0.1", "local"}, | |||
{"ipv6 localhost", "::1", "local"}, | |||
{"ipv4 zero", "0.0.0.0", "local"}, | |||
{"ipv4 first octet zero", "0.1.2.3", "local"}, | |||
// Unroutable addresses. | |||
{"ipv4 invalid bcast", "255.255.255.255", "unroutable"}, | |||
{"ipv4 rfc1918 10/8", "10.1.2.3", "unroutable"}, | |||
{"ipv4 rfc1918 172.16/12", "172.16.1.2", "unroutable"}, | |||
{"ipv4 rfc1918 192.168/16", "192.168.1.2", "unroutable"}, | |||
{"ipv6 rfc3849 2001:db8::/32", "2001:db8::1234", "unroutable"}, | |||
{"ipv4 rfc3927 169.254/16", "169.254.1.2", "unroutable"}, | |||
{"ipv6 rfc4193 fc00::/7", "fc00::1234", "unroutable"}, | |||
{"ipv6 rfc4843 2001:10::/28", "2001:10::1234", "unroutable"}, | |||
{"ipv6 rfc4862 fe80::/64", "fe80::1234", "unroutable"}, | |||
} | |||
for i, tc := range testCases { | |||
nip := net.ParseIP(tc.ip) | |||
key := groupKeyFor(types.NewNetAddressIPPort(nip, 26656), true) | |||
assert.Equal(t, tc.expKey, key, "#%d", i) | |||
} | |||
} | |||
func assertMOldAndNNewAddrsInSelection(t *testing.T, m, n int, addrs []*p2p.NetAddress, book *addrBook) { | |||
nOld, nNew := countOldAndNewAddrsInSelection(addrs, book) | |||
assert.Equal(t, m, nOld, "old addresses") | |||
assert.Equal(t, n, nNew, "new addresses") | |||
} | |||
func createTempFileName(t *testing.T, prefix string) string { | |||
t.Helper() | |||
f, err := ioutil.TempFile("", prefix) | |||
if err != nil { | |||
panic(err) | |||
} | |||
fname := f.Name() | |||
if err := f.Close(); err != nil { | |||
t.Fatal(err) | |||
} | |||
t.Cleanup(func() { _ = os.Remove(fname) }) | |||
return fname | |||
} | |||
func createAddrBookWithMOldAndNNewAddrs(t *testing.T, nOld, nNew int) (book *addrBook, fname string) { | |||
t.Helper() | |||
fname = createTempFileName(t, "addrbook_test") | |||
book = NewAddrBook(fname, true).(*addrBook) | |||
book.SetLogger(log.TestingLogger()) | |||
assert.Zero(t, book.Size()) | |||
randAddrs := randNetAddressPairs(t, nOld) | |||
for _, addr := range randAddrs { | |||
err := book.AddAddress(addr.addr, addr.src) | |||
require.NoError(t, err) | |||
book.MarkGood(addr.addr.ID) | |||
} | |||
randAddrs = randNetAddressPairs(t, nNew) | |||
for _, addr := range randAddrs { | |||
err := book.AddAddress(addr.addr, addr.src) | |||
require.NoError(t, err) | |||
} | |||
return | |||
} | |||
func countOldAndNewAddrsInSelection(addrs []*p2p.NetAddress, book *addrBook) (nOld, nNew int) { | |||
for _, addr := range addrs { | |||
if book.IsGood(addr) { | |||
nOld++ | |||
} else { | |||
nNew++ | |||
} | |||
} | |||
return | |||
} | |||
// Analyze the layout of the selection specified by 'addrs' | |||
// Returns: | |||
// - seqLens - the lengths of the sequences of addresses of same type | |||
// - seqTypes - the types of sequences in selection | |||
func analyseSelectionLayout(book *addrBook, addrs []*p2p.NetAddress) (seqLens, seqTypes []int, err error) { | |||
// address types are: 0 - nil, 1 - new, 2 - old | |||
var ( | |||
prevType = 0 | |||
currentSeqLen = 0 | |||
) | |||
for _, addr := range addrs { | |||
addrType := 0 | |||
if book.IsGood(addr) { | |||
addrType = 2 | |||
} else { | |||
addrType = 1 | |||
} | |||
if addrType != prevType && prevType != 0 { | |||
seqLens = append(seqLens, currentSeqLen) | |||
seqTypes = append(seqTypes, prevType) | |||
currentSeqLen = 0 | |||
} | |||
currentSeqLen++ | |||
prevType = addrType | |||
} | |||
seqLens = append(seqLens, currentSeqLen) | |||
seqTypes = append(seqTypes, prevType) | |||
return | |||
} |
@ -1,24 +0,0 @@ | |||
package pex | |||
import ( | |||
"testing" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
func BenchmarkAddrBook_hash(b *testing.B) { | |||
book := &addrBook{ | |||
ourAddrs: make(map[string]struct{}), | |||
privateIDs: make(map[types.NodeID]struct{}), | |||
addrLookup: make(map[types.NodeID]*knownAddress), | |||
badPeers: make(map[types.NodeID]*knownAddress), | |||
filePath: "", | |||
routabilityStrict: true, | |||
} | |||
book.init() | |||
msg := []byte(`foobar`) | |||
b.ResetTimer() | |||
for i := 0; i < b.N; i++ { | |||
_, _ = book.hash(msg) | |||
} | |||
} |
@ -1,83 +0,0 @@ | |||
package pex | |||
import ( | |||
"encoding/json" | |||
"fmt" | |||
"os" | |||
"github.com/tendermint/tendermint/internal/libs/tempfile" | |||
) | |||
/* Loading & Saving */ | |||
type addrBookJSON struct { | |||
Key string `json:"key"` | |||
Addrs []*knownAddress `json:"addrs"` | |||
} | |||
func (a *addrBook) saveToFile(filePath string) { | |||
a.mtx.Lock() | |||
defer a.mtx.Unlock() | |||
a.Logger.Info("Saving AddrBook to file", "size", a.size()) | |||
addrs := make([]*knownAddress, 0, len(a.addrLookup)) | |||
for _, ka := range a.addrLookup { | |||
addrs = append(addrs, ka) | |||
} | |||
aJSON := &addrBookJSON{ | |||
Key: a.key, | |||
Addrs: addrs, | |||
} | |||
jsonBytes, err := json.MarshalIndent(aJSON, "", "\t") | |||
if err != nil { | |||
a.Logger.Error("Failed to save AddrBook to file", "err", err) | |||
return | |||
} | |||
err = tempfile.WriteFileAtomic(filePath, jsonBytes, 0644) | |||
if err != nil { | |||
a.Logger.Error("Failed to save AddrBook to file", "file", filePath, "err", err) | |||
} | |||
} | |||
// Returns false if file does not exist. | |||
// cmn.Panics if file is corrupt. | |||
func (a *addrBook) loadFromFile(filePath string) bool { | |||
// If doesn't exist, do nothing. | |||
_, err := os.Stat(filePath) | |||
if os.IsNotExist(err) { | |||
return false | |||
} | |||
// Load addrBookJSON{} | |||
r, err := os.Open(filePath) | |||
if err != nil { | |||
panic(fmt.Sprintf("Error opening file %s: %v", filePath, err)) | |||
} | |||
defer r.Close() | |||
aJSON := &addrBookJSON{} | |||
dec := json.NewDecoder(r) | |||
err = dec.Decode(aJSON) | |||
if err != nil { | |||
panic(fmt.Sprintf("Error reading file %s: %v", filePath, err)) | |||
} | |||
// Restore all the fields... | |||
// Restore the key | |||
a.key = aJSON.Key | |||
// Restore .bucketsNew & .bucketsOld | |||
for _, ka := range aJSON.Addrs { | |||
for _, bucketIndex := range ka.Buckets { | |||
bucket := a.getBucket(ka.BucketType, bucketIndex) | |||
bucket[ka.Addr.String()] = ka | |||
} | |||
a.addrLookup[ka.ID()] = ka | |||
if ka.BucketType == bucketTypeNew { | |||
a.nNew++ | |||
} else { | |||
a.nOld++ | |||
} | |||
} | |||
return true | |||
} |
@ -1,141 +0,0 @@ | |||
package pex | |||
import ( | |||
"time" | |||
"github.com/tendermint/tendermint/internal/p2p" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// knownAddress tracks information about a known network address | |||
// that is used to determine how viable an address is. | |||
type knownAddress struct { | |||
Addr *p2p.NetAddress `json:"addr"` | |||
Src *p2p.NetAddress `json:"src"` | |||
Buckets []int `json:"buckets"` | |||
Attempts int32 `json:"attempts"` | |||
BucketType byte `json:"bucket_type"` | |||
LastAttempt time.Time `json:"last_attempt"` | |||
LastSuccess time.Time `json:"last_success"` | |||
LastBanTime time.Time `json:"last_ban_time"` | |||
} | |||
func newKnownAddress(addr *p2p.NetAddress, src *p2p.NetAddress) *knownAddress { | |||
return &knownAddress{ | |||
Addr: addr, | |||
Src: src, | |||
Attempts: 0, | |||
LastAttempt: time.Now(), | |||
BucketType: bucketTypeNew, | |||
Buckets: nil, | |||
} | |||
} | |||
func (ka *knownAddress) ID() types.NodeID { | |||
return ka.Addr.ID | |||
} | |||
func (ka *knownAddress) isOld() bool { | |||
return ka.BucketType == bucketTypeOld | |||
} | |||
func (ka *knownAddress) isNew() bool { | |||
return ka.BucketType == bucketTypeNew | |||
} | |||
func (ka *knownAddress) markAttempt() { | |||
now := time.Now() | |||
ka.LastAttempt = now | |||
ka.Attempts++ | |||
} | |||
func (ka *knownAddress) markGood() { | |||
now := time.Now() | |||
ka.LastAttempt = now | |||
ka.Attempts = 0 | |||
ka.LastSuccess = now | |||
} | |||
func (ka *knownAddress) ban(banTime time.Duration) { | |||
if ka.LastBanTime.Before(time.Now().Add(banTime)) { | |||
ka.LastBanTime = time.Now().Add(banTime) | |||
} | |||
} | |||
func (ka *knownAddress) isBanned() bool { | |||
return ka.LastBanTime.After(time.Now()) | |||
} | |||
func (ka *knownAddress) addBucketRef(bucketIdx int) int { | |||
for _, bucket := range ka.Buckets { | |||
if bucket == bucketIdx { | |||
// TODO refactor to return error? | |||
// log.Warn(Fmt("Bucket already exists in ka.Buckets: %v", ka)) | |||
return -1 | |||
} | |||
} | |||
ka.Buckets = append(ka.Buckets, bucketIdx) | |||
return len(ka.Buckets) | |||
} | |||
func (ka *knownAddress) removeBucketRef(bucketIdx int) int { | |||
buckets := []int{} | |||
for _, bucket := range ka.Buckets { | |||
if bucket != bucketIdx { | |||
buckets = append(buckets, bucket) | |||
} | |||
} | |||
if len(buckets) != len(ka.Buckets)-1 { | |||
// TODO refactor to return error? | |||
// log.Warn(Fmt("bucketIdx not found in ka.Buckets: %v", ka)) | |||
return -1 | |||
} | |||
ka.Buckets = buckets | |||
return len(ka.Buckets) | |||
} | |||
/* | |||
An address is bad if the address in question is a New address, has not been tried in the last | |||
minute, and meets one of the following criteria: | |||
1) It claims to be from the future | |||
2) It hasn't been seen in over a week | |||
3) It has failed at least three times and never succeeded | |||
4) It has failed ten times in the last week | |||
All addresses that meet these criteria are assumed to be worthless and not | |||
worth keeping hold of. | |||
*/ | |||
func (ka *knownAddress) isBad() bool { | |||
// Is Old --> good | |||
if ka.BucketType == bucketTypeOld { | |||
return false | |||
} | |||
// Has been attempted in the last minute --> good | |||
if ka.LastAttempt.After(time.Now().Add(-1 * time.Minute)) { | |||
return false | |||
} | |||
// TODO: From the future? | |||
// Too old? | |||
// TODO: should be a timestamp of last seen, not just last attempt | |||
if ka.LastAttempt.Before(time.Now().Add(-1 * numMissingDays * time.Hour * 24)) { | |||
return true | |||
} | |||
// Never succeeded? | |||
if ka.LastSuccess.IsZero() && ka.Attempts >= numRetries { | |||
return true | |||
} | |||
// Hasn't succeeded in too long? | |||
if ka.LastSuccess.Before(time.Now().Add(-1*minBadDays*time.Hour*24)) && | |||
ka.Attempts >= maxFailures { | |||
return true | |||
} | |||
return false | |||
} |
@ -1,55 +0,0 @@ | |||
package pex | |||
import "time" | |||
const ( | |||
// addresses under which the address manager will claim to need more addresses. | |||
needAddressThreshold = 1000 | |||
// interval used to dump the address cache to disk for future use. | |||
dumpAddressInterval = time.Minute * 2 | |||
// max addresses in each old address bucket. | |||
oldBucketSize = 64 | |||
// buckets we split old addresses over. | |||
oldBucketCount = 64 | |||
// max addresses in each new address bucket. | |||
newBucketSize = 64 | |||
// buckets that we spread new addresses over. | |||
newBucketCount = 256 | |||
// old buckets over which an address group will be spread. | |||
oldBucketsPerGroup = 4 | |||
// new buckets over which a source address group will be spread. | |||
newBucketsPerGroup = 32 | |||
// buckets a frequently seen new address may end up in. | |||
maxNewBucketsPerAddress = 4 | |||
// days before which we assume an address has vanished | |||
// if we have not seen it announced in that long. | |||
numMissingDays = 7 | |||
// tries without a single success before we assume an address is bad. | |||
numRetries = 3 | |||
// max failures we will accept without a success before considering an address bad. | |||
maxFailures = 10 // ? | |||
// days since the last success before we will consider evicting an address. | |||
minBadDays = 7 | |||
// % of total addresses known returned by GetSelection. | |||
getSelectionPercent = 23 | |||
// min addresses that must be returned by GetSelection. Useful for bootstrapping. | |||
minGetSelection = 32 | |||
// max addresses returned by GetSelection | |||
// NOTE: this must match "maxMsgSize" | |||
maxGetSelection = 250 | |||
) |
@ -1,862 +0,0 @@ | |||
package pex | |||
import ( | |||
"errors" | |||
"fmt" | |||
"net" | |||
"sync" | |||
"time" | |||
"github.com/gogo/protobuf/proto" | |||
"github.com/tendermint/tendermint/internal/p2p" | |||
"github.com/tendermint/tendermint/internal/p2p/conn" | |||
"github.com/tendermint/tendermint/libs/cmap" | |||
tmmath "github.com/tendermint/tendermint/libs/math" | |||
tmrand "github.com/tendermint/tendermint/libs/rand" | |||
"github.com/tendermint/tendermint/libs/service" | |||
tmp2p "github.com/tendermint/tendermint/proto/tendermint/p2p" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
type Peer = p2p.Peer | |||
const ( | |||
// PexChannel is a channel for PEX messages | |||
PexChannel = byte(0x00) | |||
// over-estimate of max NetAddress size | |||
// hexID (40) + IP (16) + Port (2) + Name (100) ... | |||
// NOTE: dont use massive DNS name .. | |||
maxAddressSize = 256 | |||
// NOTE: amplificaiton factor! | |||
// small request results in up to maxMsgSize response | |||
maxMsgSize = maxAddressSize * maxGetSelection | |||
// ensure we have enough peers | |||
defaultEnsurePeersPeriod = 30 * time.Second | |||
// Seed/Crawler constants | |||
// minTimeBetweenCrawls is a minimum time between attempts to crawl a peer. | |||
minTimeBetweenCrawls = 2 * time.Minute | |||
// check some peers every this | |||
crawlPeerPeriod = 30 * time.Second | |||
maxAttemptsToDial = 16 // ~ 35h in total (last attempt - 18h) | |||
// if node connects to seed, it does not have any trusted peers. | |||
// Especially in the beginning, node should have more trusted peers than | |||
// untrusted. | |||
biasToSelectNewPeers = 30 // 70 to select good peers | |||
// if a peer is marked bad, it will be banned for at least this time period | |||
defaultBanTime = 24 * time.Hour | |||
) | |||
type errMaxAttemptsToDial struct { | |||
} | |||
func (e errMaxAttemptsToDial) Error() string { | |||
return fmt.Sprintf("reached max attempts %d to dial", maxAttemptsToDial) | |||
} | |||
type errTooEarlyToDial struct { | |||
backoffDuration time.Duration | |||
lastDialed time.Time | |||
} | |||
func (e errTooEarlyToDial) Error() string { | |||
return fmt.Sprintf( | |||
"too early to dial (backoff duration: %d, last dialed: %v, time since: %v)", | |||
e.backoffDuration, e.lastDialed, time.Since(e.lastDialed)) | |||
} | |||
// Reactor handles PEX (peer exchange) and ensures that an | |||
// adequate number of peers are connected to the switch. | |||
// | |||
// It uses `AddrBook` (address book) to store `NetAddress`es of the peers. | |||
// | |||
// ## Preventing abuse | |||
// | |||
// Only accept pexAddrsMsg from peers we sent a corresponding pexRequestMsg too. | |||
// Only accept one pexRequestMsg every ~defaultEnsurePeersPeriod. | |||
type Reactor struct { | |||
p2p.BaseReactor | |||
book AddrBook | |||
config *ReactorConfig | |||
ensurePeersPeriod time.Duration // TODO: should go in the config | |||
// maps to prevent abuse | |||
requestsSent *cmap.CMap // ID->struct{}: unanswered send requests | |||
lastReceivedRequests *cmap.CMap // ID->time.Time: last time peer requested from us | |||
seedAddrs []*p2p.NetAddress | |||
attemptsToDial sync.Map // address (string) -> {number of attempts (int), last time dialed (time.Time)} | |||
// seed/crawled mode fields | |||
crawlPeerInfos map[types.NodeID]crawlPeerInfo | |||
} | |||
func (r *Reactor) minReceiveRequestInterval() time.Duration { | |||
// NOTE: must be less than ensurePeersPeriod, otherwise we'll request | |||
// peers too quickly from others and they'll think we're bad! | |||
return r.ensurePeersPeriod / 3 | |||
} | |||
// ReactorConfig holds reactor specific configuration data. | |||
type ReactorConfig struct { | |||
// Seed/Crawler mode | |||
SeedMode bool | |||
// We want seeds to only advertise good peers. Therefore they should wait at | |||
// least as long as we expect it to take for a peer to become good before | |||
// disconnecting. | |||
SeedDisconnectWaitPeriod time.Duration | |||
// Maximum pause when redialing a persistent peer (if zero, exponential backoff is used) | |||
PersistentPeersMaxDialPeriod time.Duration | |||
// Seeds is a list of addresses reactor may use | |||
// if it can't connect to peers in the addrbook. | |||
Seeds []string | |||
} | |||
type _attemptsToDial struct { | |||
number int | |||
lastDialed time.Time | |||
} | |||
// NewReactor creates new PEX reactor. | |||
func NewReactor(b AddrBook, config *ReactorConfig) *Reactor { | |||
r := &Reactor{ | |||
book: b, | |||
config: config, | |||
ensurePeersPeriod: defaultEnsurePeersPeriod, | |||
requestsSent: cmap.NewCMap(), | |||
lastReceivedRequests: cmap.NewCMap(), | |||
crawlPeerInfos: make(map[types.NodeID]crawlPeerInfo), | |||
} | |||
r.BaseReactor = *p2p.NewBaseReactor("PEX", r) | |||
return r | |||
} | |||
// OnStart implements BaseService | |||
func (r *Reactor) OnStart() error { | |||
err := r.book.Start() | |||
if err != nil && err != service.ErrAlreadyStarted { | |||
return err | |||
} | |||
numOnline, seedAddrs, err := r.checkSeeds() | |||
if err != nil { | |||
return err | |||
} else if numOnline == 0 && r.book.Empty() { | |||
return errors.New("address book is empty and couldn't resolve any seed nodes") | |||
} | |||
r.seedAddrs = seedAddrs | |||
// Check if this node should run | |||
// in seed/crawler mode | |||
if r.config.SeedMode { | |||
go r.crawlPeersRoutine() | |||
} else { | |||
go r.ensurePeersRoutine() | |||
} | |||
return nil | |||
} | |||
// OnStop implements BaseService | |||
func (r *Reactor) OnStop() { | |||
if err := r.book.Stop(); err != nil { | |||
r.Logger.Error("Error stopping address book", "err", err) | |||
} | |||
} | |||
// GetChannels implements Reactor | |||
func (r *Reactor) GetChannels() []*conn.ChannelDescriptor { | |||
return []*conn.ChannelDescriptor{ | |||
{ | |||
ID: PexChannel, | |||
Priority: 1, | |||
SendQueueCapacity: 10, | |||
RecvMessageCapacity: maxMsgSize, | |||
MaxSendBytes: 200, | |||
}, | |||
} | |||
} | |||
// AddPeer implements Reactor by adding peer to the address book (if inbound) | |||
// or by requesting more addresses (if outbound). | |||
func (r *Reactor) AddPeer(p Peer) { | |||
if p.IsOutbound() { | |||
// For outbound peers, the address is already in the books - | |||
// either via DialPeersAsync or r.Receive. | |||
// Ask it for more peers if we need. | |||
if r.book.NeedMoreAddrs() { | |||
r.RequestAddrs(p) | |||
} | |||
} else { | |||
// inbound peer is its own source | |||
addr, err := p.NodeInfo().NetAddress() | |||
if err != nil { | |||
r.Logger.Error("Failed to get peer NetAddress", "err", err, "peer", p) | |||
return | |||
} | |||
// Make it explicit that addr and src are the same for an inbound peer. | |||
src := addr | |||
// add to book. dont RequestAddrs right away because | |||
// we don't trust inbound as much - let ensurePeersRoutine handle it. | |||
err = r.book.AddAddress(addr, src) | |||
r.logErrAddrBook(err) | |||
} | |||
} | |||
// RemovePeer implements Reactor by resetting peer's requests info. | |||
func (r *Reactor) RemovePeer(p Peer, reason interface{}) { | |||
id := string(p.ID()) | |||
r.requestsSent.Delete(id) | |||
r.lastReceivedRequests.Delete(id) | |||
} | |||
func (r *Reactor) logErrAddrBook(err error) { | |||
if err != nil { | |||
switch err.(type) { | |||
case ErrAddrBookNilAddr: | |||
r.Logger.Error("Failed to add new address", "err", err) | |||
default: | |||
// non-routable, self, full book, private, etc. | |||
r.Logger.Debug("Failed to add new address", "err", err) | |||
} | |||
} | |||
} | |||
// Receive implements Reactor by handling incoming PEX messages. | |||
// XXX: do not call any methods that can block or incur heavy processing. | |||
// https://github.com/tendermint/tendermint/issues/2888 | |||
func (r *Reactor) Receive(chID byte, src Peer, msgBytes []byte) { | |||
msg, err := decodeMsg(msgBytes) | |||
if err != nil { | |||
r.Logger.Error("Error decoding message", "src", src, "chId", chID, "err", err) | |||
r.Switch.StopPeerForError(src, err) | |||
return | |||
} | |||
r.Logger.Debug("Received message", "src", src, "chId", chID, "msg", msg) | |||
switch msg := msg.(type) { | |||
case *tmp2p.PexRequest: | |||
// NOTE: this is a prime candidate for amplification attacks, | |||
// so it's important we | |||
// 1) restrict how frequently peers can request | |||
// 2) limit the output size | |||
// If we're a seed and this is an inbound peer, | |||
// respond once and disconnect. | |||
if r.config.SeedMode && !src.IsOutbound() { | |||
id := string(src.ID()) | |||
v := r.lastReceivedRequests.Get(id) | |||
if v != nil { | |||
// FlushStop/StopPeer are already | |||
// running in a go-routine. | |||
return | |||
} | |||
r.lastReceivedRequests.Set(id, time.Now()) | |||
// Send addrs and disconnect | |||
r.SendAddrs(src, r.book.GetSelectionWithBias(biasToSelectNewPeers)) | |||
go func() { | |||
// In a go-routine so it doesn't block .Receive. | |||
src.FlushStop() | |||
r.Switch.StopPeerGracefully(src) | |||
}() | |||
} else { | |||
// Check we're not receiving requests too frequently. | |||
if err := r.receiveRequest(src); err != nil { | |||
r.Switch.StopPeerForError(src, err) | |||
r.book.MarkBad(src.SocketAddr(), defaultBanTime) | |||
return | |||
} | |||
r.SendAddrs(src, r.book.GetSelection()) | |||
} | |||
case *tmp2p.PexResponse: | |||
// If we asked for addresses, add them to the book | |||
addrs, err := NetAddressesFromProto(msg.Addresses) | |||
if err != nil { | |||
r.Switch.StopPeerForError(src, err) | |||
r.book.MarkBad(src.SocketAddr(), defaultBanTime) | |||
return | |||
} | |||
err = r.ReceiveAddrs(addrs, src) | |||
if err != nil { | |||
r.Switch.StopPeerForError(src, err) | |||
if err == ErrUnsolicitedList { | |||
r.book.MarkBad(src.SocketAddr(), defaultBanTime) | |||
} | |||
return | |||
} | |||
default: | |||
r.Logger.Error(fmt.Sprintf("Unknown message type %T", msg)) | |||
} | |||
} | |||
// enforces a minimum amount of time between requests | |||
func (r *Reactor) receiveRequest(src Peer) error { | |||
id := string(src.ID()) | |||
v := r.lastReceivedRequests.Get(id) | |||
if v == nil { | |||
// initialize with empty time | |||
lastReceived := time.Time{} | |||
r.lastReceivedRequests.Set(id, lastReceived) | |||
return nil | |||
} | |||
lastReceived := v.(time.Time) | |||
if lastReceived.Equal(time.Time{}) { | |||
// first time gets a free pass. then we start tracking the time | |||
lastReceived = time.Now() | |||
r.lastReceivedRequests.Set(id, lastReceived) | |||
return nil | |||
} | |||
now := time.Now() | |||
minInterval := r.minReceiveRequestInterval() | |||
if now.Sub(lastReceived) < minInterval { | |||
return fmt.Errorf( | |||
"peer (%v) sent next PEX request too soon. lastReceived: %v, now: %v, minInterval: %v. Disconnecting", | |||
src.ID(), | |||
lastReceived, | |||
now, | |||
minInterval, | |||
) | |||
} | |||
r.lastReceivedRequests.Set(id, now) | |||
return nil | |||
} | |||
// RequestAddrs asks peer for more addresses if we do not already have a | |||
// request out for this peer. | |||
func (r *Reactor) RequestAddrs(p Peer) { | |||
id := string(p.ID()) | |||
if _, exists := r.requestsSent.GetOrSet(id, struct{}{}); exists { | |||
return | |||
} | |||
r.Logger.Debug("Request addrs", "from", p) | |||
p.Send(PexChannel, mustEncode(&tmp2p.PexRequest{})) | |||
} | |||
// ReceiveAddrs adds the given addrs to the addrbook if theres an open | |||
// request for this peer and deletes the open request. | |||
// If there's no open request for the src peer, it returns an error. | |||
func (r *Reactor) ReceiveAddrs(addrs []*p2p.NetAddress, src Peer) error { | |||
id := string(src.ID()) | |||
if !r.requestsSent.Has(id) { | |||
return ErrUnsolicitedList | |||
} | |||
r.requestsSent.Delete(id) | |||
srcAddr, err := src.NodeInfo().NetAddress() | |||
if err != nil { | |||
return err | |||
} | |||
srcIsSeed := false | |||
for _, seedAddr := range r.seedAddrs { | |||
if seedAddr.Equals(srcAddr) { | |||
srcIsSeed = true | |||
break | |||
} | |||
} | |||
for _, netAddr := range addrs { | |||
// NOTE: we check netAddr validity and routability in book#AddAddress. | |||
err = r.book.AddAddress(netAddr, srcAddr) | |||
if err != nil { | |||
r.logErrAddrBook(err) | |||
// XXX: should we be strict about incoming data and disconnect from a | |||
// peer here too? | |||
continue | |||
} | |||
// If this address came from a seed node, try to connect to it without | |||
// waiting (#2093) | |||
if srcIsSeed { | |||
r.Logger.Info("Will dial address, which came from seed", "addr", netAddr, "seed", srcAddr) | |||
go func(addr *p2p.NetAddress) { | |||
err := r.dialPeer(addr) | |||
if err != nil { | |||
switch err.(type) { | |||
case errMaxAttemptsToDial, errTooEarlyToDial, p2p.ErrCurrentlyDialingOrExistingAddress: | |||
r.Logger.Debug(err.Error(), "addr", addr) | |||
default: | |||
r.Logger.Error(err.Error(), "addr", addr) | |||
} | |||
} | |||
}(netAddr) | |||
} | |||
} | |||
return nil | |||
} | |||
// SendAddrs sends addrs to the peer. | |||
func (r *Reactor) SendAddrs(p Peer, netAddrs []*p2p.NetAddress) { | |||
p.Send(PexChannel, mustEncode(&tmp2p.PexResponse{Addresses: NetAddressesToProto(netAddrs)})) | |||
} | |||
// SetEnsurePeersPeriod sets period to ensure peers connected. | |||
func (r *Reactor) SetEnsurePeersPeriod(d time.Duration) { | |||
r.ensurePeersPeriod = d | |||
} | |||
// Ensures that sufficient peers are connected. (continuous) | |||
func (r *Reactor) ensurePeersRoutine() { | |||
var ( | |||
seed = tmrand.NewRand() | |||
jitter = seed.Int63n(r.ensurePeersPeriod.Nanoseconds()) | |||
) | |||
// Randomize first round of communication to avoid thundering herd. | |||
// If no peers are present directly start connecting so we guarantee swift | |||
// setup with the help of configured seeds. | |||
if r.nodeHasSomePeersOrDialingAny() { | |||
time.Sleep(time.Duration(jitter)) | |||
} | |||
// fire once immediately. | |||
// ensures we dial the seeds right away if the book is empty | |||
r.ensurePeers() | |||
// fire periodically | |||
ticker := time.NewTicker(r.ensurePeersPeriod) | |||
for { | |||
select { | |||
case <-ticker.C: | |||
r.ensurePeers() | |||
case <-r.Quit(): | |||
ticker.Stop() | |||
return | |||
} | |||
} | |||
} | |||
// ensurePeers ensures that sufficient peers are connected. (once) | |||
// | |||
// heuristic that we haven't perfected yet, or, perhaps is manually edited by | |||
// the node operator. It should not be used to compute what addresses are | |||
// already connected or not. | |||
func (r *Reactor) ensurePeers() { | |||
var ( | |||
out, in, dial = r.Switch.NumPeers() | |||
numToDial = r.Switch.MaxNumOutboundPeers() - (out + dial) | |||
) | |||
r.Logger.Info( | |||
"Ensure peers", | |||
"numOutPeers", out, | |||
"numInPeers", in, | |||
"numDialing", dial, | |||
"numToDial", numToDial, | |||
) | |||
if numToDial <= 0 { | |||
return | |||
} | |||
// bias to prefer more vetted peers when we have fewer connections. | |||
// not perfect, but somewhate ensures that we prioritize connecting to more-vetted | |||
// NOTE: range here is [10, 90]. Too high ? | |||
newBias := tmmath.MinInt(out, 8)*10 + 10 | |||
toDial := make(map[types.NodeID]*p2p.NetAddress) | |||
// Try maxAttempts times to pick numToDial addresses to dial | |||
maxAttempts := numToDial * 3 | |||
for i := 0; i < maxAttempts && len(toDial) < numToDial; i++ { | |||
try := r.book.PickAddress(newBias) | |||
if try == nil { | |||
continue | |||
} | |||
if _, selected := toDial[try.ID]; selected { | |||
continue | |||
} | |||
if r.Switch.IsDialingOrExistingAddress(try) { | |||
continue | |||
} | |||
// TODO: consider moving some checks from toDial into here | |||
// so we don't even consider dialing peers that we want to wait | |||
// before dialing again, or have dialed too many times already | |||
r.Logger.Info("Will dial address", "addr", try) | |||
toDial[try.ID] = try | |||
} | |||
// Dial picked addresses | |||
for _, addr := range toDial { | |||
go func(addr *p2p.NetAddress) { | |||
err := r.dialPeer(addr) | |||
if err != nil { | |||
switch err.(type) { | |||
case errMaxAttemptsToDial, errTooEarlyToDial: | |||
r.Logger.Debug(err.Error(), "addr", addr) | |||
default: | |||
r.Logger.Error(err.Error(), "addr", addr) | |||
} | |||
} | |||
}(addr) | |||
} | |||
if r.book.NeedMoreAddrs() { | |||
// Check if banned nodes can be reinstated | |||
r.book.ReinstateBadPeers() | |||
} | |||
if r.book.NeedMoreAddrs() { | |||
// 1) Pick a random peer and ask for more. | |||
peers := r.Switch.Peers().List() | |||
peersCount := len(peers) | |||
if peersCount > 0 { | |||
rand := tmrand.NewRand() | |||
peer := peers[rand.Int()%peersCount] | |||
r.Logger.Info("We need more addresses. Sending pexRequest to random peer", "peer", peer) | |||
r.RequestAddrs(peer) | |||
} | |||
// 2) Dial seeds if we are not dialing anyone. | |||
// This is done in addition to asking a peer for addresses to work-around | |||
// peers not participating in PEX. | |||
if len(toDial) == 0 { | |||
r.Logger.Info("No addresses to dial. Falling back to seeds") | |||
r.dialSeeds() | |||
} | |||
} | |||
} | |||
func (r *Reactor) dialAttemptsInfo(addr *p2p.NetAddress) (attempts int, lastDialed time.Time) { | |||
_attempts, ok := r.attemptsToDial.Load(addr.DialString()) | |||
if !ok { | |||
return | |||
} | |||
atd := _attempts.(_attemptsToDial) | |||
return atd.number, atd.lastDialed | |||
} | |||
func (r *Reactor) dialPeer(addr *p2p.NetAddress) error { | |||
attempts, lastDialed := r.dialAttemptsInfo(addr) | |||
if !r.Switch.IsPeerPersistent(addr) && attempts > maxAttemptsToDial { | |||
r.book.MarkBad(addr, defaultBanTime) | |||
return errMaxAttemptsToDial{} | |||
} | |||
// exponential backoff if it's not our first attempt to dial given address | |||
if attempts > 0 { | |||
rand := tmrand.NewRand() | |||
jitter := time.Duration(rand.Float64() * float64(time.Second)) // 1s == (1e9 ns) | |||
backoffDuration := jitter + ((1 << uint(attempts)) * time.Second) | |||
backoffDuration = r.maxBackoffDurationForPeer(addr, backoffDuration) | |||
sinceLastDialed := time.Since(lastDialed) | |||
if sinceLastDialed < backoffDuration { | |||
return errTooEarlyToDial{backoffDuration, lastDialed} | |||
} | |||
} | |||
err := r.Switch.DialPeerWithAddress(addr) | |||
if err != nil { | |||
if _, ok := err.(p2p.ErrCurrentlyDialingOrExistingAddress); ok { | |||
return err | |||
} | |||
markAddrInBookBasedOnErr(addr, r.book, err) | |||
switch err.(type) { | |||
case p2p.ErrSwitchAuthenticationFailure: | |||
// NOTE: addr is removed from addrbook in markAddrInBookBasedOnErr | |||
r.attemptsToDial.Delete(addr.DialString()) | |||
default: | |||
r.attemptsToDial.Store(addr.DialString(), _attemptsToDial{attempts + 1, time.Now()}) | |||
} | |||
return fmt.Errorf("dialing failed (attempts: %d): %w", attempts+1, err) | |||
} | |||
// cleanup any history | |||
r.attemptsToDial.Delete(addr.DialString()) | |||
return nil | |||
} | |||
// maxBackoffDurationForPeer caps the backoff duration for persistent peers. | |||
func (r *Reactor) maxBackoffDurationForPeer(addr *p2p.NetAddress, planned time.Duration) time.Duration { | |||
if r.config.PersistentPeersMaxDialPeriod > 0 && | |||
planned > r.config.PersistentPeersMaxDialPeriod && | |||
r.Switch.IsPeerPersistent(addr) { | |||
return r.config.PersistentPeersMaxDialPeriod | |||
} | |||
return planned | |||
} | |||
// checkSeeds checks that addresses are well formed. | |||
// Returns number of seeds we can connect to, along with all seeds addrs. | |||
// return err if user provided any badly formatted seed addresses. | |||
// Doesn't error if the seed node can't be reached. | |||
// numOnline returns -1 if no seed nodes were in the initial configuration. | |||
func (r *Reactor) checkSeeds() (numOnline int, netAddrs []*p2p.NetAddress, err error) { | |||
lSeeds := len(r.config.Seeds) | |||
if lSeeds == 0 { | |||
return -1, nil, nil | |||
} | |||
netAddrs, errs := p2p.NewNetAddressStrings(r.config.Seeds) | |||
numOnline = lSeeds - len(errs) | |||
for _, err := range errs { | |||
switch e := err.(type) { | |||
case types.ErrNetAddressLookup: | |||
r.Logger.Error("Connecting to seed failed", "err", e) | |||
default: | |||
return 0, nil, fmt.Errorf("seed node configuration has error: %w", e) | |||
} | |||
} | |||
return numOnline, netAddrs, nil | |||
} | |||
// randomly dial seeds until we connect to one or exhaust them | |||
func (r *Reactor) dialSeeds() { | |||
rand := tmrand.NewRand() | |||
perm := rand.Perm(len(r.seedAddrs)) | |||
// perm := r.Switch.rng.Perm(lSeeds) | |||
for _, i := range perm { | |||
// dial a random seed | |||
seedAddr := r.seedAddrs[i] | |||
err := r.Switch.DialPeerWithAddress(seedAddr) | |||
switch err.(type) { | |||
case nil, p2p.ErrCurrentlyDialingOrExistingAddress: | |||
return | |||
} | |||
r.Switch.Logger.Error("Error dialing seed", "err", err, "seed", seedAddr) | |||
} | |||
// do not write error message if there were no seeds specified in config | |||
if len(r.seedAddrs) > 0 { | |||
r.Switch.Logger.Error("Couldn't connect to any seeds") | |||
} | |||
} | |||
// AttemptsToDial returns the number of attempts to dial specific address. It | |||
// returns 0 if never attempted or successfully connected. | |||
func (r *Reactor) AttemptsToDial(addr *p2p.NetAddress) int { | |||
lAttempts, attempted := r.attemptsToDial.Load(addr.DialString()) | |||
if attempted { | |||
return lAttempts.(_attemptsToDial).number | |||
} | |||
return 0 | |||
} | |||
//---------------------------------------------------------- | |||
// Explores the network searching for more peers. (continuous) | |||
// Seed/Crawler Mode causes this node to quickly disconnect | |||
// from peers, except other seed nodes. | |||
func (r *Reactor) crawlPeersRoutine() { | |||
// If we have any seed nodes, consult them first | |||
if len(r.seedAddrs) > 0 { | |||
r.dialSeeds() | |||
} else { | |||
// Do an initial crawl | |||
r.crawlPeers(r.book.GetSelection()) | |||
} | |||
// Fire periodically | |||
ticker := time.NewTicker(crawlPeerPeriod) | |||
for { | |||
select { | |||
case <-ticker.C: | |||
r.attemptDisconnects() | |||
r.crawlPeers(r.book.GetSelection()) | |||
r.cleanupCrawlPeerInfos() | |||
case <-r.Quit(): | |||
return | |||
} | |||
} | |||
} | |||
// nodeHasSomePeersOrDialingAny returns true if the node is connected to some | |||
// peers or dialing them currently. | |||
func (r *Reactor) nodeHasSomePeersOrDialingAny() bool { | |||
out, in, dial := r.Switch.NumPeers() | |||
return out+in+dial > 0 | |||
} | |||
// crawlPeerInfo handles temporary data needed for the network crawling | |||
// performed during seed/crawler mode. | |||
type crawlPeerInfo struct { | |||
Addr *p2p.NetAddress `json:"addr"` | |||
// The last time we crawled the peer or attempted to do so. | |||
LastCrawled time.Time `json:"last_crawled"` | |||
} | |||
// crawlPeers will crawl the network looking for new peer addresses. | |||
func (r *Reactor) crawlPeers(addrs []*p2p.NetAddress) { | |||
now := time.Now() | |||
for _, addr := range addrs { | |||
peerInfo, ok := r.crawlPeerInfos[addr.ID] | |||
// Do not attempt to connect with peers we recently crawled. | |||
if ok && now.Sub(peerInfo.LastCrawled) < minTimeBetweenCrawls { | |||
continue | |||
} | |||
// Record crawling attempt. | |||
r.crawlPeerInfos[addr.ID] = crawlPeerInfo{ | |||
Addr: addr, | |||
LastCrawled: now, | |||
} | |||
err := r.dialPeer(addr) | |||
if err != nil { | |||
switch err.(type) { | |||
case errMaxAttemptsToDial, errTooEarlyToDial, p2p.ErrCurrentlyDialingOrExistingAddress: | |||
r.Logger.Debug(err.Error(), "addr", addr) | |||
default: | |||
r.Logger.Error(err.Error(), "addr", addr) | |||
} | |||
continue | |||
} | |||
peer := r.Switch.Peers().Get(addr.ID) | |||
if peer != nil { | |||
r.RequestAddrs(peer) | |||
} | |||
} | |||
} | |||
func (r *Reactor) cleanupCrawlPeerInfos() { | |||
for id, info := range r.crawlPeerInfos { | |||
// If we did not crawl a peer for 24 hours, it means the peer was removed | |||
// from the addrbook => remove | |||
// | |||
// 10000 addresses / maxGetSelection = 40 cycles to get all addresses in | |||
// the ideal case, | |||
// 40 * crawlPeerPeriod ~ 20 minutes | |||
if time.Since(info.LastCrawled) > 24*time.Hour { | |||
delete(r.crawlPeerInfos, id) | |||
} | |||
} | |||
} | |||
// attemptDisconnects checks if we've been with each peer long enough to disconnect | |||
func (r *Reactor) attemptDisconnects() { | |||
for _, peer := range r.Switch.Peers().List() { | |||
if peer.Status().Duration < r.config.SeedDisconnectWaitPeriod { | |||
continue | |||
} | |||
if peer.IsPersistent() { | |||
continue | |||
} | |||
r.Switch.StopPeerGracefully(peer) | |||
} | |||
} | |||
func markAddrInBookBasedOnErr(addr *p2p.NetAddress, book AddrBook, err error) { | |||
// TODO: detect more "bad peer" scenarios | |||
switch err.(type) { | |||
case p2p.ErrSwitchAuthenticationFailure: | |||
book.MarkBad(addr, defaultBanTime) | |||
default: | |||
book.MarkAttempt(addr) | |||
} | |||
} | |||
//----------------------------------------------------------------------------- | |||
// Messages | |||
// mustEncode proto encodes a tmp2p.Message | |||
func mustEncode(pb proto.Message) []byte { | |||
msg := tmp2p.PexMessage{} | |||
switch pb := pb.(type) { | |||
case *tmp2p.PexRequest: | |||
msg.Sum = &tmp2p.PexMessage_PexRequest{PexRequest: pb} | |||
case *tmp2p.PexResponse: | |||
msg.Sum = &tmp2p.PexMessage_PexResponse{PexResponse: pb} | |||
default: | |||
panic(fmt.Sprintf("Unknown message type %T", pb)) | |||
} | |||
bz, err := msg.Marshal() | |||
if err != nil { | |||
panic(fmt.Errorf("unable to marshal %T: %w", pb, err)) | |||
} | |||
return bz | |||
} | |||
func decodeMsg(bz []byte) (proto.Message, error) { | |||
pb := &tmp2p.PexMessage{} | |||
err := pb.Unmarshal(bz) | |||
if err != nil { | |||
return nil, err | |||
} | |||
switch msg := pb.Sum.(type) { | |||
case *tmp2p.PexMessage_PexRequest: | |||
return msg.PexRequest, nil | |||
case *tmp2p.PexMessage_PexResponse: | |||
return msg.PexResponse, nil | |||
default: | |||
return nil, fmt.Errorf("unknown message: %T", msg) | |||
} | |||
} | |||
//----------------------------------------------------------------------------- | |||
// address converters | |||
// NetAddressFromProto converts a Protobuf PexAddress into a native struct. | |||
func NetAddressFromProto(pb tmp2p.PexAddress) (*types.NetAddress, error) { | |||
ip := net.ParseIP(pb.IP) | |||
if ip == nil { | |||
return nil, fmt.Errorf("invalid IP address %v", pb.IP) | |||
} | |||
if pb.Port >= 1<<16 { | |||
return nil, fmt.Errorf("invalid port number %v", pb.Port) | |||
} | |||
return &types.NetAddress{ | |||
ID: types.NodeID(pb.ID), | |||
IP: ip, | |||
Port: uint16(pb.Port), | |||
}, nil | |||
} | |||
// NetAddressesFromProto converts a slice of Protobuf PexAddresses into a native slice. | |||
func NetAddressesFromProto(pbs []tmp2p.PexAddress) ([]*types.NetAddress, error) { | |||
nas := make([]*types.NetAddress, 0, len(pbs)) | |||
for _, pb := range pbs { | |||
na, err := NetAddressFromProto(pb) | |||
if err != nil { | |||
return nil, err | |||
} | |||
nas = append(nas, na) | |||
} | |||
return nas, nil | |||
} | |||
// NetAddressesToProto converts a slice of NetAddresses into a Protobuf PexAddress slice. | |||
func NetAddressesToProto(nas []*types.NetAddress) []tmp2p.PexAddress { | |||
pbs := make([]tmp2p.PexAddress, 0, len(nas)) | |||
for _, na := range nas { | |||
if na != nil { | |||
pbs = append(pbs, tmp2p.PexAddress{ | |||
ID: string(na.ID), | |||
IP: na.IP.String(), | |||
Port: uint32(na.Port), | |||
}) | |||
} | |||
} | |||
return pbs | |||
} |
@ -1,680 +0,0 @@ | |||
package pex | |||
import ( | |||
"encoding/hex" | |||
"fmt" | |||
"io/ioutil" | |||
"os" | |||
"path/filepath" | |||
"testing" | |||
"time" | |||
"github.com/gogo/protobuf/proto" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
"github.com/tendermint/tendermint/config" | |||
"github.com/tendermint/tendermint/internal/p2p" | |||
"github.com/tendermint/tendermint/internal/p2p/mock" | |||
"github.com/tendermint/tendermint/libs/log" | |||
tmp2p "github.com/tendermint/tendermint/proto/tendermint/p2p" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
var ( | |||
cfg *config.P2PConfig | |||
) | |||
func init() { | |||
cfg = config.DefaultP2PConfig() | |||
cfg.PexReactor = true | |||
cfg.AllowDuplicateIP = true | |||
} | |||
func TestPEXReactorBasic(t *testing.T) { | |||
r, _ := createReactor(t, &ReactorConfig{}) | |||
assert.NotNil(t, r) | |||
assert.NotEmpty(t, r.GetChannels()) | |||
} | |||
func TestPEXReactorAddRemovePeer(t *testing.T) { | |||
r, book := createReactor(t, &ReactorConfig{}) | |||
size := book.Size() | |||
peer := p2p.CreateRandomPeer(false) | |||
r.AddPeer(peer) | |||
assert.Equal(t, size+1, book.Size()) | |||
r.RemovePeer(peer, "peer not available") | |||
outboundPeer := p2p.CreateRandomPeer(true) | |||
r.AddPeer(outboundPeer) | |||
assert.Equal(t, size+1, book.Size(), "outbound peers should not be added to the address book") | |||
r.RemovePeer(outboundPeer, "peer not available") | |||
} | |||
// --- FAIL: TestPEXReactorRunning (11.10s) | |||
// pex_reactor_test.go:411: expected all switches to be connected to at | |||
// least one peer (switches: 0 => {outbound: 1, inbound: 0}, 1 => | |||
// {outbound: 0, inbound: 1}, 2 => {outbound: 0, inbound: 0}, ) | |||
// | |||
// EXPLANATION: peers are getting rejected because in switch#addPeer we check | |||
// if any peer (who we already connected to) has the same IP. Even though local | |||
// peers have different IP addresses, they all have the same underlying remote | |||
// IP: 127.0.0.1. | |||
// | |||
func TestPEXReactorRunning(t *testing.T) { | |||
N := 3 | |||
switches := make([]*p2p.Switch, N) | |||
// directory to store address books | |||
dir := tempDir(t) | |||
books := make([]AddrBook, N) | |||
logger := log.TestingLogger() | |||
// create switches | |||
for i := 0; i < N; i++ { | |||
switches[i] = p2p.MakeSwitch(cfg, i, "testing", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { | |||
books[i] = NewAddrBook(filepath.Join(dir, fmt.Sprintf("addrbook%d.json", i)), false) | |||
books[i].SetLogger(logger.With("pex", i)) | |||
sw.SetAddrBook(books[i]) | |||
sw.SetLogger(logger.With("pex", i)) | |||
r := NewReactor(books[i], &ReactorConfig{}) | |||
r.SetLogger(logger.With("pex", i)) | |||
r.SetEnsurePeersPeriod(250 * time.Millisecond) | |||
sw.AddReactor("pex", r) | |||
return sw | |||
}, logger) | |||
} | |||
for _, sw := range switches { | |||
err := sw.Start() // start switch and reactors | |||
require.Nil(t, err) | |||
} | |||
addOtherNodeAddrToAddrBook := func(switchIndex, otherSwitchIndex int) { | |||
addr := switches[otherSwitchIndex].NetAddress() | |||
err := books[switchIndex].AddAddress(addr, addr) | |||
require.NoError(t, err) | |||
} | |||
addOtherNodeAddrToAddrBook(0, 1) | |||
addOtherNodeAddrToAddrBook(1, 0) | |||
addOtherNodeAddrToAddrBook(2, 1) | |||
assertPeersWithTimeout(t, switches, 10*time.Millisecond, 10*time.Second, N-1) | |||
// stop them | |||
for _, s := range switches { | |||
err := s.Stop() | |||
require.NoError(t, err) | |||
} | |||
} | |||
func TestPEXReactorReceive(t *testing.T) { | |||
r, book := createReactor(t, &ReactorConfig{}) | |||
peer := p2p.CreateRandomPeer(false) | |||
// we have to send a request to receive responses | |||
r.RequestAddrs(peer) | |||
size := book.Size() | |||
na, err := peer.NodeInfo().NetAddress() | |||
require.NoError(t, err) | |||
msg := mustEncode(&tmp2p.PexResponse{Addresses: NetAddressesToProto([]*types.NetAddress{na})}) | |||
r.Receive(PexChannel, peer, msg) | |||
assert.Equal(t, size+1, book.Size()) | |||
msg = mustEncode(&tmp2p.PexRequest{}) | |||
r.Receive(PexChannel, peer, msg) // should not panic. | |||
} | |||
func TestPEXReactorRequestMessageAbuse(t *testing.T) { | |||
r, book := createReactor(t, &ReactorConfig{}) | |||
sw := createSwitchAndAddReactors(r) | |||
sw.SetAddrBook(book) | |||
peer := mock.NewPeer(nil) | |||
peerAddr := peer.SocketAddr() | |||
p2p.AddPeerToSwitchPeerSet(sw, peer) | |||
assert.True(t, sw.Peers().Has(peer.ID())) | |||
err := book.AddAddress(peerAddr, peerAddr) | |||
require.NoError(t, err) | |||
require.True(t, book.HasAddress(peerAddr)) | |||
id := string(peer.ID()) | |||
msg := mustEncode(&tmp2p.PexRequest{}) | |||
// first time creates the entry | |||
r.Receive(PexChannel, peer, msg) | |||
assert.True(t, r.lastReceivedRequests.Has(id)) | |||
assert.True(t, sw.Peers().Has(peer.ID())) | |||
// next time sets the last time value | |||
r.Receive(PexChannel, peer, msg) | |||
assert.True(t, r.lastReceivedRequests.Has(id)) | |||
assert.True(t, sw.Peers().Has(peer.ID())) | |||
// third time is too many too soon - peer is removed | |||
r.Receive(PexChannel, peer, msg) | |||
assert.False(t, r.lastReceivedRequests.Has(id)) | |||
assert.False(t, sw.Peers().Has(peer.ID())) | |||
assert.True(t, book.IsBanned(peerAddr)) | |||
} | |||
func TestPEXReactorAddrsMessageAbuse(t *testing.T) { | |||
r, book := createReactor(t, &ReactorConfig{}) | |||
sw := createSwitchAndAddReactors(r) | |||
sw.SetAddrBook(book) | |||
peer := mock.NewPeer(nil) | |||
p2p.AddPeerToSwitchPeerSet(sw, peer) | |||
assert.True(t, sw.Peers().Has(peer.ID())) | |||
id := string(peer.ID()) | |||
// request addrs from the peer | |||
r.RequestAddrs(peer) | |||
assert.True(t, r.requestsSent.Has(id)) | |||
assert.True(t, sw.Peers().Has(peer.ID())) | |||
msg := mustEncode(&tmp2p.PexResponse{Addresses: NetAddressesToProto([]*types.NetAddress{peer.SocketAddr()})}) | |||
// receive some addrs. should clear the request | |||
r.Receive(PexChannel, peer, msg) | |||
assert.False(t, r.requestsSent.Has(id)) | |||
assert.True(t, sw.Peers().Has(peer.ID())) | |||
// receiving more unsolicited addrs causes a disconnect and ban | |||
r.Receive(PexChannel, peer, msg) | |||
assert.False(t, sw.Peers().Has(peer.ID())) | |||
assert.True(t, book.IsBanned(peer.SocketAddr())) | |||
} | |||
func TestCheckSeeds(t *testing.T) { | |||
// directory to store address books | |||
dir := tempDir(t) | |||
// 1. test creating peer with no seeds works | |||
peerSwitch := testCreateDefaultPeer(dir, 0) | |||
require.Nil(t, peerSwitch.Start()) | |||
peerSwitch.Stop() // nolint:errcheck // ignore for tests | |||
// 2. create seed | |||
seed := testCreateSeed(dir, 1, []*p2p.NetAddress{}, []*p2p.NetAddress{}) | |||
// 3. test create peer with online seed works | |||
peerSwitch = testCreatePeerWithSeed(dir, 2, seed) | |||
require.Nil(t, peerSwitch.Start()) | |||
peerSwitch.Stop() // nolint:errcheck // ignore for tests | |||
// 4. test create peer with all seeds having unresolvable DNS fails | |||
badPeerConfig := &ReactorConfig{ | |||
Seeds: []string{"ed3dfd27bfc4af18f67a49862f04cc100696e84d@bad.network.addr:26657", | |||
"d824b13cb5d40fa1d8a614e089357c7eff31b670@anotherbad.network.addr:26657"}, | |||
} | |||
peerSwitch = testCreatePeerWithConfig(dir, 2, badPeerConfig) | |||
require.Error(t, peerSwitch.Start()) | |||
peerSwitch.Stop() // nolint:errcheck // ignore for tests | |||
// 5. test create peer with one good seed address succeeds | |||
badPeerConfig = &ReactorConfig{ | |||
Seeds: []string{"ed3dfd27bfc4af18f67a49862f04cc100696e84d@bad.network.addr:26657", | |||
"d824b13cb5d40fa1d8a614e089357c7eff31b670@anotherbad.network.addr:26657", | |||
seed.NetAddress().String()}, | |||
} | |||
peerSwitch = testCreatePeerWithConfig(dir, 2, badPeerConfig) | |||
require.Nil(t, peerSwitch.Start()) | |||
peerSwitch.Stop() // nolint:errcheck // ignore for tests | |||
} | |||
func TestPEXReactorUsesSeedsIfNeeded(t *testing.T) { | |||
// directory to store address books | |||
dir := tempDir(t) | |||
// 1. create seed | |||
seed := testCreateSeed(dir, 0, []*p2p.NetAddress{}, []*p2p.NetAddress{}) | |||
require.Nil(t, seed.Start()) | |||
t.Cleanup(func() { _ = seed.Stop() }) | |||
// 2. create usual peer with only seed configured. | |||
peer := testCreatePeerWithSeed(dir, 1, seed) | |||
require.Nil(t, peer.Start()) | |||
t.Cleanup(func() { _ = peer.Stop() }) | |||
// 3. check that the peer connects to seed immediately | |||
assertPeersWithTimeout(t, []*p2p.Switch{peer}, 10*time.Millisecond, 3*time.Second, 1) | |||
} | |||
func TestConnectionSpeedForPeerReceivedFromSeed(t *testing.T) { | |||
// directory to store address books | |||
dir := tempDir(t) | |||
// 1. create peer | |||
peerSwitch := testCreateDefaultPeer(dir, 1) | |||
require.Nil(t, peerSwitch.Start()) | |||
t.Cleanup(func() { _ = peerSwitch.Stop() }) | |||
// 2. Create seed which knows about the peer | |||
peerAddr := peerSwitch.NetAddress() | |||
seed := testCreateSeed(dir, 2, []*p2p.NetAddress{peerAddr}, []*p2p.NetAddress{peerAddr}) | |||
require.Nil(t, seed.Start()) | |||
t.Cleanup(func() { _ = seed.Stop() }) | |||
// 3. create another peer with only seed configured. | |||
secondPeer := testCreatePeerWithSeed(dir, 3, seed) | |||
require.Nil(t, secondPeer.Start()) | |||
t.Cleanup(func() { _ = secondPeer.Stop() }) | |||
// 4. check that the second peer connects to seed immediately | |||
assertPeersWithTimeout(t, []*p2p.Switch{secondPeer}, 10*time.Millisecond, 3*time.Second, 1) | |||
// 5. check that the second peer connects to the first peer immediately | |||
assertPeersWithTimeout(t, []*p2p.Switch{secondPeer}, 10*time.Millisecond, 1*time.Second, 2) | |||
} | |||
func TestPEXReactorSeedMode(t *testing.T) { | |||
// directory to store address books | |||
dir := tempDir(t) | |||
pexRConfig := &ReactorConfig{SeedMode: true, SeedDisconnectWaitPeriod: 10 * time.Millisecond} | |||
pexR, book := createReactor(t, pexRConfig) | |||
sw := createSwitchAndAddReactors(pexR) | |||
sw.SetAddrBook(book) | |||
require.NoError(t, sw.Start()) | |||
t.Cleanup(func() { _ = sw.Stop() }) | |||
assert.Zero(t, sw.Peers().Size()) | |||
peerSwitch := testCreateDefaultPeer(dir, 1) | |||
require.NoError(t, peerSwitch.Start()) | |||
t.Cleanup(func() { _ = peerSwitch.Stop() }) | |||
// 1. Test crawlPeers dials the peer | |||
pexR.crawlPeers([]*p2p.NetAddress{peerSwitch.NetAddress()}) | |||
assert.Equal(t, 1, sw.Peers().Size()) | |||
assert.True(t, sw.Peers().Has(peerSwitch.NodeInfo().ID())) | |||
// 2. attemptDisconnects should not disconnect because of wait period | |||
pexR.attemptDisconnects() | |||
assert.Equal(t, 1, sw.Peers().Size()) | |||
// sleep for SeedDisconnectWaitPeriod | |||
time.Sleep(pexRConfig.SeedDisconnectWaitPeriod + 1*time.Millisecond) | |||
// 3. attemptDisconnects should disconnect after wait period | |||
pexR.attemptDisconnects() | |||
assert.Equal(t, 0, sw.Peers().Size()) | |||
} | |||
func TestPEXReactorDoesNotDisconnectFromPersistentPeerInSeedMode(t *testing.T) { | |||
// directory to store address books | |||
dir := tempDir(t) | |||
pexRConfig := &ReactorConfig{SeedMode: true, SeedDisconnectWaitPeriod: 1 * time.Millisecond} | |||
pexR, book := createReactor(t, pexRConfig) | |||
sw := createSwitchAndAddReactors(pexR) | |||
sw.SetAddrBook(book) | |||
require.NoError(t, sw.Start()) | |||
t.Cleanup(func() { _ = sw.Stop() }) | |||
assert.Zero(t, sw.Peers().Size()) | |||
peerSwitch := testCreatePeerWithConfig(dir, 1, pexRConfig) | |||
require.NoError(t, peerSwitch.Start()) | |||
t.Cleanup(func() { _ = peerSwitch.Stop() }) | |||
require.NoError(t, sw.AddPersistentPeers([]string{peerSwitch.NetAddress().String()})) | |||
// 1. Test crawlPeers dials the peer | |||
pexR.crawlPeers([]*p2p.NetAddress{peerSwitch.NetAddress()}) | |||
assert.Equal(t, 1, sw.Peers().Size()) | |||
assert.True(t, sw.Peers().Has(peerSwitch.NodeInfo().ID())) | |||
// sleep for SeedDisconnectWaitPeriod | |||
time.Sleep(pexRConfig.SeedDisconnectWaitPeriod + 1*time.Millisecond) | |||
// 2. attemptDisconnects should not disconnect because the peer is persistent | |||
pexR.attemptDisconnects() | |||
assert.Equal(t, 1, sw.Peers().Size()) | |||
} | |||
func TestPEXReactorDialsPeerUpToMaxAttemptsInSeedMode(t *testing.T) { | |||
// directory to store address books | |||
pexR, book := createReactor(t, &ReactorConfig{SeedMode: true}) | |||
sw := createSwitchAndAddReactors(pexR) | |||
sw.SetAddrBook(book) | |||
// No need to start sw since crawlPeers is called manually here. | |||
peer := mock.NewPeer(nil) | |||
addr := peer.SocketAddr() | |||
require.NoError(t, book.AddAddress(addr, addr)) | |||
assert.True(t, book.HasAddress(addr)) | |||
// imitate maxAttemptsToDial reached | |||
pexR.attemptsToDial.Store(addr.DialString(), _attemptsToDial{maxAttemptsToDial + 1, time.Now()}) | |||
pexR.crawlPeers([]*p2p.NetAddress{addr}) | |||
assert.False(t, book.HasAddress(addr)) | |||
} | |||
// connect a peer to a seed, wait a bit, then stop it. | |||
// this should give it time to request addrs and for the seed | |||
// to call FlushStop, and allows us to test calling Stop concurrently | |||
// with FlushStop. Before a fix, this non-deterministically reproduced | |||
// https://github.com/tendermint/tendermint/issues/3231. | |||
func TestPEXReactorSeedModeFlushStop(t *testing.T) { | |||
t.Skip("flaky test, will be replaced by new P2P stack") | |||
N := 2 | |||
switches := make([]*p2p.Switch, N) | |||
// directory to store address books | |||
dir := tempDir(t) | |||
books := make([]AddrBook, N) | |||
logger := log.TestingLogger() | |||
// create switches | |||
for i := 0; i < N; i++ { | |||
switches[i] = p2p.MakeSwitch(cfg, i, "testing", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { | |||
books[i] = NewAddrBook(filepath.Join(dir, fmt.Sprintf("addrbook%d.json", i)), false) | |||
books[i].SetLogger(logger.With("pex", i)) | |||
sw.SetAddrBook(books[i]) | |||
sw.SetLogger(logger.With("pex", i)) | |||
config := &ReactorConfig{} | |||
if i == 0 { | |||
// first one is a seed node | |||
config = &ReactorConfig{SeedMode: true} | |||
} | |||
r := NewReactor(books[i], config) | |||
r.SetLogger(logger.With("pex", i)) | |||
r.SetEnsurePeersPeriod(250 * time.Millisecond) | |||
sw.AddReactor("pex", r) | |||
return sw | |||
}, logger) | |||
} | |||
for _, sw := range switches { | |||
err := sw.Start() // start switch and reactors | |||
require.Nil(t, err) | |||
} | |||
reactor := switches[0].Reactors()["pex"].(*Reactor) | |||
peerID := switches[1].NodeInfo().ID() | |||
assert.NoError(t, switches[1].DialPeerWithAddress(switches[0].NetAddress())) | |||
// sleep up to a second while waiting for the peer to send us a message. | |||
// this isn't perfect since it's possible the peer sends us a msg and we FlushStop | |||
// before this loop catches it. but non-deterministically it works pretty well. | |||
for i := 0; i < 1000; i++ { | |||
v := reactor.lastReceivedRequests.Get(string(peerID)) | |||
if v != nil { | |||
break | |||
} | |||
time.Sleep(time.Millisecond) | |||
} | |||
// by now the FlushStop should have happened. Try stopping the peer. | |||
// it should be safe to do this. | |||
peers := switches[0].Peers().List() | |||
for _, peer := range peers { | |||
err := peer.Stop() | |||
require.NoError(t, err) | |||
} | |||
// stop the switches | |||
for _, s := range switches { | |||
err := s.Stop() | |||
require.NoError(t, err) | |||
} | |||
} | |||
func TestPEXReactorDoesNotAddPrivatePeersToAddrBook(t *testing.T) { | |||
peer := p2p.CreateRandomPeer(false) | |||
pexR, book := createReactor(t, &ReactorConfig{}) | |||
book.AddPrivateIDs([]string{string(peer.NodeInfo().ID())}) | |||
// we have to send a request to receive responses | |||
pexR.RequestAddrs(peer) | |||
size := book.Size() | |||
na, err := peer.NodeInfo().NetAddress() | |||
require.NoError(t, err) | |||
msg := mustEncode(&tmp2p.PexResponse{Addresses: NetAddressesToProto([]*types.NetAddress{na})}) | |||
pexR.Receive(PexChannel, peer, msg) | |||
assert.Equal(t, size, book.Size()) | |||
pexR.AddPeer(peer) | |||
assert.Equal(t, size, book.Size()) | |||
} | |||
func TestPEXReactorDialPeer(t *testing.T) { | |||
pexR, book := createReactor(t, &ReactorConfig{}) | |||
sw := createSwitchAndAddReactors(pexR) | |||
sw.SetAddrBook(book) | |||
peer := mock.NewPeer(nil) | |||
addr := peer.SocketAddr() | |||
assert.Equal(t, 0, pexR.AttemptsToDial(addr)) | |||
// 1st unsuccessful attempt | |||
err := pexR.dialPeer(addr) | |||
require.Error(t, err) | |||
assert.Equal(t, 1, pexR.AttemptsToDial(addr)) | |||
// 2nd unsuccessful attempt | |||
err = pexR.dialPeer(addr) | |||
require.Error(t, err) | |||
// must be skipped because it is too early | |||
assert.Equal(t, 1, pexR.AttemptsToDial(addr)) | |||
if !testing.Short() { | |||
time.Sleep(3 * time.Second) | |||
// 3rd attempt | |||
err = pexR.dialPeer(addr) | |||
require.Error(t, err) | |||
assert.Equal(t, 2, pexR.AttemptsToDial(addr)) | |||
} | |||
} | |||
func assertPeersWithTimeout( | |||
t *testing.T, | |||
switches []*p2p.Switch, | |||
checkPeriod, timeout time.Duration, | |||
nPeers int, | |||
) { | |||
var ( | |||
ticker = time.NewTicker(checkPeriod) | |||
remaining = timeout | |||
) | |||
for { | |||
select { | |||
case <-ticker.C: | |||
// check peers are connected | |||
allGood := true | |||
for _, s := range switches { | |||
outbound, inbound, _ := s.NumPeers() | |||
if outbound+inbound < nPeers { | |||
allGood = false | |||
break | |||
} | |||
} | |||
remaining -= checkPeriod | |||
if remaining < 0 { | |||
remaining = 0 | |||
} | |||
if allGood { | |||
return | |||
} | |||
case <-time.After(remaining): | |||
numPeersStr := "" | |||
for i, s := range switches { | |||
outbound, inbound, _ := s.NumPeers() | |||
numPeersStr += fmt.Sprintf("%d => {outbound: %d, inbound: %d}, ", i, outbound, inbound) | |||
} | |||
t.Errorf( | |||
"expected all switches to be connected to at least %d peer(s) (switches: %s)", | |||
nPeers, numPeersStr, | |||
) | |||
return | |||
} | |||
} | |||
} | |||
// Creates a peer with the provided config | |||
func testCreatePeerWithConfig(dir string, id int, config *ReactorConfig) *p2p.Switch { | |||
peer := p2p.MakeSwitch( | |||
cfg, | |||
id, | |||
"127.0.0.1", | |||
"123.123.123", | |||
func(i int, sw *p2p.Switch) *p2p.Switch { | |||
book := NewAddrBook(filepath.Join(dir, fmt.Sprintf("addrbook%d.json", id)), false) | |||
book.SetLogger(log.TestingLogger()) | |||
sw.SetAddrBook(book) | |||
sw.SetLogger(log.TestingLogger()) | |||
r := NewReactor( | |||
book, | |||
config, | |||
) | |||
r.SetLogger(log.TestingLogger()) | |||
sw.AddReactor("pex", r) | |||
return sw | |||
}, | |||
log.TestingLogger(), | |||
) | |||
return peer | |||
} | |||
// Creates a peer with the default config | |||
func testCreateDefaultPeer(dir string, id int) *p2p.Switch { | |||
return testCreatePeerWithConfig(dir, id, &ReactorConfig{}) | |||
} | |||
// Creates a seed which knows about the provided addresses / source address pairs. | |||
// Starting and stopping the seed is left to the caller | |||
func testCreateSeed(dir string, id int, knownAddrs, srcAddrs []*p2p.NetAddress) *p2p.Switch { | |||
seed := p2p.MakeSwitch( | |||
cfg, | |||
id, | |||
"127.0.0.1", | |||
"123.123.123", | |||
func(i int, sw *p2p.Switch) *p2p.Switch { | |||
book := NewAddrBook(filepath.Join(dir, "addrbookSeed.json"), false) | |||
book.SetLogger(log.TestingLogger()) | |||
for j := 0; j < len(knownAddrs); j++ { | |||
book.AddAddress(knownAddrs[j], srcAddrs[j]) // nolint:errcheck // ignore for tests | |||
book.MarkGood(knownAddrs[j].ID) | |||
} | |||
sw.SetAddrBook(book) | |||
sw.SetLogger(log.TestingLogger()) | |||
r := NewReactor(book, &ReactorConfig{}) | |||
r.SetLogger(log.TestingLogger()) | |||
sw.AddReactor("pex", r) | |||
return sw | |||
}, | |||
log.TestingLogger(), | |||
) | |||
return seed | |||
} | |||
// Creates a peer which knows about the provided seed. | |||
// Starting and stopping the peer is left to the caller | |||
func testCreatePeerWithSeed(dir string, id int, seed *p2p.Switch) *p2p.Switch { | |||
conf := &ReactorConfig{ | |||
Seeds: []string{seed.NetAddress().String()}, | |||
} | |||
return testCreatePeerWithConfig(dir, id, conf) | |||
} | |||
func createReactor(t *testing.T, conf *ReactorConfig) (r *Reactor, book AddrBook) { | |||
// directory to store address book | |||
book = NewAddrBook(filepath.Join(tempDir(t), "addrbook.json"), true) | |||
book.SetLogger(log.TestingLogger()) | |||
r = NewReactor(book, conf) | |||
r.SetLogger(log.TestingLogger()) | |||
return | |||
} | |||
func createSwitchAndAddReactors(reactors ...p2p.Reactor) *p2p.Switch { | |||
sw := p2p.MakeSwitch(cfg, 0, "127.0.0.1", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { | |||
for _, r := range reactors { | |||
sw.AddReactor(r.String(), r) | |||
} | |||
return sw | |||
}, log.TestingLogger()) | |||
return sw | |||
} | |||
func TestPexVectors(t *testing.T) { | |||
addr := tmp2p.PexAddress{ | |||
ID: "1", | |||
IP: "127.0.0.1", | |||
Port: 9090, | |||
} | |||
testCases := []struct { | |||
testName string | |||
msg proto.Message | |||
expBytes string | |||
}{ | |||
{"PexRequest", &tmp2p.PexRequest{}, "0a00"}, | |||
{"PexAddrs", &tmp2p.PexResponse{Addresses: []tmp2p.PexAddress{addr}}, "12130a110a013112093132372e302e302e31188247"}, | |||
} | |||
for _, tc := range testCases { | |||
tc := tc | |||
bz := mustEncode(tc.msg) | |||
require.Equal(t, tc.expBytes, hex.EncodeToString(bz), tc.testName) | |||
} | |||
} | |||
// FIXME: This function is used in place of testing.TB.TempDir() | |||
// as the latter seems to cause test cases to fail when it is | |||
// unable to remove the temporary directory once the test case | |||
// execution terminates. This seems to happen often with pex | |||
// reactor test cases. | |||
// | |||
// References: | |||
// https://github.com/tendermint/tendermint/pull/5733 | |||
// https://github.com/tendermint/tendermint/issues/5732 | |||
func tempDir(t *testing.T) string { | |||
t.Helper() | |||
dir, err := ioutil.TempDir("", "") | |||
require.NoError(t, err) | |||
t.Cleanup(func() { _ = os.RemoveAll(dir) }) | |||
return dir | |||
} |
@ -1,207 +0,0 @@ | |||
package p2p_test | |||
import ( | |||
"sync" | |||
"testing" | |||
"github.com/gogo/protobuf/proto" | |||
"github.com/stretchr/testify/mock" | |||
"github.com/stretchr/testify/require" | |||
"github.com/tendermint/tendermint/config" | |||
"github.com/tendermint/tendermint/internal/p2p" | |||
p2pmocks "github.com/tendermint/tendermint/internal/p2p/mocks" | |||
"github.com/tendermint/tendermint/libs/log" | |||
ssproto "github.com/tendermint/tendermint/proto/tendermint/statesync" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
var ( | |||
channelID1 = byte(0x01) | |||
channelID2 = byte(0x02) | |||
p2pCfg = config.DefaultP2PConfig() | |||
testChannelShims = map[p2p.ChannelID]*p2p.ChannelDescriptorShim{ | |||
p2p.ChannelID(channelID1): { | |||
MsgType: new(ssproto.Message), | |||
Descriptor: &p2p.ChannelDescriptor{ | |||
ID: channelID1, | |||
Priority: 3, | |||
SendQueueCapacity: 10, | |||
RecvMessageCapacity: int(4e6), | |||
}, | |||
}, | |||
p2p.ChannelID(channelID2): { | |||
MsgType: new(ssproto.Message), | |||
Descriptor: &p2p.ChannelDescriptor{ | |||
ID: channelID2, | |||
Priority: 1, | |||
SendQueueCapacity: 4, | |||
RecvMessageCapacity: int(16e6), | |||
}, | |||
}, | |||
} | |||
) | |||
type reactorShimTestSuite struct { | |||
shim *p2p.ReactorShim | |||
sw *p2p.Switch | |||
} | |||
func setup(t *testing.T, peers []p2p.Peer) *reactorShimTestSuite { | |||
t.Helper() | |||
rts := &reactorShimTestSuite{ | |||
shim: p2p.NewReactorShim(log.TestingLogger(), "TestShim", testChannelShims), | |||
} | |||
rts.sw = p2p.MakeSwitch(p2pCfg, 1, "testing", "123.123.123", func(_ int, sw *p2p.Switch) *p2p.Switch { | |||
for _, peer := range peers { | |||
p2p.AddPeerToSwitchPeerSet(sw, peer) | |||
} | |||
sw.AddReactor(rts.shim.Name, rts.shim) | |||
return sw | |||
}, log.TestingLogger()) | |||
// start the reactor shim | |||
require.NoError(t, rts.shim.Start()) | |||
t.Cleanup(func() { | |||
require.NoError(t, rts.shim.Stop()) | |||
for _, chs := range rts.shim.Channels { | |||
chs.Channel.Close() | |||
} | |||
}) | |||
return rts | |||
} | |||
func simplePeer(t *testing.T, id string) (*p2pmocks.Peer, types.NodeID) { | |||
t.Helper() | |||
peerID := types.NodeID(id) | |||
peer := &p2pmocks.Peer{} | |||
peer.On("ID").Return(peerID) | |||
return peer, peerID | |||
} | |||
func TestReactorShim_GetChannel(t *testing.T) { | |||
rts := setup(t, nil) | |||
p2pCh := rts.shim.GetChannel(p2p.ChannelID(channelID1)) | |||
require.NotNil(t, p2pCh) | |||
require.Equal(t, p2pCh.ID, p2p.ChannelID(channelID1)) | |||
p2pCh = rts.shim.GetChannel(p2p.ChannelID(byte(0x03))) | |||
require.Nil(t, p2pCh) | |||
} | |||
func TestReactorShim_GetChannels(t *testing.T) { | |||
rts := setup(t, nil) | |||
p2pChs := rts.shim.GetChannels() | |||
require.Len(t, p2pChs, 2) | |||
require.Equal(t, p2p.ChannelID(p2pChs[0].ID), p2p.ChannelID(channelID1)) | |||
require.Equal(t, p2p.ChannelID(p2pChs[1].ID), p2p.ChannelID(channelID2)) | |||
} | |||
func TestReactorShim_AddPeer(t *testing.T) { | |||
peerA, peerIDA := simplePeer(t, "aa") | |||
rts := setup(t, []p2p.Peer{peerA}) | |||
var wg sync.WaitGroup | |||
wg.Add(1) | |||
var peerUpdate p2p.PeerUpdate | |||
go func() { | |||
peerUpdate = <-rts.shim.PeerUpdates.Updates() | |||
wg.Done() | |||
}() | |||
rts.shim.AddPeer(peerA) | |||
wg.Wait() | |||
require.Equal(t, peerIDA, peerUpdate.NodeID) | |||
require.Equal(t, p2p.PeerStatusUp, peerUpdate.Status) | |||
} | |||
func TestReactorShim_RemovePeer(t *testing.T) { | |||
peerA, peerIDA := simplePeer(t, "aa") | |||
rts := setup(t, []p2p.Peer{peerA}) | |||
var wg sync.WaitGroup | |||
wg.Add(1) | |||
var peerUpdate p2p.PeerUpdate | |||
go func() { | |||
peerUpdate = <-rts.shim.PeerUpdates.Updates() | |||
wg.Done() | |||
}() | |||
rts.shim.RemovePeer(peerA, "test reason") | |||
wg.Wait() | |||
require.Equal(t, peerIDA, peerUpdate.NodeID) | |||
require.Equal(t, p2p.PeerStatusDown, peerUpdate.Status) | |||
} | |||
func TestReactorShim_Receive(t *testing.T) { | |||
peerA, peerIDA := simplePeer(t, "aa") | |||
rts := setup(t, []p2p.Peer{peerA}) | |||
msg := &ssproto.Message{ | |||
Sum: &ssproto.Message_ChunkRequest{ | |||
ChunkRequest: &ssproto.ChunkRequest{Height: 1, Format: 1, Index: 1}, | |||
}, | |||
} | |||
bz, err := proto.Marshal(msg) | |||
require.NoError(t, err) | |||
var wg sync.WaitGroup | |||
var response *ssproto.Message | |||
peerA.On("Send", channelID1, mock.Anything).Run(func(args mock.Arguments) { | |||
m := &ssproto.Message{} | |||
require.NoError(t, proto.Unmarshal(args[1].([]byte), m)) | |||
response = m | |||
wg.Done() | |||
}).Return(true) | |||
p2pCh := rts.shim.Channels[p2p.ChannelID(channelID1)] | |||
wg.Add(2) | |||
// Simulate receiving the envelope in some real reactor and replying back with | |||
// the same envelope and then closing the Channel. | |||
go func() { | |||
e := <-p2pCh.Channel.In | |||
require.Equal(t, peerIDA, e.From) | |||
require.NotNil(t, e.Message) | |||
p2pCh.Channel.Out <- p2p.Envelope{To: e.From, Message: e.Message} | |||
p2pCh.Channel.Close() | |||
wg.Done() | |||
}() | |||
rts.shim.Receive(channelID1, peerA, bz) | |||
// wait until the mock peer called Send and we (fake) proxied the envelope | |||
wg.Wait() | |||
require.NotNil(t, response) | |||
m, err := response.Unwrap() | |||
require.NoError(t, err) | |||
require.Equal(t, msg.GetChunkRequest(), m) | |||
// Since p2pCh was closed in the simulated reactor above, calling Receive | |||
// should not block. | |||
rts.shim.Receive(channelID1, peerA, bz) | |||
require.Empty(t, p2pCh.Channel.In) | |||
peerA.AssertExpectations(t) | |||
} |
@ -1,932 +0,0 @@ | |||
package p2p | |||
import ( | |||
"bytes" | |||
"context" | |||
"errors" | |||
"fmt" | |||
"io/ioutil" | |||
"net" | |||
"net/http" | |||
"net/http/httptest" | |||
"regexp" | |||
"strconv" | |||
"sync/atomic" | |||
"testing" | |||
"time" | |||
"github.com/prometheus/client_golang/prometheus/promhttp" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
"github.com/tendermint/tendermint/config" | |||
"github.com/tendermint/tendermint/crypto/ed25519" | |||
tmsync "github.com/tendermint/tendermint/internal/libs/sync" | |||
"github.com/tendermint/tendermint/internal/p2p/conn" | |||
"github.com/tendermint/tendermint/libs/log" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
var ( | |||
cfg *config.P2PConfig | |||
ctx = context.Background() | |||
) | |||
func init() { | |||
cfg = config.DefaultP2PConfig() | |||
cfg.PexReactor = true | |||
cfg.AllowDuplicateIP = true | |||
} | |||
type PeerMessage struct { | |||
PeerID types.NodeID | |||
Bytes []byte | |||
Counter int | |||
} | |||
type TestReactor struct { | |||
BaseReactor | |||
mtx tmsync.Mutex | |||
channels []*conn.ChannelDescriptor | |||
logMessages bool | |||
msgsCounter int | |||
msgsReceived map[byte][]PeerMessage | |||
} | |||
func NewTestReactor(channels []*conn.ChannelDescriptor, logMessages bool) *TestReactor { | |||
tr := &TestReactor{ | |||
channels: channels, | |||
logMessages: logMessages, | |||
msgsReceived: make(map[byte][]PeerMessage), | |||
} | |||
tr.BaseReactor = *NewBaseReactor("TestReactor", tr) | |||
tr.SetLogger(log.TestingLogger()) | |||
return tr | |||
} | |||
func (tr *TestReactor) GetChannels() []*conn.ChannelDescriptor { | |||
return tr.channels | |||
} | |||
func (tr *TestReactor) AddPeer(peer Peer) {} | |||
func (tr *TestReactor) RemovePeer(peer Peer, reason interface{}) {} | |||
func (tr *TestReactor) Receive(chID byte, peer Peer, msgBytes []byte) { | |||
if tr.logMessages { | |||
tr.mtx.Lock() | |||
defer tr.mtx.Unlock() | |||
// fmt.Printf("Received: %X, %X\n", chID, msgBytes) | |||
tr.msgsReceived[chID] = append(tr.msgsReceived[chID], PeerMessage{peer.ID(), msgBytes, tr.msgsCounter}) | |||
tr.msgsCounter++ | |||
} | |||
} | |||
func (tr *TestReactor) getMsgs(chID byte) []PeerMessage { | |||
tr.mtx.Lock() | |||
defer tr.mtx.Unlock() | |||
return tr.msgsReceived[chID] | |||
} | |||
//----------------------------------------------------------------------------- | |||
// convenience method for creating two switches connected to each other. | |||
// XXX: note this uses net.Pipe and not a proper TCP conn | |||
func MakeSwitchPair(t testing.TB, initSwitch func(int, *Switch) *Switch) (*Switch, *Switch) { | |||
// Create two switches that will be interconnected. | |||
switches := MakeConnectedSwitches(cfg, 2, initSwitch, Connect2Switches) | |||
return switches[0], switches[1] | |||
} | |||
func initSwitchFunc(i int, sw *Switch) *Switch { | |||
sw.SetAddrBook(&AddrBookMock{ | |||
Addrs: make(map[string]struct{}), | |||
OurAddrs: make(map[string]struct{})}) | |||
// Make two reactors of two channels each | |||
sw.AddReactor("foo", NewTestReactor([]*conn.ChannelDescriptor{ | |||
{ID: byte(0x00), Priority: 10}, | |||
{ID: byte(0x01), Priority: 10}, | |||
}, true)) | |||
sw.AddReactor("bar", NewTestReactor([]*conn.ChannelDescriptor{ | |||
{ID: byte(0x02), Priority: 10}, | |||
{ID: byte(0x03), Priority: 10}, | |||
}, true)) | |||
return sw | |||
} | |||
func TestSwitches(t *testing.T) { | |||
s1, s2 := MakeSwitchPair(t, initSwitchFunc) | |||
t.Cleanup(func() { | |||
if err := s1.Stop(); err != nil { | |||
t.Error(err) | |||
} | |||
}) | |||
t.Cleanup(func() { | |||
if err := s2.Stop(); err != nil { | |||
t.Error(err) | |||
} | |||
}) | |||
if s1.Peers().Size() != 1 { | |||
t.Errorf("expected exactly 1 peer in s1, got %v", s1.Peers().Size()) | |||
} | |||
if s2.Peers().Size() != 1 { | |||
t.Errorf("expected exactly 1 peer in s2, got %v", s2.Peers().Size()) | |||
} | |||
// Lets send some messages | |||
ch0Msg := []byte("channel zero") | |||
ch1Msg := []byte("channel foo") | |||
ch2Msg := []byte("channel bar") | |||
s1.Broadcast(byte(0x00), ch0Msg) | |||
s1.Broadcast(byte(0x01), ch1Msg) | |||
s1.Broadcast(byte(0x02), ch2Msg) | |||
assertMsgReceivedWithTimeout(t, | |||
ch0Msg, | |||
byte(0x00), | |||
s2.Reactor("foo").(*TestReactor), 10*time.Millisecond, 5*time.Second) | |||
assertMsgReceivedWithTimeout(t, | |||
ch1Msg, | |||
byte(0x01), | |||
s2.Reactor("foo").(*TestReactor), 10*time.Millisecond, 5*time.Second) | |||
assertMsgReceivedWithTimeout(t, | |||
ch2Msg, | |||
byte(0x02), | |||
s2.Reactor("bar").(*TestReactor), 10*time.Millisecond, 5*time.Second) | |||
} | |||
func assertMsgReceivedWithTimeout( | |||
t *testing.T, | |||
msgBytes []byte, | |||
channel byte, | |||
reactor *TestReactor, | |||
checkPeriod, | |||
timeout time.Duration, | |||
) { | |||
ticker := time.NewTicker(checkPeriod) | |||
for { | |||
select { | |||
case <-ticker.C: | |||
msgs := reactor.getMsgs(channel) | |||
if len(msgs) > 0 { | |||
if !bytes.Equal(msgs[0].Bytes, msgBytes) { | |||
t.Fatalf("Unexpected message bytes. Wanted: %X, Got: %X", msgBytes, msgs[0].Bytes) | |||
} | |||
return | |||
} | |||
case <-time.After(timeout): | |||
t.Fatalf("Expected to have received 1 message in channel #%v, got zero", channel) | |||
} | |||
} | |||
} | |||
func TestSwitchFiltersOutItself(t *testing.T) { | |||
s1 := MakeSwitch(cfg, 1, "127.0.0.1", "123.123.123", initSwitchFunc, log.TestingLogger()) | |||
// simulate s1 having a public IP by creating a remote peer with the same ID | |||
rp := &remotePeer{PrivKey: s1.nodeKey.PrivKey, Config: cfg} | |||
rp.Start() | |||
// addr should be rejected in addPeer based on the same ID | |||
err := s1.DialPeerWithAddress(rp.Addr()) | |||
if assert.Error(t, err) { | |||
if err, ok := err.(ErrRejected); ok { | |||
if !err.IsSelf() { | |||
t.Errorf("expected self to be rejected") | |||
} | |||
} else { | |||
t.Errorf("expected ErrRejected") | |||
} | |||
} | |||
assert.True(t, s1.addrBook.OurAddress(rp.Addr())) | |||
assert.False(t, s1.addrBook.HasAddress(rp.Addr())) | |||
rp.Stop() | |||
assertNoPeersAfterTimeout(t, s1, 100*time.Millisecond) | |||
} | |||
func TestSwitchDialFailsOnIncompatiblePeer(t *testing.T) { | |||
s1 := MakeSwitch(cfg, 1, "127.0.0.1", "123.123.123", initSwitchFunc, log.TestingLogger()) | |||
ni := s1.NodeInfo() | |||
ni.Network = "network-a" | |||
s1.SetNodeInfo(ni) | |||
rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg, Network: "network-b"} | |||
rp.Start() | |||
defer rp.Stop() | |||
err := s1.DialPeerWithAddress(rp.Addr()) | |||
require.Error(t, err) | |||
errRejected, ok := err.(ErrRejected) | |||
require.True(t, ok, "expected error to be of type IsRejected") | |||
require.True(t, errRejected.IsIncompatible(), "expected error to be IsIncompatible") | |||
// remote peer should not have been added to the addressbook | |||
require.False(t, s1.addrBook.HasAddress(rp.Addr())) | |||
} | |||
func TestSwitchPeerFilter(t *testing.T) { | |||
var ( | |||
filters = []PeerFilterFunc{ | |||
func(_ IPeerSet, _ Peer) error { return nil }, | |||
func(_ IPeerSet, _ Peer) error { return fmt.Errorf("denied") }, | |||
func(_ IPeerSet, _ Peer) error { return nil }, | |||
} | |||
sw = MakeSwitch( | |||
cfg, | |||
1, | |||
"testing", | |||
"123.123.123", | |||
initSwitchFunc, | |||
log.TestingLogger(), | |||
SwitchPeerFilters(filters...), | |||
) | |||
) | |||
err := sw.Start() | |||
require.NoError(t, err) | |||
t.Cleanup(func() { | |||
if err := sw.Stop(); err != nil { | |||
t.Error(err) | |||
} | |||
}) | |||
// simulate remote peer | |||
rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} | |||
rp.Start() | |||
t.Cleanup(rp.Stop) | |||
c, err := sw.transport.Dial(ctx, NewEndpoint(rp.Addr())) | |||
if err != nil { | |||
t.Fatal(err) | |||
} | |||
peerInfo, _, err := c.Handshake(ctx, sw.nodeInfo, sw.nodeKey.PrivKey) | |||
if err != nil { | |||
t.Fatal(err) | |||
} | |||
p := newPeer( | |||
peerInfo, | |||
newPeerConn(true, false, c), | |||
sw.reactorsByCh, | |||
sw.StopPeerForError, | |||
) | |||
err = sw.addPeer(p) | |||
if err, ok := err.(ErrRejected); ok { | |||
if !err.IsFiltered() { | |||
t.Errorf("expected peer to be filtered") | |||
} | |||
} else { | |||
t.Errorf("expected ErrRejected") | |||
} | |||
} | |||
func TestSwitchPeerFilterTimeout(t *testing.T) { | |||
var ( | |||
filters = []PeerFilterFunc{ | |||
func(_ IPeerSet, _ Peer) error { | |||
time.Sleep(10 * time.Millisecond) | |||
return nil | |||
}, | |||
} | |||
sw = MakeSwitch( | |||
cfg, | |||
1, | |||
"testing", | |||
"123.123.123", | |||
initSwitchFunc, | |||
log.TestingLogger(), | |||
SwitchFilterTimeout(5*time.Millisecond), | |||
SwitchPeerFilters(filters...), | |||
) | |||
) | |||
err := sw.Start() | |||
require.NoError(t, err) | |||
t.Cleanup(func() { | |||
if err := sw.Stop(); err != nil { | |||
t.Log(err) | |||
} | |||
}) | |||
// simulate remote peer | |||
rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} | |||
rp.Start() | |||
defer rp.Stop() | |||
c, err := sw.transport.Dial(ctx, NewEndpoint(rp.Addr())) | |||
if err != nil { | |||
t.Fatal(err) | |||
} | |||
peerInfo, _, err := c.Handshake(ctx, sw.nodeInfo, sw.nodeKey.PrivKey) | |||
if err != nil { | |||
t.Fatal(err) | |||
} | |||
p := newPeer( | |||
peerInfo, | |||
newPeerConn(true, false, c), | |||
sw.reactorsByCh, | |||
sw.StopPeerForError, | |||
) | |||
err = sw.addPeer(p) | |||
if _, ok := err.(ErrFilterTimeout); !ok { | |||
t.Errorf("expected ErrFilterTimeout") | |||
} | |||
} | |||
func TestSwitchPeerFilterDuplicate(t *testing.T) { | |||
sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc, log.TestingLogger()) | |||
err := sw.Start() | |||
require.NoError(t, err) | |||
t.Cleanup(func() { | |||
if err := sw.Stop(); err != nil { | |||
t.Error(err) | |||
} | |||
}) | |||
// simulate remote peer | |||
rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} | |||
rp.Start() | |||
defer rp.Stop() | |||
c, err := sw.transport.Dial(ctx, NewEndpoint(rp.Addr())) | |||
if err != nil { | |||
t.Fatal(err) | |||
} | |||
peerInfo, _, err := c.Handshake(ctx, sw.nodeInfo, sw.nodeKey.PrivKey) | |||
if err != nil { | |||
t.Fatal(err) | |||
} | |||
p := newPeer( | |||
peerInfo, | |||
newPeerConn(true, false, c), | |||
sw.reactorsByCh, | |||
sw.StopPeerForError, | |||
) | |||
if err := sw.addPeer(p); err != nil { | |||
t.Fatal(err) | |||
} | |||
err = sw.addPeer(p) | |||
if errRej, ok := err.(ErrRejected); ok { | |||
if !errRej.IsDuplicate() { | |||
t.Errorf("expected peer to be duplicate. got %v", errRej) | |||
} | |||
} else { | |||
t.Errorf("expected ErrRejected, got %v", err) | |||
} | |||
} | |||
func assertNoPeersAfterTimeout(t *testing.T, sw *Switch, timeout time.Duration) { | |||
time.Sleep(timeout) | |||
if sw.Peers().Size() != 0 { | |||
t.Fatalf("Expected %v to not connect to some peers, got %d", sw, sw.Peers().Size()) | |||
} | |||
} | |||
func TestSwitchStopsNonPersistentPeerOnError(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc, log.TestingLogger()) | |||
err := sw.Start() | |||
if err != nil { | |||
t.Error(err) | |||
} | |||
t.Cleanup(func() { | |||
if err := sw.Stop(); err != nil { | |||
t.Error(err) | |||
} | |||
}) | |||
// simulate remote peer | |||
rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} | |||
rp.Start() | |||
defer rp.Stop() | |||
c, err := sw.transport.Dial(ctx, NewEndpoint(rp.Addr())) | |||
if err != nil { | |||
t.Fatal(err) | |||
} | |||
peerInfo, _, err := c.Handshake(ctx, sw.nodeInfo, sw.nodeKey.PrivKey) | |||
if err != nil { | |||
t.Fatal(err) | |||
} | |||
p := newPeer( | |||
peerInfo, | |||
newPeerConn(true, false, c), | |||
sw.reactorsByCh, | |||
sw.StopPeerForError, | |||
) | |||
err = sw.addPeer(p) | |||
require.Nil(err) | |||
require.NotNil(sw.Peers().Get(rp.ID())) | |||
// simulate failure by closing connection | |||
err = p.CloseConn() | |||
require.NoError(err) | |||
assertNoPeersAfterTimeout(t, sw, 100*time.Millisecond) | |||
assert.False(p.IsRunning()) | |||
} | |||
func TestSwitchStopPeerForError(t *testing.T) { | |||
s := httptest.NewServer(promhttp.Handler()) | |||
defer s.Close() | |||
scrapeMetrics := func() string { | |||
resp, err := http.Get(s.URL) | |||
require.NoError(t, err) | |||
defer resp.Body.Close() | |||
buf, _ := ioutil.ReadAll(resp.Body) | |||
return string(buf) | |||
} | |||
namespace, subsystem, name := config.TestInstrumentationConfig().Namespace, MetricsSubsystem, "peers" | |||
re := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + ` ([0-9\.]+)`) | |||
peersMetricValue := func() float64 { | |||
matches := re.FindStringSubmatch(scrapeMetrics()) | |||
f, _ := strconv.ParseFloat(matches[1], 64) | |||
return f | |||
} | |||
p2pMetrics := PrometheusMetrics(namespace) | |||
// make two connected switches | |||
sw1, sw2 := MakeSwitchPair(t, func(i int, sw *Switch) *Switch { | |||
// set metrics on sw1 | |||
if i == 0 { | |||
opt := WithMetrics(p2pMetrics) | |||
opt(sw) | |||
} | |||
return initSwitchFunc(i, sw) | |||
}) | |||
assert.Equal(t, len(sw1.Peers().List()), 1) | |||
assert.EqualValues(t, 1, peersMetricValue()) | |||
// send messages to the peer from sw1 | |||
p := sw1.Peers().List()[0] | |||
p.Send(0x1, []byte("here's a message to send")) | |||
// stop sw2. this should cause the p to fail, | |||
// which results in calling StopPeerForError internally | |||
t.Cleanup(func() { | |||
if err := sw2.Stop(); err != nil { | |||
t.Error(err) | |||
} | |||
}) | |||
// now call StopPeerForError explicitly, eg. from a reactor | |||
sw1.StopPeerForError(p, fmt.Errorf("some err")) | |||
assert.Equal(t, len(sw1.Peers().List()), 0) | |||
assert.EqualValues(t, 0, peersMetricValue()) | |||
} | |||
func TestSwitchReconnectsToOutboundPersistentPeer(t *testing.T) { | |||
sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc, log.TestingLogger()) | |||
err := sw.Start() | |||
require.NoError(t, err) | |||
t.Cleanup(func() { | |||
if err := sw.Stop(); err != nil { | |||
t.Error(err) | |||
} | |||
}) | |||
// 1. simulate failure by closing connection | |||
rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} | |||
rp.Start() | |||
defer rp.Stop() | |||
err = sw.AddPersistentPeers([]string{rp.Addr().String()}) | |||
require.NoError(t, err) | |||
err = sw.DialPeerWithAddress(rp.Addr()) | |||
require.Nil(t, err) | |||
require.NotNil(t, sw.Peers().Get(rp.ID())) | |||
p := sw.Peers().List()[0] | |||
err = p.(*peer).CloseConn() | |||
require.NoError(t, err) | |||
waitUntilSwitchHasAtLeastNPeers(sw, 1) | |||
assert.False(t, p.IsRunning()) // old peer instance | |||
assert.Equal(t, 1, sw.Peers().Size()) // new peer instance | |||
// 2. simulate first time dial failure | |||
rp = &remotePeer{ | |||
PrivKey: ed25519.GenPrivKey(), | |||
Config: cfg, | |||
// Use different interface to prevent duplicate IP filter, this will break | |||
// beyond two peers. | |||
listenAddr: "127.0.0.1:0", | |||
} | |||
rp.Start() | |||
defer rp.Stop() | |||
conf := config.DefaultP2PConfig() | |||
conf.TestDialFail = true // will trigger a reconnect | |||
err = sw.addOutboundPeerWithConfig(rp.Addr(), conf) | |||
require.NotNil(t, err) | |||
// DialPeerWithAddres - sw.peerConfig resets the dialer | |||
waitUntilSwitchHasAtLeastNPeers(sw, 2) | |||
assert.Equal(t, 2, sw.Peers().Size()) | |||
} | |||
func TestSwitchReconnectsToInboundPersistentPeer(t *testing.T) { | |||
sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc, log.TestingLogger()) | |||
err := sw.Start() | |||
require.NoError(t, err) | |||
t.Cleanup(func() { | |||
if err := sw.Stop(); err != nil { | |||
t.Error(err) | |||
} | |||
}) | |||
// 1. simulate failure by closing the connection | |||
rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} | |||
rp.Start() | |||
defer rp.Stop() | |||
err = sw.AddPersistentPeers([]string{rp.Addr().String()}) | |||
require.NoError(t, err) | |||
conn, err := rp.Dial(sw.NetAddress()) | |||
require.NoError(t, err) | |||
time.Sleep(50 * time.Millisecond) | |||
require.NotNil(t, sw.Peers().Get(rp.ID())) | |||
conn.Close() | |||
waitUntilSwitchHasAtLeastNPeers(sw, 1) | |||
assert.Equal(t, 1, sw.Peers().Size()) | |||
} | |||
func TestSwitchDialPeersAsync(t *testing.T) { | |||
if testing.Short() { | |||
return | |||
} | |||
sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc, log.TestingLogger()) | |||
err := sw.Start() | |||
require.NoError(t, err) | |||
t.Cleanup(func() { | |||
if err := sw.Stop(); err != nil { | |||
t.Error(err) | |||
} | |||
}) | |||
rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} | |||
rp.Start() | |||
defer rp.Stop() | |||
err = sw.DialPeersAsync([]string{rp.Addr().String()}) | |||
require.NoError(t, err) | |||
time.Sleep(dialRandomizerIntervalMilliseconds * time.Millisecond) | |||
require.NotNil(t, sw.Peers().Get(rp.ID())) | |||
} | |||
func waitUntilSwitchHasAtLeastNPeers(sw *Switch, n int) { | |||
for i := 0; i < 20; i++ { | |||
time.Sleep(250 * time.Millisecond) | |||
has := sw.Peers().Size() | |||
if has >= n { | |||
break | |||
} | |||
} | |||
} | |||
func TestSwitchFullConnectivity(t *testing.T) { | |||
switches := MakeConnectedSwitches(cfg, 3, initSwitchFunc, Connect2Switches) | |||
defer func() { | |||
for _, sw := range switches { | |||
sw := sw | |||
t.Cleanup(func() { | |||
if err := sw.Stop(); err != nil { | |||
t.Error(err) | |||
} | |||
}) | |||
} | |||
}() | |||
for i, sw := range switches { | |||
if sw.Peers().Size() != 2 { | |||
t.Fatalf("Expected each switch to be connected to 2 other, but %d switch only connected to %d", sw.Peers().Size(), i) | |||
} | |||
} | |||
} | |||
func TestSwitchAcceptRoutine(t *testing.T) { | |||
cfg.MaxNumInboundPeers = 5 | |||
// Create some unconditional peers. | |||
const unconditionalPeersNum = 2 | |||
var ( | |||
unconditionalPeers = make([]*remotePeer, unconditionalPeersNum) | |||
unconditionalPeerIDs = make([]string, unconditionalPeersNum) | |||
) | |||
for i := 0; i < unconditionalPeersNum; i++ { | |||
peer := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} | |||
peer.Start() | |||
unconditionalPeers[i] = peer | |||
unconditionalPeerIDs[i] = string(peer.ID()) | |||
} | |||
// make switch | |||
sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc, log.TestingLogger()) | |||
err := sw.AddUnconditionalPeerIDs(unconditionalPeerIDs) | |||
require.NoError(t, err) | |||
err = sw.Start() | |||
require.NoError(t, err) | |||
t.Cleanup(func() { | |||
err := sw.Stop() | |||
require.NoError(t, err) | |||
}) | |||
// 0. check there are no peers | |||
assert.Equal(t, 0, sw.Peers().Size()) | |||
// 1. check we connect up to MaxNumInboundPeers | |||
peers := make([]*remotePeer, 0) | |||
for i := 0; i < cfg.MaxNumInboundPeers; i++ { | |||
peer := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} | |||
peers = append(peers, peer) | |||
peer.Start() | |||
c, err := peer.Dial(sw.NetAddress()) | |||
require.NoError(t, err) | |||
// spawn a reading routine to prevent connection from closing | |||
go func(c net.Conn) { | |||
for { | |||
one := make([]byte, 1) | |||
_, err := c.Read(one) | |||
if err != nil { | |||
return | |||
} | |||
} | |||
}(c) | |||
} | |||
time.Sleep(100 * time.Millisecond) | |||
assert.Equal(t, cfg.MaxNumInboundPeers, sw.Peers().Size()) | |||
// 2. check we close new connections if we already have MaxNumInboundPeers peers | |||
peer := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} | |||
peer.Start() | |||
conn, err := peer.Dial(sw.NetAddress()) | |||
require.NoError(t, err) | |||
// check conn is closed | |||
one := make([]byte, 1) | |||
_ = conn.SetReadDeadline(time.Now().Add(10 * time.Millisecond)) | |||
_, err = conn.Read(one) | |||
assert.Error(t, err) | |||
assert.Equal(t, cfg.MaxNumInboundPeers, sw.Peers().Size()) | |||
peer.Stop() | |||
// 3. check we connect to unconditional peers despite the limit. | |||
for _, peer := range unconditionalPeers { | |||
c, err := peer.Dial(sw.NetAddress()) | |||
require.NoError(t, err) | |||
// spawn a reading routine to prevent connection from closing | |||
go func(c net.Conn) { | |||
for { | |||
one := make([]byte, 1) | |||
_, err := c.Read(one) | |||
if err != nil { | |||
return | |||
} | |||
} | |||
}(c) | |||
} | |||
time.Sleep(10 * time.Millisecond) | |||
assert.Equal(t, cfg.MaxNumInboundPeers+unconditionalPeersNum, sw.Peers().Size()) | |||
for _, peer := range peers { | |||
peer.Stop() | |||
} | |||
for _, peer := range unconditionalPeers { | |||
peer.Stop() | |||
} | |||
} | |||
func TestSwitchRejectsIncompatiblePeers(t *testing.T) { | |||
sw := MakeSwitch(cfg, 1, "127.0.0.1", "123.123.123", initSwitchFunc, log.TestingLogger()) | |||
ni := sw.NodeInfo() | |||
ni.Network = "network-a" | |||
sw.SetNodeInfo(ni) | |||
err := sw.Start() | |||
require.NoError(t, err) | |||
t.Cleanup(func() { | |||
err := sw.Stop() | |||
require.NoError(t, err) | |||
}) | |||
rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg, Network: "network-b"} | |||
rp.Start() | |||
defer rp.Stop() | |||
assert.Equal(t, 0, sw.Peers().Size()) | |||
conn, err := rp.Dial(sw.NetAddress()) | |||
assert.Nil(t, err) | |||
one := make([]byte, 1) | |||
_ = conn.SetReadDeadline(time.Now().Add(10 * time.Millisecond)) | |||
_, err = conn.Read(one) | |||
assert.Error(t, err) | |||
assert.Equal(t, 0, sw.Peers().Size()) | |||
} | |||
type errorTransport struct { | |||
acceptErr error | |||
} | |||
func (et errorTransport) String() string { | |||
return "error" | |||
} | |||
func (et errorTransport) Protocols() []Protocol { | |||
return []Protocol{"error"} | |||
} | |||
func (et errorTransport) Accept() (Connection, error) { | |||
return nil, et.acceptErr | |||
} | |||
func (errorTransport) Dial(context.Context, Endpoint) (Connection, error) { | |||
panic("not implemented") | |||
} | |||
func (errorTransport) Close() error { panic("not implemented") } | |||
func (errorTransport) FlushClose() error { panic("not implemented") } | |||
func (errorTransport) Endpoints() []Endpoint { panic("not implemented") } | |||
func TestSwitchAcceptRoutineErrorCases(t *testing.T) { | |||
sw := NewSwitch(cfg, errorTransport{ErrFilterTimeout{}}) | |||
assert.NotPanics(t, func() { | |||
err := sw.Start() | |||
require.NoError(t, err) | |||
err = sw.Stop() | |||
require.NoError(t, err) | |||
}) | |||
sw = NewSwitch(cfg, errorTransport{ErrRejected{conn: nil, err: errors.New("filtered"), isFiltered: true}}) | |||
assert.NotPanics(t, func() { | |||
err := sw.Start() | |||
require.NoError(t, err) | |||
err = sw.Stop() | |||
require.NoError(t, err) | |||
}) | |||
// TODO(melekes) check we remove our address from addrBook | |||
sw = NewSwitch(cfg, errorTransport{ErrTransportClosed{}}) | |||
assert.NotPanics(t, func() { | |||
err := sw.Start() | |||
require.NoError(t, err) | |||
err = sw.Stop() | |||
require.NoError(t, err) | |||
}) | |||
} | |||
// mockReactor checks that InitPeer never called before RemovePeer. If that's | |||
// not true, InitCalledBeforeRemoveFinished will return true. | |||
type mockReactor struct { | |||
*BaseReactor | |||
// atomic | |||
removePeerInProgress uint32 | |||
initCalledBeforeRemoveFinished uint32 | |||
} | |||
func (r *mockReactor) GetChannels() []*ChannelDescriptor { | |||
return []*ChannelDescriptor{{ID: testCh, Priority: 10}} | |||
} | |||
func (r *mockReactor) RemovePeer(peer Peer, reason interface{}) { | |||
atomic.StoreUint32(&r.removePeerInProgress, 1) | |||
defer atomic.StoreUint32(&r.removePeerInProgress, 0) | |||
time.Sleep(100 * time.Millisecond) | |||
} | |||
func (r *mockReactor) InitPeer(peer Peer) Peer { | |||
if atomic.LoadUint32(&r.removePeerInProgress) == 1 { | |||
atomic.StoreUint32(&r.initCalledBeforeRemoveFinished, 1) | |||
} | |||
return peer | |||
} | |||
func (r *mockReactor) InitCalledBeforeRemoveFinished() bool { | |||
return atomic.LoadUint32(&r.initCalledBeforeRemoveFinished) == 1 | |||
} | |||
// see stopAndRemovePeer | |||
func TestSwitchInitPeerIsNotCalledBeforeRemovePeer(t *testing.T) { | |||
// make reactor | |||
reactor := &mockReactor{} | |||
reactor.BaseReactor = NewBaseReactor("mockReactor", reactor) | |||
// make switch | |||
sw := MakeSwitch(cfg, 1, "testing", "123.123.123", func(i int, sw *Switch) *Switch { | |||
sw.AddReactor("mock", reactor) | |||
return sw | |||
}, log.TestingLogger()) | |||
err := sw.Start() | |||
require.NoError(t, err) | |||
t.Cleanup(func() { | |||
if err := sw.Stop(); err != nil { | |||
t.Error(err) | |||
} | |||
}) | |||
// add peer | |||
rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} | |||
rp.Start() | |||
defer rp.Stop() | |||
_, err = rp.Dial(sw.NetAddress()) | |||
require.NoError(t, err) | |||
// wait till the switch adds rp to the peer set, then stop the peer asynchronously | |||
for { | |||
time.Sleep(20 * time.Millisecond) | |||
if peer := sw.Peers().Get(rp.ID()); peer != nil { | |||
go sw.StopPeerForError(peer, "test") | |||
break | |||
} | |||
} | |||
// simulate peer reconnecting to us | |||
_, err = rp.Dial(sw.NetAddress()) | |||
require.NoError(t, err) | |||
// wait till the switch adds rp to the peer set | |||
time.Sleep(50 * time.Millisecond) | |||
// make sure reactor.RemovePeer is finished before InitPeer is called | |||
assert.False(t, reactor.InitCalledBeforeRemoveFinished()) | |||
} | |||
func BenchmarkSwitchBroadcast(b *testing.B) { | |||
s1, s2 := MakeSwitchPair(b, func(i int, sw *Switch) *Switch { | |||
// Make bar reactors of bar channels each | |||
sw.AddReactor("foo", NewTestReactor([]*conn.ChannelDescriptor{ | |||
{ID: byte(0x00), Priority: 10}, | |||
{ID: byte(0x01), Priority: 10}, | |||
}, false)) | |||
sw.AddReactor("bar", NewTestReactor([]*conn.ChannelDescriptor{ | |||
{ID: byte(0x02), Priority: 10}, | |||
{ID: byte(0x03), Priority: 10}, | |||
}, false)) | |||
return sw | |||
}) | |||
b.Cleanup(func() { | |||
if err := s1.Stop(); err != nil { | |||
b.Error(err) | |||
} | |||
}) | |||
b.Cleanup(func() { | |||
if err := s2.Stop(); err != nil { | |||
b.Error(err) | |||
} | |||
}) | |||
// Allow time for goroutines to boot up | |||
time.Sleep(1 * time.Second) | |||
b.ResetTimer() | |||
numSuccess, numFailure := 0, 0 | |||
// Send random message from foo channel to another | |||
for i := 0; i < b.N; i++ { | |||
chID := byte(i % 4) | |||
successChan := s1.Broadcast(chID, []byte("test data")) | |||
for s := range successChan { | |||
if s { | |||
numSuccess++ | |||
} else { | |||
numFailure++ | |||
} | |||
} | |||
} | |||
b.Logf("success: %v, failure: %v", numSuccess, numFailure) | |||
} | |||
func TestNewNetAddressStrings(t *testing.T) { | |||
addrs, errs := NewNetAddressStrings([]string{ | |||
"127.0.0.1:8080", | |||
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", | |||
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeed@127.0.0.2:8080"}) | |||
assert.Len(t, errs, 1) | |||
assert.Equal(t, 2, len(addrs)) | |||
} |
@ -1,89 +0,0 @@ | |||
package core | |||
import ( | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
"github.com/tendermint/tendermint/config" | |||
"github.com/tendermint/tendermint/internal/p2p" | |||
"github.com/tendermint/tendermint/libs/log" | |||
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types" | |||
) | |||
func TestUnsafeDialSeeds(t *testing.T) { | |||
sw := p2p.MakeSwitch(config.DefaultP2PConfig(), 1, "testing", "123.123.123", | |||
func(n int, sw *p2p.Switch) *p2p.Switch { return sw }, log.TestingLogger()) | |||
err := sw.Start() | |||
require.NoError(t, err) | |||
t.Cleanup(func() { | |||
if err := sw.Stop(); err != nil { | |||
t.Error(err) | |||
} | |||
}) | |||
env := &Environment{} | |||
env.Logger = log.TestingLogger() | |||
env.P2PPeers = sw | |||
testCases := []struct { | |||
seeds []string | |||
isErr bool | |||
}{ | |||
{[]string{}, true}, | |||
{[]string{"d51fb70907db1c6c2d5237e78379b25cf1a37ab4@127.0.0.1:41198"}, false}, | |||
{[]string{"127.0.0.1:41198"}, true}, | |||
} | |||
for _, tc := range testCases { | |||
res, err := env.UnsafeDialSeeds(&rpctypes.Context{}, tc.seeds) | |||
if tc.isErr { | |||
assert.Error(t, err) | |||
} else { | |||
assert.NoError(t, err) | |||
assert.NotNil(t, res) | |||
} | |||
} | |||
} | |||
func TestUnsafeDialPeers(t *testing.T) { | |||
sw := p2p.MakeSwitch(config.DefaultP2PConfig(), 1, "testing", "123.123.123", | |||
func(n int, sw *p2p.Switch) *p2p.Switch { return sw }, log.TestingLogger()) | |||
sw.SetAddrBook(&p2p.AddrBookMock{ | |||
Addrs: make(map[string]struct{}), | |||
OurAddrs: make(map[string]struct{}), | |||
PrivateAddrs: make(map[string]struct{}), | |||
}) | |||
err := sw.Start() | |||
require.NoError(t, err) | |||
t.Cleanup(func() { | |||
if err := sw.Stop(); err != nil { | |||
t.Error(err) | |||
} | |||
}) | |||
env := &Environment{} | |||
env.Logger = log.TestingLogger() | |||
env.P2PPeers = sw | |||
testCases := []struct { | |||
peers []string | |||
persistence, unconditional, private bool | |||
isErr bool | |||
}{ | |||
{[]string{}, false, false, false, true}, | |||
{[]string{"d51fb70907db1c6c2d5237e78379b25cf1a37ab4@127.0.0.1:41198"}, true, true, true, false}, | |||
{[]string{"127.0.0.1:41198"}, true, true, false, true}, | |||
} | |||
for _, tc := range testCases { | |||
res, err := env.UnsafeDialPeers(&rpctypes.Context{}, tc.peers, tc.persistence, tc.unconditional, tc.private) | |||
if tc.isErr { | |||
assert.Error(t, err) | |||
} else { | |||
assert.NoError(t, err) | |||
assert.NotNil(t, res) | |||
} | |||
} | |||
} |
@ -1,35 +0,0 @@ | |||
// nolint: gosec | |||
package addrbook | |||
import ( | |||
"encoding/json" | |||
"fmt" | |||
"math/rand" | |||
"github.com/tendermint/tendermint/internal/p2p" | |||
"github.com/tendermint/tendermint/internal/p2p/pex" | |||
) | |||
var addrBook = pex.NewAddrBook("./testdata/addrbook.json", true) | |||
func Fuzz(data []byte) int { | |||
addr := new(p2p.NetAddress) | |||
if err := json.Unmarshal(data, addr); err != nil { | |||
return -1 | |||
} | |||
// Fuzz AddAddress. | |||
err := addrBook.AddAddress(addr, addr) | |||
if err != nil { | |||
return 0 | |||
} | |||
// Also, make sure PickAddress always returns a non-nil address. | |||
bias := rand.Intn(100) | |||
if p := addrBook.PickAddress(bias); p == nil { | |||
panic(fmt.Sprintf("picked a nil address (bias: %d, addrBook size: %v)", | |||
bias, addrBook.Size())) | |||
} | |||
return 1 | |||
} |
@ -1,33 +0,0 @@ | |||
package addrbook_test | |||
import ( | |||
"io/ioutil" | |||
"os" | |||
"path/filepath" | |||
"testing" | |||
"github.com/stretchr/testify/require" | |||
"github.com/tendermint/tendermint/test/fuzz/p2p/addrbook" | |||
) | |||
const testdataCasesDir = "testdata/cases" | |||
func TestAddrbookTestdataCases(t *testing.T) { | |||
entries, err := os.ReadDir(testdataCasesDir) | |||
require.NoError(t, err) | |||
for _, e := range entries { | |||
entry := e | |||
t.Run(entry.Name(), func(t *testing.T) { | |||
defer func() { | |||
r := recover() | |||
require.Nilf(t, r, "testdata/cases test panic") | |||
}() | |||
f, err := os.Open(filepath.Join(testdataCasesDir, entry.Name())) | |||
require.NoError(t, err) | |||
input, err := ioutil.ReadAll(f) | |||
require.NoError(t, err) | |||
addrbook.Fuzz(input) | |||
}) | |||
} | |||
} |
@ -1,59 +0,0 @@ | |||
// nolint: gosec | |||
package main | |||
import ( | |||
"encoding/json" | |||
"flag" | |||
"fmt" | |||
"io/ioutil" | |||
"log" | |||
"net" | |||
"os" | |||
"path/filepath" | |||
"github.com/tendermint/tendermint/crypto/ed25519" | |||
"github.com/tendermint/tendermint/internal/p2p" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
func main() { | |||
baseDir := flag.String("base", ".", `where the "corpus" directory will live`) | |||
flag.Parse() | |||
initCorpus(*baseDir) | |||
} | |||
func initCorpus(baseDir string) { | |||
log.SetFlags(0) | |||
// create "corpus" directory | |||
corpusDir := filepath.Join(baseDir, "corpus") | |||
if err := os.MkdirAll(corpusDir, 0755); err != nil { | |||
log.Fatalf("Creating %q err: %v", corpusDir, err) | |||
} | |||
// create corpus | |||
privKey := ed25519.GenPrivKey() | |||
addrs := []*p2p.NetAddress{ | |||
{ID: types.NodeIDFromPubKey(privKey.PubKey()), IP: net.IPv4(0, 0, 0, 0), Port: 0}, | |||
{ID: types.NodeIDFromPubKey(privKey.PubKey()), IP: net.IPv4(127, 0, 0, 0), Port: 80}, | |||
{ID: types.NodeIDFromPubKey(privKey.PubKey()), IP: net.IPv4(213, 87, 10, 200), Port: 8808}, | |||
{ID: types.NodeIDFromPubKey(privKey.PubKey()), IP: net.IPv4(111, 111, 111, 111), Port: 26656}, | |||
{ID: types.NodeIDFromPubKey(privKey.PubKey()), IP: net.ParseIP("2001:db8::68"), Port: 26656}, | |||
} | |||
for i, addr := range addrs { | |||
filename := filepath.Join(corpusDir, fmt.Sprintf("%d.json", i)) | |||
bz, err := json.Marshal(addr) | |||
if err != nil { | |||
log.Fatalf("can't marshal %v: %v", addr, err) | |||
} | |||
if err := ioutil.WriteFile(filename, bz, 0644); err != nil { | |||
log.Fatalf("can't write %v to %q: %v", addr, filename, err) | |||
} | |||
log.Printf("wrote %q", filename) | |||
} | |||
} |
@ -1,33 +0,0 @@ | |||
package pex_test | |||
import ( | |||
"io/ioutil" | |||
"os" | |||
"path/filepath" | |||
"testing" | |||
"github.com/stretchr/testify/require" | |||
"github.com/tendermint/tendermint/test/fuzz/p2p/pex" | |||
) | |||
const testdataCasesDir = "testdata/cases" | |||
func TestPexTestdataCases(t *testing.T) { | |||
entries, err := os.ReadDir(testdataCasesDir) | |||
require.NoError(t, err) | |||
for _, e := range entries { | |||
entry := e | |||
t.Run(entry.Name(), func(t *testing.T) { | |||
defer func() { | |||
r := recover() | |||
require.Nilf(t, r, "testdata/cases test panic") | |||
}() | |||
f, err := os.Open(filepath.Join(testdataCasesDir, entry.Name())) | |||
require.NoError(t, err) | |||
input, err := ioutil.ReadAll(f) | |||
require.NoError(t, err) | |||
pex.Fuzz(input) | |||
}) | |||
} | |||
} |
@ -1,84 +0,0 @@ | |||
// nolint: gosec | |||
package main | |||
import ( | |||
"flag" | |||
"fmt" | |||
"io/ioutil" | |||
"log" | |||
"math/rand" | |||
"os" | |||
"path/filepath" | |||
"github.com/tendermint/tendermint/crypto/ed25519" | |||
"github.com/tendermint/tendermint/internal/p2p" | |||
"github.com/tendermint/tendermint/internal/p2p/pex" | |||
tmp2p "github.com/tendermint/tendermint/proto/tendermint/p2p" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
func main() { | |||
baseDir := flag.String("base", ".", `where the "corpus" directory will live`) | |||
flag.Parse() | |||
initCorpus(*baseDir) | |||
} | |||
func initCorpus(rootDir string) { | |||
log.SetFlags(0) | |||
corpusDir := filepath.Join(rootDir, "corpus") | |||
if err := os.MkdirAll(corpusDir, 0755); err != nil { | |||
log.Fatalf("Creating %q err: %v", corpusDir, err) | |||
} | |||
sizes := []int{0, 1, 2, 17, 5, 31} | |||
// Make the PRNG predictable | |||
rand.Seed(10) | |||
for _, n := range sizes { | |||
var addrs []*p2p.NetAddress | |||
// IPv4 addresses | |||
for i := 0; i < n; i++ { | |||
privKey := ed25519.GenPrivKey() | |||
addr := fmt.Sprintf( | |||
"%s@%v.%v.%v.%v:26656", | |||
types.NodeIDFromPubKey(privKey.PubKey()), | |||
rand.Int()%256, | |||
rand.Int()%256, | |||
rand.Int()%256, | |||
rand.Int()%256, | |||
) | |||
netAddr, _ := types.NewNetAddressString(addr) | |||
addrs = append(addrs, netAddr) | |||
} | |||
// IPv6 addresses | |||
privKey := ed25519.GenPrivKey() | |||
ipv6a, err := types.NewNetAddressString( | |||
fmt.Sprintf("%s@[ff02::1:114]:26656", types.NodeIDFromPubKey(privKey.PubKey()))) | |||
if err != nil { | |||
log.Fatalf("can't create a new netaddress: %v", err) | |||
} | |||
addrs = append(addrs, ipv6a) | |||
msg := tmp2p.PexMessage{ | |||
Sum: &tmp2p.PexMessage_PexResponse{ | |||
PexResponse: &tmp2p.PexResponse{Addresses: pex.NetAddressesToProto(addrs)}, | |||
}, | |||
} | |||
bz, err := msg.Marshal() | |||
if err != nil { | |||
log.Fatalf("unable to marshal: %v", err) | |||
} | |||
filename := filepath.Join(rootDir, "corpus", fmt.Sprintf("%d", n)) | |||
if err := ioutil.WriteFile(filename, bz, 0644); err != nil { | |||
log.Fatalf("can't write %X to %q: %v", bz, filename, err) | |||
} | |||
log.Printf("wrote %q", filename) | |||
} | |||
} |
@ -1,95 +0,0 @@ | |||
package pex | |||
import ( | |||
"net" | |||
"github.com/tendermint/tendermint/config" | |||
"github.com/tendermint/tendermint/crypto/ed25519" | |||
"github.com/tendermint/tendermint/internal/p2p" | |||
"github.com/tendermint/tendermint/internal/p2p/pex" | |||
"github.com/tendermint/tendermint/libs/log" | |||
"github.com/tendermint/tendermint/libs/service" | |||
"github.com/tendermint/tendermint/types" | |||
"github.com/tendermint/tendermint/version" | |||
) | |||
var ( | |||
pexR *pex.Reactor | |||
peer p2p.Peer | |||
logger = log.MustNewDefaultLogger(log.LogFormatPlain, log.LogLevelInfo, false) | |||
) | |||
func init() { | |||
addrB := pex.NewAddrBook("./testdata/addrbook1", false) | |||
pexR = pex.NewReactor(addrB, &pex.ReactorConfig{SeedMode: false}) | |||
pexR.SetLogger(logger) | |||
peer = newFuzzPeer() | |||
pexR.AddPeer(peer) | |||
cfg := config.DefaultP2PConfig() | |||
cfg.PexReactor = true | |||
sw := p2p.MakeSwitch(cfg, 0, "127.0.0.1", "123.123.123", func(i int, sw *p2p.Switch) *p2p.Switch { | |||
return sw | |||
}, logger) | |||
pexR.SetSwitch(sw) | |||
} | |||
func Fuzz(data []byte) int { | |||
if len(data) == 0 { | |||
return -1 | |||
} | |||
pexR.Receive(pex.PexChannel, peer, data) | |||
if !peer.IsRunning() { | |||
// do not increase priority for msgs which lead to peer being stopped | |||
return 0 | |||
} | |||
return 1 | |||
} | |||
type fuzzPeer struct { | |||
*service.BaseService | |||
m map[string]interface{} | |||
} | |||
var _ p2p.Peer = (*fuzzPeer)(nil) | |||
func newFuzzPeer() *fuzzPeer { | |||
fp := &fuzzPeer{m: make(map[string]interface{})} | |||
fp.BaseService = service.NewBaseService(nil, "fuzzPeer", fp) | |||
return fp | |||
} | |||
var privKey = ed25519.GenPrivKey() | |||
var nodeID = types.NodeIDFromPubKey(privKey.PubKey()) | |||
var defaultNodeInfo = types.NodeInfo{ | |||
ProtocolVersion: types.ProtocolVersion{ | |||
P2P: version.P2PProtocol, | |||
Block: version.BlockProtocol, | |||
App: 0, | |||
}, | |||
NodeID: nodeID, | |||
ListenAddr: "127.0.0.1:0", | |||
Moniker: "foo1", | |||
} | |||
func (fp *fuzzPeer) FlushStop() {} | |||
func (fp *fuzzPeer) ID() types.NodeID { return nodeID } | |||
func (fp *fuzzPeer) RemoteIP() net.IP { return net.IPv4(198, 163, 190, 214) } | |||
func (fp *fuzzPeer) RemoteAddr() net.Addr { | |||
return &net.TCPAddr{IP: fp.RemoteIP(), Port: 26656, Zone: ""} | |||
} | |||
func (fp *fuzzPeer) IsOutbound() bool { return false } | |||
func (fp *fuzzPeer) IsPersistent() bool { return false } | |||
func (fp *fuzzPeer) CloseConn() error { return nil } | |||
func (fp *fuzzPeer) NodeInfo() types.NodeInfo { return defaultNodeInfo } | |||
func (fp *fuzzPeer) Status() p2p.ConnectionStatus { var cs p2p.ConnectionStatus; return cs } | |||
func (fp *fuzzPeer) SocketAddr() *p2p.NetAddress { | |||
return types.NewNetAddress(fp.ID(), fp.RemoteAddr()) | |||
} | |||
func (fp *fuzzPeer) Send(byte, []byte) bool { return true } | |||
func (fp *fuzzPeer) TrySend(byte, []byte) bool { return true } | |||
func (fp *fuzzPeer) Set(key string, value interface{}) { fp.m[key] = value } | |||
func (fp *fuzzPeer) Get(key string) interface{} { return fp.m[key] } |