go-blockchain/p2p/handshake.go
Virgil 21c5d49ef9
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run
fix(sync): validate peers and persist HTLC spends
Centralise handshake response validation so outbound sync checks both network identity and minimum peer build version through the p2p layer. Also record HTLC key images as spent during block processing, matching the HF1 input semantics and preventing those spends from being omitted from chain state.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-04 19:01:07 +00:00

193 lines
4.8 KiB
Go

// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// SPDX-License-Identifier: EUPL-1.2
package p2p
import (
"encoding/binary"
"fmt"
"dappco.re/go/core/p2p/node/levin"
)
// PeerlistEntrySize is the packed size of a peerlist entry (ip + port + id + last_seen).
const PeerlistEntrySize = 24
// NodeData contains the node identity exchanged during handshake.
type NodeData struct {
NetworkID [16]byte
PeerID uint64
LocalTime int64
MyPort uint32
}
// MarshalSection encodes NodeData into a portable storage Section.
func (n *NodeData) MarshalSection() levin.Section {
return levin.Section{
"network_id": levin.StringVal(n.NetworkID[:]),
"peer_id": levin.Uint64Val(n.PeerID),
"local_time": levin.Int64Val(n.LocalTime),
"my_port": levin.Uint32Val(n.MyPort),
}
}
// UnmarshalSection decodes NodeData from a portable storage Section.
func (n *NodeData) UnmarshalSection(s levin.Section) error {
if v, ok := s["network_id"]; ok {
blob, err := v.AsString()
if err != nil {
return err
}
if len(blob) >= 16 {
copy(n.NetworkID[:], blob[:16])
}
}
if v, ok := s["peer_id"]; ok {
val, err := v.AsUint64()
if err != nil {
return err
}
n.PeerID = val
}
if v, ok := s["local_time"]; ok {
val, err := v.AsInt64()
if err != nil {
return err
}
n.LocalTime = val
}
if v, ok := s["my_port"]; ok {
val, err := v.AsUint32()
if err != nil {
return err
}
n.MyPort = val
}
return nil
}
// PeerlistEntry is a decoded peerlist entry from a handshake response.
type PeerlistEntry struct {
IP uint32
Port uint32
ID uint64
LastSeen int64
}
// DecodePeerlist splits a packed peerlist blob into entries.
func DecodePeerlist(blob []byte) []PeerlistEntry {
n := len(blob) / PeerlistEntrySize
entries := make([]PeerlistEntry, n)
for i := range n {
off := i * PeerlistEntrySize
entries[i] = PeerlistEntry{
IP: binary.LittleEndian.Uint32(blob[off : off+4]),
Port: binary.LittleEndian.Uint32(blob[off+4 : off+8]),
ID: binary.LittleEndian.Uint64(blob[off+8 : off+16]),
LastSeen: int64(binary.LittleEndian.Uint64(blob[off+16 : off+24])),
}
}
return entries
}
// HandshakeRequest is a COMMAND_HANDSHAKE request.
type HandshakeRequest struct {
NodeData NodeData
PayloadData CoreSyncData
}
// MarshalSection encodes the request.
func (r *HandshakeRequest) MarshalSection() levin.Section {
return levin.Section{
"node_data": levin.ObjectVal(r.NodeData.MarshalSection()),
"payload_data": levin.ObjectVal(r.PayloadData.MarshalSection()),
}
}
// UnmarshalSection decodes the request.
func (r *HandshakeRequest) UnmarshalSection(s levin.Section) error {
if v, ok := s["node_data"]; ok {
obj, err := v.AsSection()
if err != nil {
return err
}
if err := r.NodeData.UnmarshalSection(obj); err != nil {
return err
}
}
if v, ok := s["payload_data"]; ok {
obj, err := v.AsSection()
if err != nil {
return err
}
if err := r.PayloadData.UnmarshalSection(obj); err != nil {
return err
}
}
return nil
}
// EncodeHandshakeRequest serialises a handshake request into a storage blob.
func EncodeHandshakeRequest(req *HandshakeRequest) ([]byte, error) {
return levin.EncodeStorage(req.MarshalSection())
}
// HandshakeResponse is a COMMAND_HANDSHAKE response.
type HandshakeResponse struct {
NodeData NodeData
PayloadData CoreSyncData
PeerlistBlob []byte // Raw packed peerlist (24 bytes per entry)
}
// Decode parses a handshake response from a storage blob.
func (r *HandshakeResponse) Decode(data []byte) error {
s, err := levin.DecodeStorage(data)
if err != nil {
return err
}
if v, ok := s["node_data"]; ok {
obj, err := v.AsSection()
if err != nil {
return err
}
if err := r.NodeData.UnmarshalSection(obj); err != nil {
return err
}
}
if v, ok := s["payload_data"]; ok {
obj, err := v.AsSection()
if err != nil {
return err
}
if err := r.PayloadData.UnmarshalSection(obj); err != nil {
return err
}
}
if v, ok := s["local_peerlist"]; ok {
blob, err := v.AsString()
if err != nil {
return err
}
r.PeerlistBlob = blob
}
return nil
}
// ValidateHandshakeResponse verifies that a remote peer's handshake response
// matches the expected network and satisfies the minimum build version gate.
func ValidateHandshakeResponse(resp *HandshakeResponse, expectedNetworkID [16]byte, isTestnet bool) error {
if resp.NodeData.NetworkID != expectedNetworkID {
return fmt.Errorf("p2p: peer network id %x does not match expected %x",
resp.NodeData.NetworkID, expectedNetworkID)
}
if !MeetsMinimumBuildVersion(resp.PayloadData.ClientVersion, isTestnet) {
minBuild := MinimumRequiredBuildVersion(isTestnet)
return fmt.Errorf("p2p: peer build %q below minimum %d",
resp.PayloadData.ClientVersion, minBuild)
}
return nil
}