diff --git a/node/node.go b/node/node.go index 9f9e3636..5238e72b 100644 --- a/node/node.go +++ b/node/node.go @@ -772,6 +772,9 @@ func makeNodeInfo( TxIndex: txIndexerStatus, RPCAddress: config.RPC.ListenAddress, }, + NodeVersion: p2p.NodeVersion{ + P2P: version.P2PProtocol, + }, } if config.P2P.PexReactor { diff --git a/p2p/node_info.go b/p2p/node_info.go index a1653594..0a8b76d4 100644 --- a/p2p/node_info.go +++ b/p2p/node_info.go @@ -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) { diff --git a/p2p/peer.go b/p2p/peer.go index 064f9181..bf26b963 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -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. diff --git a/p2p/peer_set_test.go b/p2p/peer_set_test.go index ee1c52ea..5a4c5507 100644 --- a/p2p/peer_set_test.go +++ b/p2p/peer_set_test.go @@ -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(), } diff --git a/p2p/peer_test.go b/p2p/peer_test.go index a2a2946a..235cb4ca 100644 --- a/p2p/peer_test.go +++ b/p2p/peer_test.go @@ -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) diff --git a/p2p/test_util.go b/p2p/test_util.go index 3d48aaac..41171f60 100644 --- a/p2p/test_util.go +++ b/p2p/test_util.go @@ -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 diff --git a/p2p/transport.go b/p2p/transport.go index 6f097b4f..5d6c8dd8 100644 --- a/p2p/transport.go +++ b/p2p/transport.go @@ -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, } } diff --git a/p2p/transport_test.go b/p2p/transport_test.go index 9e3cc467..1ee89ba8 100644 --- a/p2p/transport_test.go +++ b/p2p/transport_test.go @@ -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, diff --git a/version/version.go b/version/version.go index 5a089141..6a7f7553 100644 --- a/version/version.go +++ b/version/version.go @@ -25,6 +25,11 @@ const ( ABCIVersion = ABCISemVer ) +type Software struct { + Tendermint string + App string +} + // Protocol is used for implementation agnostic versioning. type Protocol uint64