mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-25 22:26:42 +00:00
Merge 23973c994d into 881bdc289f
This commit is contained in:
commit
40130bb15c
3 changed files with 915 additions and 0 deletions
320
p2p/protocols/protocol.go
Normal file
320
p2p/protocols/protocol.go
Normal file
|
|
@ -0,0 +1,320 @@
|
||||||
|
/*
|
||||||
|
Package protocols is an extension to p2p. It offers a user friendly simple way to define
|
||||||
|
devp2p subprotocols by abstracting away code standardly shared by protocols.
|
||||||
|
The subprotocol architecture is inspired by the node package. Similar to a node
|
||||||
|
the standard protocol (class) registers service contructors that are instantiated as service
|
||||||
|
instances on the protocol isntance that is launched on a p2p peer connection.
|
||||||
|
|
||||||
|
By mounting various protocol modules protocols can encapsulate vertical slices of business logic
|
||||||
|
without duplicating code related to protocol communication.
|
||||||
|
Standard protocol supports:
|
||||||
|
|
||||||
|
* mounting services instantiated with the remote peer when a protocol instance is launched on a newly
|
||||||
|
established peer connection
|
||||||
|
* registering module-specific handshakes and offers validation and renegotiation of handshakes
|
||||||
|
* registering multiple handlers for incoming messages
|
||||||
|
* automate assigments of code indexes to messages
|
||||||
|
* automate RLP decoding/encoding based on reflecting
|
||||||
|
* provide the forever loop to read incoming messages
|
||||||
|
* standardise error handling related to communication
|
||||||
|
* enables access to sister services of the same peer connection analogous to node.Service
|
||||||
|
* TODO: automatic generation of wire protocol specification for peers
|
||||||
|
* peerPool abstracting out peer management by defining a peerPool that is called to register/unregister
|
||||||
|
peers as they connect and drop (ideally the peerPool also implements the peerPool interface that the
|
||||||
|
p2p server needs to suggest peers to connect to in server-as-initiator mode of operation
|
||||||
|
see https://github.com/ethereum/go-ethereum/issues/2254 for the peer management/connectivity related
|
||||||
|
aspect
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
package protocols
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/logger"
|
||||||
|
"github.com/ethereum/go-ethereum/logger/glog"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
)
|
||||||
|
|
||||||
|
// error codes used by this protocol scheme
|
||||||
|
const (
|
||||||
|
ErrMsgTooLong = iota
|
||||||
|
ErrDecode
|
||||||
|
ErrWrite
|
||||||
|
ErrInvalidMsgCode
|
||||||
|
ErrInvalidMsgType
|
||||||
|
ErrLocalHandshake
|
||||||
|
ErrRemoteHandshake
|
||||||
|
ErrHandler
|
||||||
|
)
|
||||||
|
|
||||||
|
// error description strings associated with the codes
|
||||||
|
var errorToString = map[int]string{
|
||||||
|
ErrMsgTooLong: "Message too long",
|
||||||
|
ErrDecode: "Invalid message (RLP error)",
|
||||||
|
ErrWrite: "Error sending message",
|
||||||
|
ErrInvalidMsgCode: "Invalid message code",
|
||||||
|
ErrInvalidMsgType: "Invalid message type",
|
||||||
|
ErrLocalHandshake: "Local handshake error",
|
||||||
|
ErrRemoteHandshake: "Remote handshake error",
|
||||||
|
ErrHandler: "Message handler error",
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Error implements the standard go error interface.
|
||||||
|
Use:
|
||||||
|
|
||||||
|
errorf(code, format, params ...interface{})
|
||||||
|
|
||||||
|
Prints as:
|
||||||
|
|
||||||
|
<description>: <details>
|
||||||
|
|
||||||
|
where description is given by code in errorToString
|
||||||
|
and details is fmt.Sprintf(format, params...)
|
||||||
|
|
||||||
|
exported field Code can be checked
|
||||||
|
*/
|
||||||
|
type Error struct {
|
||||||
|
Code int
|
||||||
|
message string
|
||||||
|
format string
|
||||||
|
params []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self Error) Error() (message string) {
|
||||||
|
if len(message) == 0 {
|
||||||
|
name, ok := errorToString[self.Code]
|
||||||
|
if !ok {
|
||||||
|
panic("invalid message code")
|
||||||
|
}
|
||||||
|
self.message = name
|
||||||
|
if self.format != "" {
|
||||||
|
self.message += ": " + fmt.Sprintf(self.format, self.params...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self.message
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorf(code int, format string, params ...interface{}) *Error {
|
||||||
|
self := &Error{
|
||||||
|
Code: code,
|
||||||
|
format: format,
|
||||||
|
params: params,
|
||||||
|
}
|
||||||
|
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
// implements the code table spec
|
||||||
|
// listing the message codes and types etc
|
||||||
|
// and further metadata about the protocol
|
||||||
|
type CodeMap struct {
|
||||||
|
Name string // name of the protocol
|
||||||
|
Version uint // version
|
||||||
|
MaxMsgSize int // max length of message payload size
|
||||||
|
codes []reflect.Type // index of codes to msg types - to create zero values
|
||||||
|
messages map[reflect.Type]uint // index of types to codes, for sending by type
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCodeMap(name string, version uint, maxMsgSize int, msgs ...interface{}) *CodeMap {
|
||||||
|
self := &CodeMap{
|
||||||
|
Name: name,
|
||||||
|
Version: version,
|
||||||
|
MaxMsgSize: maxMsgSize,
|
||||||
|
messages: make(map[reflect.Type]uint),
|
||||||
|
}
|
||||||
|
self.Register(msgs...)
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *CodeMap) Length() uint64 {
|
||||||
|
return uint64(len(self.codes))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *CodeMap) Register(msgs ...interface{}) {
|
||||||
|
code := uint(len(self.codes))
|
||||||
|
for _, msg := range msgs {
|
||||||
|
typ := reflect.TypeOf(msg)
|
||||||
|
_, found := self.messages[typ]
|
||||||
|
if found {
|
||||||
|
// ignore duplicates
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// next code assigned to message type typ
|
||||||
|
self.messages[typ] = code
|
||||||
|
self.codes = append(self.codes, typ)
|
||||||
|
code++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Peer represents a remote peer or protocol instance that is running on a peer connection with
|
||||||
|
// a remote peer
|
||||||
|
type Peer struct {
|
||||||
|
ct *CodeMap // CodeMap for the protocol
|
||||||
|
*p2p.Peer // the p2p.Peer object representing the remote
|
||||||
|
rw p2p.MsgReadWriter // p2p.MsgReadWriter to send messages to and read messages from
|
||||||
|
handlers map[reflect.Type][]func(interface{}) error // message type -> message handler callback(s) map
|
||||||
|
disconnect func() // Disconnect function set differently for testing
|
||||||
|
// lastActive time.Time // tracking last active state
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPeer returns a new peer
|
||||||
|
// this constructor is called by the p2p.Protocol#Run function
|
||||||
|
// the first two arguments are comming the arguments passed to p2p.Protocol.Run function
|
||||||
|
// the third argument is the CodeMap describing the protocol messages and options
|
||||||
|
func NewPeer(p *p2p.Peer, rw p2p.MsgReadWriter, ct *CodeMap) *Peer {
|
||||||
|
return newPeer(p, rw, ct, func() {
|
||||||
|
p.Disconnect(p2p.DiscSubprotocolError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// the disconnect function needs to be set differently for testing
|
||||||
|
func NewTestPeer(p *p2p.Peer, rw p2p.MsgReadWriter, ct *CodeMap) *Peer {
|
||||||
|
return newPeer(p, rw, ct, func() {
|
||||||
|
rw.(*p2p.MsgPipeRW).Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPeer(p *p2p.Peer, rw p2p.MsgReadWriter, ct *CodeMap, disconn func()) *Peer {
|
||||||
|
return &Peer{
|
||||||
|
ct: ct,
|
||||||
|
Peer: p,
|
||||||
|
rw: rw,
|
||||||
|
handlers: make(map[reflect.Type][]func(interface{}) error),
|
||||||
|
disconnect: disconn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register is called on the peer typically within the constructor of service instances running on peer connections
|
||||||
|
// These constructors are called by the p2p.Protocol#Run function
|
||||||
|
// It ties handler callbackss for specific message types
|
||||||
|
// A message type can have several handlers registered by the same or different protocol services
|
||||||
|
// Register is meant to be called once, deregistering is not currently supported therefore
|
||||||
|
// handlers are assumed to be static across handshake renegotiations
|
||||||
|
// i.e., a service instance either handles a message or not (irrespective of the handshake)
|
||||||
|
// it panics if the message type is not defined in the CodeMap
|
||||||
|
func (self *Peer) Register(msg interface{}, handler func(interface{}) error) uint {
|
||||||
|
typ := reflect.TypeOf(msg)
|
||||||
|
code, found := self.ct.messages[typ]
|
||||||
|
if !found {
|
||||||
|
panic(fmt.Sprintf("message type '%v' unknown ", typ))
|
||||||
|
}
|
||||||
|
glog.V(logger.Debug).Infof("registered handle for %v %v", msg, typ)
|
||||||
|
self.handlers[typ] = append(self.handlers[typ], handler)
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run starts the forever loop that handles incoming messages
|
||||||
|
// called within the p2p.Protocol#Run function
|
||||||
|
func (self *Peer) Run() error {
|
||||||
|
var err error
|
||||||
|
for {
|
||||||
|
_, err = self.handleIncoming()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop disconnects a peer.
|
||||||
|
// falls back to self.disconnect which is set as p2p.Peer#Disconnect except
|
||||||
|
// for test peers where it calls p2p.MsgPipe#Close so that the readloop can terminate
|
||||||
|
// TODO: may need to implement protocol drop only? don't want to kick off the peer
|
||||||
|
// if they are useful for other protocols
|
||||||
|
// overwrite Disconnect for testing, so that protocol readloop quits
|
||||||
|
func (self *Peer) Drop() {
|
||||||
|
self.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send takes a message, encodes it in RLP, finds the right message code and sends the
|
||||||
|
// message off to the peer
|
||||||
|
// this low level call will be wrapped by libraries providing routed or broadcast sends
|
||||||
|
// but often just used to forward and push messages to directly connected peers
|
||||||
|
func (self *Peer) Send(msg interface{}) error {
|
||||||
|
typ := reflect.TypeOf(msg)
|
||||||
|
code, found := self.ct.messages[typ]
|
||||||
|
if !found {
|
||||||
|
return errorf(ErrInvalidMsgType, "%v", typ)
|
||||||
|
}
|
||||||
|
glog.V(logger.Debug).Infof("=> %v %v (%d)", msg, typ, code)
|
||||||
|
err := p2p.Send(self.rw, uint64(code), msg)
|
||||||
|
if err != nil {
|
||||||
|
self.Drop()
|
||||||
|
return errorf(ErrWrite, "(msg code: %v): %v", code, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleIncoming(code)
|
||||||
|
// is called each cycle of the main forever loop that handles and dispatches incoming messages
|
||||||
|
// if this returns an error the loop returns and the peer is disconnected with the error
|
||||||
|
// checks message size, out-of-range message codes, handles decoding with reflection,
|
||||||
|
// call handlers as callback onside
|
||||||
|
func (self *Peer) handleIncoming() (interface{}, error) {
|
||||||
|
msg, err := self.rw.ReadMsg()
|
||||||
|
glog.V(logger.Debug).Infof("<= %v", msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// make sure that the payload has been fully consumed
|
||||||
|
defer msg.Discard()
|
||||||
|
|
||||||
|
if msg.Size > uint32(self.ct.MaxMsgSize) {
|
||||||
|
return nil, errorf(ErrMsgTooLong, "%v > %v", msg.Size, self.ct.MaxMsgSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the message code is correct
|
||||||
|
maxMsgCode := uint(len(self.ct.messages))
|
||||||
|
if msg.Code >= uint64(maxMsgCode) {
|
||||||
|
return nil, errorf(ErrInvalidMsgCode, "%v (>=%v)", msg.Code, maxMsgCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// it is safe to be unsafe here
|
||||||
|
typ := self.ct.codes[msg.Code]
|
||||||
|
val := reflect.New(typ)
|
||||||
|
req := val.Elem()
|
||||||
|
req.Set(reflect.Zero(typ))
|
||||||
|
if err := msg.Decode(val.Interface()); err != nil {
|
||||||
|
return nil, errorf(ErrDecode, "<= %v: %v", msg, err)
|
||||||
|
}
|
||||||
|
glog.V(logger.Debug).Infof("<= %v %v (%d)", req, typ, msg.Code)
|
||||||
|
|
||||||
|
// call the registered handler callbacks
|
||||||
|
// a registered callback take the decoded message as argument as an interface
|
||||||
|
// which the handler is supposed to cast to the appropriate type
|
||||||
|
// it is entirely safe not to check the cast in the handler since the handler is
|
||||||
|
// chosen based on the proper type in the first place
|
||||||
|
for _, f := range self.handlers[typ] {
|
||||||
|
glog.V(6).Infof("handler for %v", typ)
|
||||||
|
err = f(req.Interface())
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorf(ErrHandler, "(msg code %v): %v", msg.Code, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return req.Interface(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handshake initiates a handshake on the peer connection
|
||||||
|
// * the argument is the local handshake to be sent to the remote peer
|
||||||
|
// * expects a remote handshake back of the same type
|
||||||
|
// returns the remote hs and an error
|
||||||
|
func (self *Peer) Handshake(hs interface{}) (interface{}, error) {
|
||||||
|
typ := reflect.TypeOf(hs)
|
||||||
|
_, found := self.ct.messages[typ]
|
||||||
|
if !found {
|
||||||
|
return nil, errorf(ErrLocalHandshake, "unknown handshake message type: %v", typ)
|
||||||
|
}
|
||||||
|
err := self.Send(hs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorf(ErrLocalHandshake, "cannot send: %v", err)
|
||||||
|
}
|
||||||
|
// receiving and validating remote handshake, expect code
|
||||||
|
rhs, err := self.handleIncoming()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorf(ErrRemoteHandshake, "'%v': %v", self.ct.Name, err)
|
||||||
|
}
|
||||||
|
return rhs, nil
|
||||||
|
}
|
||||||
372
p2p/protocols/protocol_test.go
Normal file
372
p2p/protocols/protocol_test.go
Normal file
|
|
@ -0,0 +1,372 @@
|
||||||
|
package protocols
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||||
|
p2ptest "github.com/ethereum/go-ethereum/p2p/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handshake message type
|
||||||
|
type hs0 struct {
|
||||||
|
C uint
|
||||||
|
}
|
||||||
|
|
||||||
|
// message to kill/drop the peer with index C
|
||||||
|
type kill struct {
|
||||||
|
C discover.NodeID
|
||||||
|
}
|
||||||
|
|
||||||
|
// message to drop connection
|
||||||
|
type drop struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// example peerPool to demonstrate registration of peer connections
|
||||||
|
type peerPool struct {
|
||||||
|
lock sync.Mutex
|
||||||
|
peers map[discover.NodeID]*Peer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPeerPool() *peerPool {
|
||||||
|
return &peerPool{peers: make(map[discover.NodeID]*Peer)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *peerPool) add(p *Peer) {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
self.peers[p.ID()] = p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *peerPool) remove(p *Peer) {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
delete(self.peers, p.ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *peerPool) has(n discover.NodeID) bool {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
_, ok := self.peers[n]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *peerPool) get(n discover.NodeID) *Peer {
|
||||||
|
self.lock.Lock()
|
||||||
|
defer self.lock.Unlock()
|
||||||
|
return self.peers[n]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// protoHandshake represents module-independent aspects of the protocol and is
|
||||||
|
// the first message peers send and receive as part the initial exchange
|
||||||
|
type protoHandshake struct {
|
||||||
|
Version uint // local and remote peer should have identical version
|
||||||
|
NetworkId string // local and remote peer should have identical network id
|
||||||
|
}
|
||||||
|
|
||||||
|
// function to check local and remote protoHandshake matches
|
||||||
|
func checkProtoHandshake(local, remote *protoHandshake) error {
|
||||||
|
|
||||||
|
if remote.NetworkId != local.NetworkId {
|
||||||
|
return fmt.Errorf("%s (!= %s)", remote.NetworkId, local.NetworkId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if remote.Version != local.Version {
|
||||||
|
return fmt.Errorf("%d (!= %d)", remote.Version, local.Version)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const networkId = "420"
|
||||||
|
|
||||||
|
// newProtocol sets up a protocol
|
||||||
|
// the run function here demonstrates a typical protocol using peerPool, handshake
|
||||||
|
// and messages registered to handlers
|
||||||
|
func newProtocol() (*p2p.Protocol, *peerPool) {
|
||||||
|
ct := NewCodeMap("test", 42, 1024, &protoHandshake{}, &hs0{}, &kill{}, &drop{})
|
||||||
|
pp := newPeerPool()
|
||||||
|
|
||||||
|
run := func(p *p2p.Peer, rw p2p.MsgReadWriter) error {
|
||||||
|
peer := NewTestPeer(p, rw, ct)
|
||||||
|
|
||||||
|
// demonstrates use of peerPool, killing another peer connection as a response to a message
|
||||||
|
peer.Register(&kill{}, func(msg interface{}) error {
|
||||||
|
// panics if target.C out of range
|
||||||
|
id := msg.(*kill).C
|
||||||
|
// name := fmt.Sprintf("test-%d", i)
|
||||||
|
pp.get(id).Drop()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// for testing we can trigger self induced disconnect upon receiving drop message
|
||||||
|
peer.Register(&drop{}, func(msg interface{}) error {
|
||||||
|
return fmt.Errorf("received disconnect request")
|
||||||
|
})
|
||||||
|
|
||||||
|
// initiate one-off protohandshake and check validity
|
||||||
|
phs := &protoHandshake{ct.Version, networkId}
|
||||||
|
hs, err := peer.Handshake(phs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rhs := hs.(*protoHandshake)
|
||||||
|
err = checkProtoHandshake(phs, rhs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
lhs := &hs0{1}
|
||||||
|
// module handshake demonstrating a simple repeatable exchange of same-type message
|
||||||
|
hs, err = peer.Handshake(lhs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rmhs := hs.(*hs0)
|
||||||
|
if rmhs.C != lhs.C {
|
||||||
|
return fmt.Errorf("handshake mismatch remote %v != local %v", rmhs.C, lhs.C)
|
||||||
|
}
|
||||||
|
|
||||||
|
peer.Register(lhs, func(msg interface{}) error {
|
||||||
|
rhs := msg.(*hs0)
|
||||||
|
if rhs.C != lhs.C {
|
||||||
|
return fmt.Errorf("handshake mismatch remote %v != local %v", rhs.C, lhs.C)
|
||||||
|
}
|
||||||
|
return peer.Send(rhs)
|
||||||
|
})
|
||||||
|
|
||||||
|
// add/remove peer from pool
|
||||||
|
pp.add(peer)
|
||||||
|
defer pp.remove(peer)
|
||||||
|
// this launches a forever read loop
|
||||||
|
return peer.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &p2p.Protocol{
|
||||||
|
Name: ct.Name,
|
||||||
|
Length: uint64(len(ct.codes)),
|
||||||
|
Version: 42,
|
||||||
|
Run: run,
|
||||||
|
}, pp
|
||||||
|
}
|
||||||
|
|
||||||
|
func protoHandshakeExchange(proto *protoHandshake) []p2ptest.Exchange {
|
||||||
|
|
||||||
|
return []p2ptest.Exchange{
|
||||||
|
p2ptest.Exchange{
|
||||||
|
Expects: []p2ptest.Expect{
|
||||||
|
p2ptest.Expect{
|
||||||
|
Code: 0,
|
||||||
|
Msg: &protoHandshake{42, "420"},
|
||||||
|
Peer: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
p2ptest.Exchange{
|
||||||
|
Triggers: []p2ptest.Trigger{
|
||||||
|
p2ptest.Trigger{
|
||||||
|
Code: 0,
|
||||||
|
Msg: proto,
|
||||||
|
Peer: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runProtoHandshake(t *testing.T, proto *protoHandshake, err error) {
|
||||||
|
p, _ := newProtocol()
|
||||||
|
ids := p2ptest.RandomNodeIDs(t, 1)
|
||||||
|
s := p2ptest.NewSession(t, p, ids, nil)
|
||||||
|
|
||||||
|
s.TestExchanges(protoHandshakeExchange(proto)...)
|
||||||
|
s.TestDisconnects(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProtoHandshakeVersionMismatch(t *testing.T) {
|
||||||
|
runProtoHandshake(t, &protoHandshake{41, "420"}, fmt.Errorf("41 (!= 42)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProtoHandshakeNetworkIdMismatch(t *testing.T) {
|
||||||
|
runProtoHandshake(t, &protoHandshake{42, "421"}, fmt.Errorf("421 (!= 420)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProtoHandshakeSuccess(t *testing.T) {
|
||||||
|
runProtoHandshake(t, &protoHandshake{42, "420"}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func moduleHandshakeExchange(resp uint) []p2ptest.Exchange {
|
||||||
|
|
||||||
|
return []p2ptest.Exchange{
|
||||||
|
p2ptest.Exchange{
|
||||||
|
Expects: []p2ptest.Expect{
|
||||||
|
p2ptest.Expect{
|
||||||
|
Code: 1,
|
||||||
|
Msg: &hs0{1},
|
||||||
|
Peer: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
p2ptest.Exchange{
|
||||||
|
Triggers: []p2ptest.Trigger{
|
||||||
|
p2ptest.Trigger{
|
||||||
|
Code: 1,
|
||||||
|
Msg: &hs0{resp},
|
||||||
|
Peer: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runModuleHandshake(t *testing.T, resp uint, err error) {
|
||||||
|
p, _ := newProtocol()
|
||||||
|
ids := p2ptest.RandomNodeIDs(t, 1)
|
||||||
|
s := p2ptest.NewSession(t, p, ids, nil)
|
||||||
|
|
||||||
|
s.TestExchanges(protoHandshakeExchange(&protoHandshake{42, "420"})...)
|
||||||
|
s.TestExchanges(moduleHandshakeExchange(resp)...)
|
||||||
|
s.TestDisconnects(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModuleHandshakeError(t *testing.T) {
|
||||||
|
runModuleHandshake(t, 42, fmt.Errorf("handshake mismatch remote 42 != local 1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModuleHandshakeSuccess(t *testing.T) {
|
||||||
|
runModuleHandshake(t, 1, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testing complex interactions over multiple peers, relaying, dropping
|
||||||
|
func testMultiPeerSetup() []p2ptest.Exchange {
|
||||||
|
|
||||||
|
return []p2ptest.Exchange{
|
||||||
|
p2ptest.Exchange{
|
||||||
|
Expects: []p2ptest.Expect{
|
||||||
|
p2ptest.Expect{
|
||||||
|
Code: 0,
|
||||||
|
Msg: &protoHandshake{42, "420"},
|
||||||
|
Peer: 0,
|
||||||
|
},
|
||||||
|
p2ptest.Expect{
|
||||||
|
Code: 0,
|
||||||
|
Msg: &protoHandshake{42, "420"},
|
||||||
|
Peer: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
p2ptest.Exchange{
|
||||||
|
Triggers: []p2ptest.Trigger{
|
||||||
|
p2ptest.Trigger{
|
||||||
|
Code: 0,
|
||||||
|
Msg: &protoHandshake{42, "420"},
|
||||||
|
Peer: 0,
|
||||||
|
},
|
||||||
|
p2ptest.Trigger{
|
||||||
|
Code: 0,
|
||||||
|
Msg: &protoHandshake{42, "420"},
|
||||||
|
Peer: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Expects: []p2ptest.Expect{
|
||||||
|
p2ptest.Expect{
|
||||||
|
Code: 1,
|
||||||
|
Msg: &hs0{1},
|
||||||
|
Peer: 0,
|
||||||
|
},
|
||||||
|
p2ptest.Expect{
|
||||||
|
Code: 1,
|
||||||
|
Msg: &hs0{1},
|
||||||
|
Peer: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
p2ptest.Exchange{
|
||||||
|
Triggers: []p2ptest.Trigger{
|
||||||
|
p2ptest.Trigger{
|
||||||
|
Code: 1,
|
||||||
|
Msg: &hs0{1},
|
||||||
|
Peer: 0,
|
||||||
|
},
|
||||||
|
p2ptest.Trigger{
|
||||||
|
Code: 1,
|
||||||
|
Msg: &hs0{1},
|
||||||
|
Peer: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMultiplePeers(t *testing.T, ids []discover.NodeID, peer int, errs ...error) {
|
||||||
|
p, pp := newProtocol()
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
s := p2ptest.NewSession(t, p, ids, wg)
|
||||||
|
|
||||||
|
s.TestExchanges(testMultiPeerSetup()...)
|
||||||
|
// after some exchanges of messages, we can test state changes
|
||||||
|
// here this is simply demonstrated by the peerPool
|
||||||
|
// after the handshake negotiations peers must be addded to the pool
|
||||||
|
if !pp.has(ids[0]) {
|
||||||
|
t.Fatalf("missing peer test-0: %v", pp)
|
||||||
|
}
|
||||||
|
if !pp.has(ids[1]) {
|
||||||
|
t.Fatalf("missing peer test-1: %v", pp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sending kill request for peer with index <peer>
|
||||||
|
s.TestExchanges(p2ptest.Exchange{
|
||||||
|
Triggers: []p2ptest.Trigger{
|
||||||
|
p2ptest.Trigger{
|
||||||
|
Code: 2,
|
||||||
|
Msg: &kill{ids[peer]},
|
||||||
|
Peer: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// dropping the remaining peer
|
||||||
|
s.TestExchanges(p2ptest.Exchange{
|
||||||
|
Triggers: []p2ptest.Trigger{
|
||||||
|
p2ptest.Trigger{
|
||||||
|
Code: 3,
|
||||||
|
Msg: &drop{},
|
||||||
|
Peer: (peer + 1) % 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// since drops are asyncronous, for correct testing you need to wait
|
||||||
|
//for all disconnections and error registration to complete or time out
|
||||||
|
errc := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(errc)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-errc:
|
||||||
|
case <-time.NewTimer(1000 * time.Millisecond).C:
|
||||||
|
t.Fatalf("timed out")
|
||||||
|
}
|
||||||
|
|
||||||
|
// test if disconnected peers have been removed from peerPool
|
||||||
|
if pp.has(ids[peer]) {
|
||||||
|
t.Fatalf("peer test-% not dropped: %v", peer, pp)
|
||||||
|
}
|
||||||
|
// check the actual discconnect errors on the individual peers
|
||||||
|
s.TestDisconnects(errs...)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultiplePeersDropSelf(t *testing.T) {
|
||||||
|
ids := p2ptest.RandomNodeIDs(t, 2)
|
||||||
|
runMultiplePeers(t, ids, 0, fmt.Errorf("p2p: read or write on closed message pipe"), fmt.Errorf("Message handler error: (msg code 3): received disconnect request"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultiplePeersDropOther(t *testing.T) {
|
||||||
|
ids := p2ptest.RandomNodeIDs(t, 2)
|
||||||
|
runMultiplePeers(t, ids, 1, fmt.Errorf("Message handler error: (msg code 3): received disconnect request"), fmt.Errorf("p2p: read or write on closed message pipe"))
|
||||||
|
}
|
||||||
223
p2p/testing/protocols.go
Normal file
223
p2p/testing/protocols.go
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
// Package protocols helpers_test make it easier to
|
||||||
|
// write protocol tests by providing convenience functions and structures
|
||||||
|
// protocols uses these helpers for its own tests
|
||||||
|
// but ideally should sit in p2p/protocols/testing/ subpackage
|
||||||
|
package protocols
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/logger/glog"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p"
|
||||||
|
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||||
|
)
|
||||||
|
|
||||||
|
// a session represents a protocol running on multiple peer connections with single local node
|
||||||
|
type Session struct {
|
||||||
|
IDs []discover.NodeID
|
||||||
|
Peers []*p2p.MsgPipeRW
|
||||||
|
Errs []error
|
||||||
|
t *testing.T
|
||||||
|
}
|
||||||
|
|
||||||
|
// exchanges are the basic units of protocol tests
|
||||||
|
// an exchange is defined on a session
|
||||||
|
type Exchange struct {
|
||||||
|
Triggers []Trigger
|
||||||
|
Expects []Expect
|
||||||
|
}
|
||||||
|
|
||||||
|
// part of the exchange, incoming message from a set of peers
|
||||||
|
type Trigger struct {
|
||||||
|
Msg interface{} // type of message to be sent
|
||||||
|
Code uint64 // code of message is given
|
||||||
|
Peer int // the peer to send the message to
|
||||||
|
Timeout time.Duration // timeout duration for the sending
|
||||||
|
}
|
||||||
|
|
||||||
|
type Expect struct {
|
||||||
|
Msg interface{} // type of message to expect
|
||||||
|
Code uint64 // code of message is now given
|
||||||
|
Peer int // the peer-connection index to expect the message from
|
||||||
|
Timeout time.Duration // timeout duration of receiving
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomNodeID(t *testing.T) (id discover.NodeID) {
|
||||||
|
key, err := crypto.GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate key")
|
||||||
|
}
|
||||||
|
pubkey := crypto.FromECDSAPub(&key.PublicKey)
|
||||||
|
copy(id[:], pubkey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandomNodeIDs(t *testing.T, n int) []discover.NodeID {
|
||||||
|
var ids []discover.NodeID
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
ids = append(ids, randomNodeID(t))
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSession creates a session by setting up a local peer with a prescribed set of peers
|
||||||
|
// wg if present allows wg.Wait() be used to block until all peers disconnect
|
||||||
|
// disconnect reason errors are written in session.Errs (correcponding to session,Peers)
|
||||||
|
func NewSession(t *testing.T, protocol *p2p.Protocol, ids []discover.NodeID, wg *sync.WaitGroup) *Session {
|
||||||
|
peerCount := len(ids)
|
||||||
|
self := &Session{t: t}
|
||||||
|
caps := []p2p.Cap{p2p.Cap{protocol.Name, protocol.Version}}
|
||||||
|
if wg != nil {
|
||||||
|
wg.Add(peerCount)
|
||||||
|
}
|
||||||
|
run := func(j int, rws []p2p.MsgReadWriter) {
|
||||||
|
name := fmt.Sprintf("test-%d", j)
|
||||||
|
self.Errs[j] = protocol.Run(p2p.NewPeer(ids[j], name, caps), rws[j])
|
||||||
|
if wg != nil {
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var rws []p2p.MsgReadWriter
|
||||||
|
// connect peerCount number of peers
|
||||||
|
for i := 0; i < peerCount; i++ {
|
||||||
|
rw, rrw := p2p.MsgPipe()
|
||||||
|
self.Peers = append(self.Peers, rrw)
|
||||||
|
self.Errs = append(self.Errs, nil)
|
||||||
|
rws = append(rws, rw)
|
||||||
|
}
|
||||||
|
// start protocols on each peer connection
|
||||||
|
for i := 0; i < peerCount; i++ {
|
||||||
|
go run(i, rws)
|
||||||
|
}
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger sends messages from peers
|
||||||
|
func (self Session) trigger(trig Trigger) error {
|
||||||
|
if self.Errs[trig.Peer] != nil {
|
||||||
|
return fmt.Errorf("peer %v already disconnected with %v", trig.Peer, self.Errs[trig.Peer])
|
||||||
|
}
|
||||||
|
errc := make(chan error)
|
||||||
|
go func() {
|
||||||
|
errc <- p2p.Send(self.Peers[trig.Peer], trig.Code, trig.Msg)
|
||||||
|
}()
|
||||||
|
|
||||||
|
t := trig.Timeout
|
||||||
|
if t == time.Duration(0) {
|
||||||
|
t = 1000 * time.Millisecond
|
||||||
|
}
|
||||||
|
alarm := time.NewTimer(t)
|
||||||
|
select {
|
||||||
|
case err := <-errc:
|
||||||
|
return err
|
||||||
|
case <-alarm.C:
|
||||||
|
return fmt.Errorf("timout expecting %v to send to peer %v", trig.Msg, trig.Peer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// expect checks an expectation
|
||||||
|
func (self Session) expect(exp Expect) error {
|
||||||
|
if exp.Msg == nil {
|
||||||
|
panic("no message to expect")
|
||||||
|
}
|
||||||
|
if exp.Peer >= len(self.Errs) {
|
||||||
|
panic(fmt.Sprintf("peer %v does not exist: %v", exp.Peer))
|
||||||
|
}
|
||||||
|
if self.Errs[exp.Peer] != nil {
|
||||||
|
panic(fmt.Sprintf("peer %v already disconnected with: %v", exp.Peer, self.Errs))
|
||||||
|
}
|
||||||
|
errc := make(chan error)
|
||||||
|
go func() {
|
||||||
|
glog.V(6).Infof("waiting for msg, %v", exp.Msg)
|
||||||
|
errc <- p2p.ExpectMsg(self.Peers[exp.Peer], exp.Code, exp.Msg)
|
||||||
|
}()
|
||||||
|
|
||||||
|
t := exp.Timeout
|
||||||
|
if t == time.Duration(0) {
|
||||||
|
t = 1000 * time.Millisecond
|
||||||
|
}
|
||||||
|
alarm := time.NewTimer(t)
|
||||||
|
select {
|
||||||
|
case err := <-errc:
|
||||||
|
glog.V(6).Infof("expected msg arrives with error %v", err)
|
||||||
|
return err
|
||||||
|
case <-alarm.C:
|
||||||
|
glog.V(6).Infof("caught timeout")
|
||||||
|
return fmt.Errorf("timout expecting %v sent to peer %v", exp.Msg, exp.Peer)
|
||||||
|
}
|
||||||
|
// fatal upon encountering first exchange error
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExchange tests a series of exchanges againsts the session
|
||||||
|
func (self Session) TestExchanges(exchanges ...Exchange) {
|
||||||
|
// launch all triggers of this exchanges
|
||||||
|
|
||||||
|
for i, e := range exchanges {
|
||||||
|
errc := make(chan error)
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
for _, trig := range e.Triggers {
|
||||||
|
wg.Add(1)
|
||||||
|
// separate go routing to allow parallel requests
|
||||||
|
go func(t Trigger) {
|
||||||
|
defer wg.Done()
|
||||||
|
err := self.trigger(t)
|
||||||
|
i++
|
||||||
|
if err != nil {
|
||||||
|
errc <- err
|
||||||
|
}
|
||||||
|
}(trig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// each expectation is spawned in separate go-routine
|
||||||
|
// expectations of an exchange are conjunctive but uordered, i.e., only all of them arriving constitutes a pass
|
||||||
|
// each expectation is meant to be for a different peer, otherwise they are expected to panic
|
||||||
|
// testing of an exchange blocks until all expectations are decided
|
||||||
|
// an expectation is decided if
|
||||||
|
// expected message arrives OR
|
||||||
|
// an unexpected message arrives (panic)
|
||||||
|
// times out on their individual tiemeout
|
||||||
|
for _, ex := range e.Expects {
|
||||||
|
wg.Add(1)
|
||||||
|
// expect msg spawned to separate go routine
|
||||||
|
go func(exp Expect) {
|
||||||
|
defer wg.Done()
|
||||||
|
err := self.expect(exp)
|
||||||
|
if err != nil {
|
||||||
|
glog.V(6).Infof("expect msg fails %v", err)
|
||||||
|
errc <- err
|
||||||
|
}
|
||||||
|
}(ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for all expectations
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(errc)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// time out globally or finish when all expectations satisfied
|
||||||
|
alarm := time.NewTimer(500 * time.Millisecond)
|
||||||
|
select {
|
||||||
|
|
||||||
|
case err := <-errc:
|
||||||
|
glog.V(6).Infof("expectations finished with %v", err)
|
||||||
|
if err != nil {
|
||||||
|
self.t.Fatalf("exchange failed with: %v", err)
|
||||||
|
}
|
||||||
|
case <-alarm.C:
|
||||||
|
self.t.Fatalf("exchange timed out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self Session) TestDisconnects(errs ...error) {
|
||||||
|
for i, err := range errs {
|
||||||
|
if !((err == nil && self.Errs[i] == nil) || err != nil && self.Errs[i] != nil && err.Error() == self.Errs[i].Error()) {
|
||||||
|
self.t.Fatalf("unexpected error on peer %v: '%v', wanted '%v'", i, self.Errs[i], err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue