Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions internal/handshake/protocol/genesis/genesis.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
// Copyright 2025 Blink Labs Software
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

package genesis

import (
Expand Down
237 changes: 237 additions & 0 deletions internal/handshake/protocol/messages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
// Copyright 2025 Blink Labs Software
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

package protocol

import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"net"
)

// Message types
const (
MessageVersion = 0
MessageVerack = 1
MessagePing = 2
MessagePong = 3
MessageGetAddr = 4
MessageAddr = 5
MessageGetHeaders = 10
MessageHeaders = 11
MessageSendHeaders = 12
MessageGetProof = 26
MessageProof = 27
)

const (
messageHeaderLength = 9
messageMaxPayloadLength = 8 * 1000 * 1000
)

type Message interface {
Encode() []byte
Decode([]byte) error
}

func encodeMessage(msgType uint8, payload []byte, networkMagic uint32) ([]byte, error) {
if len(payload) > messageMaxPayloadLength {
return nil, errors.New("payload is too large")
}
msg := make([]byte, messageHeaderLength+len(payload))
header := &msgHeader{
NetworkMagic: networkMagic,
MessageType: msgType,
PayloadLength: uint32(len(payload)), // nolint:gosec
}
encodedHeader := header.Encode()
copy(msg[0:messageHeaderLength], encodedHeader)
// Payload
copy(msg[9:], payload)
return msg, nil
}

func decodeMessage(header *msgHeader, payload []byte) (Message, error) {
var ret Message
switch header.MessageType {
case MessageVersion:
ret = &MsgVersion{}
case MessageVerack:
ret = &MsgVerack{}
default:
return nil, fmt.Errorf("unsupported message type: %d", header.MessageType)
}
if err := ret.Decode(payload); err != nil {
return nil, fmt.Errorf("decode message: %w", err)
}
return ret, nil
}

type msgHeader struct {
NetworkMagic uint32
MessageType uint8
PayloadLength uint32
}

func (h *msgHeader) Encode() []byte {
msgHeader := make([]byte, messageHeaderLength)
// Network magic number
binary.LittleEndian.PutUint32(msgHeader[0:4], h.NetworkMagic)
// Message type
msgHeader[4] = h.MessageType
// Payload length
binary.LittleEndian.PutUint32(msgHeader[5:9], uint32(h.PayloadLength))
return msgHeader
}

func (h *msgHeader) Decode(data []byte) error {
if len(data) != messageHeaderLength {
return errors.New("header data is incorrect size")
}
h.NetworkMagic = binary.LittleEndian.Uint32(data[0:4])
h.MessageType = data[4]
h.PayloadLength = binary.LittleEndian.Uint32(data[5:9])
return nil
}

type MsgVersion struct {
Version uint32
Services uint64
Time uint64
Remote NetAddress
Nonce [8]byte
Agent string
Height uint32
NoRelay bool
}

func (m *MsgVersion) Encode() []byte {
buf := new(bytes.Buffer)
// Protocol version
_ = binary.Write(buf, binary.LittleEndian, m.Version)
// Services
_ = binary.Write(buf, binary.LittleEndian, m.Services)
// Timestamp
_ = binary.Write(buf, binary.LittleEndian, m.Time)
// Remote address
encodedRemote := m.Remote.Encode()
_, _ = buf.Write(encodedRemote)
// Nonce
_ = binary.Write(buf, binary.LittleEndian, m.Nonce[:])
// User agent string length
_ = buf.WriteByte(byte(len(m.Agent)))
// User agent string
_, _ = buf.WriteString(m.Agent)
// Block height
_ = binary.Write(buf, binary.LittleEndian, m.Height)
// No relay
if m.NoRelay {
_ = buf.WriteByte(1)
} else {
_ = buf.WriteByte(0)
}
return buf.Bytes()
}

func (m *MsgVersion) Decode(data []byte) error {
m.Version = binary.LittleEndian.Uint32(data[0:4])
m.Services = binary.LittleEndian.Uint64(data[4:12])
m.Time = binary.LittleEndian.Uint64(data[12:20])
if err := m.Remote.Decode(data[20:108]); err != nil {
return err
}
m.Nonce = [8]byte(data[108:116])
userAgentLength := int(data[116])
m.Agent = string(data[117 : 117+userAgentLength])
m.Height = binary.LittleEndian.Uint32(data[117+userAgentLength : 117+userAgentLength+4])
noRelayByte := data[117+userAgentLength+4]
m.NoRelay = false
if noRelayByte == 1 {
m.NoRelay = true
}
return nil
}
Comment on lines +141 to +158
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add bounds checking to prevent panic.

MsgVersion.Decode accesses data[] at fixed offsets without validating that data is sufficiently long. If malformed or truncated data is provided, this will panic rather than returning an error.

Add length validation:

 func (m *MsgVersion) Decode(data []byte) error {
+	// Minimum length: 4 + 8 + 8 + 88 + 8 + 1 + 0 + 4 + 1 = 122 bytes (with 0-length agent)
+	if len(data) < 122 {
+		return fmt.Errorf("data too short for MsgVersion: got %d bytes, need at least 122", len(data))
+	}
 	m.Version = binary.LittleEndian.Uint32(data[0:4])
 	m.Services = binary.LittleEndian.Uint64(data[4:12])
 	m.Time = binary.LittleEndian.Uint64(data[12:20])
 	if err := m.Remote.Decode(data[20:108]); err != nil {
 		return err
 	}
 	m.Nonce = [8]byte(data[108:116])
 	userAgentLength := int(data[116])
+	if len(data) < 117+userAgentLength+5 {
+		return fmt.Errorf("data too short for agent string and remaining fields: got %d bytes, need %d", len(data), 117+userAgentLength+5)
+	}
 	m.Agent = string(data[117 : 117+userAgentLength])
 	m.Height = binary.LittleEndian.Uint32(data[117+userAgentLength : 117+userAgentLength+4])
 	noRelayByte := data[117+userAgentLength+4]
 	m.NoRelay = false
 	if noRelayByte == 1 {
 		m.NoRelay = true
 	}
 	return nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (m *MsgVersion) Decode(data []byte) error {
m.Version = binary.LittleEndian.Uint32(data[0:4])
m.Services = binary.LittleEndian.Uint64(data[4:12])
m.Time = binary.LittleEndian.Uint64(data[12:20])
if err := m.Remote.Decode(data[20:108]); err != nil {
return err
}
m.Nonce = [8]byte(data[108:116])
userAgentLength := int(data[116])
m.Agent = string(data[117 : 117+userAgentLength])
m.Height = binary.LittleEndian.Uint32(data[117+userAgentLength : 117+userAgentLength+4])
noRelayByte := data[117+userAgentLength+4]
m.NoRelay = false
if noRelayByte == 1 {
m.NoRelay = true
}
return nil
}
func (m *MsgVersion) Decode(data []byte) error {
// Minimum length: 4 + 8 + 8 + 88 + 8 + 1 + 0 + 4 + 1 = 122 bytes (with 0-length agent)
if len(data) < 122 {
return fmt.Errorf("data too short for MsgVersion: got %d bytes, need at least 122", len(data))
}
m.Version = binary.LittleEndian.Uint32(data[0:4])
m.Services = binary.LittleEndian.Uint64(data[4:12])
m.Time = binary.LittleEndian.Uint64(data[12:20])
if err := m.Remote.Decode(data[20:108]); err != nil {
return err
}
m.Nonce = [8]byte(data[108:116])
userAgentLength := int(data[116])
if len(data) < 117+userAgentLength+5 {
return fmt.Errorf("data too short for agent string and remaining fields: got %d bytes, need %d", len(data), 117+userAgentLength+5)
}
m.Agent = string(data[117 : 117+userAgentLength])
m.Height = binary.LittleEndian.Uint32(data[117+userAgentLength : 117+userAgentLength+4])
noRelayByte := data[117+userAgentLength+4]
m.NoRelay = false
if noRelayByte == 1 {
m.NoRelay = true
}
return nil
}
🤖 Prompt for AI Agents
In internal/handshake/protocol/messages.go around lines 141 to 158,
MsgVersion.Decode reads fixed offsets from data without verifying length which
can cause panics on truncated input; add explicit bounds checks before each
slice/index operation (ensure data length >= required minimum at start for the
fixed-size header, then validate there are enough bytes for Remote.Decode call,
nonce, userAgent length byte, the userAgent payload, and the following height
and noRelay byte), validate userAgentLength is sane and won’t overflow slice
bounds, propagate or wrap errors from Remote.Decode, and return a descriptive
error instead of letting a panic occur.


type NetAddress struct {
Time uint64
Services uint64
Host net.IP
Reserved [20]byte
Port uint16
Key [33]byte
}

func (n *NetAddress) Encode() []byte {
buf := new(bytes.Buffer)
// Time
_ = binary.Write(buf, binary.LittleEndian, n.Time)
// Services
_ = binary.Write(buf, binary.LittleEndian, n.Services)
// Address type
buf.WriteByte(0)
// Address
if n.Host.To4() != nil {
// IPv4
buf.Write([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff})
buf.Write(n.Host.To4())
} else {
// IPv6
buf.Write(n.Host.To16())
}
// Reserved
buf.Write(n.Reserved[:])
// Port
_ = binary.Write(buf, binary.BigEndian, n.Port)
// Key
buf.Write(n.Key[:])
return buf.Bytes()
}

func (n *NetAddress) Decode(data []byte) error {
if len(data) != 88 {
return errors.New("invalid NetAddress length")
}
n.Time = binary.LittleEndian.Uint64(data[0:8])
n.Services = binary.LittleEndian.Uint64(data[8:16])
// NOTE: purposely skipping byte at index 16 for address type
n.Host = net.IP(data[17:33])
copy(n.Reserved[:], data[33:53])
n.Port = binary.BigEndian.Uint16(data[53:55])
copy(n.Key[:], data[55:88])
return nil
}

type MsgVerack struct{}

func (*MsgVerack) Encode() []byte {
// No payload
return []byte{}
}

func (*MsgVerack) Decode(data []byte) error {
// No payload
return nil
}

type MsgPing struct{}

type MsgPong struct{}

type MsgGetAddr struct{}

type MsgAddr struct{}

type MsgGetHeaders struct{}

type MsgHeaders struct{}

type MsgSendHeaders struct{}

type MsgGetProof struct{}

type MsgProof struct{}
78 changes: 78 additions & 0 deletions internal/handshake/protocol/messages_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2025 Blink Labs Software
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

package protocol

import (
"encoding/hex"
"net"
"reflect"
"testing"
)

func TestMsgVersionEncodeDecode(t *testing.T) {
testDefs := []struct {
message Message
binaryHex string
}{
// Captured from hsd
{
binaryHex: "030000000100000000000000e690136900000000e69013690000000000000000000000000000000000000000000000ffff60e6a250000000000000000000000000000000000000000046c40000000000000000000000000000000000000000000000000000000000000000002de918cb7d2b6e6e0b2f6873643a382e302e302f1ca0040000",
message: &MsgVersion{
Version: 0x3,
Services: 0x1,
Time: 0x691390e6,
Remote: NetAddress{
Time: 0x691390e6,
Services: 0x0,
Host: net.IP{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0x60, 0xe6, 0xa2, 0x50},
Port: 0x46c4,
Key: [33]uint8{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}},
Nonce: [8]uint8{0x2d, 0xe9, 0x18, 0xcb, 0x7d, 0x2b, 0x6e, 0x6e},
Agent: "/hsd:8.0.0/",
Height: 0x4a01c,
NoRelay: false,
},
},
// Captured from our own Version message
{
binaryHex: "0100000000000000000000003f9e1369000000003f9e13690000000000000000000000000000000000000000000000ffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000054990d22ec7e7401072f63646e73642f0000000001",
message: &MsgVersion{
Version: 1,
Services: 0,
Time: 0x69139e3f,
Remote: NetAddress{
Time: 0x69139e3f,
Services: 0,
Host: net.IP{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0x0, 0x0, 0x0, 0x0},
Port: 0,
},
Nonce: [8]byte{0x54, 0x99, 0xd, 0x22, 0xec, 0x7e, 0x74, 0x1},
Agent: "/cdnsd/",
Height: 0,
NoRelay: true,
},
},
}
for _, testDef := range testDefs {
binaryData, err := hex.DecodeString(testDef.binaryHex)
if err != nil {
t.Fatalf("unexpected error decoding hex: %s", err)
}
testMsg := new(MsgVersion)
if err := testMsg.Decode(binaryData); err != nil {
t.Fatalf("unexpected error decoding message: %s", err)
}
if !reflect.DeepEqual(testMsg, testDef.message) {
t.Fatalf("did not get expected message after decode:\n got: %#v\n wanted: %#v", testMsg, testDef.message)
}
testEncoded := testMsg.Encode()
testEncodedHex := hex.EncodeToString(testEncoded)
if testEncodedHex != testDef.binaryHex {
t.Fatalf("did not get expected binary hex after encode:\n got: %s\n wanted: %s", testEncodedHex, testDef.binaryHex)
}
}
}
6 changes: 6 additions & 0 deletions internal/handshake/protocol/network.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
// Copyright 2025 Blink Labs Software
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

package protocol

// Shapes only - constants & configs from hsd's protocol/networks.js.
Expand Down
Loading
Loading