added example package

This commit is contained in:
2022-12-11 06:18:00 +00:00
parent fc9ed17ecd
commit 73b8a7c2a3
811 changed files with 276135 additions and 1 deletions

View File

@@ -0,0 +1,27 @@
/*
File Name: Command.go
Copyright: 2021 Peernet s.r.o.
Author: Peter Kleissner
*/
package protocol
// Commands between peers
const (
// Peer List Management
CommandAnnouncement = 0 // Announcement
CommandResponse = 1 // Response
CommandPing = 2 // Keep-alive message (no payload).
CommandPong = 3 // Response to ping (no payload).
CommandLocalDiscovery = 4 // Local discovery
CommandTraverse = 5 // Help establish a connection between 2 remote peers
// Blockchain
CommandGetBlock = 6 // Request blocks for specified peer.
// File Discovery
CommandTransfer = 8 // File transfer.
// Debug
CommandChat = 10 // Chat message [debug]
)

View File

@@ -0,0 +1,49 @@
/*
File Name: File Transfer.go
Copyright: 2021 Peernet s.r.o.
Author: Peter Kleissner
Encoding of file transfer protocol. Each transfer starts with a header:
Offset Size Info
0 8 Total File Size
8 8 Transfer Size
*/
package protocol
import (
"encoding/binary"
"errors"
"io"
)
// FileTransferWriteHeader starts writing the header for a file transfer.
func FileTransferWriteHeader(writer io.Writer, fileSize, transferSize uint64) (err error) {
// Send the header: Total File Size and Transfer Size.
header := make([]byte, 16)
binary.LittleEndian.PutUint64(header[0:8], fileSize)
binary.LittleEndian.PutUint64(header[8:16], transferSize)
if n, err := writer.Write(header); err != nil {
return err
} else if n != len(header) {
return errors.New("error sending header")
}
return nil
}
// FileTransferReadHeader starts reading the header for a file transfer. It will only read the header and keeps the connection open.
func FileTransferReadHeader(reader io.Reader) (fileSize, transferSize uint64, err error) {
// read the header
header := make([]byte, 16)
if n, err := reader.Read(header); err != nil {
return 0, 0, err
} else if n != len(header) {
return 0, 0, errors.New("error reading header")
}
fileSize = binary.LittleEndian.Uint64(header[0:8])
transferSize = binary.LittleEndian.Uint64(header[8:16])
return fileSize, transferSize, nil
}

View File

@@ -0,0 +1,27 @@
/*
File Name: Hash.go
Copyright: 2021 Peernet s.r.o.
Author: Peter Kleissner
*/
package protocol
import (
"github.com/PeernetOfficial/core/btcec"
"lukechampine.com/blake3"
)
// HashData abstracts the hash function.
func HashData(data []byte) (hash []byte) {
hash32 := blake3.Sum256(data)
return hash32[:]
}
// HashSize is blake3 hash digest size = 256 bits
const HashSize = 32
// PublicKey2NodeID translates the Public Key into the node ID used in the Kademlia network.
// It is also referenced in various other places including blockchain data at runtime. The node ID identifies the owner.
func PublicKey2NodeID(publicKey *btcec.PublicKey) (nodeID []byte) {
return HashData(publicKey.SerializeCompressed())
}

View File

@@ -0,0 +1,310 @@
/*
File Name: Message Encoding Announcement.go
Copyright: 2021 Peernet s.r.o.
Author: Peter Kleissner
*/
package protocol
import (
"encoding/binary"
"errors"
"unicode/utf8"
)
// MessageAnnouncement is the decoded announcement message.
type MessageAnnouncement struct {
*MessageRaw // Underlying raw message
Protocol uint8 // Protocol version supported (low 4 bits).
Features uint8 // Feature support
Actions uint8 // Action bit array. See ActionX
BlockchainHeight uint64 // Blockchain height
BlockchainVersion uint64 // Blockchain version
PortInternal uint16 // Internal port. Can be used to detect NATs.
PortExternal uint16 // External port if known. 0 if not. Can be used for UPnP support.
UserAgent string // User Agent. Format "Software/Version". Required in the initial announcement/bootstrap. UTF-8 encoded. Max length is 255 bytes.
FindPeerKeys []KeyHash // FIND_PEER data
FindDataKeys []KeyHash // FIND_VALUE data
InfoStoreFiles []InfoStore // INFO_STORE data
}
// KeyHash is a single blake3 key hash
type KeyHash struct {
Hash []byte
}
// InfoStore informs about files stored
type InfoStore struct {
ID KeyHash // Hash of the file
Size uint64 // Size of the file
Type uint8 // Type of the file: 0 = File, 1 = Header file containing list of parts
}
// Features are sent as bit array in the Announcement message.
const (
FeatureIPv4Listen = 0 // Sender listens on IPv4
FeatureIPv6Listen = 1 // Sender listens on IPv6
FeatureFirewall = 2 // Sender indicates a potential firewall. This informs uncontacted peers that a Traverse message might be required to establish a connection.
)
// Actions between peers, sent via Announcement message. They correspond to the bit array index.
const (
ActionFindSelf = 0 // FIND_SELF Request closest neighbors to self
ActionFindPeer = 1 // FIND_PEER Request closest neighbors to target peer
ActionFindValue = 2 // FIND_VALUE Request data or closest peers
ActionInfoStore = 3 // INFO_STORE Sender indicates storing provided data
)
// Minimum length of Announcement payload header without User Agent
const announcementPayloadHeaderSize = 24
// DecodeAnnouncement decodes the incoming announcement message. Returns nil if invalid.
func DecodeAnnouncement(msg *MessageRaw) (result *MessageAnnouncement, err error) {
result = &MessageAnnouncement{
MessageRaw: msg,
}
if len(msg.Payload) < announcementPayloadHeaderSize {
return nil, errors.New("announcement: invalid minimum length")
}
result.Protocol = msg.Payload[0] & 0x0F // Protocol version support is stored in the first 4 bits
result.Features = msg.Payload[1] // Feature support
result.Actions = msg.Payload[2]
result.BlockchainHeight = binary.LittleEndian.Uint64(msg.Payload[3 : 3+8])
result.BlockchainVersion = binary.LittleEndian.Uint64(msg.Payload[11 : 11+8])
result.PortInternal = binary.LittleEndian.Uint16(msg.Payload[19 : 19+2])
result.PortExternal = binary.LittleEndian.Uint16(msg.Payload[21 : 21+2])
userAgentLength := int(msg.Payload[23])
if userAgentLength > 0 {
if userAgentLength > len(msg.Payload)-announcementPayloadHeaderSize {
return nil, errors.New("announcement: user agent overflow")
}
userAgentB := msg.Payload[announcementPayloadHeaderSize : announcementPayloadHeaderSize+userAgentLength]
if !utf8.Valid(userAgentB) {
return nil, errors.New("announcement: user agent invalid encoding")
}
result.UserAgent = string(userAgentB)
}
data := msg.Payload[announcementPayloadHeaderSize+userAgentLength:]
// FIND_PEER
if result.Actions&(1<<ActionFindPeer) > 0 {
keys, read, valid := decodeKeys(data)
if !valid {
return nil, errors.New("announcement: FIND_PEER invalid data")
}
data = data[read:]
result.FindPeerKeys = keys
}
// FIND_VALUE
if result.Actions&(1<<ActionFindValue) > 0 {
keys, read, valid := decodeKeys(data)
if !valid {
return nil, errors.New("announcement: FIND_VALUE invalid data")
}
data = data[read:]
result.FindDataKeys = keys
}
// INFO_STORE
if result.Actions&(1<<ActionInfoStore) > 0 {
files, _, valid := decodeInfoStore(data)
if !valid {
return nil, errors.New("announcement: INFO_STORE invalid data")
}
// commented out because never used
//data = data[read:]
result.InfoStoreFiles = files
}
// Accept extra data in case future features append additional data
//if len(data) > 0 {
// return nil, errors.New("announcement: Unexpected extra data")
//}
return
}
// decodeKeys decodes keys. Header is 2 bytes (count) followed by the actual keys (each 32 bytes blake3 hash).
func decodeKeys(data []byte) (keys []KeyHash, read int, valid bool) {
if len(data) < 2+HashSize { // minimum length
return nil, 0, false
}
count := binary.LittleEndian.Uint16(data[0:2])
if read = 2 + int(count)*HashSize; len(data) < read {
return nil, 0, false
}
for n := 0; n < int(count); n++ {
key := make([]byte, HashSize)
copy(key, data[2+n*HashSize:2+n*HashSize+HashSize])
keys = append(keys, KeyHash{Hash: key})
}
return keys, read, true
}
func decodeInfoStore(data []byte) (files []InfoStore, read int, valid bool) {
if len(data) < 2+41 { // minimum length
return nil, 0, false
}
count := binary.LittleEndian.Uint16(data[0:2])
if read = 2 + int(count)*41; len(data) < read {
return nil, 0, false
}
for n := 0; n < int(count); n++ {
file := InfoStore{}
file.ID.Hash = make([]byte, HashSize)
copy(file.ID.Hash, data[2+n*41:2+n*41+HashSize])
file.Size = binary.LittleEndian.Uint64(data[2+n*41+32 : 2+n*41+32+8])
file.Type = data[2+n*41+40]
files = append(files, file)
}
return files, read, true
}
// EncodeAnnouncement encodes an announcement message. It may return multiple messages if the input does not fit into one.
// findPeer is a list of node IDs (blake3 hash of peer ID compressed form)
// findValue is a list of hashes
// files is a list of files stored to inform about
func EncodeAnnouncement(sendUA, findSelf bool, findPeer []KeyHash, findValue []KeyHash, files []InfoStore, features byte, blockchainHeight, blockchainVersion uint64, userAgent string) (packetsRaw [][]byte) {
createPacketLoop:
for {
raw := make([]byte, 64*1024) // max UDP packet size
packetSize := announcementPayloadHeaderSize
raw[0] = byte(ProtocolVersion) // Protocol
raw[1] = features // Feature support
//raw[2] = Actions // Action bit array
binary.LittleEndian.PutUint64(raw[3:3+8], blockchainHeight)
binary.LittleEndian.PutUint64(raw[11:11+8], blockchainVersion)
// only on initial announcement the User Agent must be provided according to the protocol spec
if sendUA {
userAgentB := []byte(userAgent)
if len(userAgentB) > 255 {
userAgentB = userAgentB[:255]
}
raw[23] = byte(len(userAgentB))
copy(raw[announcementPayloadHeaderSize:announcementPayloadHeaderSize+len(userAgentB)], userAgentB)
packetSize += len(userAgentB)
}
// FIND_SELF
if findSelf {
raw[2] |= 1 << ActionFindSelf
}
// FIND_PEER
if len(findPeer) > 0 {
// check if there is enough space for at least the header and 1 record
if isPacketSizeExceed(packetSize, 2+32) {
packetsRaw = append(packetsRaw, raw[:packetSize])
continue createPacketLoop
}
raw[2] |= 1 << ActionFindPeer
index := packetSize
packetSize += 2
for n, find := range findPeer {
// check if minimum length is available in packet
if isPacketSizeExceed(packetSize, 32) {
packetsRaw = append(packetsRaw, raw[:packetSize])
findPeer = findPeer[n:]
continue createPacketLoop
}
binary.LittleEndian.PutUint16(raw[index:index+2], uint16(n+1))
copy(raw[index+2+32*n:index+2+32*n+32], find.Hash)
packetSize += 32
}
findPeer = nil
}
// FIND_VALUE
if len(findValue) > 0 {
// check if there is enough space for at least the header and 1 record
if isPacketSizeExceed(packetSize, 2+32) {
packetsRaw = append(packetsRaw, raw[:packetSize])
continue createPacketLoop
}
raw[2] |= 1 << ActionFindValue
index := packetSize
packetSize += 2
for n, find := range findValue {
// check if minimum length is available in packet
if isPacketSizeExceed(packetSize, 32) {
packetsRaw = append(packetsRaw, raw[:packetSize])
findValue = findValue[n:]
continue createPacketLoop
}
binary.LittleEndian.PutUint16(raw[index:index+2], uint16(n+1))
copy(raw[index+2+32*n:index+2+32*n+32], find.Hash)
packetSize += 32
}
findValue = nil
}
// INFO_STORE
if len(files) > 0 {
// check if there is enough space for at least the header and 1 record
if isPacketSizeExceed(packetSize, 2+41) {
packetsRaw = append(packetsRaw, raw[:packetSize])
continue createPacketLoop
}
raw[2] |= 1 << ActionInfoStore
index := packetSize
packetSize += 2
for n, file := range files {
// check if minimum length is available in packet
if isPacketSizeExceed(packetSize, 41) {
packetsRaw = append(packetsRaw, raw[:packetSize])
files = files[n:]
continue createPacketLoop
}
binary.LittleEndian.PutUint16(raw[index:index+2], uint16(n+1))
copy(raw[index+2+41*n:index+2+41*n+32], file.ID.Hash)
binary.LittleEndian.PutUint64(raw[index+2+41*n+32:index+2+41*n+32+8], file.Size)
raw[index+2+41*n+40] = file.Type
packetSize += 41
}
files = nil
}
packetsRaw = append(packetsRaw, raw[:packetSize])
if len(findPeer) == 0 && len(findValue) == 0 && len(files) == 0 {
return
}
}
}

View File

@@ -0,0 +1,230 @@
/*
File Name: Message Encoding Get Block.go
Copyright: 2021 Peernet s.r.o.
Author: Peter Kleissner
Get Block message encoding:
Offset Size Info
0 1 Control
1 33 Peer ID compressed form identifying which blockchain to transfer
Control = 0: Request Blocks
34 8 Limit total count of blocks to transfer. The transfer will be terminated if the limit is reached.
42 8 Limit of bytes per block to transfer max. Blocks exceeding this limit will not be transferred.
50 16 Transfer ID. This will identify lite packets.
66 2 Count of block ranges
68 16 * ? List of block ranges
Block range:
0 8 Block number
8 8 Count of blocks
Control = 3: Active
34 ? Embedded block data as stream.
For the block stream there is a header preceding each block:
Offset Size Info
0 1 Availability
0 = Block range is available.
1 = Block range not available.
2 = Block range exceeds size limit.
1 16 Block range
17 8 Block size
The limit in block range must be 1 if a block is returned.
*/
package protocol
import (
"encoding/binary"
"errors"
"io"
"github.com/PeernetOfficial/core/btcec"
"github.com/google/uuid"
)
const (
GetBlockControlRequestStart = 0 // Request start transfer of blocks
GetBlockControlNotAvailable = 1 // Requested blockchain not available (not found)
GetBlockControlActive = 2 // Active block transfer
GetBlockControlTerminate = 3 // Terminate
GetBlockControlEmpty = 4 // Requested blockchain has 0 blocks
)
const (
GetBlockStatusAvailable = 0
GetBlockStatusNotAvailable = 1
GetBlockStatusSizeExceed = 2
)
// Min size of header for Get Block control 0 message.
const getBlockRequestHeaderSize = 68
// MessageGetBlock is the decoded Get Block message.
type MessageGetBlock struct {
*MessageRaw // Underlying raw message.
Control uint8 // Control. See TransferControlX.
BlockchainPublicKey *btcec.PublicKey // Peer ID of blockchain to transfer.
// fields valid only for GetBlockControlRequestStart
TransferID uuid.UUID // Transfer ID to identify lite packets.
LimitBlockCount uint64 // Limit total count of blocks to transfer
MaxBlockSize uint64 // Limit of bytes per block to transfer max. Blocks exceeding this limit will not be transferred.
TargetBlocks []BlockRange // Target list of block ranges to transfer.
// fields valid only for GetBlockControlActive
Data []byte // Embedded protocol data.
}
// BlockRange is a single start-count range.
type BlockRange struct {
Offset uint64 // Block number start
Limit uint64 // Count of blocks
}
// DecodeGetBlock decodes a Get Block message
func DecodeGetBlock(msg *MessageRaw) (result *MessageGetBlock, err error) {
if len(msg.Payload) < 34 {
return nil, errors.New("get block: invalid minimum length")
}
result = &MessageGetBlock{
MessageRaw: msg,
}
result.Control = msg.Payload[0]
peerIDcompressed := msg.Payload[1:34]
if result.BlockchainPublicKey, err = btcec.ParsePubKey(peerIDcompressed, btcec.S256()); err != nil {
return nil, err
}
if result.Control == GetBlockControlRequestStart {
if len(msg.Payload) < getBlockRequestHeaderSize {
return nil, errors.New("get block: invalid minimum length")
}
result.LimitBlockCount = binary.LittleEndian.Uint64(msg.Payload[34 : 34+8])
result.MaxBlockSize = binary.LittleEndian.Uint64(msg.Payload[42 : 42+8])
copy(result.TransferID[:], msg.Payload[50:50+16])
countBlockRanges := int(binary.LittleEndian.Uint16(msg.Payload[66 : 66+2]))
if countBlockRanges == 0 {
return nil, errors.New("get block: empty block range")
} else if len(msg.Payload) < getBlockRequestHeaderSize+16*countBlockRanges {
return nil, errors.New("get block: cound block ranges exceeds length")
}
index := getBlockRequestHeaderSize
for n := 0; n < countBlockRanges; n++ {
var target BlockRange
target.Offset = binary.LittleEndian.Uint64(msg.Payload[index : index+8])
target.Limit = binary.LittleEndian.Uint64(msg.Payload[index+8 : index+16])
result.TargetBlocks = append(result.TargetBlocks, target)
index += 16
}
} else if result.Control == GetBlockControlActive {
result.Data = msg.Payload[34:]
}
return result, nil
}
// EncodeGetBlock encodes a Get Block message. The embedded packet size must be smaller than TransferMaxEmbedSize.
func EncodeGetBlock(senderPrivateKey *btcec.PrivateKey, data []byte, control uint8, blockchainPublicKey *btcec.PublicKey, limitBlockCount, maxBlockSize uint64, targetBlocks []BlockRange, transferID uuid.UUID) (packetRaw []byte, err error) {
if control == GetBlockControlRequestStart && len(data) != 0 {
return nil, errors.New("get block encode: payload not allowed in start")
} else if isPacketSizeExceed(transferPayloadHeaderSize, len(data)) {
return nil, errors.New("get block encode: embedded packet too big")
} else if control == GetBlockControlRequestStart && isPacketSizeExceed(getBlockRequestHeaderSize, len(targetBlocks)*16) {
return nil, errors.New("get block encode: too many target block ranges")
}
packetSize := transferPayloadHeaderSize
if control == GetBlockControlRequestStart {
packetSize = getBlockRequestHeaderSize + len(targetBlocks)*16
} else if control == GetBlockControlActive {
packetSize += len(data)
}
raw := make([]byte, packetSize)
raw[0] = control
targetPeerID := blockchainPublicKey.SerializeCompressed()
copy(raw[1:34], targetPeerID)
if control == GetBlockControlRequestStart {
binary.LittleEndian.PutUint64(raw[34:34+8], limitBlockCount)
binary.LittleEndian.PutUint64(raw[42:42+8], maxBlockSize)
copy(raw[50:50+16], transferID[:])
binary.LittleEndian.PutUint16(raw[66:66+2], uint16(len(targetBlocks)))
index := getBlockRequestHeaderSize
for _, target := range targetBlocks {
binary.LittleEndian.PutUint64(raw[index:index+8], target.Offset)
binary.LittleEndian.PutUint64(raw[index+8:index+16], target.Limit)
index += 16
}
} else if control == GetBlockControlActive {
copy(raw[34:34+len(data)], data)
}
return raw, nil
}
// IsLast checks if the incoming message is the last one in this transfer.
func (msg *MessageGetBlock) IsLast() bool {
return msg.Control == GetBlockControlTerminate || msg.Control == GetBlockControlNotAvailable || msg.Control == GetBlockControlEmpty
}
// BlockTransferWriteHeader starts writing the header for a block transfer.
func BlockTransferWriteHeader(writer io.Writer, availability uint8, targetBlock BlockRange, blockSize uint64) (err error) {
header := make([]byte, 25)
header[0] = availability
binary.LittleEndian.PutUint64(header[1:9], targetBlock.Offset)
binary.LittleEndian.PutUint64(header[9:17], targetBlock.Limit)
binary.LittleEndian.PutUint64(header[17:25], blockSize)
_, err = writer.Write(header)
return err
}
// BlockTransferReadBlock reads the header and the block from the reader
func BlockTransferReadBlock(reader io.Reader, maxBlockSize uint64) (data []byte, targetBlock BlockRange, blockSize uint64, availability uint8, err error) {
header := make([]byte, 25)
if _, err := io.ReadAtLeast(reader, header, len(header)); err != nil {
return nil, targetBlock, 0, 0, err
}
availability = header[0]
targetBlock.Offset = binary.LittleEndian.Uint64(header[1:9])
targetBlock.Limit = binary.LittleEndian.Uint64(header[9:17])
blockSize = binary.LittleEndian.Uint64(header[17:25])
if targetBlock.Limit == 0 {
return nil, targetBlock, blockSize, availability, errors.New("empty target block limit")
} else if availability != GetBlockStatusAvailable { // return if status indicates the block is not available
return nil, targetBlock, blockSize, availability, nil
}
if blockSize > maxBlockSize {
return nil, targetBlock, blockSize, availability, errors.New("remote block size exceeds limit")
} else if targetBlock.Limit != 1 {
return nil, targetBlock, blockSize, availability, errors.New("invalid target block limit")
}
// read the block
block := make([]byte, blockSize)
if _, err := io.ReadAtLeast(reader, block, len(block)); err != nil {
return nil, targetBlock, blockSize, availability, err
}
return block, targetBlock, blockSize, availability, nil
}

View File

@@ -0,0 +1,445 @@
/*
File Name: Message Encoding Response.go
Copyright: 2021 Peernet s.r.o.
Author: Peter Kleissner
*/
package protocol
import (
"bytes"
"encoding/binary"
"errors"
"net"
"time"
"unicode/utf8"
"github.com/PeernetOfficial/core/btcec"
)
// MessageResponse is the decoded response message.
type MessageResponse struct {
*MessageRaw // Underlying raw message
Protocol uint8 // Protocol version supported (low 4 bits).
Features uint8 // Feature support (high 4 bits). Future use.
Actions uint8 // Action bit array. See ActionX
BlockchainHeight uint64 // Blockchain height
BlockchainVersion uint64 // Blockchain version
PortInternal uint16 // Internal port. Can be used to detect NATs.
PortExternal uint16 // External port if known. 0 if not. Can be used for UPnP support.
UserAgent string // User Agent. Format "Software/Version". Required in the initial announcement/bootstrap. UTF-8 encoded. Max length is 255 bytes.
Hash2Peers []Hash2Peer // List of peers that know the requested hashes or at least are close to it
FilesEmbed []EmbeddedFileData // Files that were embedded in the response
HashesNotFound [][]byte // Hashes that were reported back as not found
}
// PeerRecord informs about a peer
type PeerRecord struct {
PublicKey *btcec.PublicKey // Public Key
NodeID []byte // Kademlia Node ID
IPv4 net.IP // IPv4 address. 0 if not set.
IPv4Port uint16 // Port (actual one used for connection)
IPv4PortReportedInternal uint16 // Internal port as reported by that peer. This can be used to identify whether the peer is potentially behind a NAT.
IPv4PortReportedExternal uint16 // External port as reported by that peer. This is used in case of port forwarding (manual or automated).
IPv6 net.IP // IPv6 address. 0 if not set.
IPv6Port uint16 // Port (actual one used for connection)
IPv6PortReportedInternal uint16 // Internal port as reported by that peer. This can be used to identify whether the peer is potentially behind a NAT.
IPv6PortReportedExternal uint16 // External port as reported by that peer. This is used in case of port forwarding (manual or automated).
LastContact uint32 // Last contact in seconds
LastContactT time.Time // Last contact time translated from seconds
Features uint8 // Feature support. Same as in Announcement/Response message.
}
// Hash2Peer links a hash to peers who are known to store the data and to peers who are considered close to the hash
type Hash2Peer struct {
ID KeyHash // Hash that was queried
Closest []PeerRecord // Closest peers
Storing []PeerRecord // Peers known to store the data identified by the hash
IsLast bool // Whether it is the last records returned for the requested hash and no more results will follow
}
// EmbeddedFileData contains embedded data sent within a response
type EmbeddedFileData struct {
ID KeyHash // Hash of the file
Data []byte // Data
}
// Actions in Response message
const (
ActionSequenceLast = 0 // SEQUENCE_LAST Last response to the announcement in the sequence
)
// DecodeResponse decodes the incoming response message. Returns nil if invalid.
func DecodeResponse(msg *MessageRaw) (result *MessageResponse, err error) {
result = &MessageResponse{
MessageRaw: msg,
}
if len(msg.Payload) < announcementPayloadHeaderSize+6 {
return nil, errors.New("response: invalid minimum length")
}
result.Protocol = msg.Payload[0] & 0x0F // Protocol version support is stored in the first 4 bits
result.Features = msg.Payload[1] // Feature support
result.Actions = msg.Payload[2]
result.BlockchainHeight = binary.LittleEndian.Uint64(msg.Payload[3 : 3+8])
result.BlockchainVersion = binary.LittleEndian.Uint64(msg.Payload[11 : 11+8])
result.PortInternal = binary.LittleEndian.Uint16(msg.Payload[19 : 19+2])
result.PortExternal = binary.LittleEndian.Uint16(msg.Payload[21 : 21+2])
userAgentLength := int(msg.Payload[23])
read := announcementPayloadHeaderSize
if userAgentLength > 0 {
if userAgentLength > len(msg.Payload)-announcementPayloadHeaderSize {
return nil, errors.New("response: user agent overflow")
}
userAgentB := msg.Payload[announcementPayloadHeaderSize : announcementPayloadHeaderSize+userAgentLength]
if !utf8.Valid(userAgentB) {
return nil, errors.New("response: user agent invalid encoding")
}
result.UserAgent = string(userAgentB)
read += userAgentLength
}
countPeerResponses := binary.LittleEndian.Uint16(msg.Payload[read+0 : read+0+2])
countEmbeddedFiles := binary.LittleEndian.Uint16(msg.Payload[read+2 : read+2+2])
countHashesNotFound := binary.LittleEndian.Uint16(msg.Payload[read+4 : read+4+2])
read += 6
if countPeerResponses == 0 && countEmbeddedFiles == 0 && countHashesNotFound == 0 {
// Empty responses are allowed. They can be useful as quasi-pings to get the latest blockchain info of the peer.
return
}
data := msg.Payload[read:]
// Peer response data
if countPeerResponses > 0 {
hash2Peers, read, valid := decodePeerRecord(data, int(countPeerResponses))
if !valid {
return nil, errors.New("response: peer info invalid data")
}
data = data[read:]
result.Hash2Peers = append(result.Hash2Peers, hash2Peers...)
}
// Embedded files
if countEmbeddedFiles > 0 {
filesEmbed, read, valid := decodeEmbeddedFile(data, int(countEmbeddedFiles))
if !valid {
return nil, errors.New("response: embedded file invalid data")
}
data = data[read:]
result.FilesEmbed = append(result.FilesEmbed, filesEmbed...)
}
// Hashes not found
if countHashesNotFound > 0 {
if len(data) < int(countHashesNotFound)*32 {
return nil, errors.New("response: hash list invalid data")
}
for n := 0; n < int(countHashesNotFound); n++ {
hash := make([]byte, HashSize)
copy(hash, data[n*32:n*32+32])
result.HashesNotFound = append(result.HashesNotFound, hash)
}
}
return
}
// Length of peer record in bytes
const peerRecordSize = 70
// decodePeerRecord decodes the response data for FIND_SELF, FIND_PEER and FIND_VALUE messages
func decodePeerRecord(data []byte, count int) (hash2Peers []Hash2Peer, read int, valid bool) {
index := 0
for n := 0; n < count; n++ {
if read += 34; len(data) < read {
return nil, 0, false
}
hash := make([]byte, HashSize)
copy(hash, data[index:index+32])
countField := binary.LittleEndian.Uint16(data[index+32:index+32+2]) & 0x7FFF
isLast := binary.LittleEndian.Uint16(data[index+32:index+32+2])&0x8000 > 0
index += 34
hash2Peer := Hash2Peer{ID: KeyHash{hash}, IsLast: isLast}
// Response contains peer records
for m := 0; m < int(countField); m++ {
if read += peerRecordSize; len(data) < read {
return nil, 0, false
}
peer := PeerRecord{}
peerIDcompressed := make([]byte, 33)
copy(peerIDcompressed[:], data[index:index+33])
// IPv4
ipv4B := make([]byte, 4)
copy(ipv4B[:], data[index+33:index+33+4])
peer.IPv4 = ipv4B
peer.IPv4Port = binary.LittleEndian.Uint16(data[index+37 : index+37+2])
peer.IPv4PortReportedInternal = binary.LittleEndian.Uint16(data[index+39 : index+39+2])
peer.IPv4PortReportedExternal = binary.LittleEndian.Uint16(data[index+41 : index+41+2])
// IPv6
ipv6B := make([]byte, 16)
copy(ipv6B[:], data[index+43:index+43+16])
peer.IPv6 = ipv6B
peer.IPv6Port = binary.LittleEndian.Uint16(data[index+59 : index+59+2])
peer.IPv6PortReportedInternal = binary.LittleEndian.Uint16(data[index+61 : index+61+2])
peer.IPv6PortReportedExternal = binary.LittleEndian.Uint16(data[index+63 : index+63+2])
if peer.IPv6.To4() != nil { // IPv6 address mismatch
return nil, 0, false
}
peer.LastContact = binary.LittleEndian.Uint32(data[index+65 : index+65+4])
peer.LastContactT = time.Now().Add(-time.Second * time.Duration(peer.LastContact))
peer.Features = data[index+69] & 0x7F
reason := data[index+69] >> 7
var err error
if peer.PublicKey, err = btcec.ParsePubKey(peerIDcompressed, btcec.S256()); err != nil {
return nil, 0, false
}
peer.NodeID = PublicKey2NodeID(peer.PublicKey)
if reason == 0 { // Peer was returned because it is close to the requested hash
hash2Peer.Closest = append(hash2Peer.Closest, peer)
} else if reason == 1 { // Peer stores the data
hash2Peer.Storing = append(hash2Peer.Storing, peer)
}
index += peerRecordSize
}
hash2Peers = append(hash2Peers, hash2Peer)
}
return hash2Peers, read, true
}
// decodeEmbeddedFile decodes the embedded file response data for FIND_VALUE
func decodeEmbeddedFile(data []byte, count int) (filesEmbed []EmbeddedFileData, read int, valid bool) {
index := 0
for n := 0; n < count; n++ {
if read += 34; len(data) < read {
return nil, 0, false
}
hash := make([]byte, HashSize)
copy(hash, data[index:index+32])
sizeField := int(binary.LittleEndian.Uint16(data[index+32 : index+32+2]))
index += 34
if read += sizeField; len(data) < read {
return nil, 0, false
}
fileData := make([]byte, sizeField)
copy(fileData[:], data[index:index+sizeField])
index += sizeField
// validate the hash
if !bytes.Equal(hash, HashData(fileData)) {
return nil, read, false
}
filesEmbed = append(filesEmbed, EmbeddedFileData{ID: KeyHash{Hash: hash}, Data: fileData})
}
return filesEmbed, read, true
}
// EmbeddedFileSizeMax is the maximum size of embedded files in response messages. Any file exceeding that must be shared via regular file transfer.
const EmbeddedFileSizeMax = udpMaxPacketSize - PacketLengthMin - announcementPayloadHeaderSize - 2 - 35
// EncodeResponse encodes a response message
// hash2Peers will be modified.
func EncodeResponse(sendUA bool, hash2Peers []Hash2Peer, filesEmbed []EmbeddedFileData, hashesNotFound [][]byte, features byte, blockchainHeight, blockchainVersion uint64, userAgent string) (packetsRaw [][]byte, err error) {
for n := range filesEmbed {
if len(filesEmbed[n].Data) > EmbeddedFileSizeMax {
return nil, errors.New("embedded file too big")
}
}
createPacketLoop:
for {
raw := make([]byte, 64*1024) // max UDP packet size
packetSize := announcementPayloadHeaderSize
raw[0] = byte(ProtocolVersion) // Protocol
raw[1] = features // Feature support
//raw[2] = Actions // Action bit array
binary.LittleEndian.PutUint64(raw[3:3+8], blockchainHeight)
binary.LittleEndian.PutUint64(raw[11:11+8], blockchainVersion)
// only on initial response the User Agent must be provided according to the protocol spec
if sendUA {
userAgentB := []byte(userAgent)
if len(userAgentB) > 255 {
userAgentB = userAgentB[:255]
}
raw[23] = byte(len(userAgentB))
copy(raw[announcementPayloadHeaderSize:announcementPayloadHeaderSize+len(userAgentB)], userAgentB)
packetSize += len(userAgentB)
}
// 3 count field at raw[index]: count of peer responses, embedded files, and hashes not found
countIndex := packetSize
packetSize += 6
// Encode the peer response data for FIND_SELF, FIND_PEER and FIND_VALUE requests.
if len(hash2Peers) > 0 {
for n, hash2Peer := range hash2Peers {
if isPacketSizeExceed(packetSize, 34+peerRecordSize) { // check if minimum length is available in packet
packetsRaw = append(packetsRaw, raw[:packetSize])
hash2Peers = hash2Peers[n:]
continue createPacketLoop
}
index := packetSize
copy(raw[index:index+32], hash2Peer.ID.Hash)
count2Index := index + 32
packetSize += 34
count2 := uint16(0)
for m := range hash2Peer.Storing {
if isPacketSizeExceed(packetSize, peerRecordSize) { // check if minimum length is available in packet
packetsRaw = append(packetsRaw, raw[:packetSize])
hash2Peers = hash2Peers[n:]
hash2Peer.Storing = hash2Peer.Storing[m:]
continue createPacketLoop
}
index := packetSize
encodePeerRecord(raw[index:index+peerRecordSize], &hash2Peer.Storing[m], 1)
packetSize += peerRecordSize
binary.LittleEndian.PutUint16(raw[count2Index+0:count2Index+2], uint16(m+1))
count2++
}
hash2Peer.Storing = nil
for m := range hash2Peer.Closest {
if isPacketSizeExceed(packetSize, peerRecordSize) { // check if minimum length is available in packet
packetsRaw = append(packetsRaw, raw[:packetSize])
hash2Peers = hash2Peers[n:]
hash2Peer.Closest = hash2Peer.Closest[m:]
continue createPacketLoop
}
index := packetSize
encodePeerRecord(raw[index:index+peerRecordSize], &hash2Peer.Closest[m], 0)
packetSize += peerRecordSize
count2++
binary.LittleEndian.PutUint16(raw[count2Index+0:count2Index+2], count2)
}
binary.LittleEndian.PutUint16(raw[count2Index+0:count2Index+2], count2|0x8000) // signal the last result for the key with bit 15
binary.LittleEndian.PutUint16(raw[countIndex+0:countIndex+0+2], uint16(n+1)) // count of peer responses
}
hash2Peers = nil
}
// FIND_VALUE response embedded data
if len(filesEmbed) > 0 {
if isPacketSizeExceed(packetSize, 34+len(filesEmbed[0].Data)) { // check if there is enough space for at least the header and 1 record
packetsRaw = append(packetsRaw, raw[:packetSize])
continue createPacketLoop
}
for n, file := range filesEmbed {
if isPacketSizeExceed(packetSize, 34+len(file.Data)) { // check if minimum length is available in packet
packetsRaw = append(packetsRaw, raw[:packetSize])
filesEmbed = filesEmbed[n:]
continue createPacketLoop
}
index := packetSize
copy(raw[index:index+32], file.ID.Hash)
binary.LittleEndian.PutUint16(raw[index+32:index+32+2], uint16(len(file.Data)))
copy(raw[index+34:index+34+len(file.Data)], file.Data)
binary.LittleEndian.PutUint16(raw[countIndex+2:countIndex+2+2], uint16(n+1)) // count of embedded files
packetSize += 34 + len(file.Data)
}
filesEmbed = nil
}
// Hashes not found
if len(hashesNotFound) > 0 {
index := packetSize
for n, hash := range hashesNotFound {
if isPacketSizeExceed(packetSize, 32) { // check if there is enough space for at least the header and 1 record
packetsRaw = append(packetsRaw, raw[:packetSize])
continue createPacketLoop
}
copy(raw[index+n*32:index+n*32+32], hash)
binary.LittleEndian.PutUint16(raw[countIndex+4:countIndex+4+2], uint16(n+1)) // count of hashes not found
packetSize += 32
}
hashesNotFound = nil
}
raw[2] |= 1 << ActionSequenceLast // Indicate that no more responses will be sent in this sequence
packetsRaw = append(packetsRaw, raw[:packetSize])
if len(hash2Peers) == 0 && len(filesEmbed) == 0 && len(hashesNotFound) == 0 { // this should always be the case here
return
}
}
}
// encodePeerRecord encodes a single peer record and stores it into raw
func encodePeerRecord(raw []byte, peer *PeerRecord, reason uint8) {
copy(raw[0:0+33], peer.PublicKey.SerializeCompressed())
binary.LittleEndian.PutUint32(raw[65:65+4], peer.LastContact)
raw[69] = peer.Features | reason<<7
// IPv4
copy(raw[33:33+4], peer.IPv4.To4())
binary.LittleEndian.PutUint16(raw[37:37+2], peer.IPv4Port)
binary.LittleEndian.PutUint16(raw[39:39+2], peer.IPv4PortReportedInternal)
binary.LittleEndian.PutUint16(raw[41:41+2], peer.IPv4PortReportedExternal)
// IPv6
copy(raw[43:43+16], peer.IPv6.To16())
binary.LittleEndian.PutUint16(raw[59:59+2], peer.IPv6Port)
binary.LittleEndian.PutUint16(raw[61:61+2], peer.IPv6PortReportedInternal)
binary.LittleEndian.PutUint16(raw[63:63+2], peer.IPv6PortReportedExternal)
}
// IsLast checks if the incoming message is the last expected response in this sequence.
func (msg *MessageResponse) IsLast() bool {
return msg.Actions&(1<<ActionSequenceLast) > 0
}

View File

@@ -0,0 +1,136 @@
/*
File Name: Message Encoding Transfer.go
Copyright: 2021 Peernet s.r.o.
Author: Peter Kleissner
Transfer message encoding:
Offset Size Info
0 1 Control
1 1 Transfer Protocol
2 32 File Hash
Control = 0: Request Start
34 8 Offset to start reading in the file
42 8 Limit of bytes to read at the offset
50 16 Transfer ID. This will identify lite packets.
Offset + limit must not exceed the file size. Actual data transfer should be sent via lite packets.
The regular Peernet packets would be too CPU expensive and slow due to public key signing.
*/
package protocol
import (
"encoding/binary"
"errors"
"github.com/PeernetOfficial/core/btcec"
"github.com/google/uuid"
)
// MessageTransfer is the decoded transfer message.
// It is sent to initiate a file transfer, and to send data as part of a file transfer. The actual file data is encapsulated via UDT.
type MessageTransfer struct {
*MessageRaw // Underlying raw message.
Control uint8 // Control. See TransferControlX.
TransferProtocol uint8 // Embedded transfer protocol: 0 = UDT
Hash []byte // Hash of the file to transfer.
Offset uint64 // Offset to start reading at. Only TransferControlRequestStart.
Limit uint64 // Limit (count of bytes) to read starting at the offset. Only TransferControlRequestStart.
TransferID uuid.UUID // Transfer ID to identify lite packets.
Data []byte // Embedded protocol data. Only TransferControlActive.
}
const (
TransferControlRequestStart = 0 // Request start transfer of file. Data at byte 34 is offset and limit to read, each 8 bytes. Limit may be 0 to indicate entire file.
TransferControlNotAvailable = 1 // Requested file not available
TransferControlActive = 2 // Active file transfer
TransferControlTerminate = 3 // Terminate
)
const (
TransferProtocolUDT = 0 // UDT via lite packets. No encryption.
)
const transferPayloadHeaderSize = 34
// DecodeTransfer decodes a transfer message
func DecodeTransfer(msg *MessageRaw) (result *MessageTransfer, err error) {
if len(msg.Payload) < transferPayloadHeaderSize {
return nil, errors.New("transfer: invalid minimum length")
}
result = &MessageTransfer{
MessageRaw: msg,
Hash: make([]byte, HashSize),
}
result.Control = msg.Payload[0]
result.TransferProtocol = msg.Payload[1]
copy(result.Hash, msg.Payload[2:2+HashSize])
switch result.Control {
case TransferControlRequestStart:
// Offset and Limit must be provided after the header.
if len(msg.Payload) < transferPayloadHeaderSize+16 {
return nil, errors.New("transfer: invalid minimum length")
}
result.Offset = binary.LittleEndian.Uint64(msg.Payload[34 : 34+8])
result.Limit = binary.LittleEndian.Uint64(msg.Payload[42 : 42+8])
copy(result.TransferID[:], msg.Payload[50:50+16])
case TransferControlActive:
// Data should be transferred via lite packets for performance reasons, but it is allowed to be encapsulated in Peernet packets.
result.Data = msg.Payload[transferPayloadHeaderSize:]
}
return result, nil
}
// TransferMaxEmbedSize is a recommended default upper size of embedded data inside the Transfer message, to be used as MaxPacketSize limit in the embedded protocol.
// This value is chosen as the lowest denominator of different environments (IPv4, IPv6, Ethernet, Internet) for safe transfer, not for highest performance.
// The caller may send bigger payloads but may risk that data packets are simply dropped and never arrive. A MTU negotiation or detection could pimp that.
const TransferMaxEmbedSize = internetSafeMTU - PacketLengthMin - transferPayloadHeaderSize
// Same as TransferMaxEmbedSize but for encoding via lite packets.
const TransferMaxEmbedSizeLite = internetSafeMTU - PacketLiteSizeMin
// EncodeTransfer encodes a transfer message. The embedded packet size must be smaller than TransferMaxEmbedSize.
func EncodeTransfer(senderPrivateKey *btcec.PrivateKey, data []byte, control, transferProtocol uint8, hash []byte, offset, limit uint64, transferID uuid.UUID) (packetRaw []byte, err error) {
if control == TransferControlRequestStart && len(data) != 0 {
return nil, errors.New("transfer encode: payload not allowed in start")
} else if isPacketSizeExceed(transferPayloadHeaderSize, len(data)) {
return nil, errors.New("transfer encode: embedded packet too big")
}
packetSize := transferPayloadHeaderSize
if control == TransferControlRequestStart {
packetSize += 32
} else if control == TransferControlActive {
packetSize += len(data)
}
raw := make([]byte, packetSize)
raw[0] = control
raw[1] = transferProtocol
copy(raw[2:2+HashSize], hash)
if control == TransferControlRequestStart {
binary.LittleEndian.PutUint64(raw[34:34+8], offset)
binary.LittleEndian.PutUint64(raw[42:42+8], limit)
copy(raw[50:50+16], transferID[:])
} else if control == TransferControlActive {
copy(raw[34:34+len(data)], data)
}
return raw, nil
}
// IsLast checks if the incoming message is the last one in this transfer.
func (msg *MessageTransfer) IsLast() bool {
return msg.Control == TransferControlTerminate || msg.Control == TransferControlNotAvailable
}

View File

@@ -0,0 +1,165 @@
/*
File Name: Message Encoding Traverse.go
Copyright: 2021 Peernet s.r.o.
Author: Peter Kleissner
*/
package protocol
import (
"encoding/binary"
"errors"
"net"
"time"
"github.com/PeernetOfficial/core/btcec"
)
// MessageTraverse is the decoded traverse message.
// It is sent by an original sender to a relay, to a final receiver (targert peer).
type MessageTraverse struct {
*MessageRaw // Underlying raw message.
TargetPeer *btcec.PublicKey // End receiver peer ID.
AuthorizedRelayPeer *btcec.PublicKey // Peer ID that is authorized to relay this message to the end receiver.
Expires time.Time // Expiration time when this forwarded message becomes invalid.
EmbeddedPacketRaw []byte // Embedded packet.
SignerPublicKey *btcec.PublicKey // Public key that signed this message, ECDSA (secp256k1) 257-bit
IPv4 net.IP // IPv4 address of the original sender. Set by authorized relay. 0 if not set.
PortIPv4 uint16 // Port (actual one used for connection) of the original sender. Set by authorized relay.
PortIPv4ReportedExternal uint16 // External port as reported by the original sender. This is used in case of port forwarding (manual or automated).
IPv6 net.IP // IPv6 address of the original sender. Set by authorized relay. 0 if not set.
PortIPv6 uint16 // Port (actual one used for connection) of the original sender. Set by authorized relay.
PortIPv6ReportedExternal uint16 // External port as reported by the original sender. This is used in case of port forwarding (manual or automated).
}
const traversePayloadHeaderSize = 76 + 65 + 28
// DecodeTraverse decodes a traverse message.
// It does not verify if the receiver is authorized to read or forward this message.
// It validates the signature, but does not validate the signer.
func DecodeTraverse(msg *MessageRaw) (result *MessageTraverse, err error) {
result = &MessageTraverse{
MessageRaw: msg,
}
if len(msg.Payload) < traversePayloadHeaderSize {
return nil, errors.New("traverse: invalid minimum length")
}
targetPeerIDcompressed := msg.Payload[0:33]
authorizedRelayPeerIDcompressed := msg.Payload[33:66]
if result.TargetPeer, err = btcec.ParsePubKey(targetPeerIDcompressed, btcec.S256()); err != nil {
return nil, err
}
if result.AuthorizedRelayPeer, err = btcec.ParsePubKey(authorizedRelayPeerIDcompressed, btcec.S256()); err != nil {
return nil, err
}
// receiver and target must not be the same
if result.TargetPeer.IsEqual(result.AuthorizedRelayPeer) {
return nil, errors.New("traverse: target and relay invalid")
}
expires64 := binary.LittleEndian.Uint64(msg.Payload[66 : 66+8])
result.Expires = time.Unix(int64(expires64), 0)
sizePacketEmbed := binary.LittleEndian.Uint16(msg.Payload[74 : 74+2])
if int(sizePacketEmbed) != len(msg.Payload)-traversePayloadHeaderSize {
return nil, errors.New("traverse: size embedded packet mismatch")
}
result.EmbeddedPacketRaw = msg.Payload[76 : 76+sizePacketEmbed]
signature := msg.Payload[76+sizePacketEmbed : 76+sizePacketEmbed+65]
result.SignerPublicKey, _, err = btcec.RecoverCompact(btcec.S256(), signature, HashData(msg.Payload[:76+sizePacketEmbed]))
if err != nil {
return nil, err
}
// IPv4
ipv4B := make([]byte, 4)
copy(ipv4B[:], msg.Payload[76+sizePacketEmbed+65:76+sizePacketEmbed+65+4])
result.IPv4 = ipv4B
result.PortIPv4 = binary.LittleEndian.Uint16(msg.Payload[76+sizePacketEmbed+65+4 : 76+sizePacketEmbed+65+4+2])
result.PortIPv4ReportedExternal = binary.LittleEndian.Uint16(msg.Payload[76+sizePacketEmbed+65+6 : 76+sizePacketEmbed+65+6+2])
// IPv6
ipv6B := make([]byte, 16)
copy(ipv6B[:], msg.Payload[76+sizePacketEmbed+65+8:76+sizePacketEmbed+65+8+16])
result.IPv6 = ipv6B
result.PortIPv6 = binary.LittleEndian.Uint16(msg.Payload[76+sizePacketEmbed+65+24 : 76+sizePacketEmbed+65+24+2])
result.PortIPv6ReportedExternal = binary.LittleEndian.Uint16(msg.Payload[76+sizePacketEmbed+65+26 : 76+sizePacketEmbed+65+26+2])
// TODO: Validate IPv4 and IPv6. Only external ones allowed.
if result.IPv6.To4() != nil {
return nil, errors.New("traverse: ipv6 address mismatch")
}
return result, nil
}
// EncodeTraverse encodes a traverse message
func EncodeTraverse(senderPrivateKey *btcec.PrivateKey, embeddedPacketRaw []byte, receiverEnd *btcec.PublicKey, relayPeer *btcec.PublicKey) (packetRaw []byte, err error) {
sizePacketEmbed := len(embeddedPacketRaw)
if isPacketSizeExceed(traversePayloadHeaderSize, sizePacketEmbed) {
return nil, errors.New("traverse encode: embedded packet too big")
}
raw := make([]byte, traversePayloadHeaderSize+sizePacketEmbed)
targetPeerID := receiverEnd.SerializeCompressed()
copy(raw[0:33], targetPeerID)
authorizedRelayPeerID := relayPeer.SerializeCompressed()
copy(raw[33:66], authorizedRelayPeerID)
expires64 := time.Now().Add(time.Hour).UTC().Unix()
binary.LittleEndian.PutUint64(raw[66:66+8], uint64(expires64))
binary.LittleEndian.PutUint16(raw[74:74+2], uint16(sizePacketEmbed))
copy(raw[76:76+sizePacketEmbed], embeddedPacketRaw)
// add signature
signature, err := btcec.SignCompact(btcec.S256(), senderPrivateKey, HashData(raw[:76+sizePacketEmbed]), true)
if err != nil {
return nil, err
}
copy(raw[76+sizePacketEmbed:76+sizePacketEmbed+65], signature)
// IP and ports are to be filled by authorized relay peer
return raw, nil
}
// EncodeTraverseSetAddress sets the IP and Port in a traverse message that shall be forwarded to another peer
func EncodeTraverseSetAddress(raw []byte, IPv4 net.IP, PortIPv4, PortIPv4ReportedExternal uint16, IPv6 net.IP, PortIPv6, PortIPv6ReportedExternal uint16) (err error) {
if isPacketSizeExceed(len(raw), 0) {
return errors.New("traverse encode 2: embedded packet too big")
} else if len(raw) < traversePayloadHeaderSize {
return errors.New("traverse encode 2: invalid packet")
}
sizePacketEmbed := binary.LittleEndian.Uint16(raw[74 : 74+2])
if int(sizePacketEmbed) != len(raw)-traversePayloadHeaderSize {
return errors.New("traverse encode 2: size embedded packet mismatch")
}
// IPv4
if IPv4 != nil && len(IPv4) == net.IPv4len {
copy(raw[76+sizePacketEmbed+65:76+sizePacketEmbed+65+4], IPv4.To4())
binary.LittleEndian.PutUint16(raw[76+sizePacketEmbed+65+4:76+sizePacketEmbed+65+4+2], PortIPv4)
binary.LittleEndian.PutUint16(raw[76+sizePacketEmbed+65+6:76+sizePacketEmbed+65+6+2], PortIPv4ReportedExternal)
}
// IPv6
if IPv6 != nil && len(IPv6) == net.IPv6len {
copy(raw[76+sizePacketEmbed+65+8:76+sizePacketEmbed+65+8+16], IPv6.To16())
binary.LittleEndian.PutUint16(raw[76+sizePacketEmbed+65+24:76+sizePacketEmbed+65+24+2], PortIPv6)
binary.LittleEndian.PutUint16(raw[76+sizePacketEmbed+65+26:76+sizePacketEmbed+65+26+2], PortIPv6ReportedExternal)
}
return nil
}

View File

@@ -0,0 +1,40 @@
/*
File Name: Message Encoding.go
Copyright: 2021 Peernet s.r.o.
Author: Peter Kleissner
Intermediary between low-level packets and high-level interpretation.
*/
package protocol
import (
"github.com/PeernetOfficial/core/btcec"
)
// ProtocolVersion is the current protocol version
const ProtocolVersion = 0
// MessageRaw is a high-level message between peers that has not been decoded
type MessageRaw struct {
PacketRaw
SenderPublicKey *btcec.PublicKey // Sender Public Key, ECDSA (secp256k1) 257-bit
SequenceInfo *SequenceExpiry // Sequence
}
// The maximum packet size is = 65535 - 8 UDP byte header - 40 byte IPv6 header (IPv4 header is only 20 bytes).
// However, due to the MTU soft limit and fragmentation, packets should be as small as possible.
const udpMaxPacketSize = 65535 - 8 - 40
// internetSafeMTU is a value relatively safe to use for transmitting over the internet
// Theory: The value is different for IPv4 (min 576 bytes, Ethernet 1500 bytes) and IPv6 (min 1280 bytes). 8 byte UDP header must be subtracted, as well as the IP header (20 bytes for IPv4, 40 for IPv6).
// One simple test during development showed that 1500 - 8 - 40 - 8 worked for file transfer over IPv6 in Prague.
// For IPv6 the internet recommends the minimal possible value: 1280 bytes.
// This will be good enough for now. MTU negotiation that deviates from this value can be implemented separately (for example as part of file transfer).
// Since packets may be sent at anytime via IPv4/IPv6 connections (even concurrently on multiple), there is a single MTU value here.
const internetSafeMTU = 1280 - 8 - 40
// isPacketSizeExceed checks if the max packet size would be exceeded with the payload
func isPacketSizeExceed(currentSize int, testSize int) bool {
return currentSize+testSize > udpMaxPacketSize-PacketLengthMin
}

View File

@@ -0,0 +1,157 @@
/*
File Name: Packet Encoding.go
Copyright: 2021 Peernet s.r.o.
Author: Peter Kleissner
Basic packet structure of ALL packets:
Offset Size Info
0 4 Nonce
4 1 Protocol version = 0
5 1 Command
6 4 Sequence
10 2 Size of payload data
12 ? Payload
? Randomized garbage
? 65 Signature, ECDSA secp256k1 512-bit + 1 header byte
The peer ID of the sender, which is a ECDSA (secp256k1) 257-bit public key, can be extracted from the ECDSA signature.
The signature is applied on the entire packet, which guarantees that the signature becomes invalid should someone try to forge the receiver (i.e. forward the packet).
Because the signature could be a possible fingerpint, it is encrypted itself.
*/
package protocol
import (
"encoding/binary"
"errors"
"math/rand"
"github.com/PeernetOfficial/core/btcec"
"golang.org/x/crypto/salsa20"
)
// PacketRaw is a decrypted P2P message
type PacketRaw struct {
Protocol uint8 // Protocol version = 0
Command uint8 // 0 = Announcement
Sequence uint32 // Sequence number
Payload []byte // Payload
}
// The minimum packet size is 12 bytes (minimum header size) + 65 bytes (signature)
const PacketLengthMin = 12 + signatureSize
const signatureSize = 65
const maxRandomGarbage = 20
// PacketDecrypt decrypts the packet, verifies its signature and returns a high-level version of the packet.
func PacketDecrypt(raw []byte, receiverPublicKey *btcec.PublicKey) (packet *PacketRaw, senderPublicKey *btcec.PublicKey, err error) {
// Packet is assumed to be already checked for minimum length.
// Prepare Salsa20 nonce and key. Nonce = 2x first 4 bytes. For size reasons, only 4 bytes (instead of 8 bytes) is supplied in the packet.
// This could be a risk, but considering we only use the PUBLIC key as decryption key, it is negligible.
nonce := make([]byte, 8)
copy(nonce[0:4], raw[0:4])
copy(nonce[4:8], raw[0:4])
// Verify the signature and extract the public key from it.
var signature [signatureSize]byte
copy(signature[:], raw[len(raw)-signatureSize:])
keySalsa := publicKeyToSalsa20Key(receiverPublicKey)
salsa20.XORKeyStream(signature[:], signature[:], nonce, keySalsa)
senderPublicKey, _, err = btcec.RecoverCompact(btcec.S256(), signature[:], HashData(raw[:len(raw)-signatureSize]))
if err != nil {
return nil, nil, err
}
// Decrypt the packet using Salsa20.
bufferDecrypted := make([]byte, len(raw)-signatureSize-4) // full length -signature -nonce
salsa20.XORKeyStream(bufferDecrypted[:], raw[4:len(raw)-signatureSize], nonce, keySalsa)
// copy all fields
packet = &PacketRaw{Protocol: bufferDecrypted[0], Command: bufferDecrypted[1]}
packet.Sequence = binary.LittleEndian.Uint32(bufferDecrypted[2:6])
sizePayload := binary.LittleEndian.Uint16(bufferDecrypted[6:8])
if int(sizePayload) > len(bufferDecrypted)-8 { // invalid length?
return nil, nil, errors.New("invalid length field")
}
if sizePayload > 0 {
packet.Payload = make([]byte, int(sizePayload))
copy(packet.Payload, bufferDecrypted[8:8+int(sizePayload)])
}
return packet, senderPublicKey, nil
}
// PacketEncrypt encrypts a packet using the provided senders private key and receivers compressed public key.
func PacketEncrypt(senderPrivateKey *btcec.PrivateKey, receiverPublicKey *btcec.PublicKey, packet *PacketRaw) (raw []byte, err error) {
garbage := packetGarbage(PacketLengthMin + len(packet.Payload))
raw = make([]byte, PacketLengthMin+len(packet.Payload)+len(garbage))
nonceC := rand.Uint32()
nonce := make([]byte, 8)
binary.LittleEndian.PutUint32(nonce[0:4], nonceC)
binary.LittleEndian.PutUint32(nonce[4:8], nonceC)
copy(raw[0:4], nonce[0:4])
raw[4] = packet.Protocol
raw[5] = packet.Command
binary.LittleEndian.PutUint32(raw[6:10], uint32(packet.Sequence))
binary.LittleEndian.PutUint16(raw[10:12], uint16(len(packet.Payload)))
copy(raw[12:], packet.Payload)
copy(raw[12+len(packet.Payload):12+len(packet.Payload)+len(garbage)], garbage)
// encrypt it using Salsa20
keySalsa := publicKeyToSalsa20Key(receiverPublicKey)
salsa20.XORKeyStream(raw[4:12+len(packet.Payload)+len(garbage)], raw[4:12+len(packet.Payload)+len(garbage)], nonce, keySalsa)
// add signature
signature, err := btcec.SignCompact(btcec.S256(), senderPrivateKey, HashData(raw[:len(raw)-signatureSize]), true)
if err != nil {
return nil, err
}
salsa20.XORKeyStream(signature[:], signature[:], nonce, keySalsa)
copy(raw[len(raw)-signatureSize:], signature)
return raw, nil
}
func packetGarbage(packetLength int) (random []byte) {
// Align maximum length at 508 bytes (UDP minimum no fragmentation) and at a relatively safe MTU.
maxLength := maxRandomGarbage
switch {
case packetLength == 508, packetLength == internetSafeMTU:
return nil
case packetLength < 508 && (508-packetLength) < maxRandomGarbage:
maxLength = 508 - packetLength
case packetLength < internetSafeMTU && (internetSafeMTU-packetLength) < maxRandomGarbage:
maxLength = internetSafeMTU - packetLength
}
b := make([]byte, rand.Intn(maxLength))
if _, err := rand.Read(b); err != nil {
return nil
}
return b
}
func publicKeyToSalsa20Key(publicKey *btcec.PublicKey) (key *[32]byte) {
// bit 0 from PublicKey.Y is ignored here, but is negligible for this purpose
key = new([32]byte)
copy(key[:], publicKey.SerializeCompressed()[1:])
return key
}
// SetSelfReportedPorts sets the fields Internal Port and External Port according to the connection details.
// This is important for the remote peer to make smart decisions whether this peer is behind a NAT/firewall and supports port forwarding/UPnP.
func (packet *PacketRaw) SetSelfReportedPorts(portI, portE uint16) {
if packet.Command != CommandAnnouncement && packet.Command != CommandResponse { // only for Announcement and Response messages
return
}
binary.LittleEndian.PutUint16(packet.Payload[19:19+2], portI)
binary.LittleEndian.PutUint16(packet.Payload[21:21+2], portE)
}

View File

@@ -0,0 +1,199 @@
/*
File Name: Packet Lite.go
Copyright: 2021 Peernet s.r.o.
Author: Peter Kleissner
The lite packet header is used for encoding data transfer packets. The regular header is too expensive in terms of CPU consumption due to public key signing.
Instead, a simple session ID will identify lite packets. The ID is randomized and only valid during the session.
Unsolicited lite packets are therefore impossible; the receiver must have the ID already whitelisted for the packet to be recognized.
Offset Size Info
0 16 ID
16 2 Size of data to follow
*/
package protocol
import (
"encoding/binary"
"errors"
"sync"
"time"
"github.com/google/uuid"
)
// PacketLiteRaw is a decrypted P2P lite packet
type PacketLiteRaw struct {
ID uuid.UUID // ID
Payload []byte // Payload
Session *LiteID // Session info
}
// Minimum packet size of lite packets.
const PacketLiteSizeMin = 16 + 2
// IsPacketLite identifies a lite packet based on its ID. If the ID is not recognized, it fails.
func (router *LiteRouter) IsPacketLite(raw []byte) (isLite bool, err error) {
if len(raw) < PacketLiteSizeMin {
return false, errors.New("invalid packet size")
}
// Parse the ID and then look it up.
var id uuid.UUID
copy(id[:], raw[0:16])
return router.LookupLiteID(id) != nil, nil
}
// PacketLiteDecode a lite packet. It will identify the lite packet based on its ID. If the ID is not recognized (which is the case for regular Peernet packets), the function fails.
// It does not perform any decryption.
func (router *LiteRouter) PacketLiteDecode(raw []byte) (packet *PacketLiteRaw, err error) {
if len(raw) < PacketLiteSizeMin {
return nil, errors.New("invalid packet size")
}
// Parse the ID and look it up. It will contain information about the decryption algorithm to use.
var id uuid.UUID
copy(id[:], raw[0:16])
session := router.LookupLiteID(id)
if session == nil {
return nil, errors.New("packet ID not found")
}
// TODO: Decrypt the data if indicated by the session.
sizePayload := binary.LittleEndian.Uint16(raw[16 : 16+2])
if int(sizePayload) > len(raw)-PacketLiteSizeMin { // invalid size field?
return nil, errors.New("invalid packet size field")
}
// Valid packet received, extend expiration.
session.expires = time.Now().Add(session.timeout)
return &PacketLiteRaw{Payload: raw[PacketLiteSizeMin:], ID: id, Session: session}, nil
}
// Encodes a lite packet.
func PacketLiteEncode(id uuid.UUID, data []byte) (raw []byte, err error) {
raw = make([]byte, PacketLiteSizeMin+len(data))
copy(raw[0:16], id[:])
binary.LittleEndian.PutUint16(raw[16:16+2], uint16(len(data)))
copy(raw[PacketLiteSizeMin:], data)
return raw, nil
}
// ---- Lite packet ID management. This is similar to packet sequences. ----
// LiteRouter keeps track of accepted (expected) packet IDs.
type LiteRouter struct {
// list of recognized IDs
ids map[uuid.UUID]*LiteID
sync.Mutex // synchronized access to the IDs
}
// LiteID contains session information for a bidirectional transfer of data
type LiteID struct {
ID uuid.UUID // ID
created time.Time // When the ID was created.
expires time.Time // When the ID expires. This can be extended on the fly!
Data interface{} // Optional high-level data associated with the ID
timeout time.Duration // Timeout for receiving the next message
invalidateFunc func() // Called on expiration.
}
// Creates a new manager to keep track of accepted IDs.
func NewLiteRouter() (router *LiteRouter) {
router = &LiteRouter{
ids: make(map[uuid.UUID]*LiteID),
}
go router.autoDeleteExpired()
return
}
// autoDeleteExpired deletes all IDs that are expired.
func (router *LiteRouter) autoDeleteExpired() {
for {
time.Sleep(4 * time.Second)
now := time.Now()
router.Lock()
for id, info := range router.ids {
if info.expires.Before(now) {
delete(router.ids, id)
if info.invalidateFunc != nil {
go info.invalidateFunc()
}
}
}
router.Unlock()
}
}
func (router *LiteRouter) LookupLiteID(id uuid.UUID) (info *LiteID) {
router.Lock()
info = router.ids[id]
router.Unlock()
return info
}
// Returns a new lite ID to be used.
func (router *LiteRouter) NewLiteID(data interface{}, timeout time.Duration, invalidateFunc func()) (info *LiteID) {
info = &LiteID{
created: time.Now(),
expires: time.Now().Add(timeout),
timeout: timeout,
invalidateFunc: invalidateFunc,
Data: data,
ID: uuid.New(),
}
router.Lock()
router.ids[info.ID] = info
router.Unlock()
return
}
func (router *LiteRouter) RegisterLiteID(id uuid.UUID, data interface{}, timeout time.Duration, invalidateFunc func()) (info *LiteID) {
info = &LiteID{
ID: id,
created: time.Now(),
expires: time.Now().Add(timeout),
timeout: timeout,
invalidateFunc: invalidateFunc,
Data: data,
}
router.Lock()
existingInfo := router.ids[info.ID]
router.ids[info.ID] = info
router.Unlock()
// Call the invalidate function if there is a collision. This should never happen.
if existingInfo != nil && existingInfo.invalidateFunc != nil {
go existingInfo.invalidateFunc()
}
return
}
// Returns all lite sessions
func (router *LiteRouter) All() (sessions []*LiteID) {
router.Lock()
for _, info := range router.ids {
sessions = append(sessions, info)
}
router.Unlock()
return sessions
}

View File

@@ -0,0 +1,262 @@
/*
File Name: Sequence.go
Copyright: 2021 Peernet s.r.o.
Author: Peter Kleissner
This code caches and verifies message sequences. Sequence numbers are valid on a peer level, independent of which network connection was used.
They can be used to map incoming response messages to previous outgoing requests. The remote peer ID is used together with a consecutive sequence number as unique key.
Sequences are created for:
* Unidirectional messages (responses are one-way)
* Bidirectional messages (responses may be sent back and forth). They use a different key namespace since remote sequence numbers could collide with locally created ones.
Advantages:
* This secures against replay and poisoning attacks.
* If used correctly it can also deduplicate messages (which occurs when 2 peers have multiple registered connections to each other but none are active and subsequent fallback to broadcast).
* The round-trip time can be measured and used to determine the connection quality.
* (future) It can be used to detect missed and lost replies.
*/
package protocol
import (
"math/rand"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/PeernetOfficial/core/btcec"
)
// SequenceManager stores all message sequence numbers that are valid at the moment
type SequenceManager struct {
ReplyTimeout int // The round-trip timeout for message sequences.
// sequences is the list of sequence numbers that are valid at the moment. The value represents the time the sequence number.
// Key = Peer ID + Sequence Number
sequences map[string]*SequenceExpiry
sync.Mutex // synchronized access to the sequences
}
// SequenceExpiry contains the decoded sequence information of a message.
type SequenceExpiry struct {
SequenceNumber uint32 // Sequence number
created time.Time // When the sequence was created.
expires time.Time // When the sequence expires. This can be extended on the fly!
counter int // How many replies used the sequence. Multiple Response messages may be returned for a single Announcement one.
Data interface{} // Optional high-level data associated with the sequence
// bidirectional sequences only
bidirectional bool // Whether this sequence is used in a bidirectional way
timeout time.Duration // Timeout for receiving the next message
invalidateFunc func() // The invalidation callback is in case a sequence collision or expiration invalidates the sequence.
}
// NewSequenceManager creates a new sequence manager. The ReplyTimeout is in seconds. The expiration function is started immediately.
func NewSequenceManager(ReplyTimeout int) (manager *SequenceManager) {
manager = &SequenceManager{
ReplyTimeout: ReplyTimeout,
sequences: make(map[string]*SequenceExpiry),
}
go manager.autoDeleteExpired()
return
}
// autoDeleteExpired deletes all sequences that are expired.
func (manager *SequenceManager) autoDeleteExpired() {
for {
time.Sleep(time.Duration(manager.ReplyTimeout) * time.Second)
now := time.Now()
manager.Lock()
for key, sequence := range manager.sequences {
if sequence.expires.Before(now) {
delete(manager.sequences, key)
if sequence.invalidateFunc != nil {
go sequence.invalidateFunc()
}
}
}
manager.Unlock()
}
}
// sequence2Key creates the lookup key of a sequence for a peer.
// Since bidirectional sequence numbers may be created from either side (remote or local peer), it does not share a namespace with unidirectional sequence numbers.
func sequence2Key(bidirectional bool, publicKey *btcec.PublicKey, sequenceNumber uint32) (key string) {
if !bidirectional {
return "u" + string(publicKey.SerializeCompressed()) + strconv.FormatUint(uint64(sequenceNumber), 10)
} else {
return "b" + string(publicKey.SerializeCompressed()) + strconv.FormatUint(uint64(sequenceNumber), 10)
}
}
// NewSequence returns a new sequence and registers it. messageSequence must point to the variable holding the continuous next sequence number.
// Use only for Announcement and Ping messages.
func (manager *SequenceManager) NewSequence(publicKey *btcec.PublicKey, messageSequence *uint32, data interface{}) (info *SequenceExpiry) {
info = &SequenceExpiry{
SequenceNumber: atomic.AddUint32(messageSequence, 1),
created: time.Now(),
expires: time.Now().Add(time.Duration(manager.ReplyTimeout) * time.Second),
Data: data,
}
// Add the sequence to the list. Sequences are unique enough that collisions are unlikely and negligible.
key := sequence2Key(false, publicKey, info.SequenceNumber)
manager.Lock()
manager.sequences[key] = info
manager.Unlock()
return
}
// ArbitrarySequence returns an arbitrary sequence to be used for uncontacted peers
func (manager *SequenceManager) ArbitrarySequence(publicKey *btcec.PublicKey, data interface{}) (info *SequenceExpiry) {
info = &SequenceExpiry{
SequenceNumber: rand.Uint32(),
created: time.Now(),
expires: time.Now().Add(time.Duration(manager.ReplyTimeout) * time.Second),
Data: data,
}
// Add the sequence to the list. Sequences are unique enough that collisions are unlikely and negligible.
key := sequence2Key(false, publicKey, info.SequenceNumber)
manager.Lock()
manager.sequences[key] = info
manager.Unlock()
return
}
// ValidateSequence validates the sequence number of an incoming message. It will set raw.sequence if valid.
func (manager *SequenceManager) ValidateSequence(publicKey *btcec.PublicKey, sequenceNumber uint32, invalidate, extendValidity bool) (sequenceInfo *SequenceExpiry, valid bool, rtt time.Duration) {
key := sequence2Key(false, publicKey, sequenceNumber)
manager.Lock()
defer manager.Unlock()
// lookup the sequence
sequence, ok := manager.sequences[key]
if !ok {
return nil, false, rtt
}
// Initial reply: Store latest roundtrip time. That value might be distorted on Response vs Pong since Response messages might send data
// up to 64 KB which obviously would be transmitted slower than an empty Pong reply. However, for the real world this is good enough.
if sequence.counter == 0 {
rtt = time.Since(sequence.created)
}
sequence.counter++
// invalidate the sequence immediately?
if invalidate {
delete(manager.sequences, key)
} else if extendValidity {
// Special case CommandResponse: Extend validity in case there are follow-up responses, by half of the round-trip time since they will be sent one-way.
sequence.expires = time.Now().Add(time.Duration(manager.ReplyTimeout) * time.Second / 2)
}
return sequence, sequence.expires.After(time.Now()), rtt
}
// InvalidateSequence invalidates the sequence number. It does not call invalidateFunc.
func (manager *SequenceManager) InvalidateSequence(publicKey *btcec.PublicKey, sequenceNumber uint32, bidirectional bool) {
key := sequence2Key(bidirectional, publicKey, sequenceNumber)
manager.Lock()
delete(manager.sequences, key)
manager.Unlock()
}
// ---- bidirectional sequences ----
// RegisterSequenceBi registers a bidirectional sequence initiated by a remote peer. The caller must specify the timeout (which will be reset every time a new message appears in this sequence).
// This is needed for bidirectional responses to accept subsequent incoming messages from the remote peer.
func (manager *SequenceManager) RegisterSequenceBi(publicKey *btcec.PublicKey, sequenceNumber uint32, data interface{}, timeout time.Duration, invalidateFunc func()) (info *SequenceExpiry) {
info = &SequenceExpiry{
SequenceNumber: sequenceNumber,
created: time.Now(),
expires: time.Now().Add(timeout),
timeout: timeout,
invalidateFunc: invalidateFunc,
Data: data,
}
// Before registering the sequence, check if there is a collision. If yes, invalidate the original one.
key := sequence2Key(true, publicKey, info.SequenceNumber)
manager.Lock()
existingSequence := manager.sequences[key]
manager.sequences[key] = info
manager.Unlock()
// Call the invalidate function if there is a collision.
if existingSequence != nil && existingSequence.invalidateFunc != nil {
go existingSequence.invalidateFunc()
}
return
}
// NewSequenceBi returns a new bidirectional sequence and registers it. messageSequence must point to the variable holding the continuous next sequence number.
func (manager *SequenceManager) NewSequenceBi(publicKey *btcec.PublicKey, messageSequence *uint32, data interface{}, timeout time.Duration, invalidateFunc func()) (info *SequenceExpiry) {
info = &SequenceExpiry{
created: time.Now(),
expires: time.Now().Add(timeout),
bidirectional: true,
timeout: timeout,
invalidateFunc: invalidateFunc,
Data: data,
}
manager.Lock()
defer manager.Unlock()
// The likelihood of a collision is low but not impossible.
for n := 0; n < 10000; n++ {
info.SequenceNumber = atomic.AddUint32(messageSequence, 1)
key := sequence2Key(true, publicKey, info.SequenceNumber)
if infoE := manager.sequences[key]; infoE == nil {
manager.sequences[key] = info
return info
}
}
return nil
}
// ValidateSequenceBi validates the sequence number of an incoming message. It will set raw.sequence if valid.
func (manager *SequenceManager) ValidateSequenceBi(publicKey *btcec.PublicKey, sequenceNumber uint32, isLast bool) (sequenceInfo *SequenceExpiry, valid bool, rtt time.Duration) {
key := sequence2Key(true, publicKey, sequenceNumber)
manager.Lock()
defer manager.Unlock()
// lookup the sequence
sequence, ok := manager.sequences[key]
if !ok {
return nil, false, rtt
}
// Initial reply: Store latest roundtrip time.
if sequence.counter == 0 {
rtt = time.Since(sequence.created)
}
sequence.counter++
// invalidate the sequence immediately?
if isLast {
delete(manager.sequences, key)
} else {
sequence.expires = time.Now().Add(sequence.timeout)
}
return sequence, sequence.expires.After(time.Now()), rtt
}

View File

@@ -0,0 +1,3 @@
# Protocol
The Peernet Protocol is defined in the Whitepaper and implemented here accordingly.