diff --git a/p2p/node_info.go b/p2p/node_info.go index a468443d..7c31aaf7 100644 --- a/p2p/node_info.go +++ b/p2p/node_info.go @@ -6,6 +6,8 @@ import ( "strings" cmn "github.com/tendermint/tendermint/libs/common" + + "github.com/tendermint/tendermint/version" ) const ( @@ -132,10 +134,10 @@ func (info DefaultNodeInfo) ValidateBasic() error { // CompatibleWith checks if two DefaultNodeInfo 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 DefaultNodeInfo) CompatibleWith(other_ NodeInfo) error { - other, ok := other_.(DefaultNodeInfo) +func (info DefaultNodeInfo) CompatibleWith(otherInfo NodeInfo) error { + other, ok := otherInfo.(DefaultNodeInfo) if !ok { - return fmt.Errorf("wrong NodeInfo type. Expected DefaultNodeInfo, got %v", reflect.TypeOf(other_)) + return fmt.Errorf("wrong NodeInfo type. Expected DefaultNodeInfo, got %v", reflect.TypeOf(otherInfo)) } iMajor, _, _, iErr := splitVersion(info.Version) @@ -209,3 +211,252 @@ func splitVersion(version string) (string, string, string, error) { } return spl[0], spl[1], spl[2], nil } + +//-------------------------------------------------------------------------------------- + +// NodeInfoV5 is the basic node information exchanged +// between two peers during the Tendermint P2P handshake. +type NodeInfoV5 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 ? + // Note NetAddress currently only has IP, + // but we may want DNS name here. + ID ID `json:"id"` // authenticated identifier + ListenAddr string `json:"listen_addr"` // accepting incoming + + // ASCIIText fields + Moniker string `json:"moniker"` // arbitrary moniker +} + +// 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"` +} + +// 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 NodeInfoV5) ID() ID { + return info.Address.ID +} + +func (info NodeInfoV5) ValidateBasic() error { + + if err := info.Version.Validate(); err != nil { + return err + } + + if err := info.Address.Validate(); err != nil { + return err + } + + if err := info.Network.Validate(); err != nil { + return err + } + + 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 NodeInfoV5 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 NodeInfoV5) CompatibleWith(otherInfo NodeInfo) error { + other, ok := otherInfo.(NodeInfoV5) + if !ok { + return fmt.Errorf("wrong NodeInfo type. Expected DefaultNodeInfo, got %v", reflect.TypeOf(otherInfo)) + } + + // 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 + } + + // 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.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 ourChannels { + for _, ch2 := range otherChannels { + if ch1 == ch2 { + found = true + break OUTER_LOOP // only need one + } + } + } + if !found { + return fmt.Errorf("Peer has no common channels. Our channels: %v ; Peer channels: %v", ourChannels, otherChannels) + } + return nil +} + +// NetAddress returns a NetAddress derived from the NodeInfoV5 - +// it includes the authenticated peer ID and the self-reported +// ListenAddr. Note that the ListenAddr is not authenticated and +// may not match that address actually dialed if its an outbound peer. +func (info NodeInfoV5) NetAddress() *NetAddress { + netAddr, err := NewNetAddressString(IDAddressString(info.Address.ID, info.Address.ListenAddr)) + if err != nil { + switch err.(type) { + case ErrNetAddressLookup: + // XXX If the peer provided a host name and the lookup fails here + // we're out of luck. + // TODO: use a NetAddress in NodeInfoV5 + default: + panic(err) // everything should be well formed by now + } + } + return netAddr +} + +func (info NodeInfoV5) String() string { + return "TODO" // fmt.Sprintf("NodeInfoV5{id: %v, moniker: %v, network: %v [listen %v], version: %v (%v)}", + // info.ID, info.Moniker, info.Network, info.ListenAddr, info.Version, info.Other) +} diff --git a/version/version.go b/version/version.go index 5a089141..0f3099ef 100644 --- a/version/version.go +++ b/version/version.go @@ -25,6 +25,11 @@ const ( ABCIVersion = ABCISemVer ) +type Software struct { + Tendermint string `json:"tendermint"` + App string `json:"app"` +} + // Protocol is used for implementation agnostic versioning. type Protocol uint64