neoneo-go/pkg/network/message.go
2018-01-27 08:37:07 +01:00

298 lines
7.5 KiB
Go

package network
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"errors"
"io"
)
const (
// The minimum size of a valid message.
minMessageSize = 24
)
// NetMode type that is compatible with netModes below.
type NetMode uint32
// String implements the stringer interface.
func (n NetMode) String() string {
switch n {
case ModeDevNet:
return "devnet"
case ModeTestNet:
return "testnet"
case ModeMainNet:
return "mainnet"
default:
return ""
}
}
// Values used for the magic field, according to the docs.
const (
ModeMainNet NetMode = 0x00746e41 // 7630401
ModeTestNet = 0x74746e41 // 1953787457
ModeDevNet = 56753 // docker privnet
)
// Message is the complete message send between nodes.
//
// Size Field DataType Description
// ------------------------------------------------------
// 4 Magic uint32 Protocol ID
// 12 Command char[12] Command
// 4 length uint32 Length of payload
// 4 Checksum uint32 Checksum
// length Payload uint8[length] Content of message
type Message struct {
Magic NetMode
// Command is utf8 code, of which the length is 12 bytes,
// the extra part is filled with 0.
Command []byte
// Length of the payload
Length uint32
// Checksum is the first 4 bytes of the value that two times SHA256
// hash of the payload
Checksum uint32
// Payload send with the message.
Payload []byte
}
type commandType string
// valid commands used to send between nodes.
const (
cmdVersion commandType = "version"
cmdVerack = "verack"
cmdGetAddr = "getaddr"
cmdAddr = "addr"
cmdGetHeaders = "getheaders"
cmdHeaders = "headers"
cmdGetBlocks = "getblocks"
cmdInv = "inv"
cmdGetData = "getdata"
cmdBlock = "block"
cmdTX = "tx"
)
func newMessage(magic NetMode, cmd commandType, payload []byte) *Message {
sum := sumSHA256(sumSHA256(payload))[:4]
sumuint32 := binary.LittleEndian.Uint32(sum)
return &Message{
Magic: magic,
Command: cmdToByteSlice(cmd),
Length: uint32(len(payload)),
Checksum: sumuint32,
Payload: payload,
}
}
// Converts the 12 byte command slice to a commandType.
func (m *Message) commandType() commandType {
cmd := string(bytes.TrimRight(m.Command, "\x00"))
switch cmd {
case "version":
return cmdVersion
case "verack":
return cmdVerack
case "getaddr":
return cmdGetAddr
case "addr":
return cmdAddr
case "getheaders":
return cmdGetHeaders
case "header":
return cmdHeaders
case "getblocks":
return cmdGetBlocks
case "inv":
return cmdInv
case "getdata":
return cmdGetData
case "block":
return cmdBlock
case "tx":
return cmdTX
default:
return ""
}
}
// decode a Message from the given reader.
func (m *Message) decode(r io.Reader) error {
// 24 bytes for the fixed sized fields.
buf := make([]byte, minMessageSize)
if _, err := r.Read(buf); err != nil {
return err
}
m.Magic = NetMode(binary.LittleEndian.Uint32(buf[0:4]))
m.Command = buf[4:16]
m.Length = binary.LittleEndian.Uint32(buf[16:20])
m.Checksum = binary.LittleEndian.Uint32(buf[20:24])
payload := make([]byte, m.Length)
if _, err := r.Read(payload); err != nil {
return err
}
// Compare the checksum of the payload.
if !compareChecksum(m.Checksum, payload) {
return errors.New("checksum mismatch error")
}
m.Payload = payload
return nil
}
// encode a Message to any given io.Writer.
func (m *Message) encode(w io.Writer) error {
// 24 bytes for the fixed sized fields + the length of the payload.
buf := make([]byte, minMessageSize+m.Length)
binary.LittleEndian.PutUint32(buf[0:4], uint32(m.Magic))
copy(buf[4:16], m.Command)
binary.LittleEndian.PutUint32(buf[16:20], m.Length)
binary.LittleEndian.PutUint32(buf[20:24], m.Checksum)
copy(buf[24:len(buf)], m.Payload)
_, err := w.Write(buf)
return err
}
func (m *Message) decodePayload() (interface{}, error) {
switch m.commandType() {
case cmdVersion:
v := &Version{}
if err := v.decode(m.Payload); err != nil {
return nil, err
}
return v, nil
}
return nil, nil
}
// Version payload description.
//
// Size Field DataType Description
// ---------------------------------------------------------------------------------------------
// 4 Version uint32 Version of protocol, 0 for now
// 8 Services uint64 The service provided by the node is currently 1
// 4 Timestamp uint32 Current time
// 2 Port uint16 Port that the server is listening on, it's 0 if not used.
// 4 Nonce uint32 It's used to distinguish the node from public IP
// ? UserAgent varstr Client ID
// 4 StartHeight uint32 Height of block chain
// 1 Relay bool Whether to receive and forward
type Version struct {
// currently the version of the protocol is 0
Version uint32
// currently 1
Services uint64
// timestamp
Timestamp uint32
// port this server is listening on
Port uint16
// it's used to distinguish the node from public IP
Nonce uint32
// client id
UserAgent []byte // ?
// Height of the block chain
StartHeight uint32
// Whether to receive and forward
Relay bool
}
func newVersionPayload(p uint16, ua string, h uint32, r bool) *Version {
return &Version{
Version: 0,
Services: 1,
Timestamp: 12345,
Port: p,
Nonce: 19110,
UserAgent: []byte(ua),
StartHeight: 0,
Relay: r,
}
}
func (p *Version) decode(b []byte) error {
// Fixed fields have a total of 27 bytes. We substract this size
// with the total buffer length to know the length of the user agent.
lenUA := len(b) - 27
p.Version = binary.LittleEndian.Uint32(b[0:4])
p.Services = binary.LittleEndian.Uint64(b[4:12])
p.Timestamp = binary.LittleEndian.Uint32(b[12:16])
// FIXME: port's byteorder should be big endian according to the docs.
// but when connecting to the privnet docker image it's little endian.
p.Port = binary.LittleEndian.Uint16(b[16:18])
p.Nonce = binary.LittleEndian.Uint32(b[18:22])
p.UserAgent = b[22 : 22+lenUA]
curlen := 22 + lenUA
p.StartHeight = binary.LittleEndian.Uint32(b[curlen : curlen+4])
p.Relay = b[len(b)-1 : len(b)][0] == 1
return nil
}
func (p *Version) encode() ([]byte, error) {
// 27 bytes for the fixed size fields + the length of the user agent
// which is kinda variable, according to the docs.
buf := make([]byte, 27+len(p.UserAgent))
binary.LittleEndian.PutUint32(buf[0:4], p.Version)
binary.LittleEndian.PutUint64(buf[4:12], p.Services)
binary.LittleEndian.PutUint32(buf[12:16], p.Timestamp)
// FIXME: byte order (little / big)?
binary.LittleEndian.PutUint16(buf[16:18], p.Port)
binary.LittleEndian.PutUint32(buf[18:22], p.Nonce)
copy(buf[22:22+len(p.UserAgent)], p.UserAgent) //
curLen := 22 + len(p.UserAgent)
binary.LittleEndian.PutUint32(buf[curLen:curLen+4], p.StartHeight)
// yikes
var b []byte
if p.Relay {
b = []byte{1}
} else {
b = []byte{0}
}
copy(buf[curLen+4:len(buf)], b)
return buf, nil
}
// convert a command (string) to a byte slice filled with 0 bytes till
// size 12.
func cmdToByteSlice(cmd commandType) []byte {
cmdLen := len(cmd)
if cmdLen > 12 {
panic("exceeded command max length of size 12")
}
// The command can have max 12 bytes, rest is filled with 0.
b := []byte(cmd)
for i := 0; i < 12-cmdLen; i++ {
b = append(b, byte('\x00'))
}
return b
}
func sumSHA256(b []byte) []byte {
h := sha256.New()
h.Write(b)
return h.Sum(nil)
}
func compareChecksum(have uint32, b []byte) bool {
sum := sumSHA256(sumSHA256(b))[:4]
want := binary.LittleEndian.Uint32(sum)
return have == want
}