mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-08 22:23:11 +00:00
This cleans up the `Router` code and adds a bunch of tests. These sorts of systems are a real pain to test, since they have a bunch of asynchronous goroutines living their own lives, so the test coverage is decent but not fantastic. Luckily we've been able to move all of the complex peer management and transport logic outside of the router, as synchronous components that are much easier to test, so the core router logic is fairly small and simple. This also provides some initial test tooling in `p2p/p2ptest` that automatically sets up in-memory networks and channels for use in integration tests. It also includes channel-oriented test asserters in `p2p/p2ptest/require.go`, but these have primarily been written for router testing and should probably be adapted or extended for reactor testing.
194 lines
6.7 KiB
Go
194 lines
6.7 KiB
Go
package p2p
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
|
|
"github.com/tendermint/tendermint/crypto"
|
|
"github.com/tendermint/tendermint/p2p/conn"
|
|
)
|
|
|
|
//go:generate mockery --case underscore --name Transport|Connection
|
|
|
|
const (
|
|
// defaultProtocol is the default protocol used for NodeAddress when
|
|
// a protocol isn't explicitly given as a URL scheme.
|
|
defaultProtocol Protocol = MConnProtocol
|
|
)
|
|
|
|
// Protocol identifies a transport protocol.
|
|
type Protocol string
|
|
|
|
// Transport is a connection-oriented mechanism for exchanging data with a peer.
|
|
type Transport interface {
|
|
// Protocols returns the protocols supported by the transport. The Router
|
|
// uses this to pick a transport for an Endpoint.
|
|
Protocols() []Protocol
|
|
|
|
// Endpoints returns the local endpoints the transport is listening on, if any.
|
|
//
|
|
// How to listen is transport-dependent, e.g. MConnTransport uses Listen() while
|
|
// MemoryTransport starts listening via MemoryNetwork.CreateTransport().
|
|
Endpoints() []Endpoint
|
|
|
|
// Accept waits for the next inbound connection on a listening endpoint, blocking
|
|
// until either a connection is available or the transport is closed. On closure,
|
|
// io.EOF is returned and further Accept calls are futile.
|
|
Accept() (Connection, error)
|
|
|
|
// Dial creates an outbound connection to an endpoint.
|
|
Dial(context.Context, Endpoint) (Connection, error)
|
|
|
|
// Close stops accepting new connections, but does not close active connections.
|
|
Close() error
|
|
|
|
// Stringer is used to display the transport, e.g. in logs.
|
|
//
|
|
// Without this, the logger may use reflection to access and display
|
|
// internal fields. These can be written to concurrently, which can trigger
|
|
// the race detector or even cause a panic.
|
|
fmt.Stringer
|
|
}
|
|
|
|
// Connection represents an established connection between two endpoints.
|
|
//
|
|
// FIXME: This is a temporary interface for backwards-compatibility with the
|
|
// current MConnection-protocol, which is message-oriented. It should be
|
|
// migrated to a byte-oriented multi-stream interface instead, which would allow
|
|
// e.g. adopting QUIC and making message framing, traffic scheduling, and node
|
|
// handshakes a Router concern shared across all transports. However, this
|
|
// requires MConnection protocol changes or a shim. For details, see:
|
|
// https://github.com/tendermint/spec/pull/227
|
|
//
|
|
// FIXME: The interface is currently very broad in order to accommodate
|
|
// MConnection behavior that the legacy P2P stack relies on. It should be
|
|
// cleaned up when the legacy stack is removed.
|
|
type Connection interface {
|
|
// Handshake executes a node handshake with the remote peer. It must be
|
|
// called immediately after the connection is established, and returns the
|
|
// remote peer's node info and public key. The caller is responsible for
|
|
// validation.
|
|
//
|
|
// FIXME: The handshake should really be the Router's responsibility, but
|
|
// that requires the connection interface to be byte-oriented rather than
|
|
// message-oriented (see comment above).
|
|
Handshake(context.Context, NodeInfo, crypto.PrivKey) (NodeInfo, crypto.PubKey, error)
|
|
|
|
// ReceiveMessage returns the next message received on the connection,
|
|
// blocking until one is available. Returns io.EOF if closed.
|
|
ReceiveMessage() (ChannelID, []byte, error)
|
|
|
|
// SendMessage sends a message on the connection. Returns io.EOF if closed.
|
|
//
|
|
// FIXME: For compatibility with the legacy P2P stack, it returns an
|
|
// additional boolean false if the message timed out waiting to be accepted
|
|
// into the send buffer. This should be removed.
|
|
SendMessage(ChannelID, []byte) (bool, error)
|
|
|
|
// TrySendMessage is a non-blocking version of SendMessage that returns
|
|
// immediately if the message buffer is full. It returns true if the message
|
|
// was accepted.
|
|
//
|
|
// FIXME: This method is here for backwards-compatibility with the legacy
|
|
// P2P stack and should be removed.
|
|
TrySendMessage(ChannelID, []byte) (bool, error)
|
|
|
|
// LocalEndpoint returns the local endpoint for the connection.
|
|
LocalEndpoint() Endpoint
|
|
|
|
// RemoteEndpoint returns the remote endpoint for the connection.
|
|
RemoteEndpoint() Endpoint
|
|
|
|
// Close closes the connection.
|
|
Close() error
|
|
|
|
// FlushClose flushes all pending sends and then closes the connection.
|
|
//
|
|
// FIXME: This only exists for backwards-compatibility with the current
|
|
// MConnection implementation. There should really be a separate Flush()
|
|
// method, but there is no easy way to synchronously flush pending data with
|
|
// the current MConnection code.
|
|
FlushClose() error
|
|
|
|
// Status returns the current connection status.
|
|
// FIXME: Only here for compatibility with the current Peer code.
|
|
Status() conn.ConnectionStatus
|
|
|
|
// Stringer is used to display the connection, e.g. in logs.
|
|
//
|
|
// Without this, the logger may use reflection to access and display
|
|
// internal fields. These can be written to concurrently, which can trigger
|
|
// the race detector or even cause a panic.
|
|
fmt.Stringer
|
|
}
|
|
|
|
// Endpoint represents a transport connection endpoint, either local or remote.
|
|
//
|
|
// Endpoints are not necessarily networked (see e.g. MemoryTransport) but all
|
|
// networked endpoints must use IP as the underlying transport protocol to allow
|
|
// e.g. IP address filtering. Either IP or Path (or both) must be set.
|
|
type Endpoint struct {
|
|
// Protocol specifies the transport protocol.
|
|
Protocol Protocol
|
|
|
|
// IP is an IP address (v4 or v6) to connect to. If set, this defines the
|
|
// endpoint as a networked endpoint.
|
|
IP net.IP
|
|
|
|
// Port is a network port (either TCP or UDP). If 0, a default port may be
|
|
// used depending on the protocol.
|
|
Port uint16
|
|
|
|
// Path is an optional transport-specific path or identifier.
|
|
Path string
|
|
}
|
|
|
|
// NodeAddress converts the endpoint into a NodeAddress for the given node ID.
|
|
func (e Endpoint) NodeAddress(nodeID NodeID) NodeAddress {
|
|
address := NodeAddress{
|
|
NodeID: nodeID,
|
|
Protocol: e.Protocol,
|
|
Path: e.Path,
|
|
}
|
|
if len(e.IP) > 0 {
|
|
address.Hostname = e.IP.String()
|
|
address.Port = e.Port
|
|
}
|
|
return address
|
|
}
|
|
|
|
// String formats the endpoint as a URL string.
|
|
func (e Endpoint) String() string {
|
|
// If this is a non-networked endpoint with a valid node ID as a path,
|
|
// assume that path is a node ID (to handle opaque URLs of the form
|
|
// scheme:id).
|
|
if e.IP == nil {
|
|
if nodeID, err := NewNodeID(e.Path); err == nil {
|
|
return e.NodeAddress(nodeID).String()
|
|
}
|
|
}
|
|
return e.NodeAddress("").String()
|
|
}
|
|
|
|
// Validate validates the endpoint.
|
|
func (e Endpoint) Validate() error {
|
|
switch {
|
|
case e.Protocol == "":
|
|
return errors.New("endpoint has no protocol")
|
|
|
|
case len(e.IP) > 0 && e.IP.To16() == nil:
|
|
return fmt.Errorf("invalid IP address %v", e.IP)
|
|
|
|
case e.Port > 0 && len(e.IP) == 0:
|
|
return fmt.Errorf("endpoint has port %v but no IP", e.Port)
|
|
|
|
case len(e.IP) == 0 && e.Path == "":
|
|
return errors.New("endpoint has neither path nor IP")
|
|
|
|
default:
|
|
return nil
|
|
}
|
|
}
|