-
Notifications
You must be signed in to change notification settings - Fork 2
feat: initial handshake network peer support #422
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } | ||
|
|
||
| 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{} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add bounds checking to prevent panic.
MsgVersion.Decodeaccessesdata[]at fixed offsets without validating thatdatais 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
🤖 Prompt for AI Agents