go-blockchain/p2p/handshake_test.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

210 lines
6.1 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"
"strings"
"testing"
"dappco.re/go/core/blockchain/config"
"dappco.re/go/core/p2p/node/levin"
)
func TestEncodeHandshakeRequest_Good_Roundtrip(t *testing.T) {
req := HandshakeRequest{
NodeData: NodeData{
NetworkID: config.NetworkIDTestnet,
PeerID: 0xDEADBEEF,
LocalTime: 1708444800,
MyPort: 46942,
},
PayloadData: CoreSyncData{
CurrentHeight: 100,
ClientVersion: "test/0.1",
},
}
data, err := EncodeHandshakeRequest(&req)
if err != nil {
t.Fatalf("encode: %v", err)
}
s, err := levin.DecodeStorage(data)
if err != nil {
t.Fatalf("decode storage: %v", err)
}
var got HandshakeRequest
if err := got.UnmarshalSection(s); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if got.NodeData.NetworkID != config.NetworkIDTestnet {
t.Errorf("network_id mismatch")
}
if got.NodeData.PeerID != 0xDEADBEEF {
t.Errorf("peer_id: got %x, want DEADBEEF", got.NodeData.PeerID)
}
if got.NodeData.LocalTime != 1708444800 {
t.Errorf("local_time: got %d, want 1708444800", got.NodeData.LocalTime)
}
if got.NodeData.MyPort != 46942 {
t.Errorf("my_port: got %d, want 46942", got.NodeData.MyPort)
}
if got.PayloadData.CurrentHeight != 100 {
t.Errorf("height: got %d, want 100", got.PayloadData.CurrentHeight)
}
if got.PayloadData.ClientVersion != "test/0.1" {
t.Errorf("client_version: got %q, want %q", got.PayloadData.ClientVersion, "test/0.1")
}
}
func TestDecodeHandshakeResponse_Good_WithPeerlist(t *testing.T) {
// Build a response section manually.
nodeData := levin.Section{
"network_id": levin.StringVal(config.NetworkIDTestnet[:]),
"peer_id": levin.Uint64Val(42),
"local_time": levin.Int64Val(1708444800),
"my_port": levin.Uint32Val(46942),
}
syncData := CoreSyncData{
CurrentHeight: 6300,
ClientVersion: "Zano/2.0",
}
// Pack 2 peerlist entries into a single blob.
peerBlob := make([]byte, 48) // 2 x 24 bytes
// Entry 1: ip=10.0.0.1 (0x0100000A LE), port=46942, id=1, last_seen=1000
binary.LittleEndian.PutUint32(peerBlob[0:4], 0x0100000A) // 10.0.0.1
binary.LittleEndian.PutUint32(peerBlob[4:8], 46942)
binary.LittleEndian.PutUint64(peerBlob[8:16], 1)
binary.LittleEndian.PutUint64(peerBlob[16:24], 1000)
// Entry 2: ip=192.168.1.1, port=36942, id=2, last_seen=2000
binary.LittleEndian.PutUint32(peerBlob[24:28], 0x0101A8C0) // 192.168.1.1
binary.LittleEndian.PutUint32(peerBlob[28:32], 36942)
binary.LittleEndian.PutUint64(peerBlob[32:40], 2)
binary.LittleEndian.PutUint64(peerBlob[40:48], 2000)
s := levin.Section{
"node_data": levin.ObjectVal(nodeData),
"payload_data": levin.ObjectVal(syncData.MarshalSection()),
"local_peerlist": levin.StringVal(peerBlob),
}
data, err := levin.EncodeStorage(s)
if err != nil {
t.Fatalf("encode storage: %v", err)
}
var resp HandshakeResponse
if err := resp.Decode(data); err != nil {
t.Fatalf("decode: %v", err)
}
if resp.NodeData.PeerID != 42 {
t.Errorf("peer_id: got %d, want 42", resp.NodeData.PeerID)
}
if resp.PayloadData.CurrentHeight != 6300 {
t.Errorf("height: got %d, want 6300", resp.PayloadData.CurrentHeight)
}
if len(resp.PeerlistBlob) != 48 {
t.Fatalf("peerlist: got %d bytes, want 48", len(resp.PeerlistBlob))
}
// Decode the peerlist
entries := DecodePeerlist(resp.PeerlistBlob)
if len(entries) != 2 {
t.Fatalf("peerlist entries: got %d, want 2", len(entries))
}
if entries[0].IP != 0x0100000A {
t.Errorf("entry[0].ip: got %x, want 0100000A", entries[0].IP)
}
if entries[0].Port != 46942 {
t.Errorf("entry[0].port: got %d, want 46942", entries[0].Port)
}
if entries[0].ID != 1 {
t.Errorf("entry[0].id: got %d, want 1", entries[0].ID)
}
if entries[1].LastSeen != 2000 {
t.Errorf("entry[1].last_seen: got %d, want 2000", entries[1].LastSeen)
}
}
func TestNodeData_Good_NetworkIDBlob(t *testing.T) {
nd := NodeData{NetworkID: config.NetworkIDTestnet}
s := nd.MarshalSection()
blob, err := s["network_id"].AsString()
if err != nil {
t.Fatalf("network_id: %v", err)
}
if len(blob) != 16 {
t.Fatalf("network_id blob: got %d bytes, want 16", len(blob))
}
// Byte 10 = P2P_NETWORK_ID_TESTNET_FLAG. In the C++ source, the
// #else (testnet) branch sets this to 0 (counter-intuitive naming).
if blob[10] != 0x00 {
t.Errorf("testnet flag: got %x, want 0x00", blob[10])
}
// Byte 15 = version = 0x64 (100)
if blob[15] != 0x64 {
t.Errorf("version byte: got %x, want 0x64", blob[15])
}
}
func TestDecodePeerlist_Good_EmptyBlob(t *testing.T) {
entries := DecodePeerlist(nil)
if len(entries) != 0 {
t.Errorf("empty peerlist: got %d entries, want 0", len(entries))
}
}
func TestValidateHandshakeResponse_Good(t *testing.T) {
resp := &HandshakeResponse{
NodeData: NodeData{
NetworkID: config.NetworkIDTestnet,
},
PayloadData: CoreSyncData{
ClientVersion: "6.0.1.2[go-blockchain]",
},
}
if err := ValidateHandshakeResponse(resp, config.NetworkIDTestnet, true); err != nil {
t.Fatalf("ValidateHandshakeResponse: %v", err)
}
}
func TestValidateHandshakeResponse_BadNetwork(t *testing.T) {
resp := &HandshakeResponse{
NodeData: NodeData{
NetworkID: config.NetworkIDMainnet,
},
PayloadData: CoreSyncData{
ClientVersion: "6.0.1.2[go-blockchain]",
},
}
err := ValidateHandshakeResponse(resp, config.NetworkIDTestnet, true)
if err == nil {
t.Fatal("ValidateHandshakeResponse: expected network mismatch error")
}
if !strings.Contains(err.Error(), "network id") {
t.Fatalf("ValidateHandshakeResponse error: got %v, want network id mismatch", err)
}
}
func TestValidateHandshakeResponse_BadBuildVersion(t *testing.T) {
resp := &HandshakeResponse{
NodeData: NodeData{
NetworkID: config.NetworkIDMainnet,
},
PayloadData: CoreSyncData{
ClientVersion: "0.0.1.0",
},
}
err := ValidateHandshakeResponse(resp, config.NetworkIDMainnet, false)
if err == nil {
t.Fatal("ValidateHandshakeResponse: expected build version error")
}
if !strings.Contains(err.Error(), "below minimum") {
t.Fatalf("ValidateHandshakeResponse error: got %v, want build minimum failure", err)
}
}