Update go.mod module line, all require/replace directives, and every .go import path from forge.lthn.ai/core/go-blockchain to dappco.re/go/core/blockchain. Add replace directives to bridge dappco.re paths to existing forge.lthn.ai registry during migration. Update CLAUDE.md, README, and docs to reflect the new module path. Co-Authored-By: Virgil <virgil@lethean.io>
295 lines
7.4 KiB
Go
295 lines
7.4 KiB
Go
//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)
|
|
}
|