p2p: update NodeInfo for versions

This commit is contained in:
Ethan Buchman 2018-09-26 18:47:58 -07:00
parent ccd04587ff
commit 0254cbf1e6
9 changed files with 286 additions and 164 deletions

View File

@ -772,6 +772,9 @@ func makeNodeInfo(
TxIndex: txIndexerStatus,
RPCAddress: config.RPC.ListenAddress,
},
NodeVersion: p2p.NodeVersion{
P2P: version.P2PProtocol,
},
}
if config.P2P.PexReactor {

View File

@ -5,6 +5,7 @@ import (
"strings"
cmn "github.com/tendermint/tendermint/libs/common"
"github.com/tendermint/tendermint/version"
)
const (
@ -20,42 +21,67 @@ func MaxNodeInfoSize() int {
// NodeInfo is the basic node information exchanged
// between two peers during the Tendermint P2P handshake.
type NodeInfo struct {
Version VersionInfo `json:"version"`
Address AddressInfo `json:"id"`
Network NetworkInfo `json:"network"`
Services ServiceInfo `json:"services"`
}
// VersionInfo contains all protocol and software version information for the node.
type VersionInfo struct {
Protocol ProtocolVersion `json:"protocol"`
Software version.Software `json:"software"`
}
// ProtocolVersion contains the p2p, block, and app protocol versions.
type ProtocolVersion struct {
P2P version.Protocol `json:"p2p"`
Block version.Protocol `json:"block"`
// Don't bother with App for now until we can update it live
// App version.Protocol `json:"app"`
}
// AddressInfo contains info about the peers ID and network address.
type AddressInfo 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 NodeInfoOther `json:"other"` // other application specific data
Moniker string `json:"moniker"` // arbitrary moniker
}
// NodeInfoOther is the misc. applcation specific data
type NodeInfoOther struct {
AminoVersion string `json:"amino_version"`
P2PVersion string `json:"p2p_version"`
ConsensusVersion string `json:"consensus_version"`
RPCVersion string `json:"rpc_version"`
TxIndex string `json:"tx_index"`
RPCAddress string `json:"rpc_address"`
// NetworkInfo contains info about the network this peer is operating on.
// Currently, the only identifier is the ChainID, known here as the Name.
type NetworkInfo struct {
Name string `json:"name"`
}
func (o NodeInfoOther) String() string {
return fmt.Sprintf(
"{amino_version: %v, p2p_version: %v, consensus_version: %v, rpc_version: %v, tx_index: %v, rpc_address: %v}",
o.AminoVersion,
o.P2PVersion,
o.ConsensusVersion,
o.RPCVersion,
o.TxIndex,
o.RPCAddress,
)
// ServiceInfo describes the services this peer offers to other peers and to users.
type ServiceInfo struct {
Peers PeerServices `json:"peers`
Users UserServices `json:"users"`
}
// PeerServices describes the services this peer offers to other peers,
// in terms of active Reactor channels.
type PeerServices struct {
// Channels are HexBytes so easier to read as JSON
Channels cmn.HexBytes `json:"channels"` // channels this node knows about
}
// UserServices describes the set of services exposed to the user.
type UserServices struct {
TxIndex string `json:"tx_index"`
RPCAddress string `json:"rpc_address"`
}
//--------------------------------------------------------------------------
func (info NodeInfo) ID() ID {
return info.Address.ID
}
// Validate checks the self-reported NodeInfo is safe.
@ -72,86 +98,144 @@ func (o NodeInfoOther) String() string {
// 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)
if err := info.Version.Validate(); err != nil {
return err
}
// 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)
if err := info.Address.Validate(); err != nil {
return err
}
// Sanitize versions
// XXX: Should we be more strict about version and address formats?
other := info.Other
versions := []string{
other.AminoVersion,
other.P2PVersion,
other.ConsensusVersion,
other.RPCVersion}
for i, v := range versions {
if cmn.ASCIITrim(v) != "" && !cmn.IsASCIIText(v) {
return fmt.Errorf("info.Other[%d]=%v must be valid non-empty ASCII text without tabs", i, v)
}
}
if cmn.ASCIITrim(other.TxIndex) != "" && (other.TxIndex != "on" && other.TxIndex != "off") {
return fmt.Errorf("info.Other.TxIndex should be either 'on' or 'off', got '%v'", other.TxIndex)
}
if cmn.ASCIITrim(other.RPCAddress) != "" && !cmn.IsASCIIText(other.RPCAddress) {
return fmt.Errorf("info.Other.RPCAddress=%v must be valid non-empty ASCII text without tabs", other.RPCAddress)
if err := info.Network.Validate(); err != nil {
return err
}
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{}{}
if err := info.Services.Validate(); err != nil {
return err
}
return nil
}
// Validate checks that the protocol versions are non-zero and that the software versions are ASCII.
func (info VersionInfo) Validate() error {
// TODO
// ProtocolVersion - {P2P, Block} greater than 0
// SoftwareVersion - ASCII
return nil
}
// Validate checks that the ListenAddr is well formed and that the moniker is ASCII.
// The ID should have already been checked.
func (info AddressInfo) Validate() error {
if _, err := sanitizeASCII(info.Moniker); err != nil {
return fmt.Errorf("Moniker %v", err)
}
// ensure ListenAddr is good
_, err := NewNetAddressString(IDAddressString(info.ID, info.ListenAddr))
return err
}
// Validate checks that the NetworkInfo.Name is ASCII.
func (info NetworkInfo) Validate() error {
if _, err := sanitizeASCII(info.Name); err != nil {
return fmt.Errorf("Name %v", err)
}
return nil
}
// Validate validates the PeerServices and UserServices
func (info ServiceInfo) Validate() error {
if err := info.Peers.Validate(); err != nil {
return err
}
if err := info.Users.Validate(); err != nil {
return err
}
return nil
}
// Validate checks that there are not too many channels or any duplicate channels.
func (services PeerServices) Validate() error {
channelBytes := services.Channels
if len(channelBytes) > maxNumChannels {
return fmt.Errorf("Channels is too long (%v). Max is %v", len(channelBytes), maxNumChannels)
}
channels := make(map[byte]struct{})
for _, ch := range channelBytes {
_, ok := channels[ch]
if ok {
return fmt.Errorf("Channels contains duplicate channel id %v", ch)
}
channels[ch] = struct{}{}
}
return nil
}
func (services UserServices) Validate() error {
txIndex, err := sanitizeASCII(services.TxIndex)
if err != nil {
return fmt.Errorf("TxIndex %v", err)
}
if _, err := sanitizeASCII(services.RPCAddress); err != nil {
return fmt.Errorf("RPCAddress %v", err)
}
switch cmn.ASCIITrim(txIndex) {
case "on", "off":
// do nothing
default:
return fmt.Errorf("TxIndex should be either 'on' or 'off', got '%v'", txIndex)
}
return nil
}
func sanitizeASCII(input string) (string, error) {
if !cmn.IsASCIIText(input) || cmn.ASCIITrim(input) == "" {
return "", fmt.Errorf("must be valid non-empty ASCII text without tabs, but got %v", input)
}
return cmn.ASCIITrim(input), nil
}
// 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, _, _, iErr := splitVersion(info.Version)
oMajor, _, _, oErr := splitVersion(other.Version)
// if our own version number is not formatted right, we messed up
if iErr != nil {
return iErr
// if we have no channels, we're just testing
ourChannels := info.Services.Peers.Channels
otherChannels := other.Services.Peers.Channels
if len(ourChannels) == 0 {
return nil
}
// 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)
// nodes must have the same block version
if info.Version.Protocol.Block != other.Version.Protocol.Block {
return fmt.Errorf(
"Peer is running a different block protocol. Got %v, expected %v",
other.Version.Protocol.Block,
info.Version.Protocol.Block,
)
}
// 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
if info.Network.Name != other.Network.Name {
return fmt.Errorf("Peer is on a different network. Got %v, expected %v", other.Network.Name, info.Network.Name)
}
// for each of our channels, check if they have it
found := false
OUTER_LOOP:
for _, ch1 := range info.Channels {
for _, ch2 := range other.Channels {
for _, ch1 := range ourChannels {
for _, ch2 := range otherChannels {
if ch1 == ch2 {
found = true
break OUTER_LOOP // only need one
@ -159,7 +243,7 @@ OUTER_LOOP:
}
}
if !found {
return fmt.Errorf("Peer has no common channels. Our channels: %v ; Peer channels: %v", info.Channels, other.Channels)
return fmt.Errorf("Peer has no common channels. Our channels: %v ; Peer channels: %v", ourChannels, otherChannels)
}
return nil
}
@ -169,7 +253,7 @@ OUTER_LOOP:
// 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))
netAddr, err := NewNetAddressString(IDAddressString(info.Address.ID, info.Address.ListenAddr))
if err != nil {
switch err.(type) {
case ErrNetAddressLookup:
@ -184,8 +268,8 @@ func (info NodeInfo) NetAddress() *NetAddress {
}
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)
return "TODO" // 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) {

View File

@ -102,7 +102,7 @@ type peer struct {
// User data
Data *cmn.CMap
metrics *Metrics
metrics *Metrics
metricsTicker *time.Ticker
}
@ -120,7 +120,7 @@ func newPeer(
p := &peer{
peerConn: pc,
nodeInfo: nodeInfo,
channels: nodeInfo.Channels,
channels: nodeInfo.Services.Peers.Channels,
Data: cmn.NewCMap(),
metricsTicker: time.NewTicker(metricsTickerDuration),
metrics: NopMetrics(),
@ -177,7 +177,7 @@ func (p *peer) OnStop() {
// ID returns the peer's ID - the hex encoded hash of its pubkey.
func (p *peer) ID() ID {
return p.nodeInfo.ID
return p.nodeInfo.ID()
}
// IsOutbound returns true if the connection is outbound, false otherwise.

View File

@ -21,8 +21,10 @@ func randPeer(ip net.IP) *peer {
nodeKey := NodeKey{PrivKey: ed25519.GenPrivKey()}
p := &peer{
nodeInfo: NodeInfo{
ID: nodeKey.ID(),
ListenAddr: fmt.Sprintf("%v.%v.%v.%v:26656", cmn.RandInt()%256, cmn.RandInt()%256, cmn.RandInt()%256, cmn.RandInt()%256),
Address: AddressInfo{
ID: nodeKey.ID(),
ListenAddr: fmt.Sprintf("%v.%v.%v.%v:26656", cmn.RandInt()%256, cmn.RandInt()%256, cmn.RandInt()%256, cmn.RandInt()%256),
},
},
metrics: NopMetrics(),
}

View File

@ -14,6 +14,7 @@ import (
"github.com/tendermint/tendermint/crypto/ed25519"
cmn "github.com/tendermint/tendermint/libs/common"
"github.com/tendermint/tendermint/libs/log"
"github.com/tendermint/tendermint/version"
"github.com/tendermint/tendermint/config"
tmconn "github.com/tendermint/tendermint/p2p/conn"
@ -82,11 +83,21 @@ func createOutboundPeerAndPerformHandshake(
return nil, err
}
nodeInfo, err := pc.HandshakeTimeout(NodeInfo{
ID: addr.ID,
Moniker: "host_peer",
Network: "testing",
Version: "123.123.123",
Channels: []byte{testCh},
Address: AddressInfo{
ID: addr.ID,
Moniker: "host_peer",
},
Network: NetworkInfo{"testing"},
Version: VersionInfo{
Software: version.Software{
Tendermint: "123.123.123",
},
},
Services: ServiceInfo{
Peers: PeerServices{
Channels: []byte{testCh},
},
},
}, 1*time.Second)
if err != nil {
return nil, err
@ -192,12 +203,17 @@ func (rp *remotePeer) accept(l net.Listener) {
}
_, err = handshake(pc.conn, time.Second, NodeInfo{
ID: rp.Addr().ID,
Moniker: "remote_peer",
Network: "testing",
Version: "123.123.123",
ListenAddr: l.Addr().String(),
Channels: rp.channels,
Address: AddressInfo{
ID: rp.Addr().ID,
Moniker: "remote_peer",
ListenAddr: l.Addr().String(),
},
Network: NetworkInfo{"testing"},
Services: ServiceInfo{
Peers: PeerServices{
Channels: rp.channels,
},
},
})
if err != nil {
golog.Fatalf("Failed to perform handshake: %+v", err)

View File

@ -25,8 +25,10 @@ func CreateRandomPeer(outbound bool) *peer {
outbound: outbound,
},
nodeInfo: NodeInfo{
ID: netAddr.ID,
ListenAddr: netAddr.DialString(),
Address: AddressInfo{
ID: netAddr.ID,
ListenAddr: netAddr.DialString(),
},
},
mconn: &conn.MConnection{},
metrics: NopMetrics(),
@ -164,24 +166,24 @@ func MakeSwitch(
PrivKey: ed25519.GenPrivKey(),
}
ni = NodeInfo{
ID: nodeKey.ID(),
Moniker: fmt.Sprintf("switch%d", i),
Network: network,
Version: version,
ListenAddr: fmt.Sprintf("127.0.0.1:%d", cmn.RandIntn(64512)+1023),
Other: NodeInfoOther{
AminoVersion: "1.0",
P2PVersion: "1.0",
ConsensusVersion: "1.0",
RPCVersion: "1.0",
TxIndex: "off",
RPCAddress: fmt.Sprintf("127.0.0.1:%d", cmn.RandIntn(64512)+1023),
Address: AddressInfo{
ID: nodeKey.ID(),
ListenAddr: fmt.Sprintf("127.0.0.1:%d", cmn.RandIntn(64512)+1023),
Moniker: fmt.Sprintf("switch%d", i),
},
Network: NetworkInfo{network},
Version: VersionInfo{}, // version,
Services: ServiceInfo{
Users: UserServices{
TxIndex: "off",
RPCAddress: fmt.Sprintf("127.0.0.1:%d", cmn.RandIntn(64512)+1023),
},
},
}
)
addr, err := NewNetAddressStringWithOptionalID(
IDAddressString(nodeKey.ID(), ni.ListenAddr),
IDAddressString(nodeKey.ID(), ni.Address.ListenAddr),
)
if err != nil {
panic(err)
@ -199,7 +201,7 @@ func MakeSwitch(
sw.SetNodeKey(&nodeKey)
for ch := range sw.reactorsByCh {
ni.Channels = append(ni.Channels, ch)
ni.Services.Peers.Channels = append(ni.Services.Peers.Channels, ch)
}
// TODO: We need to setup reactors ahead of time so the NodeInfo is properly

View File

@ -351,6 +351,32 @@ func (mt *MultiplexTransport) upgrade(
}
}
nodeID := nodeInfo.NetAddress().ID
// Ensure connection key matches self reported key.
if connID := PubKeyToID(secretConn.RemotePubKey()); connID != nodeID {
return nil, NodeInfo{}, ErrRejected{
conn: c,
id: connID,
err: fmt.Errorf(
"conn.ID (%v) NodeInfo.ID (%v) missmatch",
connID,
nodeID,
),
isAuthFailure: true,
}
}
// Reject self.
if mt.nodeInfo.NetAddress().ID == nodeID {
return nil, NodeInfo{}, ErrRejected{
addr: *nodeInfo.NetAddress(),
conn: c,
id: nodeID,
isSelf: true,
}
}
if err := nodeInfo.Validate(); err != nil {
return nil, NodeInfo{}, ErrRejected{
conn: c,
@ -359,35 +385,11 @@ func (mt *MultiplexTransport) upgrade(
}
}
// Ensure connection key matches self reported key.
if connID := PubKeyToID(secretConn.RemotePubKey()); connID != nodeInfo.ID {
return nil, NodeInfo{}, ErrRejected{
conn: c,
id: connID,
err: fmt.Errorf(
"conn.ID (%v) NodeInfo.ID (%v) missmatch",
connID,
nodeInfo.ID,
),
isAuthFailure: true,
}
}
// Reject self.
if mt.nodeInfo.ID == nodeInfo.ID {
return nil, NodeInfo{}, ErrRejected{
addr: *NewNetAddress(nodeInfo.ID, c.RemoteAddr()),
conn: c,
id: nodeInfo.ID,
isSelf: true,
}
}
if err := mt.nodeInfo.CompatibleWith(nodeInfo); err != nil {
return nil, NodeInfo{}, ErrRejected{
conn: c,
err: err,
id: nodeInfo.ID,
id: nodeInfo.ID(),
isIncompatible: true,
}
}

View File

@ -138,7 +138,6 @@ func TestTransportMultiplexAcceptMultiple(t *testing.T) {
ID: PubKeyToID(pv.PubKey()),
ListenAddr: "127.0.0.1:0",
Moniker: "dialer",
Version: "1.0.0",
},
NodeKey{
PrivKey: pv,
@ -208,10 +207,11 @@ func TestTransportMultiplexAcceptNonBlocking(t *testing.T) {
var (
fastNodePV = ed25519.GenPrivKey()
fastNodeInfo = NodeInfo{
ID: PubKeyToID(fastNodePV.PubKey()),
ListenAddr: "127.0.0.1:0",
Moniker: "fastNode",
Version: "1.0.0",
Address: AddressInfo{
ID: PubKeyToID(fastNodePV.PubKey()),
ListenAddr: "127.0.0.1:0",
Moniker: "fastNode",
},
}
errc = make(chan error)
fastc = make(chan struct{})
@ -249,9 +249,11 @@ func TestTransportMultiplexAcceptNonBlocking(t *testing.T) {
}
_, err = handshake(sc, 20*time.Millisecond, NodeInfo{
ID: PubKeyToID(ed25519.GenPrivKey().PubKey()),
ListenAddr: "127.0.0.1:0",
Moniker: "slow_peer",
Address: AddressInfo{
ID: PubKeyToID(ed25519.GenPrivKey().PubKey()),
ListenAddr: "127.0.0.1:0",
Moniker: "slow_peer",
},
})
if err != nil {
errc <- err
@ -312,10 +314,11 @@ func TestTransportMultiplexValidateNodeInfo(t *testing.T) {
pv = ed25519.GenPrivKey()
dialer = NewMultiplexTransport(
NodeInfo{
ID: PubKeyToID(pv.PubKey()),
ListenAddr: "127.0.0.1:0",
Moniker: "", // Should not be empty.
Version: "1.0.0",
Address: AddressInfo{
ID: PubKeyToID(pv.PubKey()),
ListenAddr: "127.0.0.1:0",
Moniker: "", // Should not be empty.
},
},
NodeKey{
PrivKey: pv,
@ -360,10 +363,11 @@ func TestTransportMultiplexRejectMissmatchID(t *testing.T) {
go func() {
dialer := NewMultiplexTransport(
NodeInfo{
ID: PubKeyToID(ed25519.GenPrivKey().PubKey()),
ListenAddr: "127.0.0.1:0",
Moniker: "dialer",
Version: "1.0.0",
Address: AddressInfo{
ID: PubKeyToID(ed25519.GenPrivKey().PubKey()),
ListenAddr: "127.0.0.1:0",
Moniker: "dialer",
},
},
NodeKey{
PrivKey: ed25519.GenPrivKey(),
@ -409,10 +413,11 @@ func TestTransportMultiplexRejectIncompatible(t *testing.T) {
pv = ed25519.GenPrivKey()
dialer = NewMultiplexTransport(
NodeInfo{
ID: PubKeyToID(pv.PubKey()),
ListenAddr: "127.0.0.1:0",
Moniker: "dialer",
Version: "2.0.0",
Address: AddressInfo{
ID: PubKeyToID(pv.PubKey()),
ListenAddr: "127.0.0.1:0",
Moniker: "dialer",
},
},
NodeKey{
PrivKey: pv,
@ -522,7 +527,9 @@ func TestTransportHandshake(t *testing.T) {
var (
peerPV = ed25519.GenPrivKey()
peerNodeInfo = NodeInfo{
ID: PubKeyToID(peerPV.PubKey()),
Address: AddressInfo{
ID: PubKeyToID(peerPV.PubKey()),
},
}
)
@ -573,10 +580,11 @@ func testSetupMultiplexTransport(t *testing.T) *MultiplexTransport {
pv = ed25519.GenPrivKey()
mt = NewMultiplexTransport(
NodeInfo{
ID: PubKeyToID(pv.PubKey()),
ListenAddr: "127.0.0.1:0",
Moniker: "transport",
Version: "1.0.0",
Address: AddressInfo{
ID: PubKeyToID(pv.PubKey()),
ListenAddr: "127.0.0.1:0",
Moniker: "transport",
},
},
NodeKey{
PrivKey: pv,

View File

@ -25,6 +25,11 @@ const (
ABCIVersion = ABCISemVer
)
type Software struct {
Tendermint string
App string
}
// Protocol is used for implementation agnostic versioning.
type Protocol uint64