mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-26 06:36:43 +00:00
p2p/protocols: devp2p subprotocol abstraction
Package protocols is an extension to p2p. It offers a user friendly simple way to define devp2p subprotocols by abstracting away code that implementations would typically use. The package provides a standard protocol peer object of type protocols.Peer initialised from a p2p.Peer, a p2p.MsgReadWriter (the arguments passed to p2p.Protocol#Run and a protocols.CodeMap. Allowing the p2p.Protocol#Run function to construct this peer allows passing it to arbitrary service instances sitting on peer connections. These service instances can encapsulate vertical slices of business logic without duplicating code related to protocol communication. * registering multiple handler callbacks 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 * TODO: automatic generation of wire protocol specification for peers The test demonstrates the use of protocols/testing package for testing protocols see the possibly obsolete #2254 for the peer management/connectivity related aspect)
This commit is contained in:
parent
b07124f6dd
commit
23973c994d
3 changed files with 745 additions and 41 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"))
|
||||||
|
}
|
||||||
|
|
@ -11,12 +11,14 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"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"
|
||||||
"github.com/ethereum/go-ethereum/p2p/discover"
|
"github.com/ethereum/go-ethereum/p2p/discover"
|
||||||
)
|
)
|
||||||
|
|
||||||
// a session represents a protocol running on multiple peer connections with single local node
|
// a session represents a protocol running on multiple peer connections with single local node
|
||||||
type Session struct {
|
type Session struct {
|
||||||
|
IDs []discover.NodeID
|
||||||
Peers []*p2p.MsgPipeRW
|
Peers []*p2p.MsgPipeRW
|
||||||
Errs []error
|
Errs []error
|
||||||
t *testing.T
|
t *testing.T
|
||||||
|
|
@ -44,16 +46,41 @@ type Expect struct {
|
||||||
Timeout time.Duration // timeout duration of receiving
|
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
|
// 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
|
// 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)
|
// disconnect reason errors are written in session.Errs (correcponding to session,Peers)
|
||||||
func NewSession(t *testing.T, protocol *p2p.Protocol, peerCount int, wg *sync.WaitGroup) *Session {
|
func NewSession(t *testing.T, protocol *p2p.Protocol, ids []discover.NodeID, wg *sync.WaitGroup) *Session {
|
||||||
self := &Session{t: t, Errs: make([]error, peerCount)}
|
peerCount := len(ids)
|
||||||
|
self := &Session{t: t}
|
||||||
caps := []p2p.Cap{p2p.Cap{protocol.Name, protocol.Version}}
|
caps := []p2p.Cap{p2p.Cap{protocol.Name, protocol.Version}}
|
||||||
if wg != nil {
|
if wg != nil {
|
||||||
wg.Add(peerCount)
|
wg.Add(peerCount)
|
||||||
}
|
}
|
||||||
var runs []func(int)
|
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
|
var rws []p2p.MsgReadWriter
|
||||||
// connect peerCount number of peers
|
// connect peerCount number of peers
|
||||||
for i := 0; i < peerCount; i++ {
|
for i := 0; i < peerCount; i++ {
|
||||||
|
|
@ -61,24 +88,10 @@ func NewSession(t *testing.T, protocol *p2p.Protocol, peerCount int, wg *sync.Wa
|
||||||
self.Peers = append(self.Peers, rrw)
|
self.Peers = append(self.Peers, rrw)
|
||||||
self.Errs = append(self.Errs, nil)
|
self.Errs = append(self.Errs, nil)
|
||||||
rws = append(rws, rw)
|
rws = append(rws, rw)
|
||||||
key, err := crypto.GenerateKey()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to generate key")
|
|
||||||
}
|
|
||||||
pubkey := crypto.FromECDSAPub(&key.PublicKey)
|
|
||||||
var id discover.NodeID
|
|
||||||
copy(id[:], pubkey)
|
|
||||||
runs = append(runs, func(j int) {
|
|
||||||
name := fmt.Sprintf("test-%d", j)
|
|
||||||
self.Errs[j] = protocol.Run(p2p.NewPeer(id, name, caps), rws[j])
|
|
||||||
if wg != nil {
|
|
||||||
wg.Done()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
// start protocols on each peer connection
|
// start protocols on each peer connection
|
||||||
for i, f := range runs {
|
for i := 0; i < peerCount; i++ {
|
||||||
go f(i)
|
go run(i, rws)
|
||||||
}
|
}
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +108,7 @@ func (self Session) trigger(trig Trigger) error {
|
||||||
|
|
||||||
t := trig.Timeout
|
t := trig.Timeout
|
||||||
if t == time.Duration(0) {
|
if t == time.Duration(0) {
|
||||||
t = 100 * time.Millisecond
|
t = 1000 * time.Millisecond
|
||||||
}
|
}
|
||||||
alarm := time.NewTimer(t)
|
alarm := time.NewTimer(t)
|
||||||
select {
|
select {
|
||||||
|
|
@ -119,18 +132,21 @@ func (self Session) expect(exp Expect) error {
|
||||||
}
|
}
|
||||||
errc := make(chan error)
|
errc := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
|
glog.V(6).Infof("waiting for msg, %v", exp.Msg)
|
||||||
errc <- p2p.ExpectMsg(self.Peers[exp.Peer], exp.Code, exp.Msg)
|
errc <- p2p.ExpectMsg(self.Peers[exp.Peer], exp.Code, exp.Msg)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
t := exp.Timeout
|
t := exp.Timeout
|
||||||
if t == time.Duration(0) {
|
if t == time.Duration(0) {
|
||||||
t = 100 * time.Millisecond
|
t = 1000 * time.Millisecond
|
||||||
}
|
}
|
||||||
alarm := time.NewTimer(t)
|
alarm := time.NewTimer(t)
|
||||||
select {
|
select {
|
||||||
case err := <-errc:
|
case err := <-errc:
|
||||||
|
glog.V(6).Infof("expected msg arrives with error %v", err)
|
||||||
return err
|
return err
|
||||||
case <-alarm.C:
|
case <-alarm.C:
|
||||||
|
glog.V(6).Infof("caught timeout")
|
||||||
return fmt.Errorf("timout expecting %v sent to peer %v", exp.Msg, exp.Peer)
|
return fmt.Errorf("timout expecting %v sent to peer %v", exp.Msg, exp.Peer)
|
||||||
}
|
}
|
||||||
// fatal upon encountering first exchange error
|
// fatal upon encountering first exchange error
|
||||||
|
|
@ -140,10 +156,8 @@ func (self Session) expect(exp Expect) error {
|
||||||
func (self Session) TestExchanges(exchanges ...Exchange) {
|
func (self Session) TestExchanges(exchanges ...Exchange) {
|
||||||
// launch all triggers of this exchanges
|
// launch all triggers of this exchanges
|
||||||
|
|
||||||
errc := make(chan error)
|
|
||||||
ewg := &sync.WaitGroup{}
|
|
||||||
for i, e := range exchanges {
|
for i, e := range exchanges {
|
||||||
ewg.Add(1)
|
errc := make(chan error)
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
for _, trig := range e.Triggers {
|
for _, trig := range e.Triggers {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|
@ -173,32 +187,30 @@ func (self Session) TestExchanges(exchanges ...Exchange) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
err := self.expect(exp)
|
err := self.expect(exp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
glog.V(6).Infof("expect msg fails %v", err)
|
||||||
errc <- err
|
errc <- err
|
||||||
}
|
}
|
||||||
}(ex)
|
}(ex)
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for all expectations
|
// wait for all expectations
|
||||||
wg.Wait()
|
go func() {
|
||||||
ewg.Done()
|
wg.Wait()
|
||||||
|
close(errc)
|
||||||
|
}()
|
||||||
|
|
||||||
}
|
// time out globally or finish when all expectations satisfied
|
||||||
// wait for all expectations
|
alarm := time.NewTimer(500 * time.Millisecond)
|
||||||
go func() {
|
select {
|
||||||
ewg.Wait()
|
|
||||||
close(errc)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// time out globally or finish when all expectations satisfied
|
case err := <-errc:
|
||||||
alarm := time.NewTimer(5000 * time.Millisecond)
|
glog.V(6).Infof("expectations finished with %v", err)
|
||||||
select {
|
if err != nil {
|
||||||
|
self.t.Fatalf("exchange failed with: %v", err)
|
||||||
case err := <-errc:
|
}
|
||||||
if err != nil {
|
case <-alarm.C:
|
||||||
self.t.Fatalf("exchange failed with: %v", err)
|
self.t.Fatalf("exchange timed out")
|
||||||
}
|
}
|
||||||
case <-alarm.C:
|
|
||||||
self.t.Fatalf("exchange timed out")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue