//go:build integration // 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 ( "crypto/rand" "encoding/binary" "encoding/hex" "net" "testing" "time" "dappco.re/go/core/blockchain/config" "dappco.re/go/core/p2p/node/levin" "github.com/stretchr/testify/require" ) const testnetP2PAddr = "localhost:46942" // TestIntegration_Handshake connects to the C++ testnet daemon, // performs a full handshake, and verifies the response. func TestIntegration_Handshake(t *testing.T) { conn, err := net.DialTimeout("tcp", testnetP2PAddr, 10*time.Second) if err != nil { t.Skipf("testnet daemon not reachable at %s: %v", testnetP2PAddr, err) } defer conn.Close() lc := levin.NewConnection(conn) // Generate a random peer ID. var peerIDBuf [8]byte rand.Read(peerIDBuf[:]) peerID := binary.LittleEndian.Uint64(peerIDBuf[:]) // Build handshake request. req := HandshakeRequest{ NodeData: NodeData{ NetworkID: config.NetworkIDTestnet, PeerID: peerID, LocalTime: time.Now().Unix(), MyPort: 0, // We're not listening }, PayloadData: CoreSyncData{ CurrentHeight: 1, ClientVersion: config.ClientVersion, NonPruningMode: true, }, } payload, err := EncodeHandshakeRequest(&req) if err != nil { t.Fatalf("encode handshake: %v", err) } // Send handshake request. if err := lc.WritePacket(CommandHandshake, payload, true); err != nil { t.Fatalf("write handshake: %v", err) } // Read handshake response. hdr, data, err := lc.ReadPacket() if err != nil { t.Fatalf("read handshake response: %v", err) } if hdr.Command != CommandHandshake { t.Fatalf("response command: got %d, want %d", hdr.Command, CommandHandshake) } // The CryptoNote/Zano daemon handler returns 1 (not 0) on success. if hdr.ReturnCode < 0 { t.Fatalf("return code: got %d (negative = error)", hdr.ReturnCode) } // Parse response. var resp HandshakeResponse if err := resp.Decode(data); err != nil { t.Fatalf("decode response: %v", err) } // Verify network ID matches testnet. if resp.NodeData.NetworkID != config.NetworkIDTestnet { t.Errorf("network_id: got %x, want %x", resp.NodeData.NetworkID, config.NetworkIDTestnet) } // Verify we got a chain height > 0. if resp.PayloadData.CurrentHeight == 0 { t.Error("current_height is 0 — daemon may not be synced") } t.Logf("testnet height: %d", resp.PayloadData.CurrentHeight) t.Logf("testnet top_id: %x", resp.PayloadData.TopID) t.Logf("testnet version: %s", resp.PayloadData.ClientVersion) t.Logf("peerlist: %d bytes (%d entries)", len(resp.PeerlistBlob), len(resp.PeerlistBlob)/PeerlistEntrySize) // --- Ping test --- pingPayload, _ := EncodePingRequest() if err := lc.WritePacket(CommandPing, pingPayload, true); err != nil { t.Fatalf("write ping: %v", err) } hdr, data, err = lc.ReadPacket() if err != nil { t.Fatalf("read ping response: %v", err) } status, remotePeerID, err := DecodePingResponse(data) if err != nil { t.Fatalf("decode ping: %v", err) } if status != "OK" { t.Errorf("ping status: got %q, want %q", status, "OK") } t.Logf("ping OK, remote peer_id: %x", remotePeerID) } // TestIntegration_RequestChainAndGetObjects performs a full chain sync // sequence: handshake, REQUEST_CHAIN with the genesis hash, then // REQUEST_GET_OBJECTS with the first block hash from the chain response. func TestIntegration_RequestChainAndGetObjects(t *testing.T) { conn, err := net.DialTimeout("tcp", testnetP2PAddr, 10*time.Second) if err != nil { t.Skipf("testnet daemon not reachable: %v", err) } defer conn.Close() lc := levin.NewConnection(conn) // --- Handshake first --- var peerIDBuf [8]byte rand.Read(peerIDBuf[:]) peerID := binary.LittleEndian.Uint64(peerIDBuf[:]) req := HandshakeRequest{ NodeData: NodeData{ NetworkID: config.NetworkIDTestnet, PeerID: peerID, LocalTime: time.Now().Unix(), MyPort: 0, }, PayloadData: CoreSyncData{ CurrentHeight: 1, ClientVersion: config.ClientVersion, NonPruningMode: true, }, } payload, err := EncodeHandshakeRequest(&req) require.NoError(t, err) require.NoError(t, lc.WritePacket(CommandHandshake, payload, true)) hdr, _, err := lc.ReadPacket() require.NoError(t, err) require.Equal(t, uint32(CommandHandshake), hdr.Command) // --- Request chain --- genesisHash, _ := hex.DecodeString("cb9d5455ccb79451931003672c405f5e2ac51bff54021aa30bc4499b1ffc4963") chainReq := RequestChain{ BlockIDs: [][]byte{genesisHash}, } chainPayload, err := chainReq.Encode() require.NoError(t, err) require.NoError(t, lc.WritePacket(CommandRequestChain, chainPayload, false)) // Read until we get RESPONSE_CHAIN_ENTRY. The daemon may send // timed_sync or other messages between our request and the response. var chainData []byte for { hdr, data, err := lc.ReadPacket() require.NoError(t, err) if hdr.Command == CommandResponseChain { chainData = data break } t.Logf("skipping command %d", hdr.Command) } var chainResp ResponseChainEntry require.NoError(t, chainResp.Decode(chainData)) t.Logf("chain response: start=%d, total=%d, block_ids=%d", chainResp.StartHeight, chainResp.TotalHeight, len(chainResp.BlockIDs)) require.Greater(t, len(chainResp.BlockIDs), 0) // --- Request first block --- firstHash := chainResp.BlockIDs[0] if len(firstHash) < 32 { t.Fatalf("block hash too short: %d bytes", len(firstHash)) } getReq := RequestGetObjects{ Blocks: [][]byte{firstHash[:32]}, } getPayload, err := getReq.Encode() require.NoError(t, err) require.NoError(t, lc.WritePacket(CommandRequestObjects, getPayload, false)) // Read until RESPONSE_GET_OBJECTS. for { hdr, data, err := lc.ReadPacket() require.NoError(t, err) if hdr.Command == CommandResponseObjects { var getResp ResponseGetObjects require.NoError(t, getResp.Decode(data)) t.Logf("get_objects response: %d blocks, %d missed, height=%d", len(getResp.Blocks), len(getResp.MissedIDs), getResp.CurrentHeight) require.Len(t, getResp.Blocks, 1) require.Greater(t, len(getResp.Blocks[0].Block), 0) t.Logf("block blob: %d bytes", len(getResp.Blocks[0].Block)) break } t.Logf("skipping command %d", hdr.Command) } }