//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 chain import ( "context" "crypto/rand" "encoding/binary" "net" "net/http" "testing" "time" "github.com/stretchr/testify/require" "dappco.re/go/core/blockchain/config" "dappco.re/go/core/blockchain/p2p" "dappco.re/go/core/blockchain/rpc" "dappco.re/go/core/blockchain/types" levin "dappco.re/go/core/p2p/node/levin" store "dappco.re/go/core/store" ) const testnetRPCAddr = "http://localhost:46941" func TestIntegration_SyncFirst10Blocks(t *testing.T) { client := rpc.NewClientWithHTTP(testnetRPCAddr, &http.Client{Timeout: 30 * time.Second}) // Check daemon is reachable. remoteHeight, err := client.GetHeight() if err != nil { t.Skipf("testnet daemon not reachable at %s: %v", testnetRPCAddr, err) } t.Logf("testnet height: %d", remoteHeight) s, err := store.New(":memory:") if err != nil { t.Fatalf("store.New: %v", err) } defer s.Close() c := New(s) // Sync first 10 blocks (or fewer if chain is shorter). targetHeight := uint64(10) if remoteHeight < targetHeight { targetHeight = remoteHeight } // Sync in a loop, stopping early. for { h, _ := c.Height() if h >= targetHeight { break } if err := c.Sync(context.Background(), client, DefaultSyncOptions()); err != nil { t.Fatalf("Sync: %v", err) } } // Verify genesis block. _, genMeta, err := c.GetBlockByHeight(0) if err != nil { t.Fatalf("GetBlockByHeight(0): %v", err) } expectedHash, _ := types.HashFromHex(GenesisHash) if genMeta.Hash != expectedHash { t.Errorf("genesis hash: got %s, want %s", genMeta.Hash, expectedHash) } t.Logf("genesis block verified: %s", genMeta.Hash) // Verify chain height. finalHeight, _ := c.Height() t.Logf("synced %d blocks", finalHeight) if finalHeight < targetHeight { t.Errorf("expected at least %d blocks, got %d", targetHeight, finalHeight) } // Verify blocks are sequential. for i := uint64(1); i < finalHeight; i++ { _, meta, err := c.GetBlockByHeight(i) if err != nil { t.Fatalf("GetBlockByHeight(%d): %v", i, err) } _, prevMeta, err := c.GetBlockByHeight(i - 1) if err != nil { t.Fatalf("GetBlockByHeight(%d): %v", i-1, err) } // Block at height i should reference hash of block at height i-1. if meta.Height != i { t.Errorf("block %d: height %d", i, meta.Height) } _ = prevMeta // linkage verified during sync } } func TestIntegration_SyncToTip(t *testing.T) { if testing.Short() { t.Skip("skipping long sync test in short mode") } client := rpc.NewClientWithHTTP(testnetRPCAddr, &http.Client{Timeout: 60 * time.Second}) remoteHeight, err := client.GetHeight() if err != nil { t.Skipf("testnet daemon not reachable at %s: %v", testnetRPCAddr, err) } t.Logf("testnet height: %d", remoteHeight) s, err := store.New(":memory:") require.NoError(t, err) defer s.Close() c := New(s) opts := SyncOptions{ VerifySignatures: false, // first pass: no sigs Forks: config.TestnetForks, } err = c.Sync(context.Background(), client, opts) require.NoError(t, err) finalHeight, _ := c.Height() t.Logf("synced %d blocks", finalHeight) require.Equal(t, remoteHeight, finalHeight) // Verify genesis. _, genMeta, err := c.GetBlockByHeight(0) require.NoError(t, err) expectedHash, _ := types.HashFromHex(GenesisHash) require.Equal(t, expectedHash, genMeta.Hash) } func TestIntegration_SyncWithSignatures(t *testing.T) { if testing.Short() { t.Skip("skipping long sync test in short mode") } client := rpc.NewClientWithHTTP(testnetRPCAddr, &http.Client{Timeout: 60 * time.Second}) remoteHeight, err := client.GetHeight() if err != nil { t.Skipf("testnet daemon not reachable: %v", err) } s, err := store.New(":memory:") require.NoError(t, err) defer s.Close() c := New(s) opts := SyncOptions{ VerifySignatures: true, Forks: config.TestnetForks, } err = c.Sync(context.Background(), client, opts) require.NoError(t, err) finalHeight, _ := c.Height() t.Logf("synced %d blocks with signature verification", finalHeight) require.Equal(t, remoteHeight, finalHeight) } func TestIntegration_DifficultyMatchesRPC(t *testing.T) { if testing.Short() { t.Skip("skipping difficulty comparison test in short mode") } client := rpc.NewClientWithHTTP(testnetRPCAddr, &http.Client{Timeout: 60 * time.Second}) _, err := client.GetHeight() if err != nil { t.Skipf("testnet daemon not reachable: %v", err) } s, err := store.New(":memory:") require.NoError(t, err) defer s.Close() c := New(s) // Sync a portion of the chain via RPC (which stores daemon-provided difficulty). opts := SyncOptions{ VerifySignatures: false, Forks: config.TestnetForks, } err = c.Sync(context.Background(), client, opts) require.NoError(t, err) finalHeight, _ := c.Height() t.Logf("synced %d blocks, checking difficulty computation", finalHeight) // For each block from height 1 onwards, verify our NextDifficulty matches // the daemon-provided difficulty stored in BlockMeta. mismatches := 0 for h := uint64(1); h < finalHeight; h++ { meta, err := c.getBlockMeta(h) require.NoError(t, err) computed, err := c.NextDifficulty(h, config.TestnetForks) require.NoError(t, err) if computed != meta.Difficulty { if mismatches < 10 { t.Logf("difficulty mismatch at height %d: computed=%d, daemon=%d", h, computed, meta.Difficulty) } mismatches++ } } if mismatches > 0 { t.Errorf("%d/%d blocks have difficulty mismatches", mismatches, finalHeight-1) } else { t.Logf("all %d blocks have matching difficulty", finalHeight-1) } } func TestIntegration_P2PSync(t *testing.T) { if testing.Short() { t.Skip("skipping P2P sync test in short mode") } // Dial testnet daemon P2P port. conn, err := net.DialTimeout("tcp", "localhost:46942", 10*time.Second) if err != nil { t.Skipf("testnet P2P not reachable: %v", err) } defer conn.Close() lc := levin.NewConnection(conn) // Handshake. var peerIDBuf [8]byte rand.Read(peerIDBuf[:]) peerID := binary.LittleEndian.Uint64(peerIDBuf[:]) req := p2p.HandshakeRequest{ NodeData: p2p.NodeData{ NetworkID: config.NetworkIDTestnet, PeerID: peerID, LocalTime: time.Now().Unix(), MyPort: 0, }, PayloadData: p2p.CoreSyncData{ CurrentHeight: 1, ClientVersion: config.ClientVersion, NonPruningMode: true, }, } payload, err := p2p.EncodeHandshakeRequest(&req) require.NoError(t, err) require.NoError(t, lc.WritePacket(p2p.CommandHandshake, payload, true)) hdr, data, err := lc.ReadPacket() require.NoError(t, err) require.Equal(t, uint32(p2p.CommandHandshake), hdr.Command) var resp p2p.HandshakeResponse require.NoError(t, resp.Decode(data)) t.Logf("peer height: %d", resp.PayloadData.CurrentHeight) // Create P2P connection adapter with our local sync state. localSync := p2p.CoreSyncData{ CurrentHeight: 1, ClientVersion: config.ClientVersion, NonPruningMode: true, } p2pConn := NewLevinP2PConn(lc, resp.PayloadData.CurrentHeight, localSync) // Create chain and sync. s, err := store.New(":memory:") require.NoError(t, err) defer s.Close() c := New(s) opts := SyncOptions{ VerifySignatures: false, Forks: config.TestnetForks, } err = c.P2PSync(context.Background(), p2pConn, opts) require.NoError(t, err) finalHeight, _ := c.Height() t.Logf("P2P synced %d blocks", finalHeight) require.Equal(t, resp.PayloadData.CurrentHeight, finalHeight) }