mirror of
https://github.com/tendermint/tendermint.git
synced 2026-09-26 01:44:16 +00:00
detele everything
This commit is contained in:
@@ -1,11 +0,0 @@
|
||||
# p2p
|
||||
|
||||
The p2p package provides an abstraction around peer-to-peer communication.
|
||||
|
||||
Docs:
|
||||
|
||||
- [Connection](https://github.com/tendermint/tendermint/blob/master/docs/spec/docs/spec/p2p/connection.md) for details on how connections and multiplexing work
|
||||
- [Peer](https://github.com/tendermint/tendermint/blob/master/docs/spec/docs/spec/p2p/peer.md) for details on peer ID, handshakes, and peer exchange
|
||||
- [Node](https://github.com/tendermint/tendermint/blob/master/docs/spec/docs/spec/p2p/node.md) for details about different types of nodes and how they should work
|
||||
- [Pex](https://github.com/tendermint/tendermint/blob/master/docs/spec/docs/spec/reactors/pex/pex.md) for details on peer discovery and exchange
|
||||
- [Config](https://github.com/tendermint/tendermint/blob/master/docs/spec/docs/spec/p2p/config.md) for details on some config option
|
||||
@@ -1,53 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"github.com/tendermint/tendermint/p2p/conn"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
)
|
||||
|
||||
type Reactor interface {
|
||||
cmn.Service // Start, Stop
|
||||
|
||||
// SetSwitch allows setting a switch.
|
||||
SetSwitch(*Switch)
|
||||
|
||||
// GetChannels returns the list of channel descriptors.
|
||||
GetChannels() []*conn.ChannelDescriptor
|
||||
|
||||
// AddPeer is called by the switch when a new peer is added.
|
||||
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 when msgBytes is received from peer.
|
||||
//
|
||||
// NOTE reactor can not keep msgBytes around after Receive completes without
|
||||
// copying.
|
||||
//
|
||||
// CONTRACT: msgBytes are not nil.
|
||||
Receive(chID byte, peer Peer, msgBytes []byte)
|
||||
}
|
||||
|
||||
//--------------------------------------
|
||||
|
||||
type BaseReactor struct {
|
||||
cmn.BaseService // Provides Start, Stop, .Quit
|
||||
Switch *Switch
|
||||
}
|
||||
|
||||
func NewBaseReactor(name string, impl Reactor) *BaseReactor {
|
||||
return &BaseReactor{
|
||||
BaseService: *cmn.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) {}
|
||||
@@ -1,15 +0,0 @@
|
||||
// +build go1.10
|
||||
|
||||
package conn
|
||||
|
||||
// Go1.10 has a proper net.Conn implementation that
|
||||
// has the SetDeadline method implemented as per
|
||||
// https://github.com/golang/go/commit/e2dd8ca946be884bb877e074a21727f1a685a706
|
||||
// lest we run into problems like
|
||||
// https://github.com/tendermint/tendermint/issues/851
|
||||
|
||||
import "net"
|
||||
|
||||
func NetPipe() (net.Conn, net.Conn) {
|
||||
return net.Pipe()
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// +build !go1.10
|
||||
|
||||
package conn
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Only Go1.10 has a proper net.Conn implementation that
|
||||
// has the SetDeadline method implemented as per
|
||||
// https://github.com/golang/go/commit/e2dd8ca946be884bb877e074a21727f1a685a706
|
||||
// lest we run into problems like
|
||||
// https://github.com/tendermint/tendermint/issues/851
|
||||
// so for go versions < Go1.10 use our custom net.Conn creator
|
||||
// that doesn't return an `Unimplemented error` for net.Conn.
|
||||
// Before https://github.com/tendermint/tendermint/commit/49faa79bdce5663894b3febbf4955fb1d172df04
|
||||
// we hadn't cared about errors from SetDeadline so swallow them up anyways.
|
||||
type pipe struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (p *pipe) SetDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NetPipe() (net.Conn, net.Conn) {
|
||||
p1, p2 := net.Pipe()
|
||||
return &pipe{p1}, &pipe{p2}
|
||||
}
|
||||
|
||||
var _ net.Conn = (*pipe)(nil)
|
||||
@@ -1,794 +0,0 @@
|
||||
package conn
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
amino "github.com/tendermint/go-amino"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
flow "github.com/tendermint/tendermint/libs/flowrate"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMaxPacketMsgPayloadSize = 1024
|
||||
|
||||
numBatchPacketMsgs = 10
|
||||
minReadBufferSize = 1024
|
||||
minWriteBufferSize = 65536
|
||||
updateStats = 2 * time.Second
|
||||
|
||||
// some of these defaults are written in the user config
|
||||
// flushThrottle, sendRate, recvRate
|
||||
// TODO: remove values present in config
|
||||
defaultFlushThrottle = 100 * time.Millisecond
|
||||
|
||||
defaultSendQueueCapacity = 1
|
||||
defaultRecvBufferCapacity = 4096
|
||||
defaultRecvMessageCapacity = 22020096 // 21MB
|
||||
defaultSendRate = int64(512000) // 500KB/s
|
||||
defaultRecvRate = int64(512000) // 500KB/s
|
||||
defaultSendTimeout = 10 * time.Second
|
||||
defaultPingInterval = 60 * time.Second
|
||||
defaultPongTimeout = 45 * time.Second
|
||||
)
|
||||
|
||||
type receiveCbFunc func(chID byte, msgBytes []byte)
|
||||
type errorCbFunc func(interface{})
|
||||
|
||||
/*
|
||||
Each peer has one `MConnection` (multiplex connection) instance.
|
||||
|
||||
__multiplex__ *noun* a system or signal involving simultaneous transmission of
|
||||
several messages along a single channel of communication.
|
||||
|
||||
Each `MConnection` handles message transmission on multiple abstract communication
|
||||
`Channel`s. Each channel has a globally unique byte id.
|
||||
The byte id and the relative priorities of each `Channel` are configured upon
|
||||
initialization of the connection.
|
||||
|
||||
There are two methods for sending messages:
|
||||
func (m MConnection) Send(chID byte, msgBytes []byte) bool {}
|
||||
func (m MConnection) TrySend(chID byte, msgBytes []byte}) bool {}
|
||||
|
||||
`Send(chID, msgBytes)` is a blocking call that waits until `msg` is
|
||||
successfully queued for the channel with the given id byte `chID`, or until the
|
||||
request times out. The message `msg` is serialized using Go-Amino.
|
||||
|
||||
`TrySend(chID, msgBytes)` is a nonblocking call that returns false if the
|
||||
channel's queue is full.
|
||||
|
||||
Inbound message bytes are handled with an onReceive callback function.
|
||||
*/
|
||||
type MConnection struct {
|
||||
cmn.BaseService
|
||||
|
||||
conn net.Conn
|
||||
bufConnReader *bufio.Reader
|
||||
bufConnWriter *bufio.Writer
|
||||
sendMonitor *flow.Monitor
|
||||
recvMonitor *flow.Monitor
|
||||
send chan struct{}
|
||||
pong chan struct{}
|
||||
channels []*Channel
|
||||
channelsIdx map[byte]*Channel
|
||||
onReceive receiveCbFunc
|
||||
onError errorCbFunc
|
||||
errored uint32
|
||||
config MConnConfig
|
||||
|
||||
quit chan struct{}
|
||||
flushTimer *cmn.ThrottleTimer // flush writes as necessary but throttled.
|
||||
pingTimer *cmn.RepeatTimer // send pings periodically
|
||||
|
||||
// close conn if pong is not received in pongTimeout
|
||||
pongTimer *time.Timer
|
||||
pongTimeoutCh chan bool // true - timeout, false - peer sent pong
|
||||
|
||||
chStatsTimer *cmn.RepeatTimer // update channel stats periodically
|
||||
|
||||
created time.Time // time of creation
|
||||
|
||||
_maxPacketMsgSize int
|
||||
}
|
||||
|
||||
// MConnConfig is a MConnection configuration.
|
||||
type MConnConfig struct {
|
||||
SendRate int64 `mapstructure:"send_rate"`
|
||||
RecvRate int64 `mapstructure:"recv_rate"`
|
||||
|
||||
// Maximum payload size
|
||||
MaxPacketMsgPayloadSize int `mapstructure:"max_packet_msg_payload_size"`
|
||||
|
||||
// Interval to flush writes (throttled)
|
||||
FlushThrottle time.Duration `mapstructure:"flush_throttle"`
|
||||
|
||||
// Interval to send pings
|
||||
PingInterval time.Duration `mapstructure:"ping_interval"`
|
||||
|
||||
// Maximum wait time for pongs
|
||||
PongTimeout time.Duration `mapstructure:"pong_timeout"`
|
||||
}
|
||||
|
||||
// DefaultMConnConfig returns the default config.
|
||||
func DefaultMConnConfig() MConnConfig {
|
||||
return MConnConfig{
|
||||
SendRate: defaultSendRate,
|
||||
RecvRate: defaultRecvRate,
|
||||
MaxPacketMsgPayloadSize: defaultMaxPacketMsgPayloadSize,
|
||||
FlushThrottle: defaultFlushThrottle,
|
||||
PingInterval: defaultPingInterval,
|
||||
PongTimeout: defaultPongTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
// NewMConnection wraps net.Conn and creates multiplex connection
|
||||
func NewMConnection(conn net.Conn, chDescs []*ChannelDescriptor, onReceive receiveCbFunc, onError errorCbFunc) *MConnection {
|
||||
return NewMConnectionWithConfig(
|
||||
conn,
|
||||
chDescs,
|
||||
onReceive,
|
||||
onError,
|
||||
DefaultMConnConfig())
|
||||
}
|
||||
|
||||
// NewMConnectionWithConfig wraps net.Conn and creates multiplex connection with a config
|
||||
func NewMConnectionWithConfig(conn net.Conn, chDescs []*ChannelDescriptor, onReceive receiveCbFunc, onError errorCbFunc, config MConnConfig) *MConnection {
|
||||
if config.PongTimeout >= config.PingInterval {
|
||||
panic("pongTimeout must be less than pingInterval (otherwise, next ping will reset pong timer)")
|
||||
}
|
||||
|
||||
mconn := &MConnection{
|
||||
conn: conn,
|
||||
bufConnReader: bufio.NewReaderSize(conn, minReadBufferSize),
|
||||
bufConnWriter: bufio.NewWriterSize(conn, minWriteBufferSize),
|
||||
sendMonitor: flow.New(0, 0),
|
||||
recvMonitor: flow.New(0, 0),
|
||||
send: make(chan struct{}, 1),
|
||||
pong: make(chan struct{}, 1),
|
||||
onReceive: onReceive,
|
||||
onError: onError,
|
||||
config: config,
|
||||
}
|
||||
|
||||
// Create channels
|
||||
var channelsIdx = map[byte]*Channel{}
|
||||
var channels = []*Channel{}
|
||||
|
||||
for _, desc := range chDescs {
|
||||
channel := newChannel(mconn, *desc)
|
||||
channelsIdx[channel.desc.ID] = channel
|
||||
channels = append(channels, channel)
|
||||
}
|
||||
mconn.channels = channels
|
||||
mconn.channelsIdx = channelsIdx
|
||||
|
||||
mconn.BaseService = *cmn.NewBaseService(nil, "MConnection", mconn)
|
||||
|
||||
// maxPacketMsgSize() is a bit heavy, so call just once
|
||||
mconn._maxPacketMsgSize = mconn.maxPacketMsgSize()
|
||||
|
||||
return mconn
|
||||
}
|
||||
|
||||
func (c *MConnection) SetLogger(l log.Logger) {
|
||||
c.BaseService.SetLogger(l)
|
||||
for _, ch := range c.channels {
|
||||
ch.SetLogger(l)
|
||||
}
|
||||
}
|
||||
|
||||
// OnStart implements BaseService
|
||||
func (c *MConnection) OnStart() error {
|
||||
if err := c.BaseService.OnStart(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.quit = make(chan struct{})
|
||||
c.flushTimer = cmn.NewThrottleTimer("flush", c.config.FlushThrottle)
|
||||
c.pingTimer = cmn.NewRepeatTimer("ping", c.config.PingInterval)
|
||||
c.pongTimeoutCh = make(chan bool, 1)
|
||||
c.chStatsTimer = cmn.NewRepeatTimer("chStats", updateStats)
|
||||
go c.sendRoutine()
|
||||
go c.recvRoutine()
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnStop implements BaseService
|
||||
func (c *MConnection) OnStop() {
|
||||
c.BaseService.OnStop()
|
||||
c.flushTimer.Stop()
|
||||
c.pingTimer.Stop()
|
||||
c.chStatsTimer.Stop()
|
||||
if c.quit != nil {
|
||||
close(c.quit)
|
||||
}
|
||||
c.conn.Close() // nolint: errcheck
|
||||
|
||||
// We can't close pong safely here because
|
||||
// recvRoutine may write to it after we've stopped.
|
||||
// Though it doesn't need to get closed at all,
|
||||
// we close it @ recvRoutine.
|
||||
}
|
||||
|
||||
func (c *MConnection) String() string {
|
||||
return fmt.Sprintf("MConn{%v}", c.conn.RemoteAddr())
|
||||
}
|
||||
|
||||
func (c *MConnection) flush() {
|
||||
c.Logger.Debug("Flush", "conn", c)
|
||||
err := c.bufConnWriter.Flush()
|
||||
if err != nil {
|
||||
c.Logger.Error("MConnection flush failed", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Catch panics, usually caused by remote disconnects.
|
||||
func (c *MConnection) _recover() {
|
||||
if r := recover(); r != nil {
|
||||
err := cmn.ErrorWrap(r, "recovered panic in MConnection")
|
||||
c.stopForError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MConnection) stopForError(r interface{}) {
|
||||
c.Stop()
|
||||
if atomic.CompareAndSwapUint32(&c.errored, 0, 1) {
|
||||
if c.onError != nil {
|
||||
c.onError(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Queues a message to be sent to channel.
|
||||
func (c *MConnection) Send(chID byte, msgBytes []byte) bool {
|
||||
if !c.IsRunning() {
|
||||
return false
|
||||
}
|
||||
|
||||
c.Logger.Debug("Send", "channel", chID, "conn", c, "msgBytes", fmt.Sprintf("%X", msgBytes))
|
||||
|
||||
// Send message to channel.
|
||||
channel, ok := c.channelsIdx[chID]
|
||||
if !ok {
|
||||
c.Logger.Error(cmn.Fmt("Cannot send bytes, unknown channel %X", chID))
|
||||
return false
|
||||
}
|
||||
|
||||
success := channel.sendBytes(msgBytes)
|
||||
if success {
|
||||
// Wake up sendRoutine if necessary
|
||||
select {
|
||||
case c.send <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
} else {
|
||||
c.Logger.Error("Send failed", "channel", chID, "conn", c, "msgBytes", fmt.Sprintf("%X", msgBytes))
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
// Queues a message to be sent to channel.
|
||||
// Nonblocking, returns true if successful.
|
||||
func (c *MConnection) TrySend(chID byte, msgBytes []byte) bool {
|
||||
if !c.IsRunning() {
|
||||
return false
|
||||
}
|
||||
|
||||
c.Logger.Debug("TrySend", "channel", chID, "conn", c, "msgBytes", fmt.Sprintf("%X", msgBytes))
|
||||
|
||||
// Send message to channel.
|
||||
channel, ok := c.channelsIdx[chID]
|
||||
if !ok {
|
||||
c.Logger.Error(cmn.Fmt("Cannot send bytes, unknown channel %X", chID))
|
||||
return false
|
||||
}
|
||||
|
||||
ok = channel.trySendBytes(msgBytes)
|
||||
if ok {
|
||||
// Wake up sendRoutine if necessary
|
||||
select {
|
||||
case c.send <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// CanSend returns true if you can send more data onto the chID, false
|
||||
// otherwise. Use only as a heuristic.
|
||||
func (c *MConnection) CanSend(chID byte) bool {
|
||||
if !c.IsRunning() {
|
||||
return false
|
||||
}
|
||||
|
||||
channel, ok := c.channelsIdx[chID]
|
||||
if !ok {
|
||||
c.Logger.Error(cmn.Fmt("Unknown channel %X", chID))
|
||||
return false
|
||||
}
|
||||
return channel.canSend()
|
||||
}
|
||||
|
||||
// sendRoutine polls for packets to send from channels.
|
||||
func (c *MConnection) sendRoutine() {
|
||||
defer c._recover()
|
||||
|
||||
FOR_LOOP:
|
||||
for {
|
||||
var _n int64
|
||||
var err error
|
||||
SELECTION:
|
||||
select {
|
||||
case <-c.flushTimer.Ch:
|
||||
// NOTE: flushTimer.Set() must be called every time
|
||||
// something is written to .bufConnWriter.
|
||||
c.flush()
|
||||
case <-c.chStatsTimer.Chan():
|
||||
for _, channel := range c.channels {
|
||||
channel.updateStats()
|
||||
}
|
||||
case <-c.pingTimer.Chan():
|
||||
c.Logger.Debug("Send Ping")
|
||||
_n, err = cdc.MarshalBinaryWriter(c.bufConnWriter, PacketPing{})
|
||||
if err != nil {
|
||||
break SELECTION
|
||||
}
|
||||
c.sendMonitor.Update(int(_n))
|
||||
c.Logger.Debug("Starting pong timer", "dur", c.config.PongTimeout)
|
||||
c.pongTimer = time.AfterFunc(c.config.PongTimeout, func() {
|
||||
select {
|
||||
case c.pongTimeoutCh <- true:
|
||||
default:
|
||||
}
|
||||
})
|
||||
c.flush()
|
||||
case timeout := <-c.pongTimeoutCh:
|
||||
if timeout {
|
||||
c.Logger.Debug("Pong timeout")
|
||||
err = errors.New("pong timeout")
|
||||
} else {
|
||||
c.stopPongTimer()
|
||||
}
|
||||
case <-c.pong:
|
||||
c.Logger.Debug("Send Pong")
|
||||
_n, err = cdc.MarshalBinaryWriter(c.bufConnWriter, PacketPong{})
|
||||
if err != nil {
|
||||
break SELECTION
|
||||
}
|
||||
c.sendMonitor.Update(int(_n))
|
||||
c.flush()
|
||||
case <-c.quit:
|
||||
break FOR_LOOP
|
||||
case <-c.send:
|
||||
// Send some PacketMsgs
|
||||
eof := c.sendSomePacketMsgs()
|
||||
if !eof {
|
||||
// Keep sendRoutine awake.
|
||||
select {
|
||||
case c.send <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !c.IsRunning() {
|
||||
break FOR_LOOP
|
||||
}
|
||||
if err != nil {
|
||||
c.Logger.Error("Connection failed @ sendRoutine", "conn", c, "err", err)
|
||||
c.stopForError(err)
|
||||
break FOR_LOOP
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
c.stopPongTimer()
|
||||
}
|
||||
|
||||
// Returns true if messages from channels were exhausted.
|
||||
// Blocks in accordance to .sendMonitor throttling.
|
||||
func (c *MConnection) sendSomePacketMsgs() bool {
|
||||
// Block until .sendMonitor says we can write.
|
||||
// Once we're ready we send more than we asked for,
|
||||
// but amortized it should even out.
|
||||
c.sendMonitor.Limit(c._maxPacketMsgSize, atomic.LoadInt64(&c.config.SendRate), true)
|
||||
|
||||
// Now send some PacketMsgs.
|
||||
for i := 0; i < numBatchPacketMsgs; i++ {
|
||||
if c.sendPacketMsg() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Returns true if messages from channels were exhausted.
|
||||
func (c *MConnection) sendPacketMsg() bool {
|
||||
// Choose a channel to create a PacketMsg from.
|
||||
// The chosen channel will be the one whose recentlySent/priority is the least.
|
||||
var leastRatio float32 = math.MaxFloat32
|
||||
var leastChannel *Channel
|
||||
for _, channel := range c.channels {
|
||||
// If nothing to send, skip this channel
|
||||
if !channel.isSendPending() {
|
||||
continue
|
||||
}
|
||||
// Get ratio, and keep track of lowest ratio.
|
||||
ratio := float32(channel.recentlySent) / float32(channel.desc.Priority)
|
||||
if ratio < leastRatio {
|
||||
leastRatio = ratio
|
||||
leastChannel = channel
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing to send?
|
||||
if leastChannel == nil {
|
||||
return true
|
||||
}
|
||||
// c.Logger.Info("Found a msgPacket to send")
|
||||
|
||||
// Make & send a PacketMsg from this channel
|
||||
_n, err := leastChannel.writePacketMsgTo(c.bufConnWriter)
|
||||
if err != nil {
|
||||
c.Logger.Error("Failed to write PacketMsg", "err", err)
|
||||
c.stopForError(err)
|
||||
return true
|
||||
}
|
||||
c.sendMonitor.Update(int(_n))
|
||||
c.flushTimer.Set()
|
||||
return false
|
||||
}
|
||||
|
||||
// recvRoutine reads PacketMsgs and reconstructs the message using the channels' "recving" buffer.
|
||||
// After a whole message has been assembled, it's pushed to onReceive().
|
||||
// Blocks depending on how the connection is throttled.
|
||||
// Otherwise, it never blocks.
|
||||
func (c *MConnection) recvRoutine() {
|
||||
defer c._recover()
|
||||
|
||||
FOR_LOOP:
|
||||
for {
|
||||
// Block until .recvMonitor says we can read.
|
||||
c.recvMonitor.Limit(c._maxPacketMsgSize, atomic.LoadInt64(&c.config.RecvRate), true)
|
||||
|
||||
// Peek into bufConnReader for debugging
|
||||
/*
|
||||
if numBytes := c.bufConnReader.Buffered(); numBytes > 0 {
|
||||
bz, err := c.bufConnReader.Peek(cmn.MinInt(numBytes, 100))
|
||||
if err == nil {
|
||||
// return
|
||||
} else {
|
||||
c.Logger.Debug("Error peeking connection buffer", "err", err)
|
||||
// return nil
|
||||
}
|
||||
c.Logger.Info("Peek connection buffer", "numBytes", numBytes, "bz", bz)
|
||||
}
|
||||
*/
|
||||
|
||||
// Read packet type
|
||||
var packet Packet
|
||||
var _n int64
|
||||
var err error
|
||||
_n, err = cdc.UnmarshalBinaryReader(c.bufConnReader, &packet, int64(c._maxPacketMsgSize))
|
||||
c.recvMonitor.Update(int(_n))
|
||||
if err != nil {
|
||||
if c.IsRunning() {
|
||||
c.Logger.Error("Connection failed @ recvRoutine (reading byte)", "conn", c, "err", err)
|
||||
c.stopForError(err)
|
||||
}
|
||||
break FOR_LOOP
|
||||
}
|
||||
|
||||
// Read more depending on packet type.
|
||||
switch pkt := packet.(type) {
|
||||
case PacketPing:
|
||||
// TODO: prevent abuse, as they cause flush()'s.
|
||||
// https://github.com/tendermint/tendermint/issues/1190
|
||||
c.Logger.Debug("Receive Ping")
|
||||
select {
|
||||
case c.pong <- struct{}{}:
|
||||
default:
|
||||
// never block
|
||||
}
|
||||
case PacketPong:
|
||||
c.Logger.Debug("Receive Pong")
|
||||
select {
|
||||
case c.pongTimeoutCh <- false:
|
||||
default:
|
||||
// never block
|
||||
}
|
||||
case PacketMsg:
|
||||
channel, ok := c.channelsIdx[pkt.ChannelID]
|
||||
if !ok || channel == nil {
|
||||
err := fmt.Errorf("Unknown channel %X", pkt.ChannelID)
|
||||
c.Logger.Error("Connection failed @ recvRoutine", "conn", c, "err", err)
|
||||
c.stopForError(err)
|
||||
break FOR_LOOP
|
||||
}
|
||||
|
||||
msgBytes, err := channel.recvPacketMsg(pkt)
|
||||
if err != nil {
|
||||
if c.IsRunning() {
|
||||
c.Logger.Error("Connection failed @ recvRoutine", "conn", c, "err", err)
|
||||
c.stopForError(err)
|
||||
}
|
||||
break FOR_LOOP
|
||||
}
|
||||
if msgBytes != nil {
|
||||
c.Logger.Debug("Received bytes", "chID", pkt.ChannelID, "msgBytes", fmt.Sprintf("%X", msgBytes))
|
||||
// NOTE: This means the reactor.Receive runs in the same thread as the p2p recv routine
|
||||
c.onReceive(pkt.ChannelID, msgBytes)
|
||||
}
|
||||
default:
|
||||
err := fmt.Errorf("Unknown message type %v", reflect.TypeOf(packet))
|
||||
c.Logger.Error("Connection failed @ recvRoutine", "conn", c, "err", err)
|
||||
c.stopForError(err)
|
||||
break FOR_LOOP
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
close(c.pong)
|
||||
for range c.pong {
|
||||
// Drain
|
||||
}
|
||||
}
|
||||
|
||||
// not goroutine-safe
|
||||
func (c *MConnection) stopPongTimer() {
|
||||
if c.pongTimer != nil {
|
||||
_ = c.pongTimer.Stop()
|
||||
c.pongTimer = nil
|
||||
}
|
||||
}
|
||||
|
||||
// maxPacketMsgSize returns a maximum size of PacketMsg, including the overhead
|
||||
// of amino encoding.
|
||||
func (c *MConnection) maxPacketMsgSize() int {
|
||||
return len(cdc.MustMarshalBinary(PacketMsg{
|
||||
ChannelID: 0x01,
|
||||
EOF: 1,
|
||||
Bytes: make([]byte, c.config.MaxPacketMsgPayloadSize),
|
||||
})) + 10 // leave room for changes in amino
|
||||
}
|
||||
|
||||
type ConnectionStatus struct {
|
||||
Duration time.Duration
|
||||
SendMonitor flow.Status
|
||||
RecvMonitor flow.Status
|
||||
Channels []ChannelStatus
|
||||
}
|
||||
|
||||
type ChannelStatus struct {
|
||||
ID byte
|
||||
SendQueueCapacity int
|
||||
SendQueueSize int
|
||||
Priority int
|
||||
RecentlySent int64
|
||||
}
|
||||
|
||||
func (c *MConnection) Status() ConnectionStatus {
|
||||
var status ConnectionStatus
|
||||
status.Duration = time.Since(c.created)
|
||||
status.SendMonitor = c.sendMonitor.Status()
|
||||
status.RecvMonitor = c.recvMonitor.Status()
|
||||
status.Channels = make([]ChannelStatus, len(c.channels))
|
||||
for i, channel := range c.channels {
|
||||
status.Channels[i] = ChannelStatus{
|
||||
ID: channel.desc.ID,
|
||||
SendQueueCapacity: cap(channel.sendQueue),
|
||||
SendQueueSize: int(channel.sendQueueSize), // TODO use atomic
|
||||
Priority: channel.desc.Priority,
|
||||
RecentlySent: channel.recentlySent,
|
||||
}
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
type ChannelDescriptor struct {
|
||||
ID byte
|
||||
Priority int
|
||||
SendQueueCapacity int
|
||||
RecvBufferCapacity int
|
||||
RecvMessageCapacity int
|
||||
}
|
||||
|
||||
func (chDesc ChannelDescriptor) FillDefaults() (filled ChannelDescriptor) {
|
||||
if chDesc.SendQueueCapacity == 0 {
|
||||
chDesc.SendQueueCapacity = defaultSendQueueCapacity
|
||||
}
|
||||
if chDesc.RecvBufferCapacity == 0 {
|
||||
chDesc.RecvBufferCapacity = defaultRecvBufferCapacity
|
||||
}
|
||||
if chDesc.RecvMessageCapacity == 0 {
|
||||
chDesc.RecvMessageCapacity = defaultRecvMessageCapacity
|
||||
}
|
||||
filled = chDesc
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: lowercase.
|
||||
// NOTE: not goroutine-safe.
|
||||
type Channel struct {
|
||||
conn *MConnection
|
||||
desc ChannelDescriptor
|
||||
sendQueue chan []byte
|
||||
sendQueueSize int32 // atomic.
|
||||
recving []byte
|
||||
sending []byte
|
||||
recentlySent int64 // exponential moving average
|
||||
|
||||
maxPacketMsgPayloadSize int
|
||||
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
func newChannel(conn *MConnection, desc ChannelDescriptor) *Channel {
|
||||
desc = desc.FillDefaults()
|
||||
if desc.Priority <= 0 {
|
||||
cmn.PanicSanity("Channel default priority must be a positive integer")
|
||||
}
|
||||
return &Channel{
|
||||
conn: conn,
|
||||
desc: desc,
|
||||
sendQueue: make(chan []byte, desc.SendQueueCapacity),
|
||||
recving: make([]byte, 0, desc.RecvBufferCapacity),
|
||||
maxPacketMsgPayloadSize: conn.config.MaxPacketMsgPayloadSize,
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) SetLogger(l log.Logger) {
|
||||
ch.Logger = l
|
||||
}
|
||||
|
||||
// Queues message to send to this channel.
|
||||
// Goroutine-safe
|
||||
// Times out (and returns false) after defaultSendTimeout
|
||||
func (ch *Channel) sendBytes(bytes []byte) bool {
|
||||
select {
|
||||
case ch.sendQueue <- bytes:
|
||||
atomic.AddInt32(&ch.sendQueueSize, 1)
|
||||
return true
|
||||
case <-time.After(defaultSendTimeout):
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Queues message to send to this channel.
|
||||
// Nonblocking, returns true if successful.
|
||||
// Goroutine-safe
|
||||
func (ch *Channel) trySendBytes(bytes []byte) bool {
|
||||
select {
|
||||
case ch.sendQueue <- bytes:
|
||||
atomic.AddInt32(&ch.sendQueueSize, 1)
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Goroutine-safe
|
||||
func (ch *Channel) loadSendQueueSize() (size int) {
|
||||
return int(atomic.LoadInt32(&ch.sendQueueSize))
|
||||
}
|
||||
|
||||
// Goroutine-safe
|
||||
// Use only as a heuristic.
|
||||
func (ch *Channel) canSend() bool {
|
||||
return ch.loadSendQueueSize() < defaultSendQueueCapacity
|
||||
}
|
||||
|
||||
// Returns true if any PacketMsgs are pending to be sent.
|
||||
// Call before calling nextPacketMsg()
|
||||
// Goroutine-safe
|
||||
func (ch *Channel) isSendPending() bool {
|
||||
if len(ch.sending) == 0 {
|
||||
if len(ch.sendQueue) == 0 {
|
||||
return false
|
||||
}
|
||||
ch.sending = <-ch.sendQueue
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Creates a new PacketMsg to send.
|
||||
// Not goroutine-safe
|
||||
func (ch *Channel) nextPacketMsg() PacketMsg {
|
||||
packet := PacketMsg{}
|
||||
packet.ChannelID = byte(ch.desc.ID)
|
||||
maxSize := ch.maxPacketMsgPayloadSize
|
||||
packet.Bytes = ch.sending[:cmn.MinInt(maxSize, len(ch.sending))]
|
||||
if len(ch.sending) <= maxSize {
|
||||
packet.EOF = byte(0x01)
|
||||
ch.sending = nil
|
||||
atomic.AddInt32(&ch.sendQueueSize, -1) // decrement sendQueueSize
|
||||
} else {
|
||||
packet.EOF = byte(0x00)
|
||||
ch.sending = ch.sending[cmn.MinInt(maxSize, len(ch.sending)):]
|
||||
}
|
||||
return packet
|
||||
}
|
||||
|
||||
// Writes next PacketMsg to w and updates c.recentlySent.
|
||||
// Not goroutine-safe
|
||||
func (ch *Channel) writePacketMsgTo(w io.Writer) (n int64, err error) {
|
||||
var packet = ch.nextPacketMsg()
|
||||
n, err = cdc.MarshalBinaryWriter(w, packet)
|
||||
ch.recentlySent += n
|
||||
return
|
||||
}
|
||||
|
||||
// Handles incoming PacketMsgs. It returns a message bytes if message is
|
||||
// complete. NOTE message bytes may change on next call to recvPacketMsg.
|
||||
// Not goroutine-safe
|
||||
func (ch *Channel) recvPacketMsg(packet PacketMsg) ([]byte, error) {
|
||||
ch.Logger.Debug("Read PacketMsg", "conn", ch.conn, "packet", packet)
|
||||
var recvCap, recvReceived = ch.desc.RecvMessageCapacity, len(ch.recving) + len(packet.Bytes)
|
||||
if recvCap < recvReceived {
|
||||
return nil, fmt.Errorf("Received message exceeds available capacity: %v < %v", recvCap, recvReceived)
|
||||
}
|
||||
ch.recving = append(ch.recving, packet.Bytes...)
|
||||
if packet.EOF == byte(0x01) {
|
||||
msgBytes := ch.recving
|
||||
|
||||
// clear the slice without re-allocating.
|
||||
// http://stackoverflow.com/questions/16971741/how-do-you-clear-a-slice-in-go
|
||||
// suggests this could be a memory leak, but we might as well keep the memory for the channel until it closes,
|
||||
// at which point the recving slice stops being used and should be garbage collected
|
||||
ch.recving = ch.recving[:0] // make([]byte, 0, ch.desc.RecvBufferCapacity)
|
||||
return msgBytes, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Call this periodically to update stats for throttling purposes.
|
||||
// Not goroutine-safe
|
||||
func (ch *Channel) updateStats() {
|
||||
// Exponential decay of stats.
|
||||
// TODO: optimize.
|
||||
ch.recentlySent = int64(float64(ch.recentlySent) * 0.8)
|
||||
}
|
||||
|
||||
//----------------------------------------
|
||||
// Packet
|
||||
|
||||
type Packet interface {
|
||||
AssertIsPacket()
|
||||
}
|
||||
|
||||
func RegisterPacket(cdc *amino.Codec) {
|
||||
cdc.RegisterInterface((*Packet)(nil), nil)
|
||||
cdc.RegisterConcrete(PacketPing{}, "tendermint/p2p/PacketPing", nil)
|
||||
cdc.RegisterConcrete(PacketPong{}, "tendermint/p2p/PacketPong", nil)
|
||||
cdc.RegisterConcrete(PacketMsg{}, "tendermint/p2p/PacketMsg", nil)
|
||||
}
|
||||
|
||||
func (_ PacketPing) AssertIsPacket() {}
|
||||
func (_ PacketPong) AssertIsPacket() {}
|
||||
func (_ PacketMsg) AssertIsPacket() {}
|
||||
|
||||
type PacketPing struct {
|
||||
}
|
||||
|
||||
type PacketPong struct {
|
||||
}
|
||||
|
||||
type PacketMsg struct {
|
||||
ChannelID byte
|
||||
EOF byte // 1 means message ends here.
|
||||
Bytes []byte
|
||||
}
|
||||
|
||||
func (mp PacketMsg) String() string {
|
||||
return fmt.Sprintf("PacketMsg{%X:%X T:%X}", mp.ChannelID, mp.Bytes, mp.EOF)
|
||||
}
|
||||
@@ -1,492 +0,0 @@
|
||||
package conn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fortytw2/leaktest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
amino "github.com/tendermint/go-amino"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
)
|
||||
|
||||
const maxPingPongPacketSize = 1024 // bytes
|
||||
|
||||
func createTestMConnection(conn net.Conn) *MConnection {
|
||||
onReceive := func(chID byte, msgBytes []byte) {
|
||||
}
|
||||
onError := func(r interface{}) {
|
||||
}
|
||||
c := createMConnectionWithCallbacks(conn, onReceive, onError)
|
||||
c.SetLogger(log.TestingLogger())
|
||||
return c
|
||||
}
|
||||
|
||||
func createMConnectionWithCallbacks(conn net.Conn, onReceive func(chID byte, msgBytes []byte), onError func(r interface{})) *MConnection {
|
||||
cfg := DefaultMConnConfig()
|
||||
cfg.PingInterval = 90 * time.Millisecond
|
||||
cfg.PongTimeout = 45 * time.Millisecond
|
||||
chDescs := []*ChannelDescriptor{&ChannelDescriptor{ID: 0x01, Priority: 1, SendQueueCapacity: 1}}
|
||||
c := NewMConnectionWithConfig(conn, chDescs, onReceive, onError, cfg)
|
||||
c.SetLogger(log.TestingLogger())
|
||||
return c
|
||||
}
|
||||
|
||||
func TestMConnectionSend(t *testing.T) {
|
||||
server, client := NetPipe()
|
||||
defer server.Close() // nolint: errcheck
|
||||
defer client.Close() // nolint: errcheck
|
||||
|
||||
mconn := createTestMConnection(client)
|
||||
err := mconn.Start()
|
||||
require.Nil(t, err)
|
||||
defer mconn.Stop()
|
||||
|
||||
msg := []byte("Ant-Man")
|
||||
assert.True(t, mconn.Send(0x01, msg))
|
||||
// Note: subsequent Send/TrySend calls could pass because we are reading from
|
||||
// the send queue in a separate goroutine.
|
||||
_, err = server.Read(make([]byte, len(msg)))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.True(t, mconn.CanSend(0x01))
|
||||
|
||||
msg = []byte("Spider-Man")
|
||||
assert.True(t, mconn.TrySend(0x01, msg))
|
||||
_, err = server.Read(make([]byte, len(msg)))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.False(t, mconn.CanSend(0x05), "CanSend should return false because channel is unknown")
|
||||
assert.False(t, mconn.Send(0x05, []byte("Absorbing Man")), "Send should return false because channel is unknown")
|
||||
}
|
||||
|
||||
func TestMConnectionReceive(t *testing.T) {
|
||||
server, client := NetPipe()
|
||||
defer server.Close() // nolint: errcheck
|
||||
defer client.Close() // nolint: errcheck
|
||||
|
||||
receivedCh := make(chan []byte)
|
||||
errorsCh := make(chan interface{})
|
||||
onReceive := func(chID byte, msgBytes []byte) {
|
||||
receivedCh <- msgBytes
|
||||
}
|
||||
onError := func(r interface{}) {
|
||||
errorsCh <- r
|
||||
}
|
||||
mconn1 := createMConnectionWithCallbacks(client, onReceive, onError)
|
||||
err := mconn1.Start()
|
||||
require.Nil(t, err)
|
||||
defer mconn1.Stop()
|
||||
|
||||
mconn2 := createTestMConnection(server)
|
||||
err = mconn2.Start()
|
||||
require.Nil(t, err)
|
||||
defer mconn2.Stop()
|
||||
|
||||
msg := []byte("Cyclops")
|
||||
assert.True(t, mconn2.Send(0x01, msg))
|
||||
|
||||
select {
|
||||
case receivedBytes := <-receivedCh:
|
||||
assert.Equal(t, []byte(msg), receivedBytes)
|
||||
case err := <-errorsCh:
|
||||
t.Fatalf("Expected %s, got %+v", msg, err)
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
t.Fatalf("Did not receive %s message in 500ms", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMConnectionStatus(t *testing.T) {
|
||||
server, client := NetPipe()
|
||||
defer server.Close() // nolint: errcheck
|
||||
defer client.Close() // nolint: errcheck
|
||||
|
||||
mconn := createTestMConnection(client)
|
||||
err := mconn.Start()
|
||||
require.Nil(t, err)
|
||||
defer mconn.Stop()
|
||||
|
||||
status := mconn.Status()
|
||||
assert.NotNil(t, status)
|
||||
assert.Zero(t, status.Channels[0].SendQueueSize)
|
||||
}
|
||||
|
||||
func TestMConnectionPongTimeoutResultsInError(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
receivedCh := make(chan []byte)
|
||||
errorsCh := make(chan interface{})
|
||||
onReceive := func(chID byte, msgBytes []byte) {
|
||||
receivedCh <- msgBytes
|
||||
}
|
||||
onError := func(r interface{}) {
|
||||
errorsCh <- r
|
||||
}
|
||||
mconn := createMConnectionWithCallbacks(client, onReceive, onError)
|
||||
err := mconn.Start()
|
||||
require.Nil(t, err)
|
||||
defer mconn.Stop()
|
||||
|
||||
serverGotPing := make(chan struct{})
|
||||
go func() {
|
||||
// read ping
|
||||
var pkt PacketPing
|
||||
_, err = cdc.UnmarshalBinaryReader(server, &pkt, maxPingPongPacketSize)
|
||||
assert.Nil(t, err)
|
||||
serverGotPing <- struct{}{}
|
||||
}()
|
||||
<-serverGotPing
|
||||
|
||||
pongTimerExpired := mconn.config.PongTimeout + 20*time.Millisecond
|
||||
select {
|
||||
case msgBytes := <-receivedCh:
|
||||
t.Fatalf("Expected error, but got %v", msgBytes)
|
||||
case err := <-errorsCh:
|
||||
assert.NotNil(t, err)
|
||||
case <-time.After(pongTimerExpired):
|
||||
t.Fatalf("Expected to receive error after %v", pongTimerExpired)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMConnectionMultiplePongsInTheBeginning(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
receivedCh := make(chan []byte)
|
||||
errorsCh := make(chan interface{})
|
||||
onReceive := func(chID byte, msgBytes []byte) {
|
||||
receivedCh <- msgBytes
|
||||
}
|
||||
onError := func(r interface{}) {
|
||||
errorsCh <- r
|
||||
}
|
||||
mconn := createMConnectionWithCallbacks(client, onReceive, onError)
|
||||
err := mconn.Start()
|
||||
require.Nil(t, err)
|
||||
defer mconn.Stop()
|
||||
|
||||
// sending 3 pongs in a row (abuse)
|
||||
_, err = server.Write(cdc.MustMarshalBinary(PacketPong{}))
|
||||
require.Nil(t, err)
|
||||
_, err = server.Write(cdc.MustMarshalBinary(PacketPong{}))
|
||||
require.Nil(t, err)
|
||||
_, err = server.Write(cdc.MustMarshalBinary(PacketPong{}))
|
||||
require.Nil(t, err)
|
||||
|
||||
serverGotPing := make(chan struct{})
|
||||
go func() {
|
||||
// read ping (one byte)
|
||||
var packet, err = Packet(nil), error(nil)
|
||||
_, err = cdc.UnmarshalBinaryReader(server, &packet, maxPingPongPacketSize)
|
||||
require.Nil(t, err)
|
||||
serverGotPing <- struct{}{}
|
||||
// respond with pong
|
||||
_, err = server.Write(cdc.MustMarshalBinary(PacketPong{}))
|
||||
require.Nil(t, err)
|
||||
}()
|
||||
<-serverGotPing
|
||||
|
||||
pongTimerExpired := mconn.config.PongTimeout + 20*time.Millisecond
|
||||
select {
|
||||
case msgBytes := <-receivedCh:
|
||||
t.Fatalf("Expected no data, but got %v", msgBytes)
|
||||
case err := <-errorsCh:
|
||||
t.Fatalf("Expected no error, but got %v", err)
|
||||
case <-time.After(pongTimerExpired):
|
||||
assert.True(t, mconn.IsRunning())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMConnectionMultiplePings(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
receivedCh := make(chan []byte)
|
||||
errorsCh := make(chan interface{})
|
||||
onReceive := func(chID byte, msgBytes []byte) {
|
||||
receivedCh <- msgBytes
|
||||
}
|
||||
onError := func(r interface{}) {
|
||||
errorsCh <- r
|
||||
}
|
||||
mconn := createMConnectionWithCallbacks(client, onReceive, onError)
|
||||
err := mconn.Start()
|
||||
require.Nil(t, err)
|
||||
defer mconn.Stop()
|
||||
|
||||
// sending 3 pings in a row (abuse)
|
||||
// see https://github.com/tendermint/tendermint/issues/1190
|
||||
_, err = server.Write(cdc.MustMarshalBinary(PacketPing{}))
|
||||
require.Nil(t, err)
|
||||
var pkt PacketPong
|
||||
_, err = cdc.UnmarshalBinaryReader(server, &pkt, maxPingPongPacketSize)
|
||||
require.Nil(t, err)
|
||||
_, err = server.Write(cdc.MustMarshalBinary(PacketPing{}))
|
||||
require.Nil(t, err)
|
||||
_, err = cdc.UnmarshalBinaryReader(server, &pkt, maxPingPongPacketSize)
|
||||
require.Nil(t, err)
|
||||
_, err = server.Write(cdc.MustMarshalBinary(PacketPing{}))
|
||||
require.Nil(t, err)
|
||||
_, err = cdc.UnmarshalBinaryReader(server, &pkt, maxPingPongPacketSize)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.True(t, mconn.IsRunning())
|
||||
}
|
||||
|
||||
func TestMConnectionPingPongs(t *testing.T) {
|
||||
// check that we are not leaking any go-routines
|
||||
defer leaktest.CheckTimeout(t, 10*time.Second)()
|
||||
|
||||
server, client := net.Pipe()
|
||||
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
receivedCh := make(chan []byte)
|
||||
errorsCh := make(chan interface{})
|
||||
onReceive := func(chID byte, msgBytes []byte) {
|
||||
receivedCh <- msgBytes
|
||||
}
|
||||
onError := func(r interface{}) {
|
||||
errorsCh <- r
|
||||
}
|
||||
mconn := createMConnectionWithCallbacks(client, onReceive, onError)
|
||||
err := mconn.Start()
|
||||
require.Nil(t, err)
|
||||
defer mconn.Stop()
|
||||
|
||||
serverGotPing := make(chan struct{})
|
||||
go func() {
|
||||
// read ping
|
||||
var pkt PacketPing
|
||||
_, err = cdc.UnmarshalBinaryReader(server, &pkt, maxPingPongPacketSize)
|
||||
require.Nil(t, err)
|
||||
serverGotPing <- struct{}{}
|
||||
// respond with pong
|
||||
_, err = server.Write(cdc.MustMarshalBinary(PacketPong{}))
|
||||
require.Nil(t, err)
|
||||
|
||||
time.Sleep(mconn.config.PingInterval)
|
||||
|
||||
// read ping
|
||||
_, err = cdc.UnmarshalBinaryReader(server, &pkt, maxPingPongPacketSize)
|
||||
require.Nil(t, err)
|
||||
// respond with pong
|
||||
_, err = server.Write(cdc.MustMarshalBinary(PacketPong{}))
|
||||
require.Nil(t, err)
|
||||
}()
|
||||
<-serverGotPing
|
||||
|
||||
pongTimerExpired := (mconn.config.PongTimeout + 20*time.Millisecond) * 2
|
||||
select {
|
||||
case msgBytes := <-receivedCh:
|
||||
t.Fatalf("Expected no data, but got %v", msgBytes)
|
||||
case err := <-errorsCh:
|
||||
t.Fatalf("Expected no error, but got %v", err)
|
||||
case <-time.After(2 * pongTimerExpired):
|
||||
assert.True(t, mconn.IsRunning())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMConnectionStopsAndReturnsError(t *testing.T) {
|
||||
server, client := NetPipe()
|
||||
defer server.Close() // nolint: errcheck
|
||||
defer client.Close() // nolint: errcheck
|
||||
|
||||
receivedCh := make(chan []byte)
|
||||
errorsCh := make(chan interface{})
|
||||
onReceive := func(chID byte, msgBytes []byte) {
|
||||
receivedCh <- msgBytes
|
||||
}
|
||||
onError := func(r interface{}) {
|
||||
errorsCh <- r
|
||||
}
|
||||
mconn := createMConnectionWithCallbacks(client, onReceive, onError)
|
||||
err := mconn.Start()
|
||||
require.Nil(t, err)
|
||||
defer mconn.Stop()
|
||||
|
||||
if err := client.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
select {
|
||||
case receivedBytes := <-receivedCh:
|
||||
t.Fatalf("Expected error, got %v", receivedBytes)
|
||||
case err := <-errorsCh:
|
||||
assert.NotNil(t, err)
|
||||
assert.False(t, mconn.IsRunning())
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
t.Fatal("Did not receive error in 500ms")
|
||||
}
|
||||
}
|
||||
|
||||
func newClientAndServerConnsForReadErrors(t *testing.T, chOnErr chan struct{}) (*MConnection, *MConnection) {
|
||||
server, client := NetPipe()
|
||||
|
||||
onReceive := func(chID byte, msgBytes []byte) {}
|
||||
onError := func(r interface{}) {}
|
||||
|
||||
// create client conn with two channels
|
||||
chDescs := []*ChannelDescriptor{
|
||||
{ID: 0x01, Priority: 1, SendQueueCapacity: 1},
|
||||
{ID: 0x02, Priority: 1, SendQueueCapacity: 1},
|
||||
}
|
||||
mconnClient := NewMConnection(client, chDescs, onReceive, onError)
|
||||
mconnClient.SetLogger(log.TestingLogger().With("module", "client"))
|
||||
err := mconnClient.Start()
|
||||
require.Nil(t, err)
|
||||
|
||||
// create server conn with 1 channel
|
||||
// it fires on chOnErr when there's an error
|
||||
serverLogger := log.TestingLogger().With("module", "server")
|
||||
onError = func(r interface{}) {
|
||||
chOnErr <- struct{}{}
|
||||
}
|
||||
mconnServer := createMConnectionWithCallbacks(server, onReceive, onError)
|
||||
mconnServer.SetLogger(serverLogger)
|
||||
err = mconnServer.Start()
|
||||
require.Nil(t, err)
|
||||
return mconnClient, mconnServer
|
||||
}
|
||||
|
||||
func expectSend(ch chan struct{}) bool {
|
||||
after := time.After(time.Second * 5)
|
||||
select {
|
||||
case <-ch:
|
||||
return true
|
||||
case <-after:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func TestMConnectionReadErrorBadEncoding(t *testing.T) {
|
||||
chOnErr := make(chan struct{})
|
||||
mconnClient, mconnServer := newClientAndServerConnsForReadErrors(t, chOnErr)
|
||||
defer mconnClient.Stop()
|
||||
defer mconnServer.Stop()
|
||||
|
||||
client := mconnClient.conn
|
||||
|
||||
// send badly encoded msgPacket
|
||||
bz := cdc.MustMarshalBinary(PacketMsg{})
|
||||
bz[4] += 0x01 // Invalid prefix bytes.
|
||||
|
||||
// Write it.
|
||||
_, err := client.Write(bz)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, expectSend(chOnErr), "badly encoded msgPacket")
|
||||
}
|
||||
|
||||
func TestMConnectionReadErrorUnknownChannel(t *testing.T) {
|
||||
chOnErr := make(chan struct{})
|
||||
mconnClient, mconnServer := newClientAndServerConnsForReadErrors(t, chOnErr)
|
||||
defer mconnClient.Stop()
|
||||
defer mconnServer.Stop()
|
||||
|
||||
msg := []byte("Ant-Man")
|
||||
|
||||
// fail to send msg on channel unknown by client
|
||||
assert.False(t, mconnClient.Send(0x03, msg))
|
||||
|
||||
// send msg on channel unknown by the server.
|
||||
// should cause an error
|
||||
assert.True(t, mconnClient.Send(0x02, msg))
|
||||
assert.True(t, expectSend(chOnErr), "unknown channel")
|
||||
}
|
||||
|
||||
func TestMConnectionReadErrorLongMessage(t *testing.T) {
|
||||
chOnErr := make(chan struct{})
|
||||
chOnRcv := make(chan struct{})
|
||||
|
||||
mconnClient, mconnServer := newClientAndServerConnsForReadErrors(t, chOnErr)
|
||||
defer mconnClient.Stop()
|
||||
defer mconnServer.Stop()
|
||||
|
||||
mconnServer.onReceive = func(chID byte, msgBytes []byte) {
|
||||
chOnRcv <- struct{}{}
|
||||
}
|
||||
|
||||
client := mconnClient.conn
|
||||
|
||||
// send msg thats just right
|
||||
var err error
|
||||
var buf = new(bytes.Buffer)
|
||||
var packet = PacketMsg{
|
||||
ChannelID: 0x01,
|
||||
EOF: 1,
|
||||
Bytes: make([]byte, mconnClient.config.MaxPacketMsgPayloadSize),
|
||||
}
|
||||
_, err = cdc.MarshalBinaryWriter(buf, packet)
|
||||
assert.Nil(t, err)
|
||||
_, err = client.Write(buf.Bytes())
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, expectSend(chOnRcv), "msg just right")
|
||||
assert.False(t, expectSend(chOnErr), "msg just right")
|
||||
|
||||
// send msg thats too long
|
||||
buf = new(bytes.Buffer)
|
||||
packet = PacketMsg{
|
||||
ChannelID: 0x01,
|
||||
EOF: 1,
|
||||
Bytes: make([]byte, mconnClient.config.MaxPacketMsgPayloadSize+100),
|
||||
}
|
||||
_, err = cdc.MarshalBinaryWriter(buf, packet)
|
||||
assert.Nil(t, err)
|
||||
_, err = client.Write(buf.Bytes())
|
||||
assert.NotNil(t, err)
|
||||
assert.False(t, expectSend(chOnRcv), "msg too long")
|
||||
assert.True(t, expectSend(chOnErr), "msg too long")
|
||||
}
|
||||
|
||||
func TestMConnectionReadErrorUnknownMsgType(t *testing.T) {
|
||||
chOnErr := make(chan struct{})
|
||||
mconnClient, mconnServer := newClientAndServerConnsForReadErrors(t, chOnErr)
|
||||
defer mconnClient.Stop()
|
||||
defer mconnServer.Stop()
|
||||
|
||||
// send msg with unknown msg type
|
||||
err := error(nil)
|
||||
err = amino.EncodeUvarint(mconnClient.conn, 4)
|
||||
assert.Nil(t, err)
|
||||
_, err = mconnClient.conn.Write([]byte{0xFF, 0xFF, 0xFF, 0xFF})
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, expectSend(chOnErr), "unknown msg type")
|
||||
}
|
||||
|
||||
func TestMConnectionTrySend(t *testing.T) {
|
||||
server, client := NetPipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
|
||||
mconn := createTestMConnection(client)
|
||||
err := mconn.Start()
|
||||
require.Nil(t, err)
|
||||
defer mconn.Stop()
|
||||
|
||||
msg := []byte("Semicolon-Woman")
|
||||
resultCh := make(chan string, 2)
|
||||
assert.True(t, mconn.TrySend(0x01, msg))
|
||||
server.Read(make([]byte, len(msg)))
|
||||
assert.True(t, mconn.CanSend(0x01))
|
||||
assert.True(t, mconn.TrySend(0x01, msg))
|
||||
assert.False(t, mconn.CanSend(0x01))
|
||||
go func() {
|
||||
mconn.TrySend(0x01, msg)
|
||||
resultCh <- "TrySend"
|
||||
}()
|
||||
assert.False(t, mconn.CanSend(0x01))
|
||||
assert.False(t, mconn.TrySend(0x01, msg))
|
||||
assert.Equal(t, "TrySend", <-resultCh)
|
||||
}
|
||||
@@ -1,352 +0,0 @@
|
||||
// Uses nacl's secret_box to encrypt a net.Conn.
|
||||
// It is (meant to be) an implementation of the STS protocol.
|
||||
// Note we do not (yet) assume that a remote peer's pubkey
|
||||
// is known ahead of time, and thus we are technically
|
||||
// still vulnerable to MITM. (TODO!)
|
||||
// See docs/sts-final.pdf for more info
|
||||
package conn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
crand "crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
"golang.org/x/crypto/ripemd160"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
)
|
||||
|
||||
// 4 + 1024 == 1028 total frame size
|
||||
const dataLenSize = 4
|
||||
const dataMaxSize = 1024
|
||||
const totalFrameSize = dataMaxSize + dataLenSize
|
||||
const sealedFrameSize = totalFrameSize + secretbox.Overhead
|
||||
|
||||
// Implements net.Conn
|
||||
type SecretConnection struct {
|
||||
conn io.ReadWriteCloser
|
||||
recvBuffer []byte
|
||||
recvNonce *[24]byte
|
||||
sendNonce *[24]byte
|
||||
remPubKey crypto.PubKey
|
||||
shrSecret *[32]byte // shared secret
|
||||
}
|
||||
|
||||
// Performs handshake and returns a new authenticated SecretConnection.
|
||||
// Returns nil if error in handshake.
|
||||
// Caller should call conn.Close()
|
||||
// See docs/sts-final.pdf for more information.
|
||||
func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (*SecretConnection, error) {
|
||||
|
||||
locPubKey := locPrivKey.PubKey()
|
||||
|
||||
// Generate ephemeral keys for perfect forward secrecy.
|
||||
locEphPub, locEphPriv := genEphKeys()
|
||||
|
||||
// Write local ephemeral pubkey and receive one too.
|
||||
// NOTE: every 32-byte string is accepted as a Curve25519 public key
|
||||
// (see DJB's Curve25519 paper: http://cr.yp.to/ecdh/curve25519-20060209.pdf)
|
||||
remEphPub, err := shareEphPubKey(conn, locEphPub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Compute common shared secret.
|
||||
shrSecret := computeSharedSecret(remEphPub, locEphPriv)
|
||||
|
||||
// Sort by lexical order.
|
||||
loEphPub, hiEphPub := sort32(locEphPub, remEphPub)
|
||||
|
||||
// Check if the local ephemeral public key
|
||||
// was the least, lexicographically sorted.
|
||||
locIsLeast := bytes.Equal(locEphPub[:], loEphPub[:])
|
||||
|
||||
// Generate nonces to use for secretbox.
|
||||
recvNonce, sendNonce := genNonces(loEphPub, hiEphPub, locIsLeast)
|
||||
|
||||
// Generate common challenge to sign.
|
||||
challenge := genChallenge(loEphPub, hiEphPub)
|
||||
|
||||
// Construct SecretConnection.
|
||||
sc := &SecretConnection{
|
||||
conn: conn,
|
||||
recvBuffer: nil,
|
||||
recvNonce: recvNonce,
|
||||
sendNonce: sendNonce,
|
||||
shrSecret: shrSecret,
|
||||
}
|
||||
|
||||
// Sign the challenge bytes for authentication.
|
||||
locSignature := signChallenge(challenge, locPrivKey)
|
||||
|
||||
// Share (in secret) each other's pubkey & challenge signature
|
||||
authSigMsg, err := shareAuthSignature(sc, locPubKey, locSignature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remPubKey, remSignature := authSigMsg.Key, authSigMsg.Sig
|
||||
if !remPubKey.VerifyBytes(challenge[:], remSignature) {
|
||||
return nil, errors.New("Challenge verification failed")
|
||||
}
|
||||
|
||||
// We've authorized.
|
||||
sc.remPubKey = remPubKey
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// Returns authenticated remote pubkey
|
||||
func (sc *SecretConnection) RemotePubKey() crypto.PubKey {
|
||||
return sc.remPubKey
|
||||
}
|
||||
|
||||
// Writes encrypted frames of `sealedFrameSize`
|
||||
// CONTRACT: data smaller than dataMaxSize is read atomically.
|
||||
func (sc *SecretConnection) Write(data []byte) (n int, err error) {
|
||||
for 0 < len(data) {
|
||||
var frame = make([]byte, totalFrameSize)
|
||||
var chunk []byte
|
||||
if dataMaxSize < len(data) {
|
||||
chunk = data[:dataMaxSize]
|
||||
data = data[dataMaxSize:]
|
||||
} else {
|
||||
chunk = data
|
||||
data = nil
|
||||
}
|
||||
chunkLength := len(chunk)
|
||||
binary.BigEndian.PutUint32(frame, uint32(chunkLength))
|
||||
copy(frame[dataLenSize:], chunk)
|
||||
|
||||
// encrypt the frame
|
||||
var sealedFrame = make([]byte, sealedFrameSize)
|
||||
secretbox.Seal(sealedFrame[:0], frame, sc.sendNonce, sc.shrSecret)
|
||||
// fmt.Printf("secretbox.Seal(sealed:%X,sendNonce:%X,shrSecret:%X\n", sealedFrame, sc.sendNonce, sc.shrSecret)
|
||||
incr2Nonce(sc.sendNonce)
|
||||
// end encryption
|
||||
|
||||
_, err := sc.conn.Write(sealedFrame)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
n += len(chunk)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CONTRACT: data smaller than dataMaxSize is read atomically.
|
||||
func (sc *SecretConnection) Read(data []byte) (n int, err error) {
|
||||
if 0 < len(sc.recvBuffer) {
|
||||
n = copy(data, sc.recvBuffer)
|
||||
sc.recvBuffer = sc.recvBuffer[n:]
|
||||
return
|
||||
}
|
||||
|
||||
sealedFrame := make([]byte, sealedFrameSize)
|
||||
_, err = io.ReadFull(sc.conn, sealedFrame)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// decrypt the frame
|
||||
var frame = make([]byte, totalFrameSize)
|
||||
// fmt.Printf("secretbox.Open(sealed:%X,recvNonce:%X,shrSecret:%X\n", sealedFrame, sc.recvNonce, sc.shrSecret)
|
||||
_, ok := secretbox.Open(frame[:0], sealedFrame, sc.recvNonce, sc.shrSecret)
|
||||
if !ok {
|
||||
return n, errors.New("Failed to decrypt SecretConnection")
|
||||
}
|
||||
incr2Nonce(sc.recvNonce)
|
||||
// end decryption
|
||||
|
||||
var chunkLength = binary.BigEndian.Uint32(frame) // read the first two bytes
|
||||
if chunkLength > dataMaxSize {
|
||||
return 0, errors.New("chunkLength is greater than dataMaxSize")
|
||||
}
|
||||
var chunk = frame[dataLenSize : dataLenSize+chunkLength]
|
||||
|
||||
n = copy(data, chunk)
|
||||
sc.recvBuffer = chunk[n:]
|
||||
return
|
||||
}
|
||||
|
||||
// Implements net.Conn
|
||||
func (sc *SecretConnection) Close() error { return sc.conn.Close() }
|
||||
func (sc *SecretConnection) LocalAddr() net.Addr { return sc.conn.(net.Conn).LocalAddr() }
|
||||
func (sc *SecretConnection) RemoteAddr() net.Addr { return sc.conn.(net.Conn).RemoteAddr() }
|
||||
func (sc *SecretConnection) SetDeadline(t time.Time) error { return sc.conn.(net.Conn).SetDeadline(t) }
|
||||
func (sc *SecretConnection) SetReadDeadline(t time.Time) error {
|
||||
return sc.conn.(net.Conn).SetReadDeadline(t)
|
||||
}
|
||||
func (sc *SecretConnection) SetWriteDeadline(t time.Time) error {
|
||||
return sc.conn.(net.Conn).SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func genEphKeys() (ephPub, ephPriv *[32]byte) {
|
||||
var err error
|
||||
ephPub, ephPriv, err = box.GenerateKey(crand.Reader)
|
||||
if err != nil {
|
||||
panic("Could not generate ephemeral keypairs")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func shareEphPubKey(conn io.ReadWriteCloser, locEphPub *[32]byte) (remEphPub *[32]byte, err error) {
|
||||
|
||||
// Send our pubkey and receive theirs in tandem.
|
||||
var trs, _ = cmn.Parallel(
|
||||
func(_ int) (val interface{}, err error, abort bool) {
|
||||
var _, err1 = cdc.MarshalBinaryWriter(conn, locEphPub)
|
||||
if err1 != nil {
|
||||
return nil, err1, true // abort
|
||||
} else {
|
||||
return nil, nil, false
|
||||
}
|
||||
},
|
||||
func(_ int) (val interface{}, err error, abort bool) {
|
||||
var _remEphPub [32]byte
|
||||
var _, err2 = cdc.UnmarshalBinaryReader(conn, &_remEphPub, 1024*1024) // TODO
|
||||
if err2 != nil {
|
||||
return nil, err2, true // abort
|
||||
} else {
|
||||
return _remEphPub, nil, false
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// If error:
|
||||
if trs.FirstError() != nil {
|
||||
err = trs.FirstError()
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise:
|
||||
var _remEphPub = trs.FirstValue().([32]byte)
|
||||
return &_remEphPub, nil
|
||||
}
|
||||
|
||||
func computeSharedSecret(remPubKey, locPrivKey *[32]byte) (shrSecret *[32]byte) {
|
||||
shrSecret = new([32]byte)
|
||||
box.Precompute(shrSecret, remPubKey, locPrivKey)
|
||||
return
|
||||
}
|
||||
|
||||
func sort32(foo, bar *[32]byte) (lo, hi *[32]byte) {
|
||||
if bytes.Compare(foo[:], bar[:]) < 0 {
|
||||
lo = foo
|
||||
hi = bar
|
||||
} else {
|
||||
lo = bar
|
||||
hi = foo
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func genNonces(loPubKey, hiPubKey *[32]byte, locIsLo bool) (recvNonce, sendNonce *[24]byte) {
|
||||
nonce1 := hash24(append(loPubKey[:], hiPubKey[:]...))
|
||||
nonce2 := new([24]byte)
|
||||
copy(nonce2[:], nonce1[:])
|
||||
nonce2[len(nonce2)-1] ^= 0x01
|
||||
if locIsLo {
|
||||
recvNonce = nonce1
|
||||
sendNonce = nonce2
|
||||
} else {
|
||||
recvNonce = nonce2
|
||||
sendNonce = nonce1
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func genChallenge(loPubKey, hiPubKey *[32]byte) (challenge *[32]byte) {
|
||||
return hash32(append(loPubKey[:], hiPubKey[:]...))
|
||||
}
|
||||
|
||||
func signChallenge(challenge *[32]byte, locPrivKey crypto.PrivKey) (signature crypto.Signature) {
|
||||
signature, err := locPrivKey.Sign(challenge[:])
|
||||
// TODO(ismail): let signChallenge return an error instead
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type authSigMessage struct {
|
||||
Key crypto.PubKey
|
||||
Sig crypto.Signature
|
||||
}
|
||||
|
||||
func shareAuthSignature(sc *SecretConnection, pubKey crypto.PubKey, signature crypto.Signature) (recvMsg authSigMessage, err error) {
|
||||
|
||||
// Send our info and receive theirs in tandem.
|
||||
var trs, _ = cmn.Parallel(
|
||||
func(_ int) (val interface{}, err error, abort bool) {
|
||||
var _, err1 = cdc.MarshalBinaryWriter(sc, authSigMessage{pubKey, signature})
|
||||
if err1 != nil {
|
||||
return nil, err1, true // abort
|
||||
} else {
|
||||
return nil, nil, false
|
||||
}
|
||||
},
|
||||
func(_ int) (val interface{}, err error, abort bool) {
|
||||
var _recvMsg authSigMessage
|
||||
var _, err2 = cdc.UnmarshalBinaryReader(sc, &_recvMsg, 1024*1024) // TODO
|
||||
if err2 != nil {
|
||||
return nil, err2, true // abort
|
||||
} else {
|
||||
return _recvMsg, nil, false
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// If error:
|
||||
if trs.FirstError() != nil {
|
||||
err = trs.FirstError()
|
||||
return
|
||||
}
|
||||
|
||||
var _recvMsg = trs.FirstValue().(authSigMessage)
|
||||
return _recvMsg, nil
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
|
||||
// sha256
|
||||
func hash32(input []byte) (res *[32]byte) {
|
||||
hasher := sha256.New()
|
||||
hasher.Write(input) // nolint: errcheck, gas
|
||||
resSlice := hasher.Sum(nil)
|
||||
res = new([32]byte)
|
||||
copy(res[:], resSlice)
|
||||
return
|
||||
}
|
||||
|
||||
// We only fill in the first 20 bytes with ripemd160
|
||||
func hash24(input []byte) (res *[24]byte) {
|
||||
hasher := ripemd160.New()
|
||||
hasher.Write(input) // nolint: errcheck, gas
|
||||
resSlice := hasher.Sum(nil)
|
||||
res = new([24]byte)
|
||||
copy(res[:], resSlice)
|
||||
return
|
||||
}
|
||||
|
||||
// increment nonce big-endian by 2 with wraparound.
|
||||
func incr2Nonce(nonce *[24]byte) {
|
||||
incrNonce(nonce)
|
||||
incrNonce(nonce)
|
||||
}
|
||||
|
||||
// increment nonce big-endian by 1 with wraparound.
|
||||
func incrNonce(nonce *[24]byte) {
|
||||
for i := 23; 0 <= i; i-- {
|
||||
nonce[i]++
|
||||
if nonce[i] != 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
package conn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
crypto "github.com/tendermint/tendermint/crypto"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
)
|
||||
|
||||
type kvstoreConn struct {
|
||||
*io.PipeReader
|
||||
*io.PipeWriter
|
||||
}
|
||||
|
||||
func (drw kvstoreConn) Close() (err error) {
|
||||
err2 := drw.PipeWriter.CloseWithError(io.EOF)
|
||||
err1 := drw.PipeReader.Close()
|
||||
if err2 != nil {
|
||||
return err
|
||||
}
|
||||
return err1
|
||||
}
|
||||
|
||||
// Each returned ReadWriteCloser is akin to a net.Connection
|
||||
func makeKVStoreConnPair() (fooConn, barConn kvstoreConn) {
|
||||
barReader, fooWriter := io.Pipe()
|
||||
fooReader, barWriter := io.Pipe()
|
||||
return kvstoreConn{fooReader, fooWriter}, kvstoreConn{barReader, barWriter}
|
||||
}
|
||||
|
||||
func makeSecretConnPair(tb testing.TB) (fooSecConn, barSecConn *SecretConnection) {
|
||||
|
||||
var fooConn, barConn = makeKVStoreConnPair()
|
||||
var fooPrvKey = crypto.GenPrivKeyEd25519()
|
||||
var fooPubKey = fooPrvKey.PubKey()
|
||||
var barPrvKey = crypto.GenPrivKeyEd25519()
|
||||
var barPubKey = barPrvKey.PubKey()
|
||||
|
||||
// Make connections from both sides in parallel.
|
||||
var trs, ok = cmn.Parallel(
|
||||
func(_ int) (val interface{}, err error, abort bool) {
|
||||
fooSecConn, err = MakeSecretConnection(fooConn, fooPrvKey)
|
||||
if err != nil {
|
||||
tb.Errorf("Failed to establish SecretConnection for foo: %v", err)
|
||||
return nil, err, true
|
||||
}
|
||||
remotePubBytes := fooSecConn.RemotePubKey()
|
||||
if !remotePubBytes.Equals(barPubKey) {
|
||||
err = fmt.Errorf("Unexpected fooSecConn.RemotePubKey. Expected %v, got %v",
|
||||
barPubKey, fooSecConn.RemotePubKey())
|
||||
tb.Error(err)
|
||||
return nil, err, false
|
||||
}
|
||||
return nil, nil, false
|
||||
},
|
||||
func(_ int) (val interface{}, err error, abort bool) {
|
||||
barSecConn, err = MakeSecretConnection(barConn, barPrvKey)
|
||||
if barSecConn == nil {
|
||||
tb.Errorf("Failed to establish SecretConnection for bar: %v", err)
|
||||
return nil, err, true
|
||||
}
|
||||
remotePubBytes := barSecConn.RemotePubKey()
|
||||
if !remotePubBytes.Equals(fooPubKey) {
|
||||
err = fmt.Errorf("Unexpected barSecConn.RemotePubKey. Expected %v, got %v",
|
||||
fooPubKey, barSecConn.RemotePubKey())
|
||||
tb.Error(err)
|
||||
return nil, nil, false
|
||||
}
|
||||
return nil, nil, false
|
||||
},
|
||||
)
|
||||
|
||||
require.Nil(tb, trs.FirstError())
|
||||
require.True(tb, ok, "Unexpected task abortion")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestSecretConnectionHandshake(t *testing.T) {
|
||||
fooSecConn, barSecConn := makeSecretConnPair(t)
|
||||
if err := fooSecConn.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := barSecConn.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretConnectionReadWrite(t *testing.T) {
|
||||
fooConn, barConn := makeKVStoreConnPair()
|
||||
fooWrites, barWrites := []string{}, []string{}
|
||||
fooReads, barReads := []string{}, []string{}
|
||||
|
||||
// Pre-generate the things to write (for foo & bar)
|
||||
for i := 0; i < 100; i++ {
|
||||
fooWrites = append(fooWrites, cmn.RandStr((cmn.RandInt()%(dataMaxSize*5))+1))
|
||||
barWrites = append(barWrites, cmn.RandStr((cmn.RandInt()%(dataMaxSize*5))+1))
|
||||
}
|
||||
|
||||
// A helper that will run with (fooConn, fooWrites, fooReads) and vice versa
|
||||
genNodeRunner := func(id string, nodeConn kvstoreConn, nodeWrites []string, nodeReads *[]string) cmn.Task {
|
||||
return func(_ int) (interface{}, error, bool) {
|
||||
// Initiate cryptographic private key and secret connection trhough nodeConn.
|
||||
nodePrvKey := crypto.GenPrivKeyEd25519()
|
||||
nodeSecretConn, err := MakeSecretConnection(nodeConn, nodePrvKey)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to establish SecretConnection for node: %v", err)
|
||||
return nil, err, true
|
||||
}
|
||||
// In parallel, handle some reads and writes.
|
||||
var trs, ok = cmn.Parallel(
|
||||
func(_ int) (interface{}, error, bool) {
|
||||
// Node writes:
|
||||
for _, nodeWrite := range nodeWrites {
|
||||
n, err := nodeSecretConn.Write([]byte(nodeWrite))
|
||||
if err != nil {
|
||||
t.Errorf("Failed to write to nodeSecretConn: %v", err)
|
||||
return nil, err, true
|
||||
}
|
||||
if n != len(nodeWrite) {
|
||||
err = fmt.Errorf("Failed to write all bytes. Expected %v, wrote %v", len(nodeWrite), n)
|
||||
t.Error(err)
|
||||
return nil, err, true
|
||||
}
|
||||
}
|
||||
if err := nodeConn.PipeWriter.Close(); err != nil {
|
||||
t.Error(err)
|
||||
return nil, err, true
|
||||
}
|
||||
return nil, nil, false
|
||||
},
|
||||
func(_ int) (interface{}, error, bool) {
|
||||
// Node reads:
|
||||
readBuffer := make([]byte, dataMaxSize)
|
||||
for {
|
||||
n, err := nodeSecretConn.Read(readBuffer)
|
||||
if err == io.EOF {
|
||||
return nil, nil, false
|
||||
} else if err != nil {
|
||||
t.Errorf("Failed to read from nodeSecretConn: %v", err)
|
||||
return nil, err, true
|
||||
}
|
||||
*nodeReads = append(*nodeReads, string(readBuffer[:n]))
|
||||
}
|
||||
if err := nodeConn.PipeReader.Close(); err != nil {
|
||||
t.Error(err)
|
||||
return nil, err, true
|
||||
}
|
||||
return nil, nil, false
|
||||
},
|
||||
)
|
||||
assert.True(t, ok, "Unexpected task abortion")
|
||||
|
||||
// If error:
|
||||
if trs.FirstError() != nil {
|
||||
return nil, trs.FirstError(), true
|
||||
}
|
||||
|
||||
// Otherwise:
|
||||
return nil, nil, false
|
||||
}
|
||||
}
|
||||
|
||||
// Run foo & bar in parallel
|
||||
var trs, ok = cmn.Parallel(
|
||||
genNodeRunner("foo", fooConn, fooWrites, &fooReads),
|
||||
genNodeRunner("bar", barConn, barWrites, &barReads),
|
||||
)
|
||||
require.Nil(t, trs.FirstError())
|
||||
require.True(t, ok, "unexpected task abortion")
|
||||
|
||||
// A helper to ensure that the writes and reads match.
|
||||
// Additionally, small writes (<= dataMaxSize) must be atomically read.
|
||||
compareWritesReads := func(writes []string, reads []string) {
|
||||
for {
|
||||
// Pop next write & corresponding reads
|
||||
var read, write string = "", writes[0]
|
||||
var readCount = 0
|
||||
for _, readChunk := range reads {
|
||||
read += readChunk
|
||||
readCount++
|
||||
if len(write) <= len(read) {
|
||||
break
|
||||
}
|
||||
if len(write) <= dataMaxSize {
|
||||
break // atomicity of small writes
|
||||
}
|
||||
}
|
||||
// Compare
|
||||
if write != read {
|
||||
t.Errorf("Expected to read %X, got %X", write, read)
|
||||
}
|
||||
// Iterate
|
||||
writes = writes[1:]
|
||||
reads = reads[readCount:]
|
||||
if len(writes) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compareWritesReads(fooWrites, barReads)
|
||||
compareWritesReads(barWrites, fooReads)
|
||||
|
||||
}
|
||||
|
||||
func BenchmarkSecretConnection(b *testing.B) {
|
||||
b.StopTimer()
|
||||
fooSecConn, barSecConn := makeSecretConnPair(b)
|
||||
fooWriteText := cmn.RandStr(dataMaxSize)
|
||||
// Consume reads from bar's reader
|
||||
go func() {
|
||||
readBuffer := make([]byte, dataMaxSize)
|
||||
for {
|
||||
_, err := barSecConn.Read(readBuffer)
|
||||
if err == io.EOF {
|
||||
return
|
||||
} else if err != nil {
|
||||
b.Fatalf("Failed to read from barSecConn: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
b.StartTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := fooSecConn.Write([]byte(fooWriteText))
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to write to fooSecConn: %v", err)
|
||||
}
|
||||
}
|
||||
b.StopTimer()
|
||||
|
||||
if err := fooSecConn.Close(); err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
//barSecConn.Close() race condition
|
||||
}
|
||||
|
||||
func fingerprint(bz []byte) []byte {
|
||||
const fbsize = 40
|
||||
if len(bz) < fbsize {
|
||||
return bz
|
||||
} else {
|
||||
return bz[:fbsize]
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package conn
|
||||
|
||||
import (
|
||||
"github.com/tendermint/go-amino"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
)
|
||||
|
||||
var cdc *amino.Codec = amino.NewCodec()
|
||||
|
||||
func init() {
|
||||
crypto.RegisterAmino(cdc)
|
||||
RegisterPacket(cdc)
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package dummy
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
p2p "github.com/tendermint/tendermint/p2p"
|
||||
tmconn "github.com/tendermint/tendermint/p2p/conn"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
)
|
||||
|
||||
type peer struct {
|
||||
cmn.BaseService
|
||||
kv map[string]interface{}
|
||||
}
|
||||
|
||||
var _ p2p.Peer = (*peer)(nil)
|
||||
|
||||
// NewPeer creates new dummy peer.
|
||||
func NewPeer() *peer {
|
||||
p := &peer{
|
||||
kv: make(map[string]interface{}),
|
||||
}
|
||||
p.BaseService = *cmn.NewBaseService(nil, "peer", p)
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// ID always returns dummy.
|
||||
func (p *peer) ID() p2p.ID {
|
||||
return p2p.ID("dummy")
|
||||
}
|
||||
|
||||
// IsOutbound always returns false.
|
||||
func (p *peer) IsOutbound() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsPersistent always returns false.
|
||||
func (p *peer) IsPersistent() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// NodeInfo always returns empty node info.
|
||||
func (p *peer) NodeInfo() p2p.NodeInfo {
|
||||
return p2p.NodeInfo{}
|
||||
}
|
||||
|
||||
// RemoteIP always returns localhost.
|
||||
func (p *peer) RemoteIP() net.IP {
|
||||
return net.ParseIP("127.0.0.1")
|
||||
}
|
||||
|
||||
// Status always returns empry connection status.
|
||||
func (p *peer) Status() tmconn.ConnectionStatus {
|
||||
return tmconn.ConnectionStatus{}
|
||||
}
|
||||
|
||||
// Send does not do anything and just returns true.
|
||||
func (p *peer) Send(byte, []byte) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// TrySend does not do anything and just returns true.
|
||||
func (p *peer) TrySend(byte, []byte) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Set records value under key specified in the map.
|
||||
func (p *peer) Set(key string, value interface{}) {
|
||||
p.kv[key] = value
|
||||
}
|
||||
|
||||
// Get returns a value associated with the key. Nil is returned if no value
|
||||
// found.
|
||||
func (p *peer) Get(key string) interface{} {
|
||||
if value, ok := p.kv[key]; ok {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// ErrSwitchDuplicatePeerID to be raised when a peer is connecting with a known
|
||||
// ID.
|
||||
type ErrSwitchDuplicatePeerID struct {
|
||||
ID ID
|
||||
}
|
||||
|
||||
func (e ErrSwitchDuplicatePeerID) Error() string {
|
||||
return fmt.Sprintf("Duplicate peer ID %v", e.ID)
|
||||
}
|
||||
|
||||
// ErrSwitchDuplicatePeerIP to be raised whena a peer is connecting with a known
|
||||
// IP.
|
||||
type ErrSwitchDuplicatePeerIP struct {
|
||||
IP net.IP
|
||||
}
|
||||
|
||||
func (e ErrSwitchDuplicatePeerIP) Error() string {
|
||||
return fmt.Sprintf("Duplicate peer IP %v", e.IP.String())
|
||||
}
|
||||
|
||||
// ErrSwitchConnectToSelf to be raised when trying to connect to itself.
|
||||
type ErrSwitchConnectToSelf struct {
|
||||
Addr *NetAddress
|
||||
}
|
||||
|
||||
func (e ErrSwitchConnectToSelf) Error() string {
|
||||
return fmt.Sprintf("Connect to self: %v", e.Addr)
|
||||
}
|
||||
|
||||
type ErrSwitchAuthenticationFailure struct {
|
||||
Dialed *NetAddress
|
||||
Got ID
|
||||
}
|
||||
|
||||
func (e ErrSwitchAuthenticationFailure) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"Failed to authenticate peer. Dialed %v, but got peer with ID %s",
|
||||
e.Dialed,
|
||||
e.Got,
|
||||
)
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
|
||||
type ErrNetAddressNoID struct {
|
||||
Addr string
|
||||
}
|
||||
|
||||
func (e ErrNetAddressNoID) Error() string {
|
||||
return fmt.Sprintf("Address (%s) does not contain ID", e.Addr)
|
||||
}
|
||||
|
||||
type ErrNetAddressInvalid struct {
|
||||
Addr string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ErrNetAddressInvalid) Error() string {
|
||||
return fmt.Sprintf("Invalid address (%s): %v", e.Addr, e.Err)
|
||||
}
|
||||
|
||||
type ErrNetAddressLookup struct {
|
||||
Addr string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ErrNetAddressLookup) Error() string {
|
||||
return fmt.Sprintf("Error looking up host (%s): %v", e.Addr, e.Err)
|
||||
}
|
||||
-152
@@ -1,152 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tendermint/tendermint/config"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
)
|
||||
|
||||
// FuzzedConnection wraps any net.Conn and depending on the mode either delays
|
||||
// reads/writes or randomly drops reads/writes/connections.
|
||||
type FuzzedConnection struct {
|
||||
conn net.Conn
|
||||
|
||||
mtx sync.Mutex
|
||||
start <-chan time.Time
|
||||
active bool
|
||||
|
||||
config *config.FuzzConnConfig
|
||||
}
|
||||
|
||||
// FuzzConn creates a new FuzzedConnection. Fuzzing starts immediately.
|
||||
func FuzzConn(conn net.Conn) net.Conn {
|
||||
return FuzzConnFromConfig(conn, config.DefaultFuzzConnConfig())
|
||||
}
|
||||
|
||||
// FuzzConnFromConfig creates a new FuzzedConnection from a config. Fuzzing
|
||||
// starts immediately.
|
||||
func FuzzConnFromConfig(conn net.Conn, config *config.FuzzConnConfig) net.Conn {
|
||||
return &FuzzedConnection{
|
||||
conn: conn,
|
||||
start: make(<-chan time.Time),
|
||||
active: true,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// FuzzConnAfter creates a new FuzzedConnection. Fuzzing starts when the
|
||||
// duration elapses.
|
||||
func FuzzConnAfter(conn net.Conn, d time.Duration) net.Conn {
|
||||
return FuzzConnAfterFromConfig(conn, d, config.DefaultFuzzConnConfig())
|
||||
}
|
||||
|
||||
// FuzzConnAfterFromConfig creates a new FuzzedConnection from a config.
|
||||
// Fuzzing starts when the duration elapses.
|
||||
func FuzzConnAfterFromConfig(
|
||||
conn net.Conn,
|
||||
d time.Duration,
|
||||
config *config.FuzzConnConfig,
|
||||
) net.Conn {
|
||||
return &FuzzedConnection{
|
||||
conn: conn,
|
||||
start: time.After(d),
|
||||
active: false,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Config returns the connection's config.
|
||||
func (fc *FuzzedConnection) Config() *config.FuzzConnConfig {
|
||||
return fc.config
|
||||
}
|
||||
|
||||
// Read implements net.Conn.
|
||||
func (fc *FuzzedConnection) Read(data []byte) (n int, err error) {
|
||||
if fc.fuzz() {
|
||||
return 0, nil
|
||||
}
|
||||
return fc.conn.Read(data)
|
||||
}
|
||||
|
||||
// Write implements net.Conn.
|
||||
func (fc *FuzzedConnection) Write(data []byte) (n int, err error) {
|
||||
if fc.fuzz() {
|
||||
return 0, nil
|
||||
}
|
||||
return fc.conn.Write(data)
|
||||
}
|
||||
|
||||
// Close implements net.Conn.
|
||||
func (fc *FuzzedConnection) Close() error { return fc.conn.Close() }
|
||||
|
||||
// LocalAddr implements net.Conn.
|
||||
func (fc *FuzzedConnection) LocalAddr() net.Addr { return fc.conn.LocalAddr() }
|
||||
|
||||
// RemoteAddr implements net.Conn.
|
||||
func (fc *FuzzedConnection) RemoteAddr() net.Addr { return fc.conn.RemoteAddr() }
|
||||
|
||||
// SetDeadline implements net.Conn.
|
||||
func (fc *FuzzedConnection) SetDeadline(t time.Time) error { return fc.conn.SetDeadline(t) }
|
||||
|
||||
// SetReadDeadline implements net.Conn.
|
||||
func (fc *FuzzedConnection) SetReadDeadline(t time.Time) error {
|
||||
return fc.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
// SetWriteDeadline implements net.Conn.
|
||||
func (fc *FuzzedConnection) SetWriteDeadline(t time.Time) error {
|
||||
return fc.conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (fc *FuzzedConnection) randomDuration() time.Duration {
|
||||
maxDelayMillis := int(fc.config.MaxDelay.Nanoseconds() / 1000)
|
||||
return time.Millisecond * time.Duration(cmn.RandInt()%maxDelayMillis) // nolint: gas
|
||||
}
|
||||
|
||||
// implements the fuzz (delay, kill conn)
|
||||
// and returns whether or not the read/write should be ignored
|
||||
func (fc *FuzzedConnection) fuzz() bool {
|
||||
if !fc.shouldFuzz() {
|
||||
return false
|
||||
}
|
||||
|
||||
switch fc.config.Mode {
|
||||
case config.FuzzModeDrop:
|
||||
// randomly drop the r/w, drop the conn, or sleep
|
||||
r := cmn.RandFloat64()
|
||||
if r <= fc.config.ProbDropRW {
|
||||
return true
|
||||
} else if r < fc.config.ProbDropRW+fc.config.ProbDropConn {
|
||||
// XXX: can't this fail because machine precision?
|
||||
// XXX: do we need an error?
|
||||
fc.Close() // nolint: errcheck, gas
|
||||
return true
|
||||
} else if r < fc.config.ProbDropRW+fc.config.ProbDropConn+fc.config.ProbSleep {
|
||||
time.Sleep(fc.randomDuration())
|
||||
}
|
||||
case config.FuzzModeDelay:
|
||||
// sleep a bit
|
||||
time.Sleep(fc.randomDuration())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (fc *FuzzedConnection) shouldFuzz() bool {
|
||||
if fc.active {
|
||||
return true
|
||||
}
|
||||
|
||||
fc.mtx.Lock()
|
||||
defer fc.mtx.Unlock()
|
||||
|
||||
select {
|
||||
case <-fc.start:
|
||||
fc.active = true
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
crypto "github.com/tendermint/tendermint/crypto"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
)
|
||||
|
||||
// ID is a hex-encoded crypto.Address
|
||||
type ID string
|
||||
|
||||
// IDByteLength is the length of a crypto.Address. Currently only 20.
|
||||
// TODO: support other length addresses ?
|
||||
const IDByteLength = 20
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Persistent peer ID
|
||||
// TODO: encrypt on disk
|
||||
|
||||
// NodeKey is the persistent peer key.
|
||||
// It contains the nodes private key for authentication.
|
||||
type NodeKey struct {
|
||||
PrivKey crypto.PrivKey `json:"priv_key"` // our priv key
|
||||
}
|
||||
|
||||
// ID returns the peer's canonical ID - the hash of its public key.
|
||||
func (nodeKey *NodeKey) ID() ID {
|
||||
return PubKeyToID(nodeKey.PubKey())
|
||||
}
|
||||
|
||||
// PubKey returns the peer's PubKey
|
||||
func (nodeKey *NodeKey) PubKey() crypto.PubKey {
|
||||
return nodeKey.PrivKey.PubKey()
|
||||
}
|
||||
|
||||
// PubKeyToID returns the ID corresponding to the given PubKey.
|
||||
// It's the hex-encoding of the pubKey.Address().
|
||||
func PubKeyToID(pubKey crypto.PubKey) ID {
|
||||
return ID(hex.EncodeToString(pubKey.Address()))
|
||||
}
|
||||
|
||||
// LoadOrGenNodeKey attempts to load the NodeKey from the given filePath.
|
||||
// If the file does not exist, it generates and saves a new NodeKey.
|
||||
func LoadOrGenNodeKey(filePath string) (*NodeKey, error) {
|
||||
if cmn.FileExists(filePath) {
|
||||
nodeKey, err := LoadNodeKey(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nodeKey, nil
|
||||
}
|
||||
return genNodeKey(filePath)
|
||||
}
|
||||
|
||||
func LoadNodeKey(filePath string) (*NodeKey, error) {
|
||||
jsonBytes, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodeKey := new(NodeKey)
|
||||
err = cdc.UnmarshalJSON(jsonBytes, nodeKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error reading NodeKey from %v: %v", filePath, err)
|
||||
}
|
||||
return nodeKey, nil
|
||||
}
|
||||
|
||||
func genNodeKey(filePath string) (*NodeKey, error) {
|
||||
privKey := crypto.GenPrivKeyEd25519()
|
||||
nodeKey := &NodeKey{
|
||||
PrivKey: privKey,
|
||||
}
|
||||
|
||||
jsonBytes, err := cdc.MarshalJSON(nodeKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = ioutil.WriteFile(filePath, jsonBytes, 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nodeKey, nil
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// MakePoWTarget returns the big-endian encoding of 2^(targetBits - difficulty) - 1.
|
||||
// It can be used as a Proof of Work target.
|
||||
// NOTE: targetBits must be a multiple of 8 and difficulty must be less than targetBits.
|
||||
func MakePoWTarget(difficulty, targetBits uint) []byte {
|
||||
if targetBits%8 != 0 {
|
||||
panic(fmt.Sprintf("targetBits (%d) not a multiple of 8", targetBits))
|
||||
}
|
||||
if difficulty >= targetBits {
|
||||
panic(fmt.Sprintf("difficulty (%d) >= targetBits (%d)", difficulty, targetBits))
|
||||
}
|
||||
targetBytes := targetBits / 8
|
||||
zeroPrefixLen := (int(difficulty) / 8)
|
||||
prefix := bytes.Repeat([]byte{0}, zeroPrefixLen)
|
||||
mod := (difficulty % 8)
|
||||
if mod > 0 {
|
||||
nonZeroPrefix := byte(1<<(8-mod) - 1)
|
||||
prefix = append(prefix, nonZeroPrefix)
|
||||
}
|
||||
tailLen := int(targetBytes) - len(prefix)
|
||||
return append(prefix, bytes.Repeat([]byte{0xFF}, tailLen)...)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
)
|
||||
|
||||
func TestLoadOrGenNodeKey(t *testing.T) {
|
||||
filePath := filepath.Join(os.TempDir(), cmn.RandStr(12)+"_peer_id.json")
|
||||
|
||||
nodeKey, err := LoadOrGenNodeKey(filePath)
|
||||
assert.Nil(t, err)
|
||||
|
||||
nodeKey2, err := LoadOrGenNodeKey(filePath)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, nodeKey, nodeKey2)
|
||||
}
|
||||
|
||||
//----------------------------------------------------------
|
||||
|
||||
func padBytes(bz []byte, targetBytes int) []byte {
|
||||
return append(bz, bytes.Repeat([]byte{0xFF}, targetBytes-len(bz))...)
|
||||
}
|
||||
|
||||
func TestPoWTarget(t *testing.T) {
|
||||
|
||||
targetBytes := 20
|
||||
cases := []struct {
|
||||
difficulty uint
|
||||
target []byte
|
||||
}{
|
||||
{0, padBytes([]byte{}, targetBytes)},
|
||||
{1, padBytes([]byte{127}, targetBytes)},
|
||||
{8, padBytes([]byte{0}, targetBytes)},
|
||||
{9, padBytes([]byte{0, 127}, targetBytes)},
|
||||
{10, padBytes([]byte{0, 63}, targetBytes)},
|
||||
{16, padBytes([]byte{0, 0}, targetBytes)},
|
||||
{17, padBytes([]byte{0, 0, 127}, targetBytes)},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, MakePoWTarget(c.difficulty, 20*8), c.target)
|
||||
}
|
||||
}
|
||||
-284
@@ -1,284 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/p2p/upnp"
|
||||
)
|
||||
|
||||
// Listener is a network listener for stream-oriented protocols, providing
|
||||
// convenient methods to get listener's internal and external addresses.
|
||||
// Clients are supposed to read incoming connections from a channel, returned
|
||||
// by Connections() method.
|
||||
type Listener interface {
|
||||
Connections() <-chan net.Conn
|
||||
InternalAddress() *NetAddress
|
||||
ExternalAddress() *NetAddress
|
||||
ExternalAddressHost() string
|
||||
String() string
|
||||
Stop() error
|
||||
}
|
||||
|
||||
// DefaultListener is a cmn.Service, running net.Listener underneath.
|
||||
// Optionally, UPnP is used upon calling NewDefaultListener to resolve external
|
||||
// address.
|
||||
type DefaultListener struct {
|
||||
cmn.BaseService
|
||||
|
||||
listener net.Listener
|
||||
intAddr *NetAddress
|
||||
extAddr *NetAddress
|
||||
connections chan net.Conn
|
||||
}
|
||||
|
||||
var _ Listener = (*DefaultListener)(nil)
|
||||
|
||||
const (
|
||||
numBufferedConnections = 10
|
||||
defaultExternalPort = 8770
|
||||
tryListenSeconds = 5
|
||||
)
|
||||
|
||||
func splitHostPort(addr string) (host string, port int) {
|
||||
host, portStr, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
port, err = strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return host, port
|
||||
}
|
||||
|
||||
// NewDefaultListener creates a new DefaultListener on lAddr, optionally trying
|
||||
// to determine external address using UPnP.
|
||||
func NewDefaultListener(
|
||||
fullListenAddrString string,
|
||||
externalAddrString string,
|
||||
useUPnP bool,
|
||||
logger log.Logger) Listener {
|
||||
|
||||
// Split protocol, address, and port.
|
||||
protocol, lAddr := cmn.ProtocolAndAddress(fullListenAddrString)
|
||||
lAddrIP, lAddrPort := splitHostPort(lAddr)
|
||||
|
||||
// Create listener
|
||||
var listener net.Listener
|
||||
var err error
|
||||
for i := 0; i < tryListenSeconds; i++ {
|
||||
listener, err = net.Listen(protocol, lAddr)
|
||||
if err == nil {
|
||||
break
|
||||
} else if i < tryListenSeconds-1 {
|
||||
time.Sleep(time.Second * 1)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Actual listener local IP & port
|
||||
listenerIP, listenerPort := splitHostPort(listener.Addr().String())
|
||||
logger.Info("Local listener", "ip", listenerIP, "port", listenerPort)
|
||||
|
||||
// Determine internal address...
|
||||
var intAddr *NetAddress
|
||||
intAddr, err = NewNetAddressStringWithOptionalID(lAddr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
inAddrAny := lAddrIP == "" || lAddrIP == "0.0.0.0"
|
||||
|
||||
// Determine external address.
|
||||
var extAddr *NetAddress
|
||||
|
||||
if externalAddrString != "" {
|
||||
var err error
|
||||
extAddr, err = NewNetAddressStringWithOptionalID(externalAddrString)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error in ExternalAddress: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// If the lAddrIP is INADDR_ANY, try UPnP.
|
||||
if extAddr == nil && useUPnP && inAddrAny {
|
||||
extAddr = getUPNPExternalAddress(lAddrPort, listenerPort, logger)
|
||||
}
|
||||
|
||||
// Otherwise just use the local address.
|
||||
if extAddr == nil {
|
||||
defaultToIPv4 := inAddrAny
|
||||
extAddr = getNaiveExternalAddress(defaultToIPv4, listenerPort, false, logger)
|
||||
}
|
||||
if extAddr == nil {
|
||||
panic("Could not determine external address!")
|
||||
}
|
||||
|
||||
dl := &DefaultListener{
|
||||
listener: listener,
|
||||
intAddr: intAddr,
|
||||
extAddr: extAddr,
|
||||
connections: make(chan net.Conn, numBufferedConnections),
|
||||
}
|
||||
dl.BaseService = *cmn.NewBaseService(logger, "DefaultListener", dl)
|
||||
err = dl.Start() // Started upon construction
|
||||
if err != nil {
|
||||
logger.Error("Error starting base service", "err", err)
|
||||
}
|
||||
return dl
|
||||
}
|
||||
|
||||
// OnStart implements cmn.Service by spinning a goroutine, listening for new
|
||||
// connections.
|
||||
func (l *DefaultListener) OnStart() error {
|
||||
if err := l.BaseService.OnStart(); err != nil {
|
||||
return err
|
||||
}
|
||||
go l.listenRoutine()
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnStop implements cmn.Service by closing the listener.
|
||||
func (l *DefaultListener) OnStop() {
|
||||
l.BaseService.OnStop()
|
||||
l.listener.Close() // nolint: errcheck
|
||||
}
|
||||
|
||||
// Accept connections and pass on the channel
|
||||
func (l *DefaultListener) listenRoutine() {
|
||||
for {
|
||||
conn, err := l.listener.Accept()
|
||||
|
||||
if !l.IsRunning() {
|
||||
break // Go to cleanup
|
||||
}
|
||||
|
||||
// listener wasn't stopped,
|
||||
// yet we encountered an error.
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
l.connections <- conn
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
close(l.connections)
|
||||
for range l.connections {
|
||||
// Drain
|
||||
}
|
||||
}
|
||||
|
||||
// Connections returns a channel of inbound connections.
|
||||
// It gets closed when the listener closes.
|
||||
func (l *DefaultListener) Connections() <-chan net.Conn {
|
||||
return l.connections
|
||||
}
|
||||
|
||||
// InternalAddress returns the internal NetAddress (address used for
|
||||
// listening).
|
||||
func (l *DefaultListener) InternalAddress() *NetAddress {
|
||||
return l.intAddr
|
||||
}
|
||||
|
||||
// ExternalAddress returns the external NetAddress (publicly available,
|
||||
// determined using either UPnP or local resolver).
|
||||
func (l *DefaultListener) ExternalAddress() *NetAddress {
|
||||
return l.extAddr
|
||||
}
|
||||
|
||||
// ExternalAddressHost returns the external NetAddress IP string. If an IP is
|
||||
// IPv6, it's wrapped in brackets ("[2001:db8:1f70::999:de8:7648:6e8]").
|
||||
func (l *DefaultListener) ExternalAddressHost() string {
|
||||
ip := l.ExternalAddress().IP
|
||||
if isIpv6(ip) {
|
||||
// Means it's ipv6, so format it with brackets
|
||||
return "[" + ip.String() + "]"
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
func (l *DefaultListener) String() string {
|
||||
return fmt.Sprintf("Listener(@%v)", l.extAddr)
|
||||
}
|
||||
|
||||
/* external address helpers */
|
||||
|
||||
// UPNP external address discovery & port mapping
|
||||
func getUPNPExternalAddress(externalPort, internalPort int, logger log.Logger) *NetAddress {
|
||||
logger.Info("Getting UPNP external address")
|
||||
nat, err := upnp.Discover()
|
||||
if err != nil {
|
||||
logger.Info("Could not perform UPNP discover", "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
ext, err := nat.GetExternalAddress()
|
||||
if err != nil {
|
||||
logger.Info("Could not get UPNP external address", "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UPnP can't seem to get the external port, so let's just be explicit.
|
||||
if externalPort == 0 {
|
||||
externalPort = defaultExternalPort
|
||||
}
|
||||
|
||||
externalPort, err = nat.AddPortMapping("tcp", externalPort, internalPort, "tendermint", 0)
|
||||
if err != nil {
|
||||
logger.Info("Could not add UPNP port mapping", "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Info("Got UPNP external address", "address", ext)
|
||||
return NewNetAddressIPPort(ext, uint16(externalPort))
|
||||
}
|
||||
|
||||
func isIpv6(ip net.IP) bool {
|
||||
v4 := ip.To4()
|
||||
if v4 != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
ipString := ip.String()
|
||||
|
||||
// Extra check just to be sure it's IPv6
|
||||
return (strings.Contains(ipString, ":") && !strings.Contains(ipString, "."))
|
||||
}
|
||||
|
||||
// TODO: use syscalls: see issue #712
|
||||
func getNaiveExternalAddress(defaultToIPv4 bool, port int, settleForLocal bool, logger log.Logger) *NetAddress {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
panic(cmn.Fmt("Could not fetch interface addresses: %v", err))
|
||||
}
|
||||
|
||||
for _, a := range addrs {
|
||||
ipnet, ok := a.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if defaultToIPv4 || !isIpv6(ipnet.IP) {
|
||||
v4 := ipnet.IP.To4()
|
||||
if v4 == nil || (!settleForLocal && v4[0] == 127) {
|
||||
// loopback
|
||||
continue
|
||||
}
|
||||
} else if !settleForLocal && ipnet.IP.IsLoopback() {
|
||||
// IPv6, check for loopback
|
||||
continue
|
||||
}
|
||||
return NewNetAddressIPPort(ipnet.IP, uint16(port))
|
||||
}
|
||||
|
||||
// try again, but settle for local
|
||||
logger.Info("Node may not be connected to internet. Settling for local address")
|
||||
return getNaiveExternalAddress(defaultToIPv4, port, true, logger)
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
)
|
||||
|
||||
func TestListener(t *testing.T) {
|
||||
// Create a listener
|
||||
l := NewDefaultListener("tcp://:8001", "", false, log.TestingLogger())
|
||||
|
||||
// Dial the listener
|
||||
lAddr := l.ExternalAddress()
|
||||
connOut, err := lAddr.Dial()
|
||||
if err != nil {
|
||||
t.Fatalf("Could not connect to listener address %v", lAddr)
|
||||
} else {
|
||||
t.Logf("Created a connection to listener address %v", lAddr)
|
||||
}
|
||||
connIn, ok := <-l.Connections()
|
||||
if !ok {
|
||||
t.Fatalf("Could not get inbound connection from listener")
|
||||
}
|
||||
|
||||
msg := []byte("hi!")
|
||||
go func() {
|
||||
_, err := connIn.Write(msg)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
b := make([]byte, 32)
|
||||
n, err := connOut.Read(b)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading off connection: %v", err)
|
||||
}
|
||||
|
||||
b = b[:n]
|
||||
if !bytes.Equal(msg, b) {
|
||||
t.Fatalf("Got %s, expected %s", b, msg)
|
||||
}
|
||||
|
||||
// Close the server, no longer needed.
|
||||
l.Stop()
|
||||
}
|
||||
|
||||
func TestExternalAddress(t *testing.T) {
|
||||
{
|
||||
// Create a listener with no external addr. Should default
|
||||
// to local ipv4.
|
||||
l := NewDefaultListener("tcp://:8001", "", false, log.TestingLogger())
|
||||
lAddr := l.ExternalAddress().String()
|
||||
_, _, err := net.SplitHostPort(lAddr)
|
||||
require.Nil(t, err)
|
||||
spl := strings.Split(lAddr, ".")
|
||||
require.Equal(t, len(spl), 4)
|
||||
l.Stop()
|
||||
}
|
||||
|
||||
{
|
||||
// Create a listener with set external ipv4 addr.
|
||||
setExAddr := "8.8.8.8:8080"
|
||||
l := NewDefaultListener("tcp://:8001", setExAddr, false, log.TestingLogger())
|
||||
lAddr := l.ExternalAddress().String()
|
||||
require.Equal(t, lAddr, setExAddr)
|
||||
l.Stop()
|
||||
}
|
||||
|
||||
{
|
||||
// Invalid external addr causes panic
|
||||
setExAddr := "awrlsckjnal:8080"
|
||||
require.Panics(t, func() { NewDefaultListener("tcp://:8001", setExAddr, false, log.TestingLogger()) })
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/go-kit/kit/metrics/discard"
|
||||
|
||||
prometheus "github.com/go-kit/kit/metrics/prometheus"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// Metrics contains metrics exposed by this package.
|
||||
type Metrics struct {
|
||||
// Number of peers.
|
||||
Peers metrics.Gauge
|
||||
}
|
||||
|
||||
// PrometheusMetrics returns Metrics build using Prometheus client library.
|
||||
func PrometheusMetrics() *Metrics {
|
||||
return &Metrics{
|
||||
Peers: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{
|
||||
Subsystem: "p2p",
|
||||
Name: "peers",
|
||||
Help: "Number of peers.",
|
||||
}, []string{}),
|
||||
}
|
||||
}
|
||||
|
||||
// NopMetrics returns no-op Metrics.
|
||||
func NopMetrics() *Metrics {
|
||||
return &Metrics{
|
||||
Peers: discard.NewGauge(),
|
||||
}
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
// Modified for Tendermint
|
||||
// Originally Copyright (c) 2013-2014 Conformal Systems LLC.
|
||||
// https://github.com/conformal/btcd/blob/master/LICENSE
|
||||
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
)
|
||||
|
||||
// NetAddress defines information about a peer on the network
|
||||
// including its ID, IP address, and port.
|
||||
type NetAddress struct {
|
||||
ID ID `json:"id"`
|
||||
IP net.IP `json:"ip"`
|
||||
Port uint16 `json:"port"`
|
||||
|
||||
// TODO:
|
||||
// Name string `json:"name"` // optional DNS name
|
||||
|
||||
// memoize .String()
|
||||
str string
|
||||
}
|
||||
|
||||
// IDAddressString returns id@hostPort.
|
||||
func IDAddressString(id ID, hostPort string) string {
|
||||
return fmt.Sprintf("%s@%s", id, hostPort)
|
||||
}
|
||||
|
||||
// NewNetAddress returns a new NetAddress using the provided TCP
|
||||
// address. When testing, other net.Addr (except TCP) will result in
|
||||
// using 0.0.0.0:0. When normal run, other net.Addr (except TCP) will
|
||||
// panic.
|
||||
// TODO: socks proxies?
|
||||
func NewNetAddress(id ID, addr net.Addr) *NetAddress {
|
||||
tcpAddr, ok := addr.(*net.TCPAddr)
|
||||
if !ok {
|
||||
if flag.Lookup("test.v") == nil { // normal run
|
||||
cmn.PanicSanity(cmn.Fmt("Only TCPAddrs are supported. Got: %v", addr))
|
||||
} else { // in testing
|
||||
netAddr := NewNetAddressIPPort(net.IP("0.0.0.0"), 0)
|
||||
netAddr.ID = id
|
||||
return netAddr
|
||||
}
|
||||
}
|
||||
ip := tcpAddr.IP
|
||||
port := uint16(tcpAddr.Port)
|
||||
na := NewNetAddressIPPort(ip, port)
|
||||
na.ID = id
|
||||
return na
|
||||
}
|
||||
|
||||
// NewNetAddressString returns a new NetAddress using the provided address in
|
||||
// the form of "ID@IP:Port".
|
||||
// Also resolves the host if host is not an IP.
|
||||
// Errors are of type ErrNetAddressXxx where Xxx is in (NoID, Invalid, Lookup)
|
||||
func NewNetAddressString(addr string) (*NetAddress, error) {
|
||||
spl := strings.Split(addr, "@")
|
||||
if len(spl) < 2 {
|
||||
return nil, ErrNetAddressNoID{addr}
|
||||
}
|
||||
return NewNetAddressStringWithOptionalID(addr)
|
||||
}
|
||||
|
||||
// NewNetAddressStringWithOptionalID returns a new NetAddress using the
|
||||
// provided address in the form of "ID@IP:Port", where the ID is optional.
|
||||
// Also resolves the host if host is not an IP.
|
||||
func NewNetAddressStringWithOptionalID(addr string) (*NetAddress, error) {
|
||||
addrWithoutProtocol := removeProtocolIfDefined(addr)
|
||||
|
||||
var id ID
|
||||
spl := strings.Split(addrWithoutProtocol, "@")
|
||||
if len(spl) == 2 {
|
||||
idStr := spl[0]
|
||||
idBytes, err := hex.DecodeString(idStr)
|
||||
if err != nil {
|
||||
return nil, ErrNetAddressInvalid{addrWithoutProtocol, err}
|
||||
}
|
||||
if len(idBytes) != IDByteLength {
|
||||
return nil, ErrNetAddressInvalid{
|
||||
addrWithoutProtocol,
|
||||
fmt.Errorf("invalid hex length - got %d, expected %d", len(idBytes), IDByteLength)}
|
||||
}
|
||||
|
||||
id, addrWithoutProtocol = ID(idStr), spl[1]
|
||||
}
|
||||
|
||||
host, portStr, err := net.SplitHostPort(addrWithoutProtocol)
|
||||
if err != nil {
|
||||
return nil, ErrNetAddressInvalid{addrWithoutProtocol, err}
|
||||
}
|
||||
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
if len(host) > 0 {
|
||||
ips, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
return nil, ErrNetAddressLookup{host, err}
|
||||
}
|
||||
ip = ips[0]
|
||||
}
|
||||
}
|
||||
|
||||
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
return nil, ErrNetAddressInvalid{portStr, err}
|
||||
}
|
||||
|
||||
na := NewNetAddressIPPort(ip, uint16(port))
|
||||
na.ID = id
|
||||
return na, nil
|
||||
}
|
||||
|
||||
// NewNetAddressStrings returns an array of NetAddress'es build using
|
||||
// the provided strings.
|
||||
func NewNetAddressStrings(addrs []string) ([]*NetAddress, []error) {
|
||||
netAddrs := make([]*NetAddress, 0)
|
||||
errs := make([]error, 0)
|
||||
for _, addr := range addrs {
|
||||
netAddr, err := NewNetAddressString(addr)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
netAddrs = append(netAddrs, netAddr)
|
||||
}
|
||||
}
|
||||
return netAddrs, errs
|
||||
}
|
||||
|
||||
// NewNetAddressIPPort returns a new NetAddress using the provided IP
|
||||
// and port number.
|
||||
func NewNetAddressIPPort(ip net.IP, port uint16) *NetAddress {
|
||||
return &NetAddress{
|
||||
IP: ip,
|
||||
Port: port,
|
||||
}
|
||||
}
|
||||
|
||||
// Equals reports whether na and other are the same addresses,
|
||||
// including their ID, IP, and Port.
|
||||
func (na *NetAddress) Equals(other interface{}) bool {
|
||||
if o, ok := other.(*NetAddress); ok {
|
||||
return na.String() == o.String()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Same returns true is na has the same non-empty ID or DialString as other.
|
||||
func (na *NetAddress) Same(other interface{}) bool {
|
||||
if o, ok := other.(*NetAddress); ok {
|
||||
if na.DialString() == o.DialString() {
|
||||
return true
|
||||
}
|
||||
if na.ID != "" && na.ID == o.ID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// String representation: <ID>@<IP>:<PORT>
|
||||
func (na *NetAddress) String() string {
|
||||
if na.str == "" {
|
||||
addrStr := na.DialString()
|
||||
if na.ID != "" {
|
||||
addrStr = IDAddressString(na.ID, addrStr)
|
||||
}
|
||||
na.str = addrStr
|
||||
}
|
||||
return na.str
|
||||
}
|
||||
|
||||
func (na *NetAddress) DialString() string {
|
||||
return net.JoinHostPort(
|
||||
na.IP.String(),
|
||||
strconv.FormatUint(uint64(na.Port), 10),
|
||||
)
|
||||
}
|
||||
|
||||
// Dial calls net.Dial on the address.
|
||||
func (na *NetAddress) Dial() (net.Conn, error) {
|
||||
conn, err := net.Dial("tcp", na.DialString())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// DialTimeout calls net.DialTimeout on the address.
|
||||
func (na *NetAddress) DialTimeout(timeout time.Duration) (net.Conn, error) {
|
||||
conn, err := net.DialTimeout("tcp", na.DialString(), timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Routable returns true if the address is routable.
|
||||
func (na *NetAddress) Routable() bool {
|
||||
// TODO(oga) bitcoind doesn't include RFC3849 here, but should we?
|
||||
return na.Valid() && !(na.RFC1918() || na.RFC3927() || na.RFC4862() ||
|
||||
na.RFC4193() || na.RFC4843() || na.Local())
|
||||
}
|
||||
|
||||
// For IPv4 these are either a 0 or all bits set address. For IPv6 a zero
|
||||
// address or one that matches the RFC3849 documentation address format.
|
||||
func (na *NetAddress) Valid() bool {
|
||||
return na.IP != nil && !(na.IP.IsUnspecified() || na.RFC3849() ||
|
||||
na.IP.Equal(net.IPv4bcast))
|
||||
}
|
||||
|
||||
// Local returns true if it is a local address.
|
||||
func (na *NetAddress) Local() bool {
|
||||
return na.IP.IsLoopback() || zero4.Contains(na.IP)
|
||||
}
|
||||
|
||||
// ReachabilityTo checks whenever o can be reached from na.
|
||||
func (na *NetAddress) ReachabilityTo(o *NetAddress) int {
|
||||
const (
|
||||
Unreachable = 0
|
||||
Default = iota
|
||||
Teredo
|
||||
Ipv6_weak
|
||||
Ipv4
|
||||
Ipv6_strong
|
||||
)
|
||||
if !na.Routable() {
|
||||
return Unreachable
|
||||
} else if na.RFC4380() {
|
||||
if !o.Routable() {
|
||||
return Default
|
||||
} else if o.RFC4380() {
|
||||
return Teredo
|
||||
} else if o.IP.To4() != nil {
|
||||
return Ipv4
|
||||
} else { // ipv6
|
||||
return Ipv6_weak
|
||||
}
|
||||
} else if na.IP.To4() != nil {
|
||||
if o.Routable() && o.IP.To4() != nil {
|
||||
return Ipv4
|
||||
}
|
||||
return Default
|
||||
} else /* ipv6 */ {
|
||||
var tunnelled bool
|
||||
// Is our v6 is tunnelled?
|
||||
if o.RFC3964() || o.RFC6052() || o.RFC6145() {
|
||||
tunnelled = true
|
||||
}
|
||||
if !o.Routable() {
|
||||
return Default
|
||||
} else if o.RFC4380() {
|
||||
return Teredo
|
||||
} else if o.IP.To4() != nil {
|
||||
return Ipv4
|
||||
} else if tunnelled {
|
||||
// only prioritise ipv6 if we aren't tunnelling it.
|
||||
return Ipv6_weak
|
||||
}
|
||||
return Ipv6_strong
|
||||
}
|
||||
}
|
||||
|
||||
// RFC1918: IPv4 Private networks (10.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12)
|
||||
// RFC3849: IPv6 Documentation address (2001:0DB8::/32)
|
||||
// RFC3927: IPv4 Autoconfig (169.254.0.0/16)
|
||||
// RFC3964: IPv6 6to4 (2002::/16)
|
||||
// RFC4193: IPv6 unique local (FC00::/7)
|
||||
// RFC4380: IPv6 Teredo tunneling (2001::/32)
|
||||
// RFC4843: IPv6 ORCHID: (2001:10::/28)
|
||||
// RFC4862: IPv6 Autoconfig (FE80::/64)
|
||||
// RFC6052: IPv6 well known prefix (64:FF9B::/96)
|
||||
// RFC6145: IPv6 IPv4 translated address ::FFFF:0:0:0/96
|
||||
var rfc1918_10 = net.IPNet{IP: net.ParseIP("10.0.0.0"), Mask: net.CIDRMask(8, 32)}
|
||||
var rfc1918_192 = net.IPNet{IP: net.ParseIP("192.168.0.0"), Mask: net.CIDRMask(16, 32)}
|
||||
var rfc1918_172 = net.IPNet{IP: net.ParseIP("172.16.0.0"), Mask: net.CIDRMask(12, 32)}
|
||||
var rfc3849 = net.IPNet{IP: net.ParseIP("2001:0DB8::"), Mask: net.CIDRMask(32, 128)}
|
||||
var rfc3927 = net.IPNet{IP: net.ParseIP("169.254.0.0"), Mask: net.CIDRMask(16, 32)}
|
||||
var rfc3964 = net.IPNet{IP: net.ParseIP("2002::"), Mask: net.CIDRMask(16, 128)}
|
||||
var rfc4193 = net.IPNet{IP: net.ParseIP("FC00::"), Mask: net.CIDRMask(7, 128)}
|
||||
var rfc4380 = net.IPNet{IP: net.ParseIP("2001::"), Mask: net.CIDRMask(32, 128)}
|
||||
var rfc4843 = net.IPNet{IP: net.ParseIP("2001:10::"), Mask: net.CIDRMask(28, 128)}
|
||||
var rfc4862 = net.IPNet{IP: net.ParseIP("FE80::"), Mask: net.CIDRMask(64, 128)}
|
||||
var rfc6052 = net.IPNet{IP: net.ParseIP("64:FF9B::"), Mask: net.CIDRMask(96, 128)}
|
||||
var rfc6145 = net.IPNet{IP: net.ParseIP("::FFFF:0:0:0"), Mask: net.CIDRMask(96, 128)}
|
||||
var zero4 = net.IPNet{IP: net.ParseIP("0.0.0.0"), Mask: net.CIDRMask(8, 32)}
|
||||
|
||||
func (na *NetAddress) RFC1918() bool {
|
||||
return rfc1918_10.Contains(na.IP) ||
|
||||
rfc1918_192.Contains(na.IP) ||
|
||||
rfc1918_172.Contains(na.IP)
|
||||
}
|
||||
func (na *NetAddress) RFC3849() bool { return rfc3849.Contains(na.IP) }
|
||||
func (na *NetAddress) RFC3927() bool { return rfc3927.Contains(na.IP) }
|
||||
func (na *NetAddress) RFC3964() bool { return rfc3964.Contains(na.IP) }
|
||||
func (na *NetAddress) RFC4193() bool { return rfc4193.Contains(na.IP) }
|
||||
func (na *NetAddress) RFC4380() bool { return rfc4380.Contains(na.IP) }
|
||||
func (na *NetAddress) RFC4843() bool { return rfc4843.Contains(na.IP) }
|
||||
func (na *NetAddress) RFC4862() bool { return rfc4862.Contains(na.IP) }
|
||||
func (na *NetAddress) RFC6052() bool { return rfc6052.Contains(na.IP) }
|
||||
func (na *NetAddress) RFC6145() bool { return rfc6145.Contains(na.IP) }
|
||||
|
||||
func removeProtocolIfDefined(addr string) string {
|
||||
if strings.Contains(addr, "://") {
|
||||
return strings.Split(addr, "://")[1]
|
||||
}
|
||||
return addr
|
||||
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewNetAddress(t *testing.T) {
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080")
|
||||
require.Nil(t, err)
|
||||
addr := NewNetAddress("", tcpAddr)
|
||||
|
||||
assert.Equal(t, "127.0.0.1:8080", addr.String())
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
NewNetAddress("", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8000})
|
||||
}, "Calling NewNetAddress with UDPAddr should not panic in testing")
|
||||
}
|
||||
|
||||
func TestNewNetAddressStringWithOptionalID(t *testing.T) {
|
||||
testCases := []struct {
|
||||
addr string
|
||||
expected string
|
||||
correct bool
|
||||
}{
|
||||
{"127.0.0.1:8080", "127.0.0.1:8080", true},
|
||||
{"tcp://127.0.0.1:8080", "127.0.0.1:8080", true},
|
||||
{"udp://127.0.0.1:8080", "127.0.0.1:8080", true},
|
||||
{"udp//127.0.0.1:8080", "", false},
|
||||
// {"127.0.0:8080", false},
|
||||
{"notahost", "", false},
|
||||
{"127.0.0.1:notapath", "", false},
|
||||
{"notahost:8080", "", false},
|
||||
{"8082", "", false},
|
||||
{"127.0.0:8080000", "", false},
|
||||
|
||||
{"deadbeef@127.0.0.1:8080", "", false},
|
||||
{"this-isnot-hex@127.0.0.1:8080", "", false},
|
||||
{"xxxxbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "", false},
|
||||
{"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", true},
|
||||
|
||||
{"tcp://deadbeef@127.0.0.1:8080", "", false},
|
||||
{"tcp://this-isnot-hex@127.0.0.1:8080", "", false},
|
||||
{"tcp://xxxxbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "", false},
|
||||
{"tcp://deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", true},
|
||||
|
||||
{"tcp://@127.0.0.1:8080", "", false},
|
||||
{"tcp://@", "", false},
|
||||
{"", "", false},
|
||||
{"@", "", false},
|
||||
{" @", "", false},
|
||||
{" @ ", "", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
addr, err := NewNetAddressStringWithOptionalID(tc.addr)
|
||||
if tc.correct {
|
||||
if assert.Nil(t, err, tc.addr) {
|
||||
assert.Equal(t, tc.expected, addr.String())
|
||||
}
|
||||
} else {
|
||||
assert.NotNil(t, err, tc.addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewNetAddressString(t *testing.T) {
|
||||
testCases := []struct {
|
||||
addr string
|
||||
expected string
|
||||
correct bool
|
||||
}{
|
||||
{"127.0.0.1:8080", "127.0.0.1:8080", false},
|
||||
{"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
addr, err := NewNetAddressString(tc.addr)
|
||||
if tc.correct {
|
||||
if assert.Nil(t, err, tc.addr) {
|
||||
assert.Equal(t, tc.expected, addr.String())
|
||||
}
|
||||
} else {
|
||||
assert.NotNil(t, err, tc.addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
func TestNewNetAddressIPPort(t *testing.T) {
|
||||
addr := NewNetAddressIPPort(net.ParseIP("127.0.0.1"), 8080)
|
||||
assert.Equal(t, "127.0.0.1:8080", addr.String())
|
||||
}
|
||||
|
||||
func TestNetAddressProperties(t *testing.T) {
|
||||
// TODO add more test cases
|
||||
testCases := []struct {
|
||||
addr string
|
||||
valid bool
|
||||
local bool
|
||||
routable bool
|
||||
}{
|
||||
{"127.0.0.1:8080", true, true, false},
|
||||
{"ya.ru:80", true, false, true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
addr, err := NewNetAddressStringWithOptionalID(tc.addr)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, tc.valid, addr.Valid())
|
||||
assert.Equal(t, tc.local, addr.Local())
|
||||
assert.Equal(t, tc.routable, addr.Routable())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetAddressReachabilityTo(t *testing.T) {
|
||||
// TODO add more test cases
|
||||
testCases := []struct {
|
||||
addr string
|
||||
other string
|
||||
reachability int
|
||||
}{
|
||||
{"127.0.0.1:8080", "127.0.0.1:8081", 0},
|
||||
{"ya.ru:80", "127.0.0.1:8080", 1},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
addr, err := NewNetAddressStringWithOptionalID(tc.addr)
|
||||
require.Nil(t, err)
|
||||
|
||||
other, err := NewNetAddressStringWithOptionalID(tc.other)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, tc.reachability, addr.ReachabilityTo(other))
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
maxNodeInfoSize = 10240 // 10Kb
|
||||
maxNumChannels = 16 // plenty of room for upgrades, for now
|
||||
)
|
||||
|
||||
// Max size of the NodeInfo struct
|
||||
func MaxNodeInfoSize() int {
|
||||
return maxNodeInfoSize
|
||||
}
|
||||
|
||||
// NodeInfo is the basic node information exchanged
|
||||
// between two peers during the Tendermint P2P handshake.
|
||||
type NodeInfo struct {
|
||||
// Authenticate
|
||||
// TODO: replace with NetAddress
|
||||
ID ID `json:"id"` // authenticated identifier
|
||||
ListenAddr string `json:"listen_addr"` // accepting incoming
|
||||
|
||||
// Check compatibility.
|
||||
// Channels are HexBytes so easier to read as JSON
|
||||
Network string `json:"network"` // network/chain ID
|
||||
Version string `json:"version"` // major.minor.revision
|
||||
Channels cmn.HexBytes `json:"channels"` // channels this node knows about
|
||||
|
||||
// ASCIIText fields
|
||||
Moniker string `json:"moniker"` // arbitrary moniker
|
||||
Other []string `json:"other"` // other application specific data
|
||||
}
|
||||
|
||||
// Validate checks the self-reported NodeInfo is safe.
|
||||
// It returns an error if there
|
||||
// are too many Channels, if there are any duplicate Channels,
|
||||
// if the ListenAddr is malformed, or if the ListenAddr is a host name
|
||||
// that can not be resolved to some IP.
|
||||
// TODO: constraints for Moniker/Other? Or is that for the UI ?
|
||||
// JAE: It needs to be done on the client, but to prevent ambiguous
|
||||
// unicode characters, maybe it's worth sanitizing it here.
|
||||
// In the future we might want to validate these, once we have a
|
||||
// name-resolution system up.
|
||||
// International clients could then use punycode (or we could use
|
||||
// url-encoding), and we just need to be careful with how we handle that in our
|
||||
// clients. (e.g. off by default).
|
||||
func (info NodeInfo) Validate() error {
|
||||
if len(info.Channels) > maxNumChannels {
|
||||
return fmt.Errorf("info.Channels is too long (%v). Max is %v", len(info.Channels), maxNumChannels)
|
||||
}
|
||||
|
||||
// Sanitize ASCII text fields.
|
||||
if !cmn.IsASCIIText(info.Moniker) || cmn.ASCIITrim(info.Moniker) == "" {
|
||||
return fmt.Errorf("info.Moniker must be valid non-empty ASCII text without tabs, but got %v.", info.Moniker)
|
||||
}
|
||||
for i, s := range info.Other {
|
||||
if !cmn.IsASCIIText(s) || cmn.ASCIITrim(s) == "" {
|
||||
return fmt.Errorf("info.Other[%v] must be valid non-empty ASCII text without tabs, but got %v.", i, s)
|
||||
}
|
||||
}
|
||||
|
||||
channels := make(map[byte]struct{})
|
||||
for _, ch := range info.Channels {
|
||||
_, ok := channels[ch]
|
||||
if ok {
|
||||
return fmt.Errorf("info.Channels contains duplicate channel id %v", ch)
|
||||
}
|
||||
channels[ch] = struct{}{}
|
||||
}
|
||||
|
||||
// ensure ListenAddr is good
|
||||
_, err := NewNetAddressString(IDAddressString(info.ID, info.ListenAddr))
|
||||
return err
|
||||
}
|
||||
|
||||
// CompatibleWith checks if two NodeInfo are compatible with eachother.
|
||||
// CONTRACT: two nodes are compatible if the major version matches and network match
|
||||
// and they have at least one channel in common.
|
||||
func (info NodeInfo) CompatibleWith(other NodeInfo) error {
|
||||
iMajor, iMinor, _, iErr := splitVersion(info.Version)
|
||||
oMajor, oMinor, _, oErr := splitVersion(other.Version)
|
||||
|
||||
// if our own version number is not formatted right, we messed up
|
||||
if iErr != nil {
|
||||
return iErr
|
||||
}
|
||||
|
||||
// version number must be formatted correctly ("x.x.x")
|
||||
if oErr != nil {
|
||||
return oErr
|
||||
}
|
||||
|
||||
// major version must match
|
||||
if iMajor != oMajor {
|
||||
return fmt.Errorf("Peer is on a different major version. Got %v, expected %v", oMajor, iMajor)
|
||||
}
|
||||
|
||||
// minor version can differ
|
||||
if iMinor != oMinor {
|
||||
// ok
|
||||
}
|
||||
|
||||
// nodes must be on the same network
|
||||
if info.Network != other.Network {
|
||||
return fmt.Errorf("Peer is on a different network. Got %v, expected %v", other.Network, info.Network)
|
||||
}
|
||||
|
||||
// if we have no channels, we're just testing
|
||||
if len(info.Channels) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// for each of our channels, check if they have it
|
||||
found := false
|
||||
OUTER_LOOP:
|
||||
for _, ch1 := range info.Channels {
|
||||
for _, ch2 := range other.Channels {
|
||||
if ch1 == ch2 {
|
||||
found = true
|
||||
break OUTER_LOOP // only need one
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("Peer has no common channels. Our channels: %v ; Peer channels: %v", info.Channels, other.Channels)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetAddress returns a NetAddress derived from the NodeInfo -
|
||||
// it includes the authenticated peer ID and the self-reported
|
||||
// ListenAddr. Note that the ListenAddr is not authenticated and
|
||||
// may not match that address actually dialed if its an outbound peer.
|
||||
func (info NodeInfo) NetAddress() *NetAddress {
|
||||
netAddr, err := NewNetAddressString(IDAddressString(info.ID, info.ListenAddr))
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case ErrNetAddressLookup:
|
||||
// XXX If the peer provided a host name and the lookup fails here
|
||||
// we're out of luck.
|
||||
// TODO: use a NetAddress in NodeInfo
|
||||
default:
|
||||
panic(err) // everything should be well formed by now
|
||||
}
|
||||
}
|
||||
return netAddr
|
||||
}
|
||||
|
||||
func (info NodeInfo) String() string {
|
||||
return fmt.Sprintf("NodeInfo{id: %v, moniker: %v, network: %v [listen %v], version: %v (%v)}",
|
||||
info.ID, info.Moniker, info.Network, info.ListenAddr, info.Version, info.Other)
|
||||
}
|
||||
|
||||
func splitVersion(version string) (string, string, string, error) {
|
||||
spl := strings.Split(version, ".")
|
||||
if len(spl) != 3 {
|
||||
return "", "", "", fmt.Errorf("Invalid version format %v", version)
|
||||
}
|
||||
return spl[0], spl[1], spl[2], nil
|
||||
}
|
||||
-431
@@ -1,431 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
crypto "github.com/tendermint/tendermint/crypto"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
|
||||
"github.com/tendermint/tendermint/config"
|
||||
tmconn "github.com/tendermint/tendermint/p2p/conn"
|
||||
)
|
||||
|
||||
var testIPSuffix uint32
|
||||
|
||||
// Peer is an interface representing a peer connected on a reactor.
|
||||
type Peer interface {
|
||||
cmn.Service
|
||||
|
||||
ID() ID // peer's cryptographic ID
|
||||
RemoteIP() net.IP // remote IP of the connection
|
||||
IsOutbound() bool // did we dial the peer
|
||||
IsPersistent() bool // do we redial this peer when we disconnect
|
||||
NodeInfo() NodeInfo // peer's info
|
||||
Status() tmconn.ConnectionStatus
|
||||
|
||||
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
|
||||
config *config.P2PConfig
|
||||
conn net.Conn // source connection
|
||||
ip net.IP
|
||||
}
|
||||
|
||||
// ID only exists for SecretConnection.
|
||||
// NOTE: Will panic if conn is not *SecretConnection.
|
||||
func (pc peerConn) ID() ID {
|
||||
return PubKeyToID(pc.conn.(*tmconn.SecretConnection).RemotePubKey())
|
||||
}
|
||||
|
||||
// Return the IP from the connection RemoteAddr
|
||||
func (pc peerConn) RemoteIP() net.IP {
|
||||
if pc.ip != nil {
|
||||
return pc.ip
|
||||
}
|
||||
|
||||
// In test cases a conn could not be present at all or be an in-memory
|
||||
// implementation where we want to return a fake ip.
|
||||
if pc.conn == nil || pc.conn.RemoteAddr().String() == "pipe" {
|
||||
pc.ip = net.IP{172, 16, 0, byte(atomic.AddUint32(&testIPSuffix, 1))}
|
||||
|
||||
return pc.ip
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(pc.conn.RemoteAddr().String())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ips, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pc.ip = ips[0]
|
||||
|
||||
return pc.ip
|
||||
}
|
||||
|
||||
// peer implements Peer.
|
||||
//
|
||||
// Before using a peer, you will need to perform a handshake on connection.
|
||||
type peer struct {
|
||||
cmn.BaseService
|
||||
|
||||
// raw peerConn and the multiplex connection
|
||||
peerConn
|
||||
mconn *tmconn.MConnection
|
||||
|
||||
// peer's node info and the channel it knows about
|
||||
// channels = nodeInfo.Channels
|
||||
// cached to avoid copying nodeInfo in hasChannel
|
||||
nodeInfo NodeInfo
|
||||
channels []byte
|
||||
|
||||
// User data
|
||||
Data *cmn.CMap
|
||||
}
|
||||
|
||||
func newPeer(
|
||||
pc peerConn,
|
||||
mConfig tmconn.MConnConfig,
|
||||
nodeInfo NodeInfo,
|
||||
reactorsByCh map[byte]Reactor,
|
||||
chDescs []*tmconn.ChannelDescriptor,
|
||||
onPeerError func(Peer, interface{}),
|
||||
) *peer {
|
||||
p := &peer{
|
||||
peerConn: pc,
|
||||
nodeInfo: nodeInfo,
|
||||
channels: nodeInfo.Channels,
|
||||
Data: cmn.NewCMap(),
|
||||
}
|
||||
|
||||
p.mconn = createMConnection(
|
||||
pc.conn,
|
||||
p,
|
||||
reactorsByCh,
|
||||
chDescs,
|
||||
onPeerError,
|
||||
mConfig,
|
||||
)
|
||||
p.BaseService = *cmn.NewBaseService(nil, "Peer", p)
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func newOutboundPeerConn(
|
||||
addr *NetAddress,
|
||||
config *config.P2PConfig,
|
||||
persistent bool,
|
||||
ourNodePrivKey crypto.PrivKey,
|
||||
) (peerConn, error) {
|
||||
conn, err := dial(addr, config)
|
||||
if err != nil {
|
||||
return peerConn{}, cmn.ErrorWrap(err, "Error creating peer")
|
||||
}
|
||||
|
||||
pc, err := newPeerConn(conn, config, true, persistent, ourNodePrivKey)
|
||||
if err != nil {
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
return peerConn{}, cmn.ErrorWrap(err, cerr.Error())
|
||||
}
|
||||
return peerConn{}, err
|
||||
}
|
||||
|
||||
// ensure dialed ID matches connection ID
|
||||
if addr.ID != pc.ID() {
|
||||
if cerr := conn.Close(); cerr != nil {
|
||||
return peerConn{}, cmn.ErrorWrap(err, cerr.Error())
|
||||
}
|
||||
return peerConn{}, ErrSwitchAuthenticationFailure{addr, pc.ID()}
|
||||
}
|
||||
|
||||
return pc, nil
|
||||
}
|
||||
|
||||
func newInboundPeerConn(
|
||||
conn net.Conn,
|
||||
config *config.P2PConfig,
|
||||
ourNodePrivKey crypto.PrivKey,
|
||||
) (peerConn, error) {
|
||||
|
||||
// TODO: issue PoW challenge
|
||||
|
||||
return newPeerConn(conn, config, false, false, ourNodePrivKey)
|
||||
}
|
||||
|
||||
func newPeerConn(
|
||||
rawConn net.Conn,
|
||||
cfg *config.P2PConfig,
|
||||
outbound, persistent bool,
|
||||
ourNodePrivKey crypto.PrivKey,
|
||||
) (pc peerConn, err error) {
|
||||
conn := rawConn
|
||||
|
||||
// Fuzz connection
|
||||
if cfg.TestFuzz {
|
||||
// so we have time to do peer handshakes and get set up
|
||||
conn = FuzzConnAfterFromConfig(conn, 10*time.Second, cfg.TestFuzzConfig)
|
||||
}
|
||||
|
||||
// Set deadline for secret handshake
|
||||
dl := time.Now().Add(cfg.HandshakeTimeout)
|
||||
if err := conn.SetDeadline(dl); err != nil {
|
||||
return pc, cmn.ErrorWrap(
|
||||
err,
|
||||
"Error setting deadline while encrypting connection",
|
||||
)
|
||||
}
|
||||
|
||||
// Encrypt connection
|
||||
conn, err = tmconn.MakeSecretConnection(conn, ourNodePrivKey)
|
||||
if err != nil {
|
||||
return pc, cmn.ErrorWrap(err, "Error creating peer")
|
||||
}
|
||||
|
||||
// Only the information we already have
|
||||
return peerConn{
|
||||
config: cfg,
|
||||
outbound: outbound,
|
||||
persistent: persistent,
|
||||
conn: conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
//---------------------------------------------------
|
||||
// Implements cmn.Service
|
||||
|
||||
// SetLogger implements BaseService.
|
||||
func (p *peer) SetLogger(l log.Logger) {
|
||||
p.Logger = l
|
||||
p.mconn.SetLogger(l)
|
||||
}
|
||||
|
||||
// OnStart implements BaseService.
|
||||
func (p *peer) OnStart() error {
|
||||
if err := p.BaseService.OnStart(); err != nil {
|
||||
return err
|
||||
}
|
||||
err := p.mconn.Start()
|
||||
return err
|
||||
}
|
||||
|
||||
// OnStop implements BaseService.
|
||||
func (p *peer) OnStop() {
|
||||
p.BaseService.OnStop()
|
||||
p.mconn.Stop() // stop everything and close the conn
|
||||
}
|
||||
|
||||
//---------------------------------------------------
|
||||
// Implements Peer
|
||||
|
||||
// ID returns the peer's ID - the hex encoded hash of its pubkey.
|
||||
func (p *peer) ID() ID {
|
||||
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() NodeInfo {
|
||||
return p.nodeInfo
|
||||
}
|
||||
|
||||
// Status returns the peer's ConnectionStatus.
|
||||
func (p *peer) Status() tmconn.ConnectionStatus {
|
||||
return p.mconn.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
|
||||
}
|
||||
return p.mconn.Send(chID, msgBytes)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
return p.mconn.TrySend(chID, msgBytes)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
//---------------------------------------------------
|
||||
// methods used by the Switch
|
||||
|
||||
// CloseConn should be called by the Switch if the peer was created but never
|
||||
// started.
|
||||
func (pc *peerConn) CloseConn() {
|
||||
pc.conn.Close() // nolint: errcheck
|
||||
}
|
||||
|
||||
// HandshakeTimeout performs the Tendermint P2P handshake between a given node
|
||||
// and the peer by exchanging their NodeInfo. It sets the received nodeInfo on
|
||||
// the peer.
|
||||
// NOTE: blocking
|
||||
func (pc *peerConn) HandshakeTimeout(
|
||||
ourNodeInfo NodeInfo,
|
||||
timeout time.Duration,
|
||||
) (peerNodeInfo NodeInfo, err error) {
|
||||
// Set deadline for handshake so we don't block forever on conn.ReadFull
|
||||
if err := pc.conn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
||||
return peerNodeInfo, cmn.ErrorWrap(err, "Error setting deadline")
|
||||
}
|
||||
|
||||
var trs, _ = cmn.Parallel(
|
||||
func(_ int) (val interface{}, err error, abort bool) {
|
||||
_, err = cdc.MarshalBinaryWriter(pc.conn, ourNodeInfo)
|
||||
return
|
||||
},
|
||||
func(_ int) (val interface{}, err error, abort bool) {
|
||||
_, err = cdc.UnmarshalBinaryReader(
|
||||
pc.conn,
|
||||
&peerNodeInfo,
|
||||
int64(MaxNodeInfoSize()),
|
||||
)
|
||||
return
|
||||
},
|
||||
)
|
||||
if err := trs.FirstError(); err != nil {
|
||||
return peerNodeInfo, cmn.ErrorWrap(err, "Error during handshake")
|
||||
}
|
||||
|
||||
// Remove deadline
|
||||
if err := pc.conn.SetDeadline(time.Time{}); err != nil {
|
||||
return peerNodeInfo, cmn.ErrorWrap(err, "Error removing deadline")
|
||||
}
|
||||
|
||||
return peerNodeInfo, nil
|
||||
}
|
||||
|
||||
// Addr returns peer's remote network address.
|
||||
func (p *peer) Addr() net.Addr {
|
||||
return p.peerConn.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
// CanSend returns true if the send queue is not full, false otherwise.
|
||||
func (p *peer) CanSend(chID byte) bool {
|
||||
if !p.IsRunning() {
|
||||
return false
|
||||
}
|
||||
return p.mconn.CanSend(chID)
|
||||
}
|
||||
|
||||
// String representation.
|
||||
func (p *peer) String() string {
|
||||
if p.outbound {
|
||||
return fmt.Sprintf("Peer{%v %v out}", p.mconn, p.ID())
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Peer{%v %v in}", p.mconn, p.ID())
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------
|
||||
// helper funcs
|
||||
|
||||
func dial(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 createMConnection(
|
||||
conn net.Conn,
|
||||
p *peer,
|
||||
reactorsByCh map[byte]Reactor,
|
||||
chDescs []*tmconn.ChannelDescriptor,
|
||||
onPeerError func(Peer, interface{}),
|
||||
config tmconn.MConnConfig,
|
||||
) *tmconn.MConnection {
|
||||
|
||||
onReceive := func(chID byte, msgBytes []byte) {
|
||||
reactor := reactorsByCh[chID]
|
||||
if reactor == nil {
|
||||
// Note that its ok to panic here as it's caught in the conn._recover,
|
||||
// which does onPeerError.
|
||||
panic(cmn.Fmt("Unknown channel %X", chID))
|
||||
}
|
||||
reactor.Receive(chID, p, msgBytes)
|
||||
}
|
||||
|
||||
onError := func(r interface{}) {
|
||||
onPeerError(p, r)
|
||||
}
|
||||
|
||||
return tmconn.NewMConnectionWithConfig(
|
||||
conn,
|
||||
chDescs,
|
||||
onReceive,
|
||||
onError,
|
||||
config,
|
||||
)
|
||||
}
|
||||
-144
@@ -1,144 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// IPeerSet has a (immutable) subset of the methods of PeerSet.
|
||||
type IPeerSet interface {
|
||||
Has(key ID) bool
|
||||
HasIP(ip net.IP) bool
|
||||
Get(key ID) 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 sync.Mutex
|
||||
lookup map[ID]*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[ID]*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 ID) 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 ID) 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.
|
||||
func (ps *PeerSet) Remove(peer Peer) {
|
||||
ps.mtx.Lock()
|
||||
defer ps.mtx.Unlock()
|
||||
|
||||
item := ps.lookup[peer.ID()]
|
||||
if item == nil {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
// 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,172 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
crypto "github.com/tendermint/tendermint/crypto"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
)
|
||||
|
||||
// Returns an empty kvstore peer
|
||||
func randPeer(ip net.IP) *peer {
|
||||
if ip == nil {
|
||||
ip = net.IP{127, 0, 0, 1}
|
||||
}
|
||||
|
||||
nodeKey := NodeKey{PrivKey: crypto.GenPrivKeyEd25519()}
|
||||
p := &peer{
|
||||
nodeInfo: NodeInfo{
|
||||
ID: nodeKey.ID(),
|
||||
ListenAddr: cmn.Fmt("%v.%v.%v.%v:26656", rand.Int()%256, rand.Int()%256, rand.Int()%256, rand.Int()%256),
|
||||
},
|
||||
}
|
||||
|
||||
p.ip = ip
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func TestPeerSetAddRemoveOne(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
peerSet := NewPeerSet()
|
||||
|
||||
var peerList []Peer
|
||||
for i := 0; i < 5; i++ {
|
||||
p := randPeer(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 {
|
||||
peerSet.Remove(peerAtFront)
|
||||
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
|
||||
peerSet.Remove(peerAtFront)
|
||||
}
|
||||
}
|
||||
|
||||
// 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]
|
||||
peerSet.Remove(peerAtEnd)
|
||||
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 := randPeer(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 {
|
||||
peerSet.Remove(peer)
|
||||
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 := randPeer(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 = randPeer(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,175 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
golog "log"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
crypto "github.com/tendermint/tendermint/crypto"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
|
||||
"github.com/tendermint/tendermint/config"
|
||||
tmconn "github.com/tendermint/tendermint/p2p/conn"
|
||||
)
|
||||
|
||||
const testCh = 0x01
|
||||
|
||||
func TestPeerBasic(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
// simulate remote peer
|
||||
rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519(), Config: cfg}
|
||||
rp.Start()
|
||||
defer rp.Stop()
|
||||
|
||||
p, err := createOutboundPeerAndPerformHandshake(rp.Addr(), cfg, tmconn.DefaultMConnConfig())
|
||||
require.Nil(err)
|
||||
|
||||
err = p.Start()
|
||||
require.Nil(err)
|
||||
defer p.Stop()
|
||||
|
||||
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.Addr().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: crypto.GenPrivKeyEd25519(), Config: config}
|
||||
rp.Start()
|
||||
defer rp.Stop()
|
||||
|
||||
p, err := createOutboundPeerAndPerformHandshake(rp.Addr(), config, tmconn.DefaultMConnConfig())
|
||||
require.Nil(err)
|
||||
|
||||
err = p.Start()
|
||||
require.Nil(err)
|
||||
|
||||
defer p.Stop()
|
||||
|
||||
assert.True(p.CanSend(testCh))
|
||||
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},
|
||||
}
|
||||
reactorsByCh := map[byte]Reactor{testCh: NewTestReactor(chDescs, true)}
|
||||
pk := crypto.GenPrivKeyEd25519()
|
||||
pc, err := newOutboundPeerConn(addr, config, false, pk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodeInfo, err := pc.HandshakeTimeout(NodeInfo{
|
||||
ID: addr.ID,
|
||||
Moniker: "host_peer",
|
||||
Network: "testing",
|
||||
Version: "123.123.123",
|
||||
Channels: []byte{testCh},
|
||||
}, 1*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := newPeer(pc, mConfig, nodeInfo, reactorsByCh, chDescs, func(p Peer, r interface{}) {})
|
||||
p.SetLogger(log.TestingLogger().With("peer", addr))
|
||||
return p, nil
|
||||
}
|
||||
|
||||
type remotePeer struct {
|
||||
PrivKey crypto.PrivKey
|
||||
Config *config.P2PConfig
|
||||
addr *NetAddress
|
||||
quit chan struct{}
|
||||
channels cmn.HexBytes
|
||||
listenAddr string
|
||||
}
|
||||
|
||||
func (rp *remotePeer) Addr() *NetAddress {
|
||||
return rp.addr
|
||||
}
|
||||
|
||||
func (rp *remotePeer) ID() ID {
|
||||
return PubKeyToID(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.addr = NewNetAddress(PubKeyToID(rp.PrivKey.PubKey()), l.Addr())
|
||||
rp.quit = make(chan struct{})
|
||||
if rp.channels == nil {
|
||||
rp.channels = []byte{testCh}
|
||||
}
|
||||
go rp.accept(l)
|
||||
}
|
||||
|
||||
func (rp *remotePeer) Stop() {
|
||||
close(rp.quit)
|
||||
}
|
||||
|
||||
func (rp *remotePeer) accept(l net.Listener) {
|
||||
conns := []net.Conn{}
|
||||
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
golog.Fatalf("Failed to accept conn: %+v", err)
|
||||
}
|
||||
|
||||
pc, err := newInboundPeerConn(conn, rp.Config, rp.PrivKey)
|
||||
if err != nil {
|
||||
golog.Fatalf("Failed to create a peer: %+v", err)
|
||||
}
|
||||
|
||||
_, err = pc.HandshakeTimeout(NodeInfo{
|
||||
ID: rp.Addr().ID,
|
||||
Moniker: "remote_peer",
|
||||
Network: "testing",
|
||||
Version: "123.123.123",
|
||||
ListenAddr: l.Addr().String(),
|
||||
Channels: rp.channels,
|
||||
}, 1*time.Second)
|
||||
if err != nil {
|
||||
golog.Fatalf("Failed to perform handshake: %+v", err)
|
||||
}
|
||||
|
||||
conns = append(conns, conn)
|
||||
|
||||
select {
|
||||
case <-rp.quit:
|
||||
for _, conn := range conns {
|
||||
if err := conn.Close(); err != nil {
|
||||
golog.Fatal(err)
|
||||
}
|
||||
}
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,813 +0,0 @@
|
||||
// Modified for Tendermint
|
||||
// Originally Copyright (c) 2013-2014 Conformal Systems LLC.
|
||||
// https://github.com/conformal/btcd/blob/master/LICENSE
|
||||
|
||||
package pex
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
crypto "github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/p2p"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
)
|
||||
|
||||
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 {
|
||||
cmn.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
|
||||
|
||||
// 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
|
||||
|
||||
// Pick an address to dial
|
||||
PickAddress(biasTowardsNewAddrs int) *p2p.NetAddress
|
||||
|
||||
// Mark address
|
||||
MarkGood(*p2p.NetAddress)
|
||||
MarkAttempt(*p2p.NetAddress)
|
||||
MarkBad(*p2p.NetAddress)
|
||||
|
||||
IsGood(*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
|
||||
|
||||
// TODO: remove
|
||||
ListOfKnownAddresses() []*knownAddress
|
||||
|
||||
// Persist to disk
|
||||
Save()
|
||||
}
|
||||
|
||||
var _ AddrBook = (*addrBook)(nil)
|
||||
|
||||
// addrBook - concurrency safe peer address manager.
|
||||
// Implements AddrBook.
|
||||
type addrBook struct {
|
||||
cmn.BaseService
|
||||
|
||||
// immutable after creation
|
||||
filePath string
|
||||
routabilityStrict bool
|
||||
key string // random prefix for bucket placement
|
||||
|
||||
// accessed concurrently
|
||||
mtx sync.Mutex
|
||||
rand *cmn.Rand
|
||||
ourAddrs map[string]struct{}
|
||||
addrLookup map[p2p.ID]*knownAddress // new & old
|
||||
bucketsOld []map[string]*knownAddress
|
||||
bucketsNew []map[string]*knownAddress
|
||||
nOld int
|
||||
nNew int
|
||||
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewAddrBook creates a new address book.
|
||||
// Use Start to begin processing asynchronous address updates.
|
||||
func NewAddrBook(filePath string, routabilityStrict bool) *addrBook {
|
||||
am := &addrBook{
|
||||
rand: cmn.NewRand(),
|
||||
ourAddrs: make(map[string]struct{}),
|
||||
addrLookup: make(map[p2p.ID]*knownAddress),
|
||||
filePath: filePath,
|
||||
routabilityStrict: routabilityStrict,
|
||||
}
|
||||
am.init()
|
||||
am.BaseService = *cmn.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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
_, ok := a.ourAddrs[addr.String()]
|
||||
a.mtx.Unlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
// 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()
|
||||
ka := a.addrLookup[addr.ID]
|
||||
if ka == nil {
|
||||
return
|
||||
}
|
||||
a.Logger.Info("Remove address from book", "addr", ka.Addr, "ID", ka.ID())
|
||||
a.removeFromAllBuckets(ka)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (a *addrBook) PickAddress(biasTowardsNewAddrs int) *p2p.NetAddress {
|
||||
a.mtx.Lock()
|
||||
defer a.mtx.Unlock()
|
||||
|
||||
bookSize := a.size()
|
||||
if bookSize <= 0 {
|
||||
if bookSize < 0 {
|
||||
a.Logger.Error("Addrbook size less than 0", "nNew", a.nNew, "nOld", 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)*a.rand.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[a.rand.Intn(len(a.bucketsOld))]
|
||||
} else {
|
||||
bucket = a.bucketsNew[a.rand.Intn(len(a.bucketsNew))]
|
||||
}
|
||||
}
|
||||
// pick a random index and loop over the map to return that index
|
||||
randIndex := a.rand.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(addr *p2p.NetAddress) {
|
||||
a.mtx.Lock()
|
||||
defer a.mtx.Unlock()
|
||||
ka := a.addrLookup[addr.ID]
|
||||
if ka == nil {
|
||||
return
|
||||
}
|
||||
ka.markGood()
|
||||
if ka.isNew() {
|
||||
a.moveToOld(ka)
|
||||
}
|
||||
}
|
||||
|
||||
// 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. Currently it just ejects the address.
|
||||
// TODO: black list for some amount of time
|
||||
func (a *addrBook) MarkBad(addr *p2p.NetAddress) {
|
||||
a.RemoveAddress(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 {
|
||||
a.Logger.Error("Addrbook size less than 0", "nNew", a.nNew, "nOld", a.nOld)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
numAddresses := cmn.MaxInt(
|
||||
cmn.MinInt(minGetSelection, bookSize),
|
||||
bookSize*getSelectionPercent/100)
|
||||
numAddresses = cmn.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
|
||||
j := cmn.RandIntn(len(allAddr)-i) + i
|
||||
allAddr[i], allAddr[j] = allAddr[j], allAddr[i]
|
||||
}
|
||||
|
||||
// slice off the limit we are willing to share.
|
||||
return allAddr[:numAddresses]
|
||||
}
|
||||
|
||||
// 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 {
|
||||
a.Logger.Error("Addrbook size less than 0", "nNew", a.nNew, "nOld", a.nOld)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if biasTowardsNewAddrs > 100 {
|
||||
biasTowardsNewAddrs = 100
|
||||
}
|
||||
if biasTowardsNewAddrs < 0 {
|
||||
biasTowardsNewAddrs = 0
|
||||
}
|
||||
|
||||
numAddresses := cmn.MaxInt(
|
||||
cmn.MinInt(minGetSelection, bookSize),
|
||||
bookSize*getSelectionPercent/100)
|
||||
numAddresses = cmn.MinInt(maxGetSelection, numAddresses)
|
||||
|
||||
selection := make([]*p2p.NetAddress, numAddresses)
|
||||
|
||||
oldBucketToAddrsMap := make(map[int]map[string]struct{})
|
||||
var oldIndex int
|
||||
newBucketToAddrsMap := make(map[int]map[string]struct{})
|
||||
var newIndex int
|
||||
|
||||
selectionIndex := 0
|
||||
ADDRS_LOOP:
|
||||
for selectionIndex < numAddresses {
|
||||
pickFromOldBucket := int((float64(selectionIndex)/float64(numAddresses))*100) >= biasTowardsNewAddrs
|
||||
pickFromOldBucket = (pickFromOldBucket && a.nOld > 0) || a.nNew == 0
|
||||
bucket := make(map[string]*knownAddress)
|
||||
|
||||
// loop until we pick a random non-empty bucket
|
||||
for len(bucket) == 0 {
|
||||
if pickFromOldBucket {
|
||||
oldIndex = a.rand.Intn(len(a.bucketsOld))
|
||||
bucket = a.bucketsOld[oldIndex]
|
||||
} else {
|
||||
newIndex = a.rand.Intn(len(a.bucketsNew))
|
||||
bucket = a.bucketsNew[newIndex]
|
||||
}
|
||||
}
|
||||
|
||||
// pick a random index
|
||||
randIndex := a.rand.Intn(len(bucket))
|
||||
|
||||
// loop over the map to return that index
|
||||
var selectedAddr *p2p.NetAddress
|
||||
for _, ka := range bucket {
|
||||
if randIndex == 0 {
|
||||
selectedAddr = ka.Addr
|
||||
break
|
||||
}
|
||||
randIndex--
|
||||
}
|
||||
|
||||
// if we have selected the address before, restart the loop
|
||||
// otherwise, record it and continue
|
||||
if pickFromOldBucket {
|
||||
if addrsMap, ok := oldBucketToAddrsMap[oldIndex]; ok {
|
||||
if _, ok = addrsMap[selectedAddr.String()]; ok {
|
||||
continue ADDRS_LOOP
|
||||
}
|
||||
} else {
|
||||
oldBucketToAddrsMap[oldIndex] = make(map[string]struct{})
|
||||
}
|
||||
oldBucketToAddrsMap[oldIndex][selectedAddr.String()] = struct{}{}
|
||||
} else {
|
||||
if addrsMap, ok := newBucketToAddrsMap[newIndex]; ok {
|
||||
if _, ok = addrsMap[selectedAddr.String()]; ok {
|
||||
continue ADDRS_LOOP
|
||||
}
|
||||
} else {
|
||||
newBucketToAddrsMap[newIndex] = make(map[string]struct{})
|
||||
}
|
||||
newBucketToAddrsMap[newIndex][selectedAddr.String()] = struct{}{}
|
||||
}
|
||||
|
||||
selection[selectionIndex] = selectedAddr
|
||||
selectionIndex++
|
||||
}
|
||||
|
||||
return selection
|
||||
}
|
||||
|
||||
// ListOfKnownAddresses returns the new and old addresses.
|
||||
func (a *addrBook) ListOfKnownAddresses() []*knownAddress {
|
||||
a.mtx.Lock()
|
||||
defer a.mtx.Unlock()
|
||||
|
||||
addrs := []*knownAddress{}
|
||||
for _, addr := range a.addrLookup {
|
||||
addrs = append(addrs, addr.copy())
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
|
||||
//------------------------------------------------
|
||||
|
||||
// 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)
|
||||
a.Logger.Info("Address handler done")
|
||||
}
|
||||
|
||||
//----------------------------------------------------------
|
||||
|
||||
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:
|
||||
cmn.PanicSanity("Should not happen")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Sanity check
|
||||
if ka.isOld() {
|
||||
a.Logger.Error("Failed Sanity Check! Cant add old address to new bucket", "ka", ka, "bucket", bucketIdx)
|
||||
return
|
||||
}
|
||||
|
||||
addrStr := ka.Addr.String()
|
||||
bucket := a.getBucket(bucketTypeNew, bucketIdx)
|
||||
|
||||
// Already exists?
|
||||
if _, ok := bucket[addrStr]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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(cmn.Fmt("Cannot add new address to old bucket: %v", ka))
|
||||
return false
|
||||
}
|
||||
if len(ka.Buckets) != 0 {
|
||||
a.Logger.Error(cmn.Fmt("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(cmn.Fmt("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 a.routabilityStrict && !addr.Routable() {
|
||||
return ErrAddrBookNonRoutable{addr}
|
||||
}
|
||||
// TODO: we should track ourAddrs by ID and by IP:PORT and refuse both.
|
||||
if _, ok := a.ourAddrs[addr.String()]; ok {
|
||||
return ErrAddrBookSelf{addr}
|
||||
}
|
||||
|
||||
ka := a.addrLookup[addr.ID]
|
||||
if ka != nil {
|
||||
// If its already old and the addr is the same, ignore it.
|
||||
if ka.isOld() && ka.Addr.Equals(addr) {
|
||||
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))
|
||||
if a.rand.Int31n(factor) != 0 {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
ka = newKnownAddress(addr, src)
|
||||
}
|
||||
|
||||
bucket := a.calcNewBucket(addr, src)
|
||||
a.addToNewBucket(ka, bucket)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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(cmn.Fmt("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) {
|
||||
// Sanity check
|
||||
if ka.isOld() {
|
||||
a.Logger.Error(cmn.Fmt("Cannot promote address that is already old %v", ka))
|
||||
return
|
||||
}
|
||||
if len(ka.Buckets) == 0 {
|
||||
a.Logger.Error(cmn.Fmt("Cannot promote address that isn't in any new buckets %v", ka))
|
||||
return
|
||||
}
|
||||
|
||||
// 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 := a.calcOldBucket(ka.Addr)
|
||||
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 := a.calcNewBucket(oldest.Addr, oldest.Src)
|
||||
a.addToNewBucket(oldest, newBucketIdx)
|
||||
|
||||
// Finally, add our ka to old bucket again.
|
||||
added = a.addToOldBucket(ka, oldBucketIdx)
|
||||
if !added {
|
||||
a.Logger.Error(cmn.Fmt("Could not re-add ka %v to oldBucketIdx %v", ka, oldBucketIdx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// calculate bucket placements
|
||||
|
||||
// doublesha256( key + sourcegroup +
|
||||
// int64(doublesha256(key + group + sourcegroup))%bucket_per_group ) % num_new_buckets
|
||||
func (a *addrBook) calcNewBucket(addr, src *p2p.NetAddress) int {
|
||||
data1 := []byte{}
|
||||
data1 = append(data1, []byte(a.key)...)
|
||||
data1 = append(data1, []byte(a.groupKey(addr))...)
|
||||
data1 = append(data1, []byte(a.groupKey(src))...)
|
||||
hash1 := doubleSha256(data1)
|
||||
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 := doubleSha256(data2)
|
||||
return int(binary.BigEndian.Uint64(hash2) % newBucketCount)
|
||||
}
|
||||
|
||||
// doublesha256( key + group +
|
||||
// int64(doublesha256(key + addr))%buckets_per_group ) % num_old_buckets
|
||||
func (a *addrBook) calcOldBucket(addr *p2p.NetAddress) int {
|
||||
data1 := []byte{}
|
||||
data1 = append(data1, []byte(a.key)...)
|
||||
data1 = append(data1, []byte(addr.String())...)
|
||||
hash1 := doubleSha256(data1)
|
||||
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 := doubleSha256(data2)
|
||||
return int(binary.BigEndian.Uint64(hash2) % oldBucketCount)
|
||||
}
|
||||
|
||||
// Return a string representing the network group of this address.
|
||||
// This is the /16 for IPv4, 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 {
|
||||
if a.routabilityStrict && na.Local() {
|
||||
return "local"
|
||||
}
|
||||
if a.routabilityStrict && !na.Routable() {
|
||||
return "unroutable"
|
||||
}
|
||||
|
||||
if ipv4 := na.IP.To4(); ipv4 != nil {
|
||||
return (&net.IPNet{IP: na.IP, Mask: net.CIDRMask(16, 32)}).String()
|
||||
}
|
||||
if na.RFC6145() || na.RFC6052() {
|
||||
// last four bytes are the ip address
|
||||
ip := net.IP(na.IP[12:16])
|
||||
return (&net.IPNet{IP: ip, Mask: net.CIDRMask(16, 32)}).String()
|
||||
}
|
||||
|
||||
if na.RFC3964() {
|
||||
ip := net.IP(na.IP[2:7])
|
||||
return (&net.IPNet{IP: 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 (&net.IPNet{IP: ip, Mask: net.CIDRMask(16, 32)}).String()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
return (&net.IPNet{IP: na.IP, Mask: net.CIDRMask(bits, 128)}).String()
|
||||
}
|
||||
|
||||
// doubleSha256 calculates sha256(sha256(b)) and returns the resulting bytes.
|
||||
func doubleSha256(b []byte) []byte {
|
||||
hasher := sha256.New()
|
||||
hasher.Write(b) // nolint: errcheck, gas
|
||||
sum := hasher.Sum(nil)
|
||||
hasher.Reset()
|
||||
hasher.Write(sum) // nolint: errcheck, gas
|
||||
return hasher.Sum(nil)
|
||||
}
|
||||
@@ -1,356 +0,0 @@
|
||||
package pex
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tendermint/tendermint/p2p"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
)
|
||||
|
||||
func createTempFileName(prefix string) string {
|
||||
f, err := ioutil.TempFile("", prefix)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fname := f.Name()
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return fname
|
||||
}
|
||||
|
||||
func deleteTempFile(fname string) {
|
||||
err := os.Remove(fname)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddrBookPickAddress(t *testing.T) {
|
||||
fname := createTempFileName("addrbook_test")
|
||||
defer deleteTempFile(fname)
|
||||
|
||||
// 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]
|
||||
book.AddAddress(addrSrc.addr, addrSrc.src)
|
||||
|
||||
// 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)
|
||||
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("addrbook_test")
|
||||
defer deleteTempFile(fname)
|
||||
|
||||
// 0 addresses
|
||||
book := NewAddrBook(fname, true)
|
||||
book.SetLogger(log.TestingLogger())
|
||||
book.saveToFile(fname)
|
||||
|
||||
book = NewAddrBook(fname, true)
|
||||
book.SetLogger(log.TestingLogger())
|
||||
book.loadFromFile(fname)
|
||||
|
||||
assert.Zero(t, book.Size())
|
||||
|
||||
// 100 addresses
|
||||
randAddrs := randNetAddressPairs(t, 100)
|
||||
|
||||
for _, addrSrc := range randAddrs {
|
||||
book.AddAddress(addrSrc.addr, addrSrc.src)
|
||||
}
|
||||
|
||||
assert.Equal(t, 100, book.Size())
|
||||
book.saveToFile(fname)
|
||||
|
||||
book = NewAddrBook(fname, true)
|
||||
book.SetLogger(log.TestingLogger())
|
||||
book.loadFromFile(fname)
|
||||
|
||||
assert.Equal(t, 100, book.Size())
|
||||
}
|
||||
|
||||
func TestAddrBookLookup(t *testing.T) {
|
||||
fname := createTempFileName("addrbook_test")
|
||||
defer deleteTempFile(fname)
|
||||
|
||||
randAddrs := randNetAddressPairs(t, 100)
|
||||
|
||||
book := NewAddrBook(fname, true)
|
||||
book.SetLogger(log.TestingLogger())
|
||||
for _, addrSrc := range randAddrs {
|
||||
addr := addrSrc.addr
|
||||
src := addrSrc.src
|
||||
book.AddAddress(addr, src)
|
||||
|
||||
ka := book.addrLookup[addr.ID]
|
||||
assert.NotNil(t, ka, "Expected to find KnownAddress %v but wasn't there.", addr)
|
||||
|
||||
if !(ka.Addr.Equals(addr) && ka.Src.Equals(src)) {
|
||||
t.Fatalf("KnownAddress doesn't match addr & src")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddrBookPromoteToOld(t *testing.T) {
|
||||
fname := createTempFileName("addrbook_test")
|
||||
defer deleteTempFile(fname)
|
||||
|
||||
randAddrs := randNetAddressPairs(t, 100)
|
||||
|
||||
book := NewAddrBook(fname, true)
|
||||
book.SetLogger(log.TestingLogger())
|
||||
for _, addrSrc := range randAddrs {
|
||||
book.AddAddress(addrSrc.addr, addrSrc.src)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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("addrbook_test")
|
||||
defer deleteTempFile(fname)
|
||||
|
||||
book := NewAddrBook(fname, true)
|
||||
book.SetLogger(log.TestingLogger())
|
||||
|
||||
randAddrs := randNetAddressPairs(t, 100)
|
||||
|
||||
differentSrc := randIPv4Address(t)
|
||||
for _, addrSrc := range randAddrs {
|
||||
book.AddAddress(addrSrc.addr, addrSrc.src)
|
||||
book.AddAddress(addrSrc.addr, addrSrc.src) // duplicate
|
||||
book.AddAddress(addrSrc.addr, differentSrc) // different src
|
||||
}
|
||||
|
||||
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",
|
||||
rand.Intn(254)+1,
|
||||
rand.Intn(255),
|
||||
rand.Intn(255),
|
||||
rand.Intn(255),
|
||||
)
|
||||
port := rand.Intn(65535-1) + 1
|
||||
id := p2p.ID(hex.EncodeToString(cmn.RandBytes(p2p.IDByteLength)))
|
||||
idAddr := p2p.IDAddressString(id, fmt.Sprintf("%v:%v", ip, port))
|
||||
addr, err := p2p.NewNetAddressString(idAddr)
|
||||
assert.Nil(t, err, "error generating rand network address")
|
||||
if addr.Routable() {
|
||||
return addr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddrBookRemoveAddress(t *testing.T) {
|
||||
fname := createTempFileName("addrbook_test")
|
||||
defer deleteTempFile(fname)
|
||||
|
||||
book := NewAddrBook(fname, true)
|
||||
book.SetLogger(log.TestingLogger())
|
||||
|
||||
addr := randIPv4Address(t)
|
||||
book.AddAddress(addr, addr)
|
||||
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 TestAddrBookGetSelection(t *testing.T) {
|
||||
fname := createTempFileName("addrbook_test")
|
||||
defer deleteTempFile(fname)
|
||||
|
||||
book := NewAddrBook(fname, true)
|
||||
book.SetLogger(log.TestingLogger())
|
||||
|
||||
// 1) empty book
|
||||
assert.Empty(t, book.GetSelection())
|
||||
|
||||
// 2) add one address
|
||||
addr := randIPv4Address(t)
|
||||
book.AddAddress(addr, addr)
|
||||
|
||||
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 {
|
||||
book.AddAddress(addrSrc.addr, addrSrc.src)
|
||||
}
|
||||
|
||||
// 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("addrbook_test")
|
||||
defer deleteTempFile(fname)
|
||||
|
||||
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)
|
||||
book.AddAddress(addr, addr)
|
||||
|
||||
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 {
|
||||
book.AddAddress(addrSrc.addr, addrSrc.src)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if got >= expected {
|
||||
t.Fatalf("expected more 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("addrbook_test")
|
||||
defer deleteTempFile(fname)
|
||||
|
||||
book := NewAddrBook(fname, true)
|
||||
book.SetLogger(log.TestingLogger())
|
||||
addr := randIPv4Address(t)
|
||||
book.AddAddress(addr, addr)
|
||||
|
||||
assert.True(t, book.HasAddress(addr))
|
||||
|
||||
book.RemoveAddress(addr)
|
||||
|
||||
assert.False(t, book.HasAddress(addr))
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package pex
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/tendermint/tendermint/p2p"
|
||||
)
|
||||
|
||||
type ErrAddrBookNonRoutable struct {
|
||||
Addr *p2p.NetAddress
|
||||
}
|
||||
|
||||
func (err ErrAddrBookNonRoutable) Error() string {
|
||||
return fmt.Sprintf("Cannot add non-routable address %v", err.Addr)
|
||||
}
|
||||
|
||||
type ErrAddrBookSelf struct {
|
||||
Addr *p2p.NetAddress
|
||||
}
|
||||
|
||||
func (err ErrAddrBookSelf) Error() string {
|
||||
return fmt.Sprintf("Cannot add ourselves with address %v", err.Addr)
|
||||
}
|
||||
|
||||
type ErrAddrBookNilAddr struct {
|
||||
Addr *p2p.NetAddress
|
||||
Src *p2p.NetAddress
|
||||
}
|
||||
|
||||
func (err ErrAddrBookNilAddr) Error() string {
|
||||
return fmt.Sprintf("Cannot add a nil address. Got (addr, src) = (%v, %v)", err.Addr, err.Src)
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package pex
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
)
|
||||
|
||||
/* Loading & Saving */
|
||||
|
||||
type addrBookJSON struct {
|
||||
Key string `json:"key"`
|
||||
Addrs []*knownAddress `json:"addrs"`
|
||||
}
|
||||
|
||||
func (a *addrBook) saveToFile(filePath string) {
|
||||
a.Logger.Info("Saving AddrBook to file", "size", a.Size())
|
||||
|
||||
a.mtx.Lock()
|
||||
defer a.mtx.Unlock()
|
||||
// Compile Addrs
|
||||
addrs := []*knownAddress{}
|
||||
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 = cmn.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 {
|
||||
cmn.PanicCrisis(cmn.Fmt("Error opening file %s: %v", filePath, err))
|
||||
}
|
||||
defer r.Close() // nolint: errcheck
|
||||
aJSON := &addrBookJSON{}
|
||||
dec := json.NewDecoder(r)
|
||||
err = dec.Decode(aJSON)
|
||||
if err != nil {
|
||||
cmn.PanicCrisis(cmn.Fmt("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/p2p"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
Attempts int32 `json:"attempts"`
|
||||
LastAttempt time.Time `json:"last_attempt"`
|
||||
LastSuccess time.Time `json:"last_success"`
|
||||
BucketType byte `json:"bucket_type"`
|
||||
Buckets []int `json:"buckets"`
|
||||
}
|
||||
|
||||
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() p2p.ID {
|
||||
return ka.Addr.ID
|
||||
}
|
||||
|
||||
func (ka *knownAddress) copy() *knownAddress {
|
||||
return &knownAddress{
|
||||
Addr: ka.Addr,
|
||||
Src: ka.Src,
|
||||
Attempts: ka.Attempts,
|
||||
LastAttempt: ka.LastAttempt,
|
||||
LastSuccess: ka.LastSuccess,
|
||||
BucketType: ka.BucketType,
|
||||
Buckets: ka.Buckets,
|
||||
}
|
||||
}
|
||||
|
||||
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) 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,700 +0,0 @@
|
||||
package pex
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
amino "github.com/tendermint/go-amino"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
|
||||
"github.com/tendermint/tendermint/p2p"
|
||||
"github.com/tendermint/tendermint/p2p/conn"
|
||||
)
|
||||
|
||||
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
|
||||
defaultMinNumOutboundPeers = p2p.DefaultMinNumOutboundPeers
|
||||
|
||||
// Seed/Crawler constants
|
||||
|
||||
// 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.
|
||||
// see consensus/reactor.go: blocksToContributeToBecomeGoodPeer
|
||||
// 10000 blocks assuming 1s blocks ~ 2.7 hours.
|
||||
defaultSeedDisconnectWaitPeriod = 3 * time.Hour
|
||||
|
||||
defaultCrawlPeerInterval = 2 * time.Minute // don't redial for this. TODO: back-off. what for?
|
||||
|
||||
defaultCrawlPeersPeriod = 30 * time.Second // check some peers every this
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
// PEXReactor 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 PEXReactor struct {
|
||||
p2p.BaseReactor
|
||||
|
||||
book AddrBook
|
||||
config *PEXReactorConfig
|
||||
ensurePeersPeriod time.Duration // TODO: should go in the config
|
||||
|
||||
// maps to prevent abuse
|
||||
requestsSent *cmn.CMap // ID->struct{}: unanswered send requests
|
||||
lastReceivedRequests *cmn.CMap // ID->time.Time: last time peer requested from us
|
||||
|
||||
attemptsToDial sync.Map // address (string) -> {number of attempts (int), last time dialed (time.Time)}
|
||||
}
|
||||
|
||||
func (r *PEXReactor) 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
|
||||
}
|
||||
|
||||
// PEXReactorConfig holds reactor specific configuration data.
|
||||
type PEXReactorConfig struct {
|
||||
// Seed/Crawler mode
|
||||
SeedMode bool
|
||||
|
||||
// Seeds is a list of addresses reactor may use
|
||||
// if it can't connect to peers in the addrbook.
|
||||
Seeds []string
|
||||
|
||||
// PrivatePeerIDs is a list of peer IDs, which must not be gossiped to other
|
||||
// peers.
|
||||
PrivatePeerIDs []string
|
||||
}
|
||||
|
||||
type _attemptsToDial struct {
|
||||
number int
|
||||
lastDialed time.Time
|
||||
}
|
||||
|
||||
// NewPEXReactor creates new PEX reactor.
|
||||
func NewPEXReactor(b AddrBook, config *PEXReactorConfig) *PEXReactor {
|
||||
r := &PEXReactor{
|
||||
book: b,
|
||||
config: config,
|
||||
ensurePeersPeriod: defaultEnsurePeersPeriod,
|
||||
requestsSent: cmn.NewCMap(),
|
||||
lastReceivedRequests: cmn.NewCMap(),
|
||||
}
|
||||
r.BaseReactor = *p2p.NewBaseReactor("PEXReactor", r)
|
||||
return r
|
||||
}
|
||||
|
||||
// OnStart implements BaseService
|
||||
func (r *PEXReactor) OnStart() error {
|
||||
if err := r.BaseReactor.OnStart(); err != nil {
|
||||
return err
|
||||
}
|
||||
err := r.book.Start()
|
||||
if err != nil && err != cmn.ErrAlreadyStarted {
|
||||
return err
|
||||
}
|
||||
|
||||
// return err if user provided a bad seed address
|
||||
// or a host name that we cant resolve
|
||||
if err := r.checkSeeds(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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 *PEXReactor) OnStop() {
|
||||
r.BaseReactor.OnStop()
|
||||
r.book.Stop()
|
||||
}
|
||||
|
||||
// GetChannels implements Reactor
|
||||
func (r *PEXReactor) GetChannels() []*conn.ChannelDescriptor {
|
||||
return []*conn.ChannelDescriptor{
|
||||
{
|
||||
ID: PexChannel,
|
||||
Priority: 1,
|
||||
SendQueueCapacity: 10,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// AddPeer implements Reactor by adding peer to the address book (if inbound)
|
||||
// or by requesting more addresses (if outbound).
|
||||
func (r *PEXReactor) 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 := p.NodeInfo().NetAddress()
|
||||
src := addr
|
||||
|
||||
// ignore private addrs
|
||||
if isAddrPrivate(addr, r.config.PrivatePeerIDs) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PEXReactor) 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, etc.
|
||||
r.Logger.Debug("Failed to add new address", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RemovePeer implements Reactor.
|
||||
func (r *PEXReactor) RemovePeer(p Peer, reason interface{}) {
|
||||
id := string(p.ID())
|
||||
r.requestsSent.Delete(id)
|
||||
r.lastReceivedRequests.Delete(id)
|
||||
}
|
||||
|
||||
// Receive implements Reactor by handling incoming PEX messages.
|
||||
func (r *PEXReactor) Receive(chID byte, src Peer, msgBytes []byte) {
|
||||
msg, err := decodeMsg(msgBytes)
|
||||
if err != nil {
|
||||
r.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes)
|
||||
r.Switch.StopPeerForError(src, err)
|
||||
return
|
||||
}
|
||||
r.Logger.Debug("Received message", "src", src, "chId", chID, "msg", msg)
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case *pexRequestMessage:
|
||||
// Check we're not receiving too many requests
|
||||
if err := r.receiveRequest(src); err != nil {
|
||||
r.Switch.StopPeerForError(src, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Seeds disconnect after sending a batch of addrs
|
||||
// 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 r.config.SeedMode {
|
||||
r.SendAddrs(src, r.book.GetSelectionWithBias(biasToSelectNewPeers))
|
||||
r.Switch.StopPeerGracefully(src)
|
||||
} else {
|
||||
r.SendAddrs(src, r.book.GetSelection())
|
||||
}
|
||||
|
||||
case *pexAddrsMessage:
|
||||
// If we asked for addresses, add them to the book
|
||||
if err := r.ReceiveAddrs(msg.Addrs, src); err != nil {
|
||||
r.Switch.StopPeerForError(src, err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
r.Logger.Error(fmt.Sprintf("Unknown message type %v", reflect.TypeOf(msg)))
|
||||
}
|
||||
}
|
||||
|
||||
// enforces a minimum amount of time between requests
|
||||
func (r *PEXReactor) 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 *PEXReactor) RequestAddrs(p Peer) {
|
||||
r.Logger.Debug("Request addrs", "from", p)
|
||||
id := string(p.ID())
|
||||
if r.requestsSent.Has(id) {
|
||||
return
|
||||
}
|
||||
r.requestsSent.Set(id, struct{}{})
|
||||
p.Send(PexChannel, cdc.MustMarshalBinaryBare(&pexRequestMessage{}))
|
||||
}
|
||||
|
||||
// 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 *PEXReactor) ReceiveAddrs(addrs []*p2p.NetAddress, src Peer) error {
|
||||
|
||||
id := string(src.ID())
|
||||
if !r.requestsSent.Has(id) {
|
||||
return cmn.NewError("Received unsolicited pexAddrsMessage")
|
||||
}
|
||||
r.requestsSent.Delete(id)
|
||||
|
||||
srcAddr := src.NodeInfo().NetAddress()
|
||||
for _, netAddr := range addrs {
|
||||
// NOTE: GetSelection methods should never return nil addrs
|
||||
if netAddr == nil {
|
||||
return cmn.NewError("received nil addr")
|
||||
}
|
||||
|
||||
// ignore private peers
|
||||
// TODO: give private peers to AddrBook so it can enforce this on AddAddress.
|
||||
// We'd then have to check for ErrPrivatePeer on AddAddress here, which is
|
||||
// an error we just ignore (maybe peer is probing us for our private peers :P)
|
||||
if isAddrPrivate(netAddr, r.config.PrivatePeerIDs) {
|
||||
continue
|
||||
}
|
||||
|
||||
err := r.book.AddAddress(netAddr, srcAddr)
|
||||
r.logErrAddrBook(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendAddrs sends addrs to the peer.
|
||||
func (r *PEXReactor) SendAddrs(p Peer, netAddrs []*p2p.NetAddress) {
|
||||
p.Send(PexChannel, cdc.MustMarshalBinaryBare(&pexAddrsMessage{Addrs: netAddrs}))
|
||||
}
|
||||
|
||||
// SetEnsurePeersPeriod sets period to ensure peers connected.
|
||||
func (r *PEXReactor) SetEnsurePeersPeriod(d time.Duration) {
|
||||
r.ensurePeersPeriod = d
|
||||
}
|
||||
|
||||
// Ensures that sufficient peers are connected. (continuous)
|
||||
func (r *PEXReactor) ensurePeersRoutine() {
|
||||
var (
|
||||
seed = cmn.NewRand()
|
||||
jitter = seed.Int63n(r.ensurePeersPeriod.Nanoseconds())
|
||||
)
|
||||
|
||||
// Randomize first round of communication to avoid thundering herd.
|
||||
// If no potential peers are present directly start connecting so we guarantee
|
||||
// swift setup with the help of configured seeds.
|
||||
if r.hasPotentialPeers() {
|
||||
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 *PEXReactor) ensurePeers() {
|
||||
var (
|
||||
out, in, dial = r.Switch.NumPeers()
|
||||
numToDial = defaultMinNumOutboundPeers - (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 := cmn.MinInt(out, 8)*10 + 10
|
||||
|
||||
toDial := make(map[p2p.ID]*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 dialling := r.Switch.IsDialing(try.ID); dialling {
|
||||
continue
|
||||
}
|
||||
if connected := r.Switch.Peers().Has(try.ID); connected {
|
||||
continue
|
||||
}
|
||||
// TODO: consider moving some checks from toDial into here
|
||||
// so we don't even consider dialing peers that we want to wait
|
||||
// before dialling 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 r.dialPeer(addr)
|
||||
}
|
||||
|
||||
// If we need more addresses, pick a random peer and ask for more.
|
||||
if r.book.NeedMoreAddrs() {
|
||||
peers := r.Switch.Peers().List()
|
||||
peersCount := len(peers)
|
||||
if peersCount > 0 {
|
||||
peer := peers[cmn.RandInt()%peersCount] // nolint: gas
|
||||
r.Logger.Info("We need more addresses. Sending pexRequest to random peer", "peer", peer)
|
||||
r.RequestAddrs(peer)
|
||||
}
|
||||
}
|
||||
|
||||
// If we are not connected to nor dialing anybody, fallback to dialing a seed.
|
||||
if out+in+dial+len(toDial) == 0 {
|
||||
r.Logger.Info("No addresses to dial nor connected peers. Falling back to seeds")
|
||||
r.dialSeeds()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PEXReactor) 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 *PEXReactor) dialPeer(addr *p2p.NetAddress) {
|
||||
attempts, lastDialed := r.dialAttemptsInfo(addr)
|
||||
|
||||
if attempts > maxAttemptsToDial {
|
||||
r.Logger.Error("Reached max attempts to dial", "addr", addr, "attempts", attempts)
|
||||
r.book.MarkBad(addr)
|
||||
return
|
||||
}
|
||||
|
||||
// exponential backoff if it's not our first attempt to dial given address
|
||||
if attempts > 0 {
|
||||
jitterSeconds := time.Duration(cmn.RandFloat64() * float64(time.Second)) // 1s == (1e9 ns)
|
||||
backoffDuration := jitterSeconds + ((1 << uint(attempts)) * time.Second)
|
||||
sinceLastDialed := time.Since(lastDialed)
|
||||
if sinceLastDialed < backoffDuration {
|
||||
r.Logger.Debug("Too early to dial", "addr", addr, "backoff_duration", backoffDuration, "last_dialed", lastDialed, "time_since", sinceLastDialed)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err := r.Switch.DialPeerWithAddress(addr, false)
|
||||
if err != nil {
|
||||
r.Logger.Error("Dialing failed", "addr", addr, "err", err, "attempts", attempts)
|
||||
// TODO: detect more "bad peer" scenarios
|
||||
if _, ok := err.(p2p.ErrSwitchAuthenticationFailure); ok {
|
||||
r.book.MarkBad(addr)
|
||||
r.attemptsToDial.Delete(addr.DialString())
|
||||
} else {
|
||||
r.book.MarkAttempt(addr)
|
||||
// FIXME: if the addr is going to be removed from the addrbook (hard to
|
||||
// tell at this point), we need to Delete it from attemptsToDial, not
|
||||
// record another attempt.
|
||||
// record attempt
|
||||
r.attemptsToDial.Store(addr.DialString(), _attemptsToDial{attempts + 1, time.Now()})
|
||||
}
|
||||
} else {
|
||||
// cleanup any history
|
||||
r.attemptsToDial.Delete(addr.DialString())
|
||||
}
|
||||
}
|
||||
|
||||
// check seed addresses are well formed
|
||||
func (r *PEXReactor) checkSeeds() error {
|
||||
lSeeds := len(r.config.Seeds)
|
||||
if lSeeds == 0 {
|
||||
return nil
|
||||
}
|
||||
_, errs := p2p.NewNetAddressStrings(r.config.Seeds)
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// randomly dial seeds until we connect to one or exhaust them
|
||||
func (r *PEXReactor) dialSeeds() {
|
||||
lSeeds := len(r.config.Seeds)
|
||||
if lSeeds == 0 {
|
||||
return
|
||||
}
|
||||
seedAddrs, _ := p2p.NewNetAddressStrings(r.config.Seeds)
|
||||
|
||||
perm := cmn.RandPerm(lSeeds)
|
||||
// perm := r.Switch.rng.Perm(lSeeds)
|
||||
for _, i := range perm {
|
||||
// dial a random seed
|
||||
seedAddr := seedAddrs[i]
|
||||
err := r.Switch.DialPeerWithAddress(seedAddr, false)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
r.Switch.Logger.Error("Error dialing seed", "err", err, "seed", seedAddr)
|
||||
}
|
||||
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 *PEXReactor) 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 *PEXReactor) crawlPeersRoutine() {
|
||||
// Do an initial crawl
|
||||
r.crawlPeers()
|
||||
|
||||
// Fire periodically
|
||||
ticker := time.NewTicker(defaultCrawlPeersPeriod)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
r.attemptDisconnects()
|
||||
r.crawlPeers()
|
||||
case <-r.Quit():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hasPotentialPeers indicates if there is a potential peer to connect to, by
|
||||
// consulting the Switch as well as the AddrBook.
|
||||
func (r *PEXReactor) hasPotentialPeers() bool {
|
||||
out, in, dial := r.Switch.NumPeers()
|
||||
|
||||
return out+in+dial > 0 && len(r.book.ListOfKnownAddresses()) > 0
|
||||
}
|
||||
|
||||
// crawlPeerInfo handles temporary data needed for the
|
||||
// network crawling performed during seed/crawler mode.
|
||||
type crawlPeerInfo struct {
|
||||
// The listening address of a potential peer we learned about
|
||||
Addr *p2p.NetAddress
|
||||
|
||||
// The last time we attempt to reach this address
|
||||
LastAttempt time.Time
|
||||
|
||||
// The last time we successfully reached this address
|
||||
LastSuccess time.Time
|
||||
}
|
||||
|
||||
// oldestFirst implements sort.Interface for []crawlPeerInfo
|
||||
// based on the LastAttempt field.
|
||||
type oldestFirst []crawlPeerInfo
|
||||
|
||||
func (of oldestFirst) Len() int { return len(of) }
|
||||
func (of oldestFirst) Swap(i, j int) { of[i], of[j] = of[j], of[i] }
|
||||
func (of oldestFirst) Less(i, j int) bool { return of[i].LastAttempt.Before(of[j].LastAttempt) }
|
||||
|
||||
// getPeersToCrawl returns addresses of potential peers that we wish to validate.
|
||||
// NOTE: The status information is ordered as described above.
|
||||
func (r *PEXReactor) getPeersToCrawl() []crawlPeerInfo {
|
||||
var of oldestFirst
|
||||
|
||||
// TODO: be more selective
|
||||
addrs := r.book.ListOfKnownAddresses()
|
||||
for _, addr := range addrs {
|
||||
if len(addr.ID()) == 0 {
|
||||
continue // dont use peers without id
|
||||
}
|
||||
|
||||
of = append(of, crawlPeerInfo{
|
||||
Addr: addr.Addr,
|
||||
LastAttempt: addr.LastAttempt,
|
||||
LastSuccess: addr.LastSuccess,
|
||||
})
|
||||
}
|
||||
sort.Sort(of)
|
||||
return of
|
||||
}
|
||||
|
||||
// crawlPeers will crawl the network looking for new peer addresses. (once)
|
||||
func (r *PEXReactor) crawlPeers() {
|
||||
peerInfos := r.getPeersToCrawl()
|
||||
|
||||
now := time.Now()
|
||||
// Use addresses we know of to reach additional peers
|
||||
for _, pi := range peerInfos {
|
||||
// Do not attempt to connect with peers we recently dialed
|
||||
if now.Sub(pi.LastAttempt) < defaultCrawlPeerInterval {
|
||||
continue
|
||||
}
|
||||
// Otherwise, attempt to connect with the known address
|
||||
err := r.Switch.DialPeerWithAddress(pi.Addr, false)
|
||||
if err != nil {
|
||||
r.book.MarkAttempt(pi.Addr)
|
||||
continue
|
||||
}
|
||||
// Ask for more addresses
|
||||
peer := r.Switch.Peers().Get(pi.Addr.ID)
|
||||
if peer != nil {
|
||||
r.RequestAddrs(peer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// attemptDisconnects checks if we've been with each peer long enough to disconnect
|
||||
func (r *PEXReactor) attemptDisconnects() {
|
||||
for _, peer := range r.Switch.Peers().List() {
|
||||
if peer.Status().Duration < defaultSeedDisconnectWaitPeriod {
|
||||
continue
|
||||
}
|
||||
if peer.IsPersistent() {
|
||||
continue
|
||||
}
|
||||
r.Switch.StopPeerGracefully(peer)
|
||||
}
|
||||
}
|
||||
|
||||
// isAddrPrivate returns true if addr.ID is a private ID.
|
||||
func isAddrPrivate(addr *p2p.NetAddress, privatePeerIDs []string) bool {
|
||||
for _, id := range privatePeerIDs {
|
||||
if string(addr.ID) == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Messages
|
||||
|
||||
// PexMessage is a primary type for PEX messages. Underneath, it could contain
|
||||
// either pexRequestMessage, or pexAddrsMessage messages.
|
||||
type PexMessage interface{}
|
||||
|
||||
func RegisterPexMessage(cdc *amino.Codec) {
|
||||
cdc.RegisterInterface((*PexMessage)(nil), nil)
|
||||
cdc.RegisterConcrete(&pexRequestMessage{}, "tendermint/p2p/PexRequestMessage", nil)
|
||||
cdc.RegisterConcrete(&pexAddrsMessage{}, "tendermint/p2p/PexAddrsMessage", nil)
|
||||
}
|
||||
|
||||
func decodeMsg(bz []byte) (msg PexMessage, err error) {
|
||||
if len(bz) > maxMsgSize {
|
||||
return msg, fmt.Errorf("Msg exceeds max size (%d > %d)", len(bz), maxMsgSize)
|
||||
}
|
||||
err = cdc.UnmarshalBinaryBare(bz, &msg)
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
A pexRequestMessage requests additional peer addresses.
|
||||
*/
|
||||
type pexRequestMessage struct {
|
||||
}
|
||||
|
||||
func (m *pexRequestMessage) String() string {
|
||||
return "[pexRequest]"
|
||||
}
|
||||
|
||||
/*
|
||||
A message with announced peer addresses.
|
||||
*/
|
||||
type pexAddrsMessage struct {
|
||||
Addrs []*p2p.NetAddress
|
||||
}
|
||||
|
||||
func (m *pexAddrsMessage) String() string {
|
||||
return fmt.Sprintf("[pexAddrs %v]", m.Addrs)
|
||||
}
|
||||
@@ -1,454 +0,0 @@
|
||||
package pex
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
crypto "github.com/tendermint/tendermint/crypto"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
|
||||
"github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/p2p"
|
||||
"github.com/tendermint/tendermint/p2p/conn"
|
||||
)
|
||||
|
||||
var (
|
||||
cfg *config.P2PConfig
|
||||
)
|
||||
|
||||
func init() {
|
||||
cfg = config.DefaultP2PConfig()
|
||||
cfg.PexReactor = true
|
||||
cfg.AllowDuplicateIP = true
|
||||
}
|
||||
|
||||
func TestPEXReactorBasic(t *testing.T) {
|
||||
r, book := createReactor(&PEXReactorConfig{})
|
||||
defer teardownReactor(book)
|
||||
|
||||
assert.NotNil(t, r)
|
||||
assert.NotEmpty(t, r.GetChannels())
|
||||
}
|
||||
|
||||
func TestPEXReactorAddRemovePeer(t *testing.T) {
|
||||
r, book := createReactor(&PEXReactorConfig{})
|
||||
defer teardownReactor(book)
|
||||
|
||||
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, err := ioutil.TempDir("", "pex_reactor")
|
||||
require.Nil(t, err)
|
||||
defer os.RemoveAll(dir) // nolint: errcheck
|
||||
|
||||
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 := NewPEXReactor(books[i], &PEXReactorConfig{})
|
||||
r.SetLogger(logger.With("pex", i))
|
||||
r.SetEnsurePeersPeriod(250 * time.Millisecond)
|
||||
sw.AddReactor("pex", r)
|
||||
|
||||
return sw
|
||||
})
|
||||
}
|
||||
|
||||
addOtherNodeAddrToAddrBook := func(switchIndex, otherSwitchIndex int) {
|
||||
addr := switches[otherSwitchIndex].NodeInfo().NetAddress()
|
||||
books[switchIndex].AddAddress(addr, addr)
|
||||
}
|
||||
|
||||
addOtherNodeAddrToAddrBook(0, 1)
|
||||
addOtherNodeAddrToAddrBook(1, 0)
|
||||
addOtherNodeAddrToAddrBook(2, 1)
|
||||
|
||||
for i, sw := range switches {
|
||||
sw.AddListener(p2p.NewDefaultListener("tcp://"+sw.NodeInfo().ListenAddr, "", false, logger.With("pex", i)))
|
||||
|
||||
err := sw.Start() // start switch and reactors
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
assertPeersWithTimeout(t, switches, 10*time.Millisecond, 10*time.Second, N-1)
|
||||
|
||||
// stop them
|
||||
for _, s := range switches {
|
||||
s.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func TestPEXReactorReceive(t *testing.T) {
|
||||
r, book := createReactor(&PEXReactorConfig{})
|
||||
defer teardownReactor(book)
|
||||
|
||||
peer := p2p.CreateRandomPeer(false)
|
||||
|
||||
// we have to send a request to receive responses
|
||||
r.RequestAddrs(peer)
|
||||
|
||||
size := book.Size()
|
||||
addrs := []*p2p.NetAddress{peer.NodeInfo().NetAddress()}
|
||||
msg := cdc.MustMarshalBinaryBare(&pexAddrsMessage{Addrs: addrs})
|
||||
r.Receive(PexChannel, peer, msg)
|
||||
assert.Equal(t, size+1, book.Size())
|
||||
|
||||
msg = cdc.MustMarshalBinaryBare(&pexRequestMessage{})
|
||||
r.Receive(PexChannel, peer, msg) // should not panic.
|
||||
}
|
||||
|
||||
func TestPEXReactorRequestMessageAbuse(t *testing.T) {
|
||||
r, book := createReactor(&PEXReactorConfig{})
|
||||
defer teardownReactor(book)
|
||||
|
||||
sw := createSwitchAndAddReactors(r)
|
||||
sw.SetAddrBook(book)
|
||||
|
||||
peer := newMockPeer()
|
||||
p2p.AddPeerToSwitch(sw, peer)
|
||||
assert.True(t, sw.Peers().Has(peer.ID()))
|
||||
|
||||
id := string(peer.ID())
|
||||
msg := cdc.MustMarshalBinaryBare(&pexRequestMessage{})
|
||||
|
||||
// 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()))
|
||||
}
|
||||
|
||||
func TestPEXReactorAddrsMessageAbuse(t *testing.T) {
|
||||
r, book := createReactor(&PEXReactorConfig{})
|
||||
defer teardownReactor(book)
|
||||
|
||||
sw := createSwitchAndAddReactors(r)
|
||||
sw.SetAddrBook(book)
|
||||
|
||||
peer := newMockPeer()
|
||||
p2p.AddPeerToSwitch(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()))
|
||||
|
||||
addrs := []*p2p.NetAddress{peer.NodeInfo().NetAddress()}
|
||||
msg := cdc.MustMarshalBinaryBare(&pexAddrsMessage{Addrs: addrs})
|
||||
|
||||
// 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 addrs causes a disconnect
|
||||
r.Receive(PexChannel, peer, msg)
|
||||
assert.False(t, sw.Peers().Has(peer.ID()))
|
||||
}
|
||||
|
||||
func TestPEXReactorUsesSeedsIfNeeded(t *testing.T) {
|
||||
// directory to store address books
|
||||
dir, err := ioutil.TempDir("", "pex_reactor")
|
||||
require.Nil(t, err)
|
||||
defer os.RemoveAll(dir) // nolint: errcheck
|
||||
|
||||
// 1. create seed
|
||||
seed := p2p.MakeSwitch(
|
||||
cfg,
|
||||
0,
|
||||
"127.0.0.1",
|
||||
"123.123.123",
|
||||
func(i int, sw *p2p.Switch) *p2p.Switch {
|
||||
book := NewAddrBook(filepath.Join(dir, "addrbook0.json"), false)
|
||||
book.SetLogger(log.TestingLogger())
|
||||
sw.SetAddrBook(book)
|
||||
|
||||
sw.SetLogger(log.TestingLogger())
|
||||
|
||||
r := NewPEXReactor(book, &PEXReactorConfig{})
|
||||
r.SetLogger(log.TestingLogger())
|
||||
sw.AddReactor("pex", r)
|
||||
return sw
|
||||
},
|
||||
)
|
||||
seed.AddListener(
|
||||
p2p.NewDefaultListener("tcp://"+seed.NodeInfo().ListenAddr, "", false, log.TestingLogger()),
|
||||
)
|
||||
require.Nil(t, seed.Start())
|
||||
defer seed.Stop()
|
||||
|
||||
// 2. create usual peer with only seed configured.
|
||||
peer := p2p.MakeSwitch(
|
||||
cfg,
|
||||
1,
|
||||
"127.0.0.1",
|
||||
"123.123.123",
|
||||
func(i int, sw *p2p.Switch) *p2p.Switch {
|
||||
book := NewAddrBook(filepath.Join(dir, "addrbook1.json"), false)
|
||||
book.SetLogger(log.TestingLogger())
|
||||
sw.SetAddrBook(book)
|
||||
|
||||
sw.SetLogger(log.TestingLogger())
|
||||
|
||||
r := NewPEXReactor(
|
||||
book,
|
||||
&PEXReactorConfig{
|
||||
Seeds: []string{seed.NodeInfo().NetAddress().String()},
|
||||
},
|
||||
)
|
||||
r.SetLogger(log.TestingLogger())
|
||||
sw.AddReactor("pex", r)
|
||||
return sw
|
||||
},
|
||||
)
|
||||
require.Nil(t, peer.Start())
|
||||
defer peer.Stop()
|
||||
|
||||
// 3. check that the peer connects to seed immediately
|
||||
assertPeersWithTimeout(t, []*p2p.Switch{peer}, 10*time.Millisecond, 3*time.Second, 1)
|
||||
}
|
||||
|
||||
func TestPEXReactorCrawlStatus(t *testing.T) {
|
||||
pexR, book := createReactor(&PEXReactorConfig{SeedMode: true})
|
||||
defer teardownReactor(book)
|
||||
|
||||
// Seed/Crawler mode uses data from the Switch
|
||||
sw := createSwitchAndAddReactors(pexR)
|
||||
sw.SetAddrBook(book)
|
||||
|
||||
// Create a peer, add it to the peer set and the addrbook.
|
||||
peer := p2p.CreateRandomPeer(false)
|
||||
p2p.AddPeerToSwitch(pexR.Switch, peer)
|
||||
addr1 := peer.NodeInfo().NetAddress()
|
||||
pexR.book.AddAddress(addr1, addr1)
|
||||
|
||||
// Add a non-connected address to the book.
|
||||
_, addr2 := p2p.CreateRoutableAddr()
|
||||
pexR.book.AddAddress(addr2, addr1)
|
||||
|
||||
// Get some peerInfos to crawl
|
||||
peerInfos := pexR.getPeersToCrawl()
|
||||
|
||||
// Make sure it has the proper number of elements
|
||||
assert.Equal(t, 2, len(peerInfos))
|
||||
|
||||
// TODO: test
|
||||
}
|
||||
|
||||
func TestPEXReactorDoesNotAddPrivatePeersToAddrBook(t *testing.T) {
|
||||
peer := p2p.CreateRandomPeer(false)
|
||||
|
||||
pexR, book := createReactor(&PEXReactorConfig{PrivatePeerIDs: []string{string(peer.NodeInfo().ID)}})
|
||||
defer teardownReactor(book)
|
||||
|
||||
// we have to send a request to receive responses
|
||||
pexR.RequestAddrs(peer)
|
||||
|
||||
size := book.Size()
|
||||
addrs := []*p2p.NetAddress{peer.NodeInfo().NetAddress()}
|
||||
msg := cdc.MustMarshalBinaryBare(&pexAddrsMessage{Addrs: addrs})
|
||||
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(&PEXReactorConfig{})
|
||||
defer teardownReactor(book)
|
||||
|
||||
sw := createSwitchAndAddReactors(pexR)
|
||||
sw.SetAddrBook(book)
|
||||
|
||||
peer := newMockPeer()
|
||||
addr := peer.NodeInfo().NetAddress()
|
||||
|
||||
assert.Equal(t, 0, pexR.AttemptsToDial(addr))
|
||||
|
||||
// 1st unsuccessful attempt
|
||||
pexR.dialPeer(addr)
|
||||
|
||||
assert.Equal(t, 1, pexR.AttemptsToDial(addr))
|
||||
|
||||
// 2nd unsuccessful attempt
|
||||
pexR.dialPeer(addr)
|
||||
|
||||
// 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
|
||||
pexR.dialPeer(addr)
|
||||
|
||||
assert.Equal(t, 2, pexR.AttemptsToDial(addr))
|
||||
}
|
||||
}
|
||||
|
||||
type mockPeer struct {
|
||||
*cmn.BaseService
|
||||
pubKey crypto.PubKey
|
||||
addr *p2p.NetAddress
|
||||
outbound, persistent bool
|
||||
}
|
||||
|
||||
func newMockPeer() mockPeer {
|
||||
_, netAddr := p2p.CreateRoutableAddr()
|
||||
mp := mockPeer{
|
||||
addr: netAddr,
|
||||
pubKey: crypto.GenPrivKeyEd25519().PubKey(),
|
||||
}
|
||||
mp.BaseService = cmn.NewBaseService(nil, "MockPeer", mp)
|
||||
mp.Start()
|
||||
return mp
|
||||
}
|
||||
|
||||
func (mp mockPeer) ID() p2p.ID { return mp.addr.ID }
|
||||
func (mp mockPeer) IsOutbound() bool { return mp.outbound }
|
||||
func (mp mockPeer) IsPersistent() bool { return mp.persistent }
|
||||
func (mp mockPeer) NodeInfo() p2p.NodeInfo {
|
||||
return p2p.NodeInfo{
|
||||
ID: mp.addr.ID,
|
||||
ListenAddr: mp.addr.DialString(),
|
||||
}
|
||||
}
|
||||
func (mp mockPeer) RemoteIP() net.IP { return net.ParseIP("127.0.0.1") }
|
||||
func (mp mockPeer) Status() conn.ConnectionStatus { return conn.ConnectionStatus{} }
|
||||
func (mp mockPeer) Send(byte, []byte) bool { return false }
|
||||
func (mp mockPeer) TrySend(byte, []byte) bool { return false }
|
||||
func (mp mockPeer) Set(string, interface{}) {}
|
||||
func (mp mockPeer) Get(string) interface{} { return nil }
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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 one peer (switches: %s)",
|
||||
numPeersStr,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createReactor(conf *PEXReactorConfig) (r *PEXReactor, book *addrBook) {
|
||||
// directory to store address book
|
||||
dir, err := ioutil.TempDir("", "pex_reactor")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
book = NewAddrBook(filepath.Join(dir, "addrbook.json"), true)
|
||||
book.SetLogger(log.TestingLogger())
|
||||
|
||||
r = NewPEXReactor(book, conf)
|
||||
r.SetLogger(log.TestingLogger())
|
||||
return
|
||||
}
|
||||
|
||||
func teardownReactor(book *addrBook) {
|
||||
err := os.RemoveAll(filepath.Dir(book.FilePath()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
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 { return sw })
|
||||
sw.SetLogger(log.TestingLogger())
|
||||
for _, r := range reactors {
|
||||
sw.AddReactor(r.String(), r)
|
||||
r.SetSwitch(sw)
|
||||
}
|
||||
return sw
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package pex
|
||||
|
||||
import (
|
||||
"github.com/tendermint/go-amino"
|
||||
)
|
||||
|
||||
var cdc *amino.Codec = amino.NewCodec()
|
||||
|
||||
func init() {
|
||||
RegisterPexMessage(cdc)
|
||||
}
|
||||
-662
@@ -1,662 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/p2p/conn"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
)
|
||||
|
||||
const (
|
||||
// wait a random amount of time from this interval
|
||||
// before dialing peers or reconnecting to help prevent DoS
|
||||
dialRandomizerIntervalMilliseconds = 3000
|
||||
|
||||
// repeatedly try to reconnect for a few minutes
|
||||
// ie. 5 * 20 = 100s
|
||||
reconnectAttempts = 20
|
||||
reconnectInterval = 5 * time.Second
|
||||
|
||||
// then move into exponential backoff mode for ~1day
|
||||
// ie. 3**10 = 16hrs
|
||||
reconnectBackOffAttempts = 10
|
||||
reconnectBackOffBaseSeconds = 3
|
||||
|
||||
// keep at least this many outbound peers
|
||||
// TODO: move to config
|
||||
DefaultMinNumOutboundPeers = 10
|
||||
)
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// An AddrBook represents an address book from the pex package, which is used
|
||||
// to store peer addresses.
|
||||
type AddrBook interface {
|
||||
AddAddress(addr *NetAddress, src *NetAddress) error
|
||||
AddOurAddress(*NetAddress)
|
||||
OurAddress(*NetAddress) bool
|
||||
MarkGood(*NetAddress)
|
||||
RemoveAddress(*NetAddress)
|
||||
HasAddress(*NetAddress) bool
|
||||
Save()
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// Switch handles peer connections and exposes an API to receive incoming messages
|
||||
// on `Reactors`. Each `Reactor` is responsible for handling incoming messages of one
|
||||
// or more `Channels`. So while sending outgoing messages is typically performed on the peer,
|
||||
// incoming messages are received on the reactor.
|
||||
type Switch struct {
|
||||
cmn.BaseService
|
||||
|
||||
config *config.P2PConfig
|
||||
listeners []Listener
|
||||
reactors map[string]Reactor
|
||||
chDescs []*conn.ChannelDescriptor
|
||||
reactorsByCh map[byte]Reactor
|
||||
peers *PeerSet
|
||||
dialing *cmn.CMap
|
||||
reconnecting *cmn.CMap
|
||||
nodeInfo NodeInfo // our node info
|
||||
nodeKey *NodeKey // our node privkey
|
||||
addrBook AddrBook
|
||||
|
||||
filterConnByAddr func(net.Addr) error
|
||||
filterConnByID func(ID) error
|
||||
|
||||
mConfig conn.MConnConfig
|
||||
|
||||
rng *cmn.Rand // seed for randomizing dial times and orders
|
||||
|
||||
metrics *Metrics
|
||||
}
|
||||
|
||||
// SwitchOption sets an optional parameter on the Switch.
|
||||
type SwitchOption func(*Switch)
|
||||
|
||||
// NewSwitch creates a new Switch with the given config.
|
||||
func NewSwitch(cfg *config.P2PConfig, options ...SwitchOption) *Switch {
|
||||
sw := &Switch{
|
||||
config: cfg,
|
||||
reactors: make(map[string]Reactor),
|
||||
chDescs: make([]*conn.ChannelDescriptor, 0),
|
||||
reactorsByCh: make(map[byte]Reactor),
|
||||
peers: NewPeerSet(),
|
||||
dialing: cmn.NewCMap(),
|
||||
reconnecting: cmn.NewCMap(),
|
||||
metrics: NopMetrics(),
|
||||
}
|
||||
|
||||
// Ensure we have a completely undeterministic PRNG.
|
||||
sw.rng = cmn.NewRand()
|
||||
|
||||
mConfig := conn.DefaultMConnConfig()
|
||||
mConfig.FlushThrottle = time.Duration(cfg.FlushThrottleTimeout) * time.Millisecond
|
||||
mConfig.SendRate = cfg.SendRate
|
||||
mConfig.RecvRate = cfg.RecvRate
|
||||
mConfig.MaxPacketMsgPayloadSize = cfg.MaxPacketMsgPayloadSize
|
||||
|
||||
sw.mConfig = mConfig
|
||||
|
||||
sw.BaseService = *cmn.NewBaseService(nil, "P2P Switch", sw)
|
||||
|
||||
for _, option := range options {
|
||||
option(sw)
|
||||
}
|
||||
|
||||
return sw
|
||||
}
|
||||
|
||||
// WithMetrics sets the metrics.
|
||||
func WithMetrics(metrics *Metrics) SwitchOption {
|
||||
return func(sw *Switch) { sw.metrics = metrics }
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Switch setup
|
||||
|
||||
// AddReactor adds the given reactor to the switch.
|
||||
// NOTE: Not goroutine safe.
|
||||
func (sw *Switch) AddReactor(name string, reactor Reactor) Reactor {
|
||||
// Validate the reactor.
|
||||
// No two reactors can share the same channel.
|
||||
reactorChannels := reactor.GetChannels()
|
||||
for _, chDesc := range reactorChannels {
|
||||
chID := chDesc.ID
|
||||
if sw.reactorsByCh[chID] != nil {
|
||||
cmn.PanicSanity(fmt.Sprintf("Channel %X has multiple reactors %v & %v", chID, sw.reactorsByCh[chID], reactor))
|
||||
}
|
||||
sw.chDescs = append(sw.chDescs, chDesc)
|
||||
sw.reactorsByCh[chID] = reactor
|
||||
}
|
||||
sw.reactors[name] = reactor
|
||||
reactor.SetSwitch(sw)
|
||||
return reactor
|
||||
}
|
||||
|
||||
// Reactors returns a map of reactors registered on the switch.
|
||||
// NOTE: Not goroutine safe.
|
||||
func (sw *Switch) Reactors() map[string]Reactor {
|
||||
return sw.reactors
|
||||
}
|
||||
|
||||
// Reactor returns the reactor with the given name.
|
||||
// NOTE: Not goroutine safe.
|
||||
func (sw *Switch) Reactor(name string) Reactor {
|
||||
return sw.reactors[name]
|
||||
}
|
||||
|
||||
// AddListener adds the given listener to the switch for listening to incoming peer connections.
|
||||
// NOTE: Not goroutine safe.
|
||||
func (sw *Switch) AddListener(l Listener) {
|
||||
sw.listeners = append(sw.listeners, l)
|
||||
}
|
||||
|
||||
// Listeners returns the list of listeners the switch listens on.
|
||||
// NOTE: Not goroutine safe.
|
||||
func (sw *Switch) Listeners() []Listener {
|
||||
return sw.listeners
|
||||
}
|
||||
|
||||
// IsListening returns true if the switch has at least one listener.
|
||||
// NOTE: Not goroutine safe.
|
||||
func (sw *Switch) IsListening() bool {
|
||||
return len(sw.listeners) > 0
|
||||
}
|
||||
|
||||
// SetNodeInfo sets the switch's NodeInfo for checking compatibility and handshaking with other nodes.
|
||||
// NOTE: Not goroutine safe.
|
||||
func (sw *Switch) SetNodeInfo(nodeInfo NodeInfo) {
|
||||
sw.nodeInfo = nodeInfo
|
||||
}
|
||||
|
||||
// NodeInfo returns the switch's NodeInfo.
|
||||
// NOTE: Not goroutine safe.
|
||||
func (sw *Switch) NodeInfo() NodeInfo {
|
||||
return sw.nodeInfo
|
||||
}
|
||||
|
||||
// SetNodeKey sets the switch's private key for authenticated encryption.
|
||||
// NOTE: Not goroutine safe.
|
||||
func (sw *Switch) SetNodeKey(nodeKey *NodeKey) {
|
||||
sw.nodeKey = nodeKey
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Service start/stop
|
||||
|
||||
// OnStart implements BaseService. It starts all the reactors, peers, and listeners.
|
||||
func (sw *Switch) OnStart() error {
|
||||
// Start reactors
|
||||
for _, reactor := range sw.reactors {
|
||||
err := reactor.Start()
|
||||
if err != nil {
|
||||
return cmn.ErrorWrap(err, "failed to start %v", reactor)
|
||||
}
|
||||
}
|
||||
// Start listeners
|
||||
for _, listener := range sw.listeners {
|
||||
go sw.listenerRoutine(listener)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnStop implements BaseService. It stops all listeners, peers, and reactors.
|
||||
func (sw *Switch) OnStop() {
|
||||
// Stop listeners
|
||||
for _, listener := range sw.listeners {
|
||||
listener.Stop()
|
||||
}
|
||||
sw.listeners = nil
|
||||
// Stop peers
|
||||
for _, peer := range sw.peers.List() {
|
||||
peer.Stop()
|
||||
sw.peers.Remove(peer)
|
||||
}
|
||||
// Stop reactors
|
||||
sw.Logger.Debug("Switch: Stopping reactors")
|
||||
for _, reactor := range sw.reactors {
|
||||
reactor.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Peers
|
||||
|
||||
// Broadcast runs a go routine for each attempted send, which will block trying
|
||||
// to send for defaultSendTimeoutSeconds. Returns a channel which receives
|
||||
// success values for each attempted send (false if times out). Channel will be
|
||||
// closed once msg bytes are sent to all peers (or time out).
|
||||
//
|
||||
// NOTE: Broadcast uses goroutines, so order of broadcast may not be preserved.
|
||||
func (sw *Switch) Broadcast(chID byte, msgBytes []byte) chan bool {
|
||||
successChan := make(chan bool, len(sw.peers.List()))
|
||||
sw.Logger.Debug("Broadcast", "channel", chID, "msgBytes", fmt.Sprintf("%X", msgBytes))
|
||||
var wg sync.WaitGroup
|
||||
for _, peer := range sw.peers.List() {
|
||||
wg.Add(1)
|
||||
go func(peer Peer) {
|
||||
defer wg.Done()
|
||||
success := peer.Send(chID, msgBytes)
|
||||
successChan <- success
|
||||
}(peer)
|
||||
}
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(successChan)
|
||||
}()
|
||||
return successChan
|
||||
}
|
||||
|
||||
// NumPeers returns the count of outbound/inbound and outbound-dialing peers.
|
||||
func (sw *Switch) NumPeers() (outbound, inbound, dialing int) {
|
||||
peers := sw.peers.List()
|
||||
for _, peer := range peers {
|
||||
if peer.IsOutbound() {
|
||||
outbound++
|
||||
} else {
|
||||
inbound++
|
||||
}
|
||||
}
|
||||
dialing = sw.dialing.Size()
|
||||
return
|
||||
}
|
||||
|
||||
// Peers returns the set of peers that are connected to the switch.
|
||||
func (sw *Switch) Peers() IPeerSet {
|
||||
return sw.peers
|
||||
}
|
||||
|
||||
// StopPeerForError disconnects from a peer due to external error.
|
||||
// If the peer is persistent, it will attempt to reconnect.
|
||||
// TODO: make record depending on reason.
|
||||
func (sw *Switch) StopPeerForError(peer Peer, reason interface{}) {
|
||||
sw.Logger.Error("Stopping peer for error", "peer", peer, "err", reason)
|
||||
sw.stopAndRemovePeer(peer, reason)
|
||||
|
||||
if peer.IsPersistent() {
|
||||
// NOTE: this is the self-reported addr, not the original we dialed
|
||||
go sw.reconnectToPeer(peer.NodeInfo().NetAddress())
|
||||
}
|
||||
}
|
||||
|
||||
// StopPeerGracefully disconnects from a peer gracefully.
|
||||
// TODO: handle graceful disconnects.
|
||||
func (sw *Switch) StopPeerGracefully(peer Peer) {
|
||||
sw.Logger.Info("Stopping peer gracefully")
|
||||
sw.stopAndRemovePeer(peer, nil)
|
||||
}
|
||||
|
||||
func (sw *Switch) stopAndRemovePeer(peer Peer, reason interface{}) {
|
||||
sw.peers.Remove(peer)
|
||||
sw.metrics.Peers.Add(float64(-1))
|
||||
peer.Stop()
|
||||
for _, reactor := range sw.reactors {
|
||||
reactor.RemovePeer(peer, reason)
|
||||
}
|
||||
}
|
||||
|
||||
// reconnectToPeer tries to reconnect to the addr, first repeatedly
|
||||
// with a fixed interval, then with exponential backoff.
|
||||
// If no success after all that, it stops trying, and leaves it
|
||||
// to the PEX/Addrbook to find the peer with the addr again
|
||||
// NOTE: this will keep trying even if the handshake or auth fails.
|
||||
// TODO: be more explicit with error types so we only retry on certain failures
|
||||
// - ie. if we're getting ErrDuplicatePeer we can stop
|
||||
// because the addrbook got us the peer back already
|
||||
func (sw *Switch) reconnectToPeer(addr *NetAddress) {
|
||||
if sw.reconnecting.Has(string(addr.ID)) {
|
||||
return
|
||||
}
|
||||
sw.reconnecting.Set(string(addr.ID), addr)
|
||||
defer sw.reconnecting.Delete(string(addr.ID))
|
||||
|
||||
start := time.Now()
|
||||
sw.Logger.Info("Reconnecting to peer", "addr", addr)
|
||||
for i := 0; i < reconnectAttempts; i++ {
|
||||
if !sw.IsRunning() {
|
||||
return
|
||||
}
|
||||
|
||||
err := sw.DialPeerWithAddress(addr, true)
|
||||
if err == nil {
|
||||
return // success
|
||||
}
|
||||
|
||||
sw.Logger.Info("Error reconnecting to peer. Trying again", "tries", i, "err", err, "addr", addr)
|
||||
// sleep a set amount
|
||||
sw.randomSleep(reconnectInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
sw.Logger.Error("Failed to reconnect to peer. Beginning exponential backoff",
|
||||
"addr", addr, "elapsed", time.Since(start))
|
||||
for i := 0; i < reconnectBackOffAttempts; i++ {
|
||||
if !sw.IsRunning() {
|
||||
return
|
||||
}
|
||||
|
||||
// sleep an exponentially increasing amount
|
||||
sleepIntervalSeconds := math.Pow(reconnectBackOffBaseSeconds, float64(i))
|
||||
sw.randomSleep(time.Duration(sleepIntervalSeconds) * time.Second)
|
||||
err := sw.DialPeerWithAddress(addr, true)
|
||||
if err == nil {
|
||||
return // success
|
||||
}
|
||||
sw.Logger.Info("Error reconnecting to peer. Trying again", "tries", i, "err", err, "addr", addr)
|
||||
}
|
||||
sw.Logger.Error("Failed to reconnect to peer. Giving up", "addr", addr, "elapsed", time.Since(start))
|
||||
}
|
||||
|
||||
// SetAddrBook allows to set address book on Switch.
|
||||
func (sw *Switch) SetAddrBook(addrBook AddrBook) {
|
||||
sw.addrBook = addrBook
|
||||
}
|
||||
|
||||
// MarkPeerAsGood marks the given peer as good when it did something useful
|
||||
// like contributed to consensus.
|
||||
func (sw *Switch) MarkPeerAsGood(peer Peer) {
|
||||
if sw.addrBook != nil {
|
||||
sw.addrBook.MarkGood(peer.NodeInfo().NetAddress())
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Dialing
|
||||
|
||||
// IsDialing returns true if the switch is currently dialing the given ID.
|
||||
func (sw *Switch) IsDialing(id ID) bool {
|
||||
return sw.dialing.Has(string(id))
|
||||
}
|
||||
|
||||
// DialPeersAsync dials a list of peers asynchronously in random order (optionally, making them persistent).
|
||||
// Used to dial peers from config on startup or from unsafe-RPC (trusted sources).
|
||||
// TODO: remove addrBook arg since it's now set on the switch
|
||||
func (sw *Switch) DialPeersAsync(addrBook AddrBook, peers []string, persistent bool) error {
|
||||
netAddrs, errs := NewNetAddressStrings(peers)
|
||||
// only log errors, dial correct addresses
|
||||
for _, err := range errs {
|
||||
sw.Logger.Error("Error in peer's address", "err", err)
|
||||
}
|
||||
|
||||
ourAddr := sw.nodeInfo.NetAddress()
|
||||
|
||||
// TODO: this code feels like it's in the wrong place.
|
||||
// The integration tests depend on the addrBook being saved
|
||||
// right away but maybe we can change that. Recall that
|
||||
// the addrBook is only written to disk every 2min
|
||||
if addrBook != nil {
|
||||
// add peers to `addrBook`
|
||||
for _, netAddr := range netAddrs {
|
||||
// do not add our address or ID
|
||||
if !netAddr.Same(ourAddr) {
|
||||
if err := addrBook.AddAddress(netAddr, ourAddr); err != nil {
|
||||
sw.Logger.Error("Can't add peer's address to addrbook", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Persist some peers to disk right away.
|
||||
// NOTE: integration tests depend on this
|
||||
addrBook.Save()
|
||||
}
|
||||
|
||||
// permute the list, dial them in random order.
|
||||
perm := sw.rng.Perm(len(netAddrs))
|
||||
for i := 0; i < len(perm); i++ {
|
||||
go func(i int) {
|
||||
j := perm[i]
|
||||
|
||||
addr := netAddrs[j]
|
||||
// do not dial ourselves
|
||||
if addr.Same(ourAddr) {
|
||||
return
|
||||
}
|
||||
|
||||
sw.randomSleep(0)
|
||||
err := sw.DialPeerWithAddress(addr, persistent)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case ErrSwitchConnectToSelf, ErrSwitchDuplicatePeerID:
|
||||
sw.Logger.Debug("Error dialing peer", "err", err)
|
||||
default:
|
||||
sw.Logger.Error("Error dialing peer", "err", err)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DialPeerWithAddress dials the given peer and runs sw.addPeer if it connects and authenticates successfully.
|
||||
// If `persistent == true`, the switch will always try to reconnect to this peer if the connection ever fails.
|
||||
func (sw *Switch) DialPeerWithAddress(addr *NetAddress, persistent bool) error {
|
||||
sw.dialing.Set(string(addr.ID), addr)
|
||||
defer sw.dialing.Delete(string(addr.ID))
|
||||
return sw.addOutboundPeerWithConfig(addr, sw.config, persistent)
|
||||
}
|
||||
|
||||
// sleep for interval plus some random amount of ms on [0, dialRandomizerIntervalMilliseconds]
|
||||
func (sw *Switch) randomSleep(interval time.Duration) {
|
||||
r := time.Duration(sw.rng.Int63n(dialRandomizerIntervalMilliseconds)) * time.Millisecond
|
||||
time.Sleep(r + interval)
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------------
|
||||
// Connection filtering
|
||||
|
||||
// FilterConnByAddr returns an error if connecting to the given address is forbidden.
|
||||
func (sw *Switch) FilterConnByAddr(addr net.Addr) error {
|
||||
if sw.filterConnByAddr != nil {
|
||||
return sw.filterConnByAddr(addr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilterConnByID returns an error if connecting to the given peer ID is forbidden.
|
||||
func (sw *Switch) FilterConnByID(id ID) error {
|
||||
if sw.filterConnByID != nil {
|
||||
return sw.filterConnByID(id)
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// SetAddrFilter sets the function for filtering connections by address.
|
||||
func (sw *Switch) SetAddrFilter(f func(net.Addr) error) {
|
||||
sw.filterConnByAddr = f
|
||||
}
|
||||
|
||||
// SetIDFilter sets the function for filtering connections by peer ID.
|
||||
func (sw *Switch) SetIDFilter(f func(ID) error) {
|
||||
sw.filterConnByID = f
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------------
|
||||
|
||||
func (sw *Switch) listenerRoutine(l Listener) {
|
||||
for {
|
||||
inConn, ok := <-l.Connections()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
// ignore connection if we already have enough
|
||||
// leave room for MinNumOutboundPeers
|
||||
maxPeers := sw.config.MaxNumPeers - DefaultMinNumOutboundPeers
|
||||
if maxPeers <= sw.peers.Size() {
|
||||
sw.Logger.Info("Ignoring inbound connection: already have enough peers", "address", inConn.RemoteAddr().String(), "numPeers", sw.peers.Size(), "max", maxPeers)
|
||||
continue
|
||||
}
|
||||
|
||||
// New inbound connection!
|
||||
err := sw.addInboundPeerWithConfig(inConn, sw.config)
|
||||
if err != nil {
|
||||
sw.Logger.Info("Ignoring inbound connection: error while adding peer", "address", inConn.RemoteAddr().String(), "err", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup
|
||||
}
|
||||
|
||||
func (sw *Switch) addInboundPeerWithConfig(
|
||||
conn net.Conn,
|
||||
config *config.P2PConfig,
|
||||
) error {
|
||||
peerConn, err := newInboundPeerConn(conn, config, sw.nodeKey.PrivKey)
|
||||
if err != nil {
|
||||
conn.Close() // peer is nil
|
||||
return err
|
||||
}
|
||||
if err = sw.addPeer(peerConn); err != nil {
|
||||
peerConn.CloseConn()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// dial the peer; make secret connection; authenticate against the dialed ID;
|
||||
// add the peer.
|
||||
// if dialing fails, start the reconnect loop. If handhsake fails, its over.
|
||||
// If peer is started succesffuly, reconnectLoop will start when
|
||||
// StopPeerForError is called
|
||||
func (sw *Switch) addOutboundPeerWithConfig(
|
||||
addr *NetAddress,
|
||||
config *config.P2PConfig,
|
||||
persistent bool,
|
||||
) error {
|
||||
sw.Logger.Info("Dialing peer", "address", addr)
|
||||
peerConn, err := newOutboundPeerConn(
|
||||
addr,
|
||||
config,
|
||||
persistent,
|
||||
sw.nodeKey.PrivKey,
|
||||
)
|
||||
if err != nil {
|
||||
if persistent {
|
||||
go sw.reconnectToPeer(addr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := sw.addPeer(peerConn); err != nil {
|
||||
peerConn.CloseConn()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addPeer performs the Tendermint P2P handshake with a peer
|
||||
// that already has a SecretConnection. If all goes well,
|
||||
// it starts the peer and adds it to the switch.
|
||||
// NOTE: This performs a blocking handshake before the peer is added.
|
||||
// NOTE: If error is returned, caller is responsible for calling
|
||||
// peer.CloseConn()
|
||||
func (sw *Switch) addPeer(pc peerConn) error {
|
||||
|
||||
addr := pc.conn.RemoteAddr()
|
||||
if err := sw.FilterConnByAddr(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Exchange NodeInfo on the conn
|
||||
peerNodeInfo, err := pc.HandshakeTimeout(sw.nodeInfo, time.Duration(sw.config.HandshakeTimeout))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peerID := peerNodeInfo.ID
|
||||
|
||||
// ensure connection key matches self reported key
|
||||
connID := pc.ID()
|
||||
|
||||
if peerID != connID {
|
||||
return fmt.Errorf(
|
||||
"nodeInfo.ID() (%v) doesn't match conn.ID() (%v)",
|
||||
peerID,
|
||||
connID,
|
||||
)
|
||||
}
|
||||
|
||||
// Validate the peers nodeInfo
|
||||
if err := peerNodeInfo.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Avoid self
|
||||
if sw.nodeKey.ID() == peerID {
|
||||
addr := peerNodeInfo.NetAddress()
|
||||
// remove the given address from the address book
|
||||
// and add to our addresses to avoid dialing again
|
||||
sw.addrBook.RemoveAddress(addr)
|
||||
sw.addrBook.AddOurAddress(addr)
|
||||
return ErrSwitchConnectToSelf{addr}
|
||||
}
|
||||
|
||||
// Avoid duplicate
|
||||
if sw.peers.Has(peerID) {
|
||||
return ErrSwitchDuplicatePeerID{peerID}
|
||||
}
|
||||
|
||||
// Check for duplicate connection or peer info IP.
|
||||
if !sw.config.AllowDuplicateIP &&
|
||||
(sw.peers.HasIP(pc.RemoteIP()) ||
|
||||
sw.peers.HasIP(peerNodeInfo.NetAddress().IP)) {
|
||||
return ErrSwitchDuplicatePeerIP{pc.RemoteIP()}
|
||||
}
|
||||
|
||||
// Filter peer against ID white list
|
||||
if err := sw.FilterConnByID(peerID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check version, chain id
|
||||
if err := sw.nodeInfo.CompatibleWith(peerNodeInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peer := newPeer(pc, sw.mConfig, peerNodeInfo, sw.reactorsByCh, sw.chDescs, sw.StopPeerForError)
|
||||
peer.SetLogger(sw.Logger.With("peer", addr))
|
||||
|
||||
peer.Logger.Info("Successful handshake with peer", "peerNodeInfo", peerNodeInfo)
|
||||
|
||||
// All good. Start peer
|
||||
if sw.IsRunning() {
|
||||
if err = sw.startInitPeer(peer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Add the peer to .peers.
|
||||
// We start it first so that a peer in the list is safe to Stop.
|
||||
// It should not err since we already checked peers.Has().
|
||||
if err := sw.peers.Add(peer); err != nil {
|
||||
return err
|
||||
}
|
||||
sw.metrics.Peers.Add(float64(1))
|
||||
|
||||
sw.Logger.Info("Added peer", "peer", peer)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sw *Switch) startInitPeer(peer *peer) error {
|
||||
err := peer.Start() // spawn send/recv routines
|
||||
if err != nil {
|
||||
// Should never happen
|
||||
sw.Logger.Error("Error starting peer", "peer", peer, "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, reactor := range sw.reactors {
|
||||
reactor.AddPeer(peer)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,428 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
crypto "github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
|
||||
"github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/p2p/conn"
|
||||
)
|
||||
|
||||
var (
|
||||
cfg *config.P2PConfig
|
||||
)
|
||||
|
||||
func init() {
|
||||
cfg = config.DefaultP2PConfig()
|
||||
cfg.PexReactor = true
|
||||
cfg.AllowDuplicateIP = true
|
||||
}
|
||||
|
||||
type PeerMessage struct {
|
||||
PeerID ID
|
||||
Bytes []byte
|
||||
Counter int
|
||||
}
|
||||
|
||||
type TestReactor struct {
|
||||
BaseReactor
|
||||
|
||||
mtx sync.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)
|
||||
defer s1.Stop()
|
||||
defer s2.Stop()
|
||||
|
||||
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 TestConnAddrFilter(t *testing.T) {
|
||||
s1 := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc)
|
||||
s2 := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc)
|
||||
defer s1.Stop()
|
||||
defer s2.Stop()
|
||||
|
||||
c1, c2 := conn.NetPipe()
|
||||
|
||||
s1.SetAddrFilter(func(addr net.Addr) error {
|
||||
if addr.String() == c1.RemoteAddr().String() {
|
||||
return fmt.Errorf("Error: pipe is blacklisted")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// connect to good peer
|
||||
go func() {
|
||||
err := s1.addPeerWithConnection(c1)
|
||||
assert.NotNil(t, err, "expected err")
|
||||
}()
|
||||
go func() {
|
||||
err := s2.addPeerWithConnection(c2)
|
||||
assert.NotNil(t, err, "expected err")
|
||||
}()
|
||||
|
||||
assertNoPeersAfterTimeout(t, s1, 400*time.Millisecond)
|
||||
assertNoPeersAfterTimeout(t, s2, 400*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestSwitchFiltersOutItself(t *testing.T) {
|
||||
s1 := MakeSwitch(cfg, 1, "127.0.0.1", "123.123.123", initSwitchFunc)
|
||||
// addr := s1.NodeInfo().NetAddress()
|
||||
|
||||
// // add ourselves like we do in node.go#427
|
||||
// s1.addrBook.AddOurAddress(addr)
|
||||
|
||||
// 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(), false)
|
||||
if assert.Error(t, err) {
|
||||
assert.Equal(t, ErrSwitchConnectToSelf{rp.Addr()}.Error(), err.Error())
|
||||
}
|
||||
|
||||
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 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 TestConnIDFilter(t *testing.T) {
|
||||
s1 := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc)
|
||||
s2 := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc)
|
||||
defer s1.Stop()
|
||||
defer s2.Stop()
|
||||
|
||||
c1, c2 := conn.NetPipe()
|
||||
|
||||
s1.SetIDFilter(func(id ID) error {
|
||||
if id == s2.nodeInfo.ID {
|
||||
return fmt.Errorf("Error: pipe is blacklisted")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
s2.SetIDFilter(func(id ID) error {
|
||||
if id == s1.nodeInfo.ID {
|
||||
return fmt.Errorf("Error: pipe is blacklisted")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
go func() {
|
||||
err := s1.addPeerWithConnection(c1)
|
||||
assert.NotNil(t, err, "expected error")
|
||||
}()
|
||||
go func() {
|
||||
err := s2.addPeerWithConnection(c2)
|
||||
assert.NotNil(t, err, "expected error")
|
||||
}()
|
||||
|
||||
assertNoPeersAfterTimeout(t, s1, 400*time.Millisecond)
|
||||
assertNoPeersAfterTimeout(t, s2, 400*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestSwitchStopsNonPersistentPeerOnError(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc)
|
||||
err := sw.Start()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer sw.Stop()
|
||||
|
||||
// simulate remote peer
|
||||
rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519(), Config: cfg}
|
||||
rp.Start()
|
||||
defer rp.Stop()
|
||||
|
||||
pc, err := newOutboundPeerConn(rp.Addr(), cfg, false, sw.nodeKey.PrivKey)
|
||||
require.Nil(err)
|
||||
err = sw.addPeer(pc)
|
||||
require.Nil(err)
|
||||
|
||||
peer := sw.Peers().Get(rp.ID())
|
||||
require.NotNil(peer)
|
||||
|
||||
// simulate failure by closing connection
|
||||
pc.CloseConn()
|
||||
|
||||
assertNoPeersAfterTimeout(t, sw, 100*time.Millisecond)
|
||||
assert.False(peer.IsRunning())
|
||||
}
|
||||
|
||||
func TestSwitchReconnectsToPersistentPeer(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc)
|
||||
err := sw.Start()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer sw.Stop()
|
||||
|
||||
// simulate remote peer
|
||||
rp := &remotePeer{PrivKey: crypto.GenPrivKeyEd25519(), Config: cfg}
|
||||
rp.Start()
|
||||
defer rp.Stop()
|
||||
|
||||
pc, err := newOutboundPeerConn(rp.Addr(), cfg, true, sw.nodeKey.PrivKey)
|
||||
// sw.reactorsByCh, sw.chDescs, sw.StopPeerForError, sw.nodeKey.PrivKey,
|
||||
require.Nil(err)
|
||||
|
||||
require.Nil(sw.addPeer(pc))
|
||||
|
||||
peer := sw.Peers().Get(rp.ID())
|
||||
require.NotNil(peer)
|
||||
|
||||
// simulate failure by closing connection
|
||||
pc.CloseConn()
|
||||
|
||||
// TODO: remove sleep, detect the disconnection, wait for reconnect
|
||||
npeers := sw.Peers().Size()
|
||||
for i := 0; i < 20; i++ {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
npeers = sw.Peers().Size()
|
||||
if npeers > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.NotZero(npeers)
|
||||
assert.False(peer.IsRunning())
|
||||
|
||||
// simulate another remote peer
|
||||
rp = &remotePeer{
|
||||
PrivKey: crypto.GenPrivKeyEd25519(),
|
||||
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()
|
||||
|
||||
// simulate first time dial failure
|
||||
conf := config.DefaultP2PConfig()
|
||||
conf.TestDialFail = true
|
||||
err = sw.addOutboundPeerWithConfig(rp.Addr(), conf, true)
|
||||
require.NotNil(err)
|
||||
|
||||
// DialPeerWithAddres - sw.peerConfig resets the dialer
|
||||
|
||||
// TODO: same as above
|
||||
for i := 0; i < 20; i++ {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
npeers = sw.Peers().Size()
|
||||
if npeers > 1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.EqualValues(2, npeers)
|
||||
}
|
||||
|
||||
func TestSwitchFullConnectivity(t *testing.T) {
|
||||
switches := MakeConnectedSwitches(cfg, 3, initSwitchFunc, Connect2Switches)
|
||||
defer func() {
|
||||
for _, sw := range switches {
|
||||
sw.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
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 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
|
||||
})
|
||||
defer s1.Stop()
|
||||
defer s2.Stop()
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
type addrBookMock struct {
|
||||
addrs map[string]struct{}
|
||||
ourAddrs map[string]struct{}
|
||||
}
|
||||
|
||||
var _ AddrBook = (*addrBookMock)(nil)
|
||||
|
||||
func (book *addrBookMock) AddAddress(addr *NetAddress, src *NetAddress) error {
|
||||
book.addrs[addr.String()] = struct{}{}
|
||||
return nil
|
||||
}
|
||||
func (book *addrBookMock) AddOurAddress(addr *NetAddress) { book.ourAddrs[addr.String()] = struct{}{} }
|
||||
func (book *addrBookMock) OurAddress(addr *NetAddress) bool {
|
||||
_, ok := book.ourAddrs[addr.String()]
|
||||
return ok
|
||||
}
|
||||
func (book *addrBookMock) MarkGood(*NetAddress) {}
|
||||
func (book *addrBookMock) HasAddress(addr *NetAddress) bool {
|
||||
_, ok := book.addrs[addr.String()]
|
||||
return ok
|
||||
}
|
||||
func (book *addrBookMock) RemoveAddress(addr *NetAddress) {
|
||||
delete(book.addrs, addr.String())
|
||||
}
|
||||
func (book *addrBookMock) Save() {}
|
||||
@@ -1,156 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
crypto "github.com/tendermint/tendermint/crypto"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
|
||||
"github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/p2p/conn"
|
||||
)
|
||||
|
||||
func AddPeerToSwitch(sw *Switch, peer Peer) {
|
||||
sw.peers.Add(peer)
|
||||
}
|
||||
|
||||
func CreateRandomPeer(outbound bool) *peer {
|
||||
addr, netAddr := CreateRoutableAddr()
|
||||
p := &peer{
|
||||
peerConn: peerConn{
|
||||
outbound: outbound,
|
||||
},
|
||||
nodeInfo: NodeInfo{
|
||||
ID: netAddr.ID,
|
||||
ListenAddr: netAddr.DialString(),
|
||||
},
|
||||
mconn: &conn.MConnection{},
|
||||
}
|
||||
p.SetLogger(log.TestingLogger().With("peer", addr))
|
||||
return p
|
||||
}
|
||||
|
||||
func CreateRoutableAddr() (addr string, netAddr *NetAddress) {
|
||||
for {
|
||||
var err error
|
||||
addr = cmn.Fmt("%X@%v.%v.%v.%v:26656", cmn.RandBytes(20), cmn.RandInt()%256, cmn.RandInt()%256, cmn.RandInt()%256, cmn.RandInt()%256)
|
||||
netAddr, err = NewNetAddressString(addr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if netAddr.Routable() {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------
|
||||
// Connects switches via arbitrary net.Conn. Used for testing.
|
||||
|
||||
const TEST_HOST = "localhost"
|
||||
|
||||
// MakeConnectedSwitches returns n switches, connected according to the connect func.
|
||||
// If connect==Connect2Switches, the switches will be fully connected.
|
||||
// initSwitch defines how the i'th switch should be initialized (ie. with what reactors).
|
||||
// NOTE: panics if any switch fails to start.
|
||||
func MakeConnectedSwitches(cfg *config.P2PConfig, n int, initSwitch func(int, *Switch) *Switch, connect func([]*Switch, int, int)) []*Switch {
|
||||
switches := make([]*Switch, n)
|
||||
for i := 0; i < n; i++ {
|
||||
switches[i] = MakeSwitch(cfg, i, TEST_HOST, "123.123.123", initSwitch)
|
||||
}
|
||||
|
||||
if err := StartSwitches(switches); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
for j := i + 1; j < n; j++ {
|
||||
connect(switches, i, j)
|
||||
}
|
||||
}
|
||||
|
||||
return switches
|
||||
}
|
||||
|
||||
// Connect2Switches will connect switches i and j via net.Pipe().
|
||||
// Blocks until a connection is established.
|
||||
// NOTE: caller ensures i and j are within bounds.
|
||||
func Connect2Switches(switches []*Switch, i, j int) {
|
||||
switchI := switches[i]
|
||||
switchJ := switches[j]
|
||||
|
||||
c1, c2 := conn.NetPipe()
|
||||
|
||||
doneCh := make(chan struct{})
|
||||
go func() {
|
||||
err := switchI.addPeerWithConnection(c1)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
doneCh <- struct{}{}
|
||||
}()
|
||||
go func() {
|
||||
err := switchJ.addPeerWithConnection(c2)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
doneCh <- struct{}{}
|
||||
}()
|
||||
<-doneCh
|
||||
<-doneCh
|
||||
}
|
||||
|
||||
func (sw *Switch) addPeerWithConnection(conn net.Conn) error {
|
||||
pc, err := newInboundPeerConn(conn, sw.config, sw.nodeKey.PrivKey)
|
||||
if err != nil {
|
||||
if err := conn.Close(); err != nil {
|
||||
sw.Logger.Error("Error closing connection", "err", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err = sw.addPeer(pc); err != nil {
|
||||
pc.CloseConn()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartSwitches calls sw.Start() for each given switch.
|
||||
// It returns the first encountered error.
|
||||
func StartSwitches(switches []*Switch) error {
|
||||
for _, s := range switches {
|
||||
err := s.Start() // start switch and reactors
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func MakeSwitch(cfg *config.P2PConfig, i int, network, version string, initSwitch func(int, *Switch) *Switch) *Switch {
|
||||
// new switch, add reactors
|
||||
// TODO: let the config be passed in?
|
||||
nodeKey := &NodeKey{
|
||||
PrivKey: crypto.GenPrivKeyEd25519(),
|
||||
}
|
||||
sw := NewSwitch(cfg)
|
||||
sw.SetLogger(log.TestingLogger())
|
||||
sw = initSwitch(i, sw)
|
||||
ni := NodeInfo{
|
||||
ID: nodeKey.ID(),
|
||||
Moniker: cmn.Fmt("switch%d", i),
|
||||
Network: network,
|
||||
Version: version,
|
||||
ListenAddr: fmt.Sprintf("127.0.0.1:%d", cmn.RandIntn(64512)+1023),
|
||||
}
|
||||
for ch := range sw.reactorsByCh {
|
||||
ni.Channels = append(ni.Channels, ch)
|
||||
}
|
||||
sw.SetNodeInfo(ni)
|
||||
sw.SetNodeKey(nodeKey)
|
||||
return sw
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package trust
|
||||
|
||||
import "time"
|
||||
|
||||
// TrustMetricConfig - Configures the weight functions and time intervals for the metric
|
||||
type TrustMetricConfig struct {
|
||||
// Determines the percentage given to current behavior
|
||||
ProportionalWeight float64
|
||||
|
||||
// Determines the percentage given to prior behavior
|
||||
IntegralWeight float64
|
||||
|
||||
// The window of time that the trust metric will track events across.
|
||||
// This can be set to cover many days without issue
|
||||
TrackingWindow time.Duration
|
||||
|
||||
// Each interval should be short for adapability.
|
||||
// Less than 30 seconds is too sensitive,
|
||||
// and greater than 5 minutes will make the metric numb
|
||||
IntervalLength time.Duration
|
||||
}
|
||||
|
||||
// DefaultConfig returns a config with values that have been tested and produce desirable results
|
||||
func DefaultConfig() TrustMetricConfig {
|
||||
return TrustMetricConfig{
|
||||
ProportionalWeight: 0.4,
|
||||
IntegralWeight: 0.6,
|
||||
TrackingWindow: (time.Minute * 60 * 24) * 14, // 14 days.
|
||||
IntervalLength: 1 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
// Ensures that all configuration elements have valid values
|
||||
func customConfig(tmc TrustMetricConfig) TrustMetricConfig {
|
||||
config := DefaultConfig()
|
||||
|
||||
// Check the config for set values, and setup appropriately
|
||||
if tmc.ProportionalWeight > 0 {
|
||||
config.ProportionalWeight = tmc.ProportionalWeight
|
||||
}
|
||||
|
||||
if tmc.IntegralWeight > 0 {
|
||||
config.IntegralWeight = tmc.IntegralWeight
|
||||
}
|
||||
|
||||
if tmc.IntervalLength > time.Duration(0) {
|
||||
config.IntervalLength = tmc.IntervalLength
|
||||
}
|
||||
|
||||
if tmc.TrackingWindow > time.Duration(0) &&
|
||||
tmc.TrackingWindow >= config.IntervalLength {
|
||||
config.TrackingWindow = tmc.TrackingWindow
|
||||
}
|
||||
return config
|
||||
}
|
||||
@@ -1,412 +0,0 @@
|
||||
// Copyright 2017 Tendermint. All rights reserved.
|
||||
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
|
||||
|
||||
package trust
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
)
|
||||
|
||||
//---------------------------------------------------------------------------------------
|
||||
|
||||
const (
|
||||
// The weight applied to the derivative when current behavior is >= previous behavior
|
||||
defaultDerivativeGamma1 = 0
|
||||
|
||||
// The weight applied to the derivative when current behavior is less than previous behavior
|
||||
defaultDerivativeGamma2 = 1.0
|
||||
|
||||
// The weight applied to history data values when calculating the history value
|
||||
defaultHistoryDataWeight = 0.8
|
||||
)
|
||||
|
||||
// MetricHistoryJSON - history data necessary to save the trust metric
|
||||
type MetricHistoryJSON struct {
|
||||
NumIntervals int `json:"intervals"`
|
||||
History []float64 `json:"history"`
|
||||
}
|
||||
|
||||
// TrustMetric - keeps track of peer reliability
|
||||
// See tendermint/docs/architecture/adr-006-trust-metric.md for details
|
||||
type TrustMetric struct {
|
||||
cmn.BaseService
|
||||
|
||||
// Mutex that protects the metric from concurrent access
|
||||
mtx sync.Mutex
|
||||
|
||||
// Determines the percentage given to current behavior
|
||||
proportionalWeight float64
|
||||
|
||||
// Determines the percentage given to prior behavior
|
||||
integralWeight float64
|
||||
|
||||
// Count of how many time intervals this metric has been tracking
|
||||
numIntervals int
|
||||
|
||||
// Size of the time interval window for this trust metric
|
||||
maxIntervals int
|
||||
|
||||
// The time duration for a single time interval
|
||||
intervalLen time.Duration
|
||||
|
||||
// Stores the trust history data for this metric
|
||||
history []float64
|
||||
|
||||
// Weights applied to the history data when calculating the history value
|
||||
historyWeights []float64
|
||||
|
||||
// The sum of the history weights used when calculating the history value
|
||||
historyWeightSum float64
|
||||
|
||||
// The current number of history data elements
|
||||
historySize int
|
||||
|
||||
// The maximum number of history data elements
|
||||
historyMaxSize int
|
||||
|
||||
// The calculated history value for the current time interval
|
||||
historyValue float64
|
||||
|
||||
// The number of recorded good and bad events for the current time interval
|
||||
bad, good float64
|
||||
|
||||
// While true, history data is not modified
|
||||
paused bool
|
||||
|
||||
// Used during testing in order to control the passing of time intervals
|
||||
testTicker MetricTicker
|
||||
}
|
||||
|
||||
// NewMetric returns a trust metric with the default configuration.
|
||||
// Use Start to begin tracking the quality of peer behavior over time
|
||||
func NewMetric() *TrustMetric {
|
||||
return NewMetricWithConfig(DefaultConfig())
|
||||
}
|
||||
|
||||
// NewMetricWithConfig returns a trust metric with a custom configuration.
|
||||
// Use Start to begin tracking the quality of peer behavior over time
|
||||
func NewMetricWithConfig(tmc TrustMetricConfig) *TrustMetric {
|
||||
tm := new(TrustMetric)
|
||||
config := customConfig(tmc)
|
||||
|
||||
// Setup using the configuration values
|
||||
tm.proportionalWeight = config.ProportionalWeight
|
||||
tm.integralWeight = config.IntegralWeight
|
||||
tm.intervalLen = config.IntervalLength
|
||||
// The maximum number of time intervals is the tracking window / interval length
|
||||
tm.maxIntervals = int(config.TrackingWindow / tm.intervalLen)
|
||||
// The history size will be determined by the maximum number of time intervals
|
||||
tm.historyMaxSize = intervalToHistoryOffset(tm.maxIntervals) + 1
|
||||
// This metric has a perfect history so far
|
||||
tm.historyValue = 1.0
|
||||
|
||||
tm.BaseService = *cmn.NewBaseService(nil, "TrustMetric", tm)
|
||||
return tm
|
||||
}
|
||||
|
||||
// OnStart implements Service
|
||||
func (tm *TrustMetric) OnStart() error {
|
||||
if err := tm.BaseService.OnStart(); err != nil {
|
||||
return err
|
||||
}
|
||||
go tm.processRequests()
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnStop implements Service
|
||||
// Nothing to do since the goroutine shuts down by itself via BaseService.Quit()
|
||||
func (tm *TrustMetric) OnStop() {}
|
||||
|
||||
// Returns a snapshot of the trust metric history data
|
||||
func (tm *TrustMetric) HistoryJSON() MetricHistoryJSON {
|
||||
tm.mtx.Lock()
|
||||
defer tm.mtx.Unlock()
|
||||
|
||||
return MetricHistoryJSON{
|
||||
NumIntervals: tm.numIntervals,
|
||||
History: tm.history,
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiates a trust metric by loading the history data for a single peer.
|
||||
// This is called only once and only right after creation, which is why the
|
||||
// lock is not held while accessing the trust metric struct members
|
||||
func (tm *TrustMetric) Init(hist MetricHistoryJSON) {
|
||||
// Restore the number of time intervals we have previously tracked
|
||||
if hist.NumIntervals > tm.maxIntervals {
|
||||
hist.NumIntervals = tm.maxIntervals
|
||||
}
|
||||
tm.numIntervals = hist.NumIntervals
|
||||
// Restore the history and its current size
|
||||
if len(hist.History) > tm.historyMaxSize {
|
||||
// Keep the history no larger than historyMaxSize
|
||||
last := len(hist.History) - tm.historyMaxSize
|
||||
hist.History = hist.History[last:]
|
||||
}
|
||||
tm.history = hist.History
|
||||
tm.historySize = len(tm.history)
|
||||
// Create the history weight values and weight sum
|
||||
for i := 1; i <= tm.numIntervals; i++ {
|
||||
x := math.Pow(defaultHistoryDataWeight, float64(i)) // Optimistic weight
|
||||
tm.historyWeights = append(tm.historyWeights, x)
|
||||
}
|
||||
|
||||
for _, v := range tm.historyWeights {
|
||||
tm.historyWeightSum += v
|
||||
}
|
||||
// Calculate the history value based on the loaded history data
|
||||
tm.historyValue = tm.calcHistoryValue()
|
||||
}
|
||||
|
||||
// Pause tells the metric to pause recording data over time intervals.
|
||||
// All method calls that indicate events will unpause the metric
|
||||
func (tm *TrustMetric) Pause() {
|
||||
tm.mtx.Lock()
|
||||
defer tm.mtx.Unlock()
|
||||
|
||||
// Pause the metric for now
|
||||
tm.paused = true
|
||||
}
|
||||
|
||||
// BadEvents indicates that an undesirable event(s) took place
|
||||
func (tm *TrustMetric) BadEvents(num int) {
|
||||
tm.mtx.Lock()
|
||||
defer tm.mtx.Unlock()
|
||||
|
||||
tm.unpause()
|
||||
tm.bad += float64(num)
|
||||
}
|
||||
|
||||
// GoodEvents indicates that a desirable event(s) took place
|
||||
func (tm *TrustMetric) GoodEvents(num int) {
|
||||
tm.mtx.Lock()
|
||||
defer tm.mtx.Unlock()
|
||||
|
||||
tm.unpause()
|
||||
tm.good += float64(num)
|
||||
}
|
||||
|
||||
// TrustValue gets the dependable trust value; always between 0 and 1
|
||||
func (tm *TrustMetric) TrustValue() float64 {
|
||||
tm.mtx.Lock()
|
||||
defer tm.mtx.Unlock()
|
||||
|
||||
return tm.calcTrustValue()
|
||||
}
|
||||
|
||||
// TrustScore gets a score based on the trust value always between 0 and 100
|
||||
func (tm *TrustMetric) TrustScore() int {
|
||||
score := tm.TrustValue() * 100
|
||||
|
||||
return int(math.Floor(score))
|
||||
}
|
||||
|
||||
// NextTimeInterval saves current time interval data and prepares for the following interval
|
||||
func (tm *TrustMetric) NextTimeInterval() {
|
||||
tm.mtx.Lock()
|
||||
defer tm.mtx.Unlock()
|
||||
|
||||
if tm.paused {
|
||||
// Do not prepare for the next time interval while paused
|
||||
return
|
||||
}
|
||||
|
||||
// Add the current trust value to the history data
|
||||
newHist := tm.calcTrustValue()
|
||||
tm.history = append(tm.history, newHist)
|
||||
|
||||
// Update history and interval counters
|
||||
if tm.historySize < tm.historyMaxSize {
|
||||
tm.historySize++
|
||||
} else {
|
||||
// Keep the history no larger than historyMaxSize
|
||||
last := len(tm.history) - tm.historyMaxSize
|
||||
tm.history = tm.history[last:]
|
||||
}
|
||||
|
||||
if tm.numIntervals < tm.maxIntervals {
|
||||
tm.numIntervals++
|
||||
// Add the optimistic weight for the new time interval
|
||||
wk := math.Pow(defaultHistoryDataWeight, float64(tm.numIntervals))
|
||||
tm.historyWeights = append(tm.historyWeights, wk)
|
||||
tm.historyWeightSum += wk
|
||||
}
|
||||
|
||||
// Update the history data using Faded Memories
|
||||
tm.updateFadedMemory()
|
||||
// Calculate the history value for the upcoming time interval
|
||||
tm.historyValue = tm.calcHistoryValue()
|
||||
tm.good = 0
|
||||
tm.bad = 0
|
||||
}
|
||||
|
||||
// SetTicker allows a TestTicker to be provided that will manually control
|
||||
// the passing of time from the perspective of the TrustMetric.
|
||||
// The ticker must be set before Start is called on the metric
|
||||
func (tm *TrustMetric) SetTicker(ticker MetricTicker) {
|
||||
tm.mtx.Lock()
|
||||
defer tm.mtx.Unlock()
|
||||
|
||||
tm.testTicker = ticker
|
||||
}
|
||||
|
||||
// Copy returns a new trust metric with members containing the same values
|
||||
func (tm *TrustMetric) Copy() *TrustMetric {
|
||||
if tm == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
tm.mtx.Lock()
|
||||
defer tm.mtx.Unlock()
|
||||
|
||||
return &TrustMetric{
|
||||
proportionalWeight: tm.proportionalWeight,
|
||||
integralWeight: tm.integralWeight,
|
||||
numIntervals: tm.numIntervals,
|
||||
maxIntervals: tm.maxIntervals,
|
||||
intervalLen: tm.intervalLen,
|
||||
history: tm.history,
|
||||
historyWeights: tm.historyWeights,
|
||||
historyWeightSum: tm.historyWeightSum,
|
||||
historySize: tm.historySize,
|
||||
historyMaxSize: tm.historyMaxSize,
|
||||
historyValue: tm.historyValue,
|
||||
good: tm.good,
|
||||
bad: tm.bad,
|
||||
paused: tm.paused,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Private methods */
|
||||
|
||||
// This method is for a goroutine that handles all requests on the metric
|
||||
func (tm *TrustMetric) processRequests() {
|
||||
t := tm.testTicker
|
||||
if t == nil {
|
||||
// No test ticker was provided, so we create a normal ticker
|
||||
t = NewTicker(tm.intervalLen)
|
||||
}
|
||||
defer t.Stop()
|
||||
// Obtain the raw channel
|
||||
tick := t.GetChannel()
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-tick:
|
||||
tm.NextTimeInterval()
|
||||
case <-tm.Quit():
|
||||
// Stop all further tracking for this metric
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wakes the trust metric up if it is currently paused
|
||||
// This method needs to be called with the mutex locked
|
||||
func (tm *TrustMetric) unpause() {
|
||||
// Check if this is the first experience with
|
||||
// what we are tracking since being paused
|
||||
if tm.paused {
|
||||
tm.good = 0
|
||||
tm.bad = 0
|
||||
// New events cause us to unpause the metric
|
||||
tm.paused = false
|
||||
}
|
||||
}
|
||||
|
||||
// Calculates the trust value for the request processing
|
||||
func (tm *TrustMetric) calcTrustValue() float64 {
|
||||
weightedP := tm.proportionalWeight * tm.proportionalValue()
|
||||
weightedI := tm.integralWeight * tm.historyValue
|
||||
weightedD := tm.weightedDerivative()
|
||||
|
||||
tv := weightedP + weightedI + weightedD
|
||||
// Do not return a negative value.
|
||||
if tv < 0 {
|
||||
tv = 0
|
||||
}
|
||||
return tv
|
||||
}
|
||||
|
||||
// Calculates the current score for good/bad experiences
|
||||
func (tm *TrustMetric) proportionalValue() float64 {
|
||||
value := 1.0
|
||||
|
||||
total := tm.good + tm.bad
|
||||
if total > 0 {
|
||||
value = tm.good / total
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// Strengthens the derivative component when the change is negative
|
||||
func (tm *TrustMetric) weightedDerivative() float64 {
|
||||
var weight float64 = defaultDerivativeGamma1
|
||||
|
||||
d := tm.derivativeValue()
|
||||
if d < 0 {
|
||||
weight = defaultDerivativeGamma2
|
||||
}
|
||||
return weight * d
|
||||
}
|
||||
|
||||
// Calculates the derivative component
|
||||
func (tm *TrustMetric) derivativeValue() float64 {
|
||||
return tm.proportionalValue() - tm.historyValue
|
||||
}
|
||||
|
||||
// Calculates the integral (history) component of the trust value
|
||||
func (tm *TrustMetric) calcHistoryValue() float64 {
|
||||
var hv float64
|
||||
|
||||
for i := 0; i < tm.numIntervals; i++ {
|
||||
hv += tm.fadedMemoryValue(i) * tm.historyWeights[i]
|
||||
}
|
||||
|
||||
return hv / tm.historyWeightSum
|
||||
}
|
||||
|
||||
// Retrieves the actual history data value that represents the requested time interval
|
||||
func (tm *TrustMetric) fadedMemoryValue(interval int) float64 {
|
||||
first := tm.historySize - 1
|
||||
|
||||
if interval == 0 {
|
||||
// Base case
|
||||
return tm.history[first]
|
||||
}
|
||||
|
||||
offset := intervalToHistoryOffset(interval)
|
||||
return tm.history[first-offset]
|
||||
}
|
||||
|
||||
// Performs the update for our Faded Memories process, which allows the
|
||||
// trust metric tracking window to be large while maintaining a small
|
||||
// number of history data values
|
||||
func (tm *TrustMetric) updateFadedMemory() {
|
||||
if tm.historySize < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
end := tm.historySize - 1
|
||||
// Keep the most recent history element
|
||||
for count := 1; count < tm.historySize; count++ {
|
||||
i := end - count
|
||||
// The older the data is, the more we spread it out
|
||||
x := math.Pow(2, float64(count))
|
||||
// Two history data values are merged into a single value
|
||||
tm.history[i] = ((tm.history[i] * (x - 1)) + tm.history[i+1]) / x
|
||||
}
|
||||
}
|
||||
|
||||
// Map the interval value down to an offset from the beginning of history
|
||||
func intervalToHistoryOffset(interval int) int {
|
||||
// The system maintains 2^m interval values in the form of m history
|
||||
// data values. Therefore, we access the ith interval by obtaining
|
||||
// the history data index = the floor of log2(i)
|
||||
return int(math.Floor(math.Log2(float64(interval))))
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTrustMetricScores(t *testing.T) {
|
||||
tm := NewMetric()
|
||||
tm.Start()
|
||||
|
||||
// Perfect score
|
||||
tm.GoodEvents(1)
|
||||
score := tm.TrustScore()
|
||||
assert.Equal(t, 100, score)
|
||||
|
||||
// Less than perfect score
|
||||
tm.BadEvents(10)
|
||||
score = tm.TrustScore()
|
||||
assert.NotEqual(t, 100, score)
|
||||
tm.Stop()
|
||||
}
|
||||
|
||||
func TestTrustMetricConfig(t *testing.T) {
|
||||
// 7 days
|
||||
window := time.Minute * 60 * 24 * 7
|
||||
config := TrustMetricConfig{
|
||||
TrackingWindow: window,
|
||||
IntervalLength: 2 * time.Minute,
|
||||
}
|
||||
|
||||
tm := NewMetricWithConfig(config)
|
||||
tm.Start()
|
||||
|
||||
// The max time intervals should be the TrackingWindow / IntervalLen
|
||||
assert.Equal(t, int(config.TrackingWindow/config.IntervalLength), tm.maxIntervals)
|
||||
|
||||
dc := DefaultConfig()
|
||||
// These weights should still be the default values
|
||||
assert.Equal(t, dc.ProportionalWeight, tm.proportionalWeight)
|
||||
assert.Equal(t, dc.IntegralWeight, tm.integralWeight)
|
||||
tm.Stop()
|
||||
tm.Wait()
|
||||
|
||||
config.ProportionalWeight = 0.3
|
||||
config.IntegralWeight = 0.7
|
||||
tm = NewMetricWithConfig(config)
|
||||
tm.Start()
|
||||
|
||||
// These weights should be equal to our custom values
|
||||
assert.Equal(t, config.ProportionalWeight, tm.proportionalWeight)
|
||||
assert.Equal(t, config.IntegralWeight, tm.integralWeight)
|
||||
tm.Stop()
|
||||
tm.Wait()
|
||||
}
|
||||
|
||||
func TestTrustMetricCopyNilPointer(t *testing.T) {
|
||||
var tm *TrustMetric
|
||||
|
||||
ctm := tm.Copy()
|
||||
|
||||
assert.Nil(t, ctm)
|
||||
}
|
||||
|
||||
// XXX: This test fails non-deterministically
|
||||
func _TestTrustMetricStopPause(t *testing.T) {
|
||||
// The TestTicker will provide manual control over
|
||||
// the passing of time within the metric
|
||||
tt := NewTestTicker()
|
||||
tm := NewMetric()
|
||||
tm.SetTicker(tt)
|
||||
tm.Start()
|
||||
// Allow some time intervals to pass and pause
|
||||
tt.NextTick()
|
||||
tt.NextTick()
|
||||
tm.Pause()
|
||||
|
||||
// could be 1 or 2 because Pause and NextTick race
|
||||
first := tm.Copy().numIntervals
|
||||
|
||||
// Allow more time to pass and check the intervals are unchanged
|
||||
tt.NextTick()
|
||||
tt.NextTick()
|
||||
assert.Equal(t, first, tm.Copy().numIntervals)
|
||||
|
||||
// Get the trust metric activated again
|
||||
tm.GoodEvents(5)
|
||||
// Allow some time intervals to pass and stop
|
||||
tt.NextTick()
|
||||
tt.NextTick()
|
||||
tm.Stop()
|
||||
tm.Wait()
|
||||
|
||||
second := tm.Copy().numIntervals
|
||||
// Allow more intervals to pass while the metric is stopped
|
||||
// and check that the number of intervals match
|
||||
tm.NextTimeInterval()
|
||||
tm.NextTimeInterval()
|
||||
// XXX: fails non-deterministically:
|
||||
// expected 5, got 6
|
||||
assert.Equal(t, second+2, tm.Copy().numIntervals)
|
||||
|
||||
if first > second {
|
||||
t.Fatalf("numIntervals should always increase or stay the same over time")
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
// Copyright 2017 Tendermint. All rights reserved.
|
||||
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
|
||||
|
||||
package trust
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
dbm "github.com/tendermint/tendermint/libs/db"
|
||||
)
|
||||
|
||||
const defaultStorePeriodicSaveInterval = 1 * time.Minute
|
||||
|
||||
var trustMetricKey = []byte("trustMetricStore")
|
||||
|
||||
// TrustMetricStore - Manages all trust metrics for peers
|
||||
type TrustMetricStore struct {
|
||||
cmn.BaseService
|
||||
|
||||
// Maps a Peer.Key to that peer's TrustMetric
|
||||
peerMetrics map[string]*TrustMetric
|
||||
|
||||
// Mutex that protects the map and history data file
|
||||
mtx sync.Mutex
|
||||
|
||||
// The db where peer trust metric history data will be stored
|
||||
db dbm.DB
|
||||
|
||||
// This configuration will be used when creating new TrustMetrics
|
||||
config TrustMetricConfig
|
||||
}
|
||||
|
||||
// NewTrustMetricStore returns a store that saves data to the DB
|
||||
// and uses the config when creating new trust metrics.
|
||||
// Use Start to to initialize the trust metric store
|
||||
func NewTrustMetricStore(db dbm.DB, tmc TrustMetricConfig) *TrustMetricStore {
|
||||
tms := &TrustMetricStore{
|
||||
peerMetrics: make(map[string]*TrustMetric),
|
||||
db: db,
|
||||
config: tmc,
|
||||
}
|
||||
|
||||
tms.BaseService = *cmn.NewBaseService(nil, "TrustMetricStore", tms)
|
||||
return tms
|
||||
}
|
||||
|
||||
// OnStart implements Service
|
||||
func (tms *TrustMetricStore) OnStart() error {
|
||||
if err := tms.BaseService.OnStart(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tms.mtx.Lock()
|
||||
defer tms.mtx.Unlock()
|
||||
|
||||
tms.loadFromDB()
|
||||
go tms.saveRoutine()
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnStop implements Service
|
||||
func (tms *TrustMetricStore) OnStop() {
|
||||
tms.BaseService.OnStop()
|
||||
|
||||
tms.mtx.Lock()
|
||||
defer tms.mtx.Unlock()
|
||||
|
||||
// Stop all trust metric go-routines
|
||||
for _, tm := range tms.peerMetrics {
|
||||
tm.Stop()
|
||||
}
|
||||
|
||||
// Make the final trust history data save
|
||||
tms.saveToDB()
|
||||
}
|
||||
|
||||
// Size returns the number of entries in the trust metric store
|
||||
func (tms *TrustMetricStore) Size() int {
|
||||
tms.mtx.Lock()
|
||||
defer tms.mtx.Unlock()
|
||||
|
||||
return tms.size()
|
||||
}
|
||||
|
||||
// AddPeerTrustMetric takes an existing trust metric and associates it with a peer key.
|
||||
// The caller is expected to call Start on the TrustMetric being added
|
||||
func (tms *TrustMetricStore) AddPeerTrustMetric(key string, tm *TrustMetric) {
|
||||
tms.mtx.Lock()
|
||||
defer tms.mtx.Unlock()
|
||||
|
||||
if key == "" || tm == nil {
|
||||
return
|
||||
}
|
||||
tms.peerMetrics[key] = tm
|
||||
}
|
||||
|
||||
// GetPeerTrustMetric returns a trust metric by peer key
|
||||
func (tms *TrustMetricStore) GetPeerTrustMetric(key string) *TrustMetric {
|
||||
tms.mtx.Lock()
|
||||
defer tms.mtx.Unlock()
|
||||
|
||||
tm, ok := tms.peerMetrics[key]
|
||||
if !ok {
|
||||
// If the metric is not available, we will create it
|
||||
tm = NewMetricWithConfig(tms.config)
|
||||
tm.Start()
|
||||
// The metric needs to be in the map
|
||||
tms.peerMetrics[key] = tm
|
||||
}
|
||||
return tm
|
||||
}
|
||||
|
||||
// PeerDisconnected pauses the trust metric associated with the peer identified by the key
|
||||
func (tms *TrustMetricStore) PeerDisconnected(key string) {
|
||||
tms.mtx.Lock()
|
||||
defer tms.mtx.Unlock()
|
||||
|
||||
// If the Peer that disconnected has a metric, pause it
|
||||
if tm, ok := tms.peerMetrics[key]; ok {
|
||||
tm.Pause()
|
||||
}
|
||||
}
|
||||
|
||||
// Saves the history data for all peers to the store DB.
|
||||
// This public method acquires the trust metric store lock
|
||||
func (tms *TrustMetricStore) SaveToDB() {
|
||||
tms.mtx.Lock()
|
||||
defer tms.mtx.Unlock()
|
||||
|
||||
tms.saveToDB()
|
||||
}
|
||||
|
||||
/* Private methods */
|
||||
|
||||
// size returns the number of entries in the store without acquiring the mutex
|
||||
func (tms *TrustMetricStore) size() int {
|
||||
return len(tms.peerMetrics)
|
||||
}
|
||||
|
||||
/* Loading & Saving */
|
||||
/* Both loadFromDB and savetoDB assume the mutex has been acquired */
|
||||
|
||||
// Loads the history data for all peers from the store DB
|
||||
// cmn.Panics if file is corrupt
|
||||
func (tms *TrustMetricStore) loadFromDB() bool {
|
||||
// Obtain the history data we have so far
|
||||
bytes := tms.db.Get(trustMetricKey)
|
||||
if bytes == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
peers := make(map[string]MetricHistoryJSON)
|
||||
err := json.Unmarshal(bytes, &peers)
|
||||
if err != nil {
|
||||
cmn.PanicCrisis(cmn.Fmt("Could not unmarshal Trust Metric Store DB data: %v", err))
|
||||
}
|
||||
|
||||
// If history data exists in the file,
|
||||
// load it into trust metric
|
||||
for key, p := range peers {
|
||||
tm := NewMetricWithConfig(tms.config)
|
||||
|
||||
tm.Start()
|
||||
tm.Init(p)
|
||||
// Load the peer trust metric into the store
|
||||
tms.peerMetrics[key] = tm
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Saves the history data for all peers to the store DB
|
||||
func (tms *TrustMetricStore) saveToDB() {
|
||||
tms.Logger.Debug("Saving TrustHistory to DB", "size", tms.size())
|
||||
|
||||
peers := make(map[string]MetricHistoryJSON)
|
||||
|
||||
for key, tm := range tms.peerMetrics {
|
||||
// Add an entry for the peer identified by key
|
||||
peers[key] = tm.HistoryJSON()
|
||||
}
|
||||
|
||||
// Write all the data back to the DB
|
||||
bytes, err := json.Marshal(peers)
|
||||
if err != nil {
|
||||
tms.Logger.Error("Failed to encode the TrustHistory", "err", err)
|
||||
return
|
||||
}
|
||||
tms.db.SetSync(trustMetricKey, bytes)
|
||||
}
|
||||
|
||||
// Periodically saves the trust history data to the DB
|
||||
func (tms *TrustMetricStore) saveRoutine() {
|
||||
t := time.NewTicker(defaultStorePeriodicSaveInterval)
|
||||
defer t.Stop()
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
tms.SaveToDB()
|
||||
case <-tms.Quit():
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
// Copyright 2017 Tendermint. All rights reserved.
|
||||
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
|
||||
|
||||
package trust
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
dbm "github.com/tendermint/tendermint/libs/db"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
)
|
||||
|
||||
func TestTrustMetricStoreSaveLoad(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "trust_test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.Remove(dir)
|
||||
|
||||
historyDB := dbm.NewDB("trusthistory", "goleveldb", dir)
|
||||
|
||||
// 0 peers saved
|
||||
store := NewTrustMetricStore(historyDB, DefaultConfig())
|
||||
store.SetLogger(log.TestingLogger())
|
||||
store.saveToDB()
|
||||
// Load the data from the file
|
||||
store = NewTrustMetricStore(historyDB, DefaultConfig())
|
||||
store.SetLogger(log.TestingLogger())
|
||||
store.Start()
|
||||
// Make sure we still have 0 entries
|
||||
assert.Zero(t, store.Size())
|
||||
|
||||
// 100 TestTickers
|
||||
var tt []*TestTicker
|
||||
for i := 0; i < 100; i++ {
|
||||
// The TestTicker will provide manual control over
|
||||
// the passing of time within the metric
|
||||
tt = append(tt, NewTestTicker())
|
||||
}
|
||||
// 100 peers
|
||||
for i := 0; i < 100; i++ {
|
||||
key := fmt.Sprintf("peer_%d", i)
|
||||
tm := NewMetric()
|
||||
|
||||
tm.SetTicker(tt[i])
|
||||
tm.Start()
|
||||
store.AddPeerTrustMetric(key, tm)
|
||||
|
||||
tm.BadEvents(10)
|
||||
tm.GoodEvents(1)
|
||||
}
|
||||
// Check that we have 100 entries and save
|
||||
assert.Equal(t, 100, store.Size())
|
||||
// Give the 100 metrics time to process the history data
|
||||
for i := 0; i < 100; i++ {
|
||||
tt[i].NextTick()
|
||||
tt[i].NextTick()
|
||||
}
|
||||
// Stop all the trust metrics and save
|
||||
store.Stop()
|
||||
|
||||
// Load the data from the DB
|
||||
store = NewTrustMetricStore(historyDB, DefaultConfig())
|
||||
store.SetLogger(log.TestingLogger())
|
||||
store.Start()
|
||||
|
||||
// Check that we still have 100 peers with imperfect trust values
|
||||
assert.Equal(t, 100, store.Size())
|
||||
for _, tm := range store.peerMetrics {
|
||||
assert.NotEqual(t, 1.0, tm.TrustValue())
|
||||
}
|
||||
|
||||
store.Stop()
|
||||
}
|
||||
|
||||
func TestTrustMetricStoreConfig(t *testing.T) {
|
||||
historyDB := dbm.NewDB("", "memdb", "")
|
||||
|
||||
config := TrustMetricConfig{
|
||||
ProportionalWeight: 0.5,
|
||||
IntegralWeight: 0.5,
|
||||
}
|
||||
|
||||
// Create a store with custom config
|
||||
store := NewTrustMetricStore(historyDB, config)
|
||||
store.SetLogger(log.TestingLogger())
|
||||
store.Start()
|
||||
|
||||
// Have the store make us a metric with the config
|
||||
tm := store.GetPeerTrustMetric("TestKey")
|
||||
|
||||
// Check that the options made it to the metric
|
||||
assert.Equal(t, 0.5, tm.proportionalWeight)
|
||||
assert.Equal(t, 0.5, tm.integralWeight)
|
||||
store.Stop()
|
||||
}
|
||||
|
||||
func TestTrustMetricStoreLookup(t *testing.T) {
|
||||
historyDB := dbm.NewDB("", "memdb", "")
|
||||
|
||||
store := NewTrustMetricStore(historyDB, DefaultConfig())
|
||||
store.SetLogger(log.TestingLogger())
|
||||
store.Start()
|
||||
|
||||
// Create 100 peers in the trust metric store
|
||||
for i := 0; i < 100; i++ {
|
||||
key := fmt.Sprintf("peer_%d", i)
|
||||
store.GetPeerTrustMetric(key)
|
||||
|
||||
// Check that the trust metric was successfully entered
|
||||
ktm := store.peerMetrics[key]
|
||||
assert.NotNil(t, ktm, "Expected to find TrustMetric %s but wasn't there.", key)
|
||||
}
|
||||
|
||||
store.Stop()
|
||||
}
|
||||
|
||||
func TestTrustMetricStorePeerScore(t *testing.T) {
|
||||
historyDB := dbm.NewDB("", "memdb", "")
|
||||
|
||||
store := NewTrustMetricStore(historyDB, DefaultConfig())
|
||||
store.SetLogger(log.TestingLogger())
|
||||
store.Start()
|
||||
|
||||
key := "TestKey"
|
||||
tm := store.GetPeerTrustMetric(key)
|
||||
|
||||
// This peer is innocent so far
|
||||
first := tm.TrustScore()
|
||||
assert.Equal(t, 100, first)
|
||||
|
||||
// Add some undesirable events and disconnect
|
||||
tm.BadEvents(1)
|
||||
first = tm.TrustScore()
|
||||
assert.NotEqual(t, 100, first)
|
||||
tm.BadEvents(10)
|
||||
second := tm.TrustScore()
|
||||
|
||||
if second > first {
|
||||
t.Errorf("A greater number of bad events should lower the trust score")
|
||||
}
|
||||
store.PeerDisconnected(key)
|
||||
|
||||
// We will remember our experiences with this peer
|
||||
tm = store.GetPeerTrustMetric(key)
|
||||
assert.NotEqual(t, 100, tm.TrustScore())
|
||||
store.Stop()
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright 2017 Tendermint. All rights reserved.
|
||||
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
|
||||
|
||||
package trust
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// MetricTicker provides a single ticker interface for the trust metric
|
||||
type MetricTicker interface {
|
||||
// GetChannel returns the receive only channel that fires at each time interval
|
||||
GetChannel() <-chan time.Time
|
||||
|
||||
// Stop will halt further activity on the ticker channel
|
||||
Stop()
|
||||
}
|
||||
|
||||
// The ticker used during testing that provides manual control over time intervals
|
||||
type TestTicker struct {
|
||||
C chan time.Time
|
||||
stopped bool
|
||||
}
|
||||
|
||||
// NewTestTicker returns our ticker used within test routines
|
||||
func NewTestTicker() *TestTicker {
|
||||
c := make(chan time.Time)
|
||||
return &TestTicker{
|
||||
C: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TestTicker) GetChannel() <-chan time.Time {
|
||||
return t.C
|
||||
}
|
||||
|
||||
func (t *TestTicker) Stop() {
|
||||
t.stopped = true
|
||||
}
|
||||
|
||||
// NextInterval manually sends Time on the ticker channel
|
||||
func (t *TestTicker) NextTick() {
|
||||
if t.stopped {
|
||||
return
|
||||
}
|
||||
t.C <- time.Now()
|
||||
}
|
||||
|
||||
// Ticker is just a wrap around time.Ticker that allows it
|
||||
// to meet the requirements of our interface
|
||||
type Ticker struct {
|
||||
*time.Ticker
|
||||
}
|
||||
|
||||
// NewTicker returns a normal time.Ticker wrapped to meet our interface
|
||||
func NewTicker(d time.Duration) *Ticker {
|
||||
return &Ticker{time.NewTicker(d)}
|
||||
}
|
||||
|
||||
func (t *Ticker) GetChannel() <-chan time.Time {
|
||||
return t.C
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"github.com/tendermint/tendermint/p2p/conn"
|
||||
)
|
||||
|
||||
type ChannelDescriptor = conn.ChannelDescriptor
|
||||
type ConnectionStatus = conn.ConnectionStatus
|
||||
@@ -1,112 +0,0 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
)
|
||||
|
||||
type UPNPCapabilities struct {
|
||||
PortMapping bool
|
||||
Hairpin bool
|
||||
}
|
||||
|
||||
func makeUPNPListener(intPort int, extPort int, logger log.Logger) (NAT, net.Listener, net.IP, error) {
|
||||
nat, err := Discover()
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("NAT upnp could not be discovered: %v", err)
|
||||
}
|
||||
logger.Info(cmn.Fmt("ourIP: %v", nat.(*upnpNAT).ourIP))
|
||||
|
||||
ext, err := nat.GetExternalAddress()
|
||||
if err != nil {
|
||||
return nat, nil, nil, fmt.Errorf("External address error: %v", err)
|
||||
}
|
||||
logger.Info(cmn.Fmt("External address: %v", ext))
|
||||
|
||||
port, err := nat.AddPortMapping("tcp", extPort, intPort, "Tendermint UPnP Probe", 0)
|
||||
if err != nil {
|
||||
return nat, nil, ext, fmt.Errorf("Port mapping error: %v", err)
|
||||
}
|
||||
logger.Info(cmn.Fmt("Port mapping mapped: %v", port))
|
||||
|
||||
// also run the listener, open for all remote addresses.
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%v", intPort))
|
||||
if err != nil {
|
||||
return nat, nil, ext, fmt.Errorf("Error establishing listener: %v", err)
|
||||
}
|
||||
return nat, listener, ext, nil
|
||||
}
|
||||
|
||||
func testHairpin(listener net.Listener, extAddr string, logger log.Logger) (supportsHairpin bool) {
|
||||
// Listener
|
||||
go func() {
|
||||
inConn, err := listener.Accept()
|
||||
if err != nil {
|
||||
logger.Info(cmn.Fmt("Listener.Accept() error: %v", err))
|
||||
return
|
||||
}
|
||||
logger.Info(cmn.Fmt("Accepted incoming connection: %v -> %v", inConn.LocalAddr(), inConn.RemoteAddr()))
|
||||
buf := make([]byte, 1024)
|
||||
n, err := inConn.Read(buf)
|
||||
if err != nil {
|
||||
logger.Info(cmn.Fmt("Incoming connection read error: %v", err))
|
||||
return
|
||||
}
|
||||
logger.Info(cmn.Fmt("Incoming connection read %v bytes: %X", n, buf))
|
||||
if string(buf) == "test data" {
|
||||
supportsHairpin = true
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// Establish outgoing
|
||||
outConn, err := net.Dial("tcp", extAddr)
|
||||
if err != nil {
|
||||
logger.Info(cmn.Fmt("Outgoing connection dial error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
n, err := outConn.Write([]byte("test data"))
|
||||
if err != nil {
|
||||
logger.Info(cmn.Fmt("Outgoing connection write error: %v", err))
|
||||
return
|
||||
}
|
||||
logger.Info(cmn.Fmt("Outgoing connection wrote %v bytes", n))
|
||||
|
||||
// Wait for data receipt
|
||||
time.Sleep(1 * time.Second)
|
||||
return
|
||||
}
|
||||
|
||||
func Probe(logger log.Logger) (caps UPNPCapabilities, err error) {
|
||||
logger.Info("Probing for UPnP!")
|
||||
|
||||
intPort, extPort := 8001, 8001
|
||||
|
||||
nat, listener, ext, err := makeUPNPListener(intPort, extPort, logger)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
caps.PortMapping = true
|
||||
|
||||
// Deferred cleanup
|
||||
defer func() {
|
||||
if err := nat.DeletePortMapping("tcp", intPort, extPort); err != nil {
|
||||
logger.Error(cmn.Fmt("Port mapping delete error: %v", err))
|
||||
}
|
||||
if err := listener.Close(); err != nil {
|
||||
logger.Error(cmn.Fmt("Listener closing error: %v", err))
|
||||
}
|
||||
}()
|
||||
|
||||
supportsHairpin := testHairpin(listener, fmt.Sprintf("%v:%v", ext, extPort), logger)
|
||||
if supportsHairpin {
|
||||
caps.Hairpin = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
// Taken from taipei-torrent.
|
||||
// Just enough UPnP to be able to forward ports
|
||||
// For more information, see: http://www.upnp-hacks.org/upnp.html
|
||||
package upnp
|
||||
|
||||
// TODO: use syscalls to get actual ourIP, see issue #712
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type upnpNAT struct {
|
||||
serviceURL string
|
||||
ourIP string
|
||||
urnDomain string
|
||||
}
|
||||
|
||||
// protocol is either "udp" or "tcp"
|
||||
type NAT interface {
|
||||
GetExternalAddress() (addr net.IP, err error)
|
||||
AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error)
|
||||
DeletePortMapping(protocol string, externalPort, internalPort int) (err error)
|
||||
}
|
||||
|
||||
func Discover() (nat NAT, err error) {
|
||||
ssdp, err := net.ResolveUDPAddr("udp4", "239.255.255.250:1900")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn, err := net.ListenPacket("udp4", ":0")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
socket := conn.(*net.UDPConn)
|
||||
defer socket.Close() // nolint: errcheck
|
||||
|
||||
if err := socket.SetDeadline(time.Now().Add(3 * time.Second)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
st := "InternetGatewayDevice:1"
|
||||
|
||||
buf := bytes.NewBufferString(
|
||||
"M-SEARCH * HTTP/1.1\r\n" +
|
||||
"HOST: 239.255.255.250:1900\r\n" +
|
||||
"ST: ssdp:all\r\n" +
|
||||
"MAN: \"ssdp:discover\"\r\n" +
|
||||
"MX: 2\r\n\r\n")
|
||||
message := buf.Bytes()
|
||||
answerBytes := make([]byte, 1024)
|
||||
for i := 0; i < 3; i++ {
|
||||
_, err = socket.WriteToUDP(message, ssdp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var n int
|
||||
_, _, err = socket.ReadFromUDP(answerBytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for {
|
||||
n, _, err = socket.ReadFromUDP(answerBytes)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
answer := string(answerBytes[0:n])
|
||||
if !strings.Contains(answer, st) {
|
||||
continue
|
||||
}
|
||||
// HTTP header field names are case-insensitive.
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
|
||||
locString := "\r\nlocation:"
|
||||
answer = strings.ToLower(answer)
|
||||
locIndex := strings.Index(answer, locString)
|
||||
if locIndex < 0 {
|
||||
continue
|
||||
}
|
||||
loc := answer[locIndex+len(locString):]
|
||||
endIndex := strings.Index(loc, "\r\n")
|
||||
if endIndex < 0 {
|
||||
continue
|
||||
}
|
||||
locURL := strings.TrimSpace(loc[0:endIndex])
|
||||
var serviceURL, urnDomain string
|
||||
serviceURL, urnDomain, err = getServiceURL(locURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var ourIP net.IP
|
||||
ourIP, err = localIPv4()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
nat = &upnpNAT{serviceURL: serviceURL, ourIP: ourIP.String(), urnDomain: urnDomain}
|
||||
return
|
||||
}
|
||||
}
|
||||
err = errors.New("UPnP port discovery failed")
|
||||
return
|
||||
}
|
||||
|
||||
type Envelope struct {
|
||||
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
|
||||
Soap *SoapBody
|
||||
}
|
||||
type SoapBody struct {
|
||||
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
|
||||
ExternalIP *ExternalIPAddressResponse
|
||||
}
|
||||
|
||||
type ExternalIPAddressResponse struct {
|
||||
XMLName xml.Name `xml:"GetExternalIPAddressResponse"`
|
||||
IPAddress string `xml:"NewExternalIPAddress"`
|
||||
}
|
||||
|
||||
type ExternalIPAddress struct {
|
||||
XMLName xml.Name `xml:"NewExternalIPAddress"`
|
||||
IP string
|
||||
}
|
||||
|
||||
type UPNPService struct {
|
||||
ServiceType string `xml:"serviceType"`
|
||||
ControlURL string `xml:"controlURL"`
|
||||
}
|
||||
|
||||
type DeviceList struct {
|
||||
Device []Device `xml:"device"`
|
||||
}
|
||||
|
||||
type ServiceList struct {
|
||||
Service []UPNPService `xml:"service"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
XMLName xml.Name `xml:"device"`
|
||||
DeviceType string `xml:"deviceType"`
|
||||
DeviceList DeviceList `xml:"deviceList"`
|
||||
ServiceList ServiceList `xml:"serviceList"`
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
Device Device
|
||||
}
|
||||
|
||||
func getChildDevice(d *Device, deviceType string) *Device {
|
||||
dl := d.DeviceList.Device
|
||||
for i := 0; i < len(dl); i++ {
|
||||
if strings.Contains(dl[i].DeviceType, deviceType) {
|
||||
return &dl[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getChildService(d *Device, serviceType string) *UPNPService {
|
||||
sl := d.ServiceList.Service
|
||||
for i := 0; i < len(sl); i++ {
|
||||
if strings.Contains(sl[i].ServiceType, serviceType) {
|
||||
return &sl[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func localIPv4() (net.IP, error) {
|
||||
tt, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range tt {
|
||||
aa, err := t.Addrs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, a := range aa {
|
||||
ipnet, ok := a.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
v4 := ipnet.IP.To4()
|
||||
if v4 == nil || v4[0] == 127 { // loopback address
|
||||
continue
|
||||
}
|
||||
return v4, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("cannot find local IP address")
|
||||
}
|
||||
|
||||
func getServiceURL(rootURL string) (url, urnDomain string, err error) {
|
||||
r, err := http.Get(rootURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer r.Body.Close() // nolint: errcheck
|
||||
|
||||
if r.StatusCode >= 400 {
|
||||
err = errors.New(string(r.StatusCode))
|
||||
return
|
||||
}
|
||||
var root Root
|
||||
err = xml.NewDecoder(r.Body).Decode(&root)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
a := &root.Device
|
||||
if !strings.Contains(a.DeviceType, "InternetGatewayDevice:1") {
|
||||
err = errors.New("No InternetGatewayDevice")
|
||||
return
|
||||
}
|
||||
b := getChildDevice(a, "WANDevice:1")
|
||||
if b == nil {
|
||||
err = errors.New("No WANDevice")
|
||||
return
|
||||
}
|
||||
c := getChildDevice(b, "WANConnectionDevice:1")
|
||||
if c == nil {
|
||||
err = errors.New("No WANConnectionDevice")
|
||||
return
|
||||
}
|
||||
d := getChildService(c, "WANIPConnection:1")
|
||||
if d == nil {
|
||||
// Some routers don't follow the UPnP spec, and put WanIPConnection under WanDevice,
|
||||
// instead of under WanConnectionDevice
|
||||
d = getChildService(b, "WANIPConnection:1")
|
||||
|
||||
if d == nil {
|
||||
err = errors.New("No WANIPConnection")
|
||||
return
|
||||
}
|
||||
}
|
||||
// Extract the domain name, which isn't always 'schemas-upnp-org'
|
||||
urnDomain = strings.Split(d.ServiceType, ":")[1]
|
||||
url = combineURL(rootURL, d.ControlURL)
|
||||
return
|
||||
}
|
||||
|
||||
func combineURL(rootURL, subURL string) string {
|
||||
protocolEnd := "://"
|
||||
protoEndIndex := strings.Index(rootURL, protocolEnd)
|
||||
a := rootURL[protoEndIndex+len(protocolEnd):]
|
||||
rootIndex := strings.Index(a, "/")
|
||||
return rootURL[0:protoEndIndex+len(protocolEnd)+rootIndex] + subURL
|
||||
}
|
||||
|
||||
func soapRequest(url, function, message, domain string) (r *http.Response, err error) {
|
||||
fullMessage := "<?xml version=\"1.0\" ?>" +
|
||||
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n" +
|
||||
"<s:Body>" + message + "</s:Body></s:Envelope>"
|
||||
|
||||
req, err := http.NewRequest("POST", url, strings.NewReader(fullMessage))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "text/xml ; charset=\"utf-8\"")
|
||||
req.Header.Set("User-Agent", "Darwin/10.0.0, UPnP/1.0, MiniUPnPc/1.3")
|
||||
//req.Header.Set("Transfer-Encoding", "chunked")
|
||||
req.Header.Set("SOAPAction", "\"urn:"+domain+":service:WANIPConnection:1#"+function+"\"")
|
||||
req.Header.Set("Connection", "Close")
|
||||
req.Header.Set("Cache-Control", "no-cache")
|
||||
req.Header.Set("Pragma", "no-cache")
|
||||
|
||||
// log.Stderr("soapRequest ", req)
|
||||
|
||||
r, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
/*if r.Body != nil {
|
||||
defer r.Body.Close()
|
||||
}*/
|
||||
|
||||
if r.StatusCode >= 400 {
|
||||
// log.Stderr(function, r.StatusCode)
|
||||
err = errors.New("Error " + strconv.Itoa(r.StatusCode) + " for " + function)
|
||||
r = nil
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type statusInfo struct {
|
||||
externalIpAddress string
|
||||
}
|
||||
|
||||
func (n *upnpNAT) getExternalIPAddress() (info statusInfo, err error) {
|
||||
|
||||
message := "<u:GetExternalIPAddress xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" +
|
||||
"</u:GetExternalIPAddress>"
|
||||
|
||||
var response *http.Response
|
||||
response, err = soapRequest(n.serviceURL, "GetExternalIPAddress", message, n.urnDomain)
|
||||
if response != nil {
|
||||
defer response.Body.Close() // nolint: errcheck
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var envelope Envelope
|
||||
data, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
reader := bytes.NewReader(data)
|
||||
err = xml.NewDecoder(reader).Decode(&envelope)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
info = statusInfo{envelope.Soap.ExternalIP.IPAddress}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetExternalAddress returns an external IP. If GetExternalIPAddress action
|
||||
// fails or IP returned is invalid, GetExternalAddress returns an error.
|
||||
func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) {
|
||||
info, err := n.getExternalIPAddress()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
addr = net.ParseIP(info.externalIpAddress)
|
||||
if addr == nil {
|
||||
err = fmt.Errorf("Failed to parse IP: %v", info.externalIpAddress)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (n *upnpNAT) AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) {
|
||||
// A single concatenation would break ARM compilation.
|
||||
message := "<u:AddPortMapping xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" +
|
||||
"<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort)
|
||||
message += "</NewExternalPort><NewProtocol>" + protocol + "</NewProtocol>"
|
||||
message += "<NewInternalPort>" + strconv.Itoa(internalPort) + "</NewInternalPort>" +
|
||||
"<NewInternalClient>" + n.ourIP + "</NewInternalClient>" +
|
||||
"<NewEnabled>1</NewEnabled><NewPortMappingDescription>"
|
||||
message += description +
|
||||
"</NewPortMappingDescription><NewLeaseDuration>" + strconv.Itoa(timeout) +
|
||||
"</NewLeaseDuration></u:AddPortMapping>"
|
||||
|
||||
var response *http.Response
|
||||
response, err = soapRequest(n.serviceURL, "AddPortMapping", message, n.urnDomain)
|
||||
if response != nil {
|
||||
defer response.Body.Close() // nolint: errcheck
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: check response to see if the port was forwarded
|
||||
// log.Println(message, response)
|
||||
// JAE:
|
||||
// body, err := ioutil.ReadAll(response.Body)
|
||||
// fmt.Println(string(body), err)
|
||||
mappedExternalPort = externalPort
|
||||
_ = response
|
||||
return
|
||||
}
|
||||
|
||||
func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) {
|
||||
|
||||
message := "<u:DeletePortMapping xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" +
|
||||
"<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort) +
|
||||
"</NewExternalPort><NewProtocol>" + protocol + "</NewProtocol>" +
|
||||
"</u:DeletePortMapping>"
|
||||
|
||||
var response *http.Response
|
||||
response, err = soapRequest(n.serviceURL, "DeletePortMapping", message, n.urnDomain)
|
||||
if response != nil {
|
||||
defer response.Body.Close() // nolint: errcheck
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: check response to see if the port was deleted
|
||||
// log.Println(message, response)
|
||||
_ = response
|
||||
return
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package p2p
|
||||
|
||||
const Version = "0.5.0"
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"github.com/tendermint/go-amino"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
)
|
||||
|
||||
var cdc = amino.NewCodec()
|
||||
|
||||
func init() {
|
||||
crypto.RegisterAmino(cdc)
|
||||
}
|
||||
Reference in New Issue
Block a user