1166 lines
30 KiB
Go
1166 lines
30 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 chain
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"dappco.re/go/core/blockchain/config"
|
|
"dappco.re/go/core/blockchain/rpc"
|
|
"dappco.re/go/core/blockchain/types"
|
|
"dappco.re/go/core/blockchain/wire"
|
|
|
|
store "dappco.re/go/core/store"
|
|
)
|
|
|
|
// makeGenesisBlockBlob creates a minimal genesis block and returns its hex blob and hash.
|
|
func makeGenesisBlockBlob() (hexBlob string, hash types.Hash) {
|
|
blk := types.Block{
|
|
BlockHeader: types.BlockHeader{
|
|
MajorVersion: 1,
|
|
Nonce: 101011010221,
|
|
Timestamp: 1770897600,
|
|
},
|
|
MinerTx: types.Transaction{
|
|
Version: 1,
|
|
Vin: []types.TxInput{types.TxInputGenesis{Height: 0}},
|
|
Vout: []types.TxOutput{
|
|
types.TxOutputBare{
|
|
Amount: 1000000000000,
|
|
Target: types.TxOutToKey{Key: types.PublicKey{0x01}},
|
|
},
|
|
},
|
|
Extra: wire.EncodeVarint(0),
|
|
Attachment: wire.EncodeVarint(0),
|
|
},
|
|
}
|
|
var buf bytes.Buffer
|
|
enc := wire.NewEncoder(&buf)
|
|
wire.EncodeBlock(enc, &blk)
|
|
hexBlob = hex.EncodeToString(buf.Bytes())
|
|
hash = wire.BlockHash(&blk)
|
|
return
|
|
}
|
|
|
|
func TestSyncOptions_Default(t *testing.T) {
|
|
opts := DefaultSyncOptions()
|
|
assert.False(t, opts.VerifySignatures)
|
|
assert.Equal(t, config.MainnetForks, opts.Forks)
|
|
}
|
|
|
|
func TestSync_Good_SingleBlock(t *testing.T) {
|
|
genesisBlob, genesisHash := makeGenesisBlockBlob()
|
|
|
|
// Override genesis hash for this test.
|
|
orig := GenesisHash
|
|
GenesisHash = genesisHash.String()
|
|
t.Cleanup(func() { GenesisHash = orig })
|
|
|
|
// Mock RPC server.
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if r.URL.Path == "/getheight" {
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"height": 1,
|
|
"status": "OK",
|
|
})
|
|
return
|
|
}
|
|
|
|
// JSON-RPC dispatcher.
|
|
var req struct {
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params"`
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
|
|
switch req.Method {
|
|
case "get_blocks_details":
|
|
result := map[string]any{
|
|
"blocks": []map[string]any{{
|
|
"height": uint64(0),
|
|
"timestamp": uint64(1770897600),
|
|
"base_reward": uint64(1000000000000),
|
|
"id": genesisHash.String(),
|
|
"difficulty": "1",
|
|
"type": uint64(1),
|
|
"blob": genesisBlob,
|
|
"transactions_details": []any{},
|
|
}},
|
|
"status": "OK",
|
|
}
|
|
resultBytes, _ := json.Marshal(result)
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": "0",
|
|
"result": json.RawMessage(resultBytes),
|
|
})
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s, _ := store.New(":memory:")
|
|
defer s.Close()
|
|
c := New(s)
|
|
|
|
client := rpc.NewClient(srv.URL)
|
|
|
|
err := c.Sync(context.Background(), client, DefaultSyncOptions())
|
|
if err != nil {
|
|
t.Fatalf("Sync: %v", err)
|
|
}
|
|
|
|
h, _ := c.Height()
|
|
if h != 1 {
|
|
t.Errorf("height after sync: got %d, want 1", h)
|
|
}
|
|
|
|
blk, meta, err := c.GetBlockByHeight(0)
|
|
if err != nil {
|
|
t.Fatalf("GetBlockByHeight(0): %v", err)
|
|
}
|
|
if blk.MajorVersion != 1 {
|
|
t.Errorf("major_version: got %d, want 1", blk.MajorVersion)
|
|
}
|
|
if meta.Hash != genesisHash {
|
|
t.Errorf("hash: got %s, want %s", meta.Hash, genesisHash)
|
|
}
|
|
}
|
|
|
|
func TestSync_Good_TwoBlocks_WithRegularTx(t *testing.T) {
|
|
// --- Build genesis block (block 0) ---
|
|
genesisBlob, genesisHash := makeGenesisBlockBlob()
|
|
|
|
// --- Build regular transaction for block 1 ---
|
|
regularTx := types.Transaction{
|
|
Version: 1,
|
|
Vin: []types.TxInput{
|
|
types.TxInputToKey{
|
|
Amount: 1000000000000,
|
|
KeyOffsets: []types.TxOutRef{{
|
|
Tag: types.RefTypeGlobalIndex,
|
|
GlobalIndex: 0,
|
|
}},
|
|
KeyImage: types.KeyImage{0xaa, 0xbb, 0xcc},
|
|
EtcDetails: wire.EncodeVarint(0),
|
|
},
|
|
},
|
|
Vout: []types.TxOutput{
|
|
types.TxOutputBare{
|
|
Amount: 900000000000,
|
|
Target: types.TxOutToKey{Key: types.PublicKey{0x02}},
|
|
},
|
|
},
|
|
Extra: wire.EncodeVarint(0),
|
|
Attachment: wire.EncodeVarint(0),
|
|
}
|
|
|
|
// Wire-encode the regular tx to get its blob and hash.
|
|
var txBuf bytes.Buffer
|
|
txEnc := wire.NewEncoder(&txBuf)
|
|
wire.EncodeTransaction(txEnc, ®ularTx)
|
|
regularTxBlob := hex.EncodeToString(txBuf.Bytes())
|
|
regularTxHash := wire.TransactionHash(®ularTx)
|
|
|
|
// --- Build block 1 ---
|
|
minerTx1 := testCoinbaseTx(1)
|
|
block1 := types.Block{
|
|
BlockHeader: types.BlockHeader{
|
|
MajorVersion: 1,
|
|
Nonce: 42,
|
|
PrevID: genesisHash,
|
|
Timestamp: 1770897720,
|
|
},
|
|
MinerTx: minerTx1,
|
|
TxHashes: []types.Hash{regularTxHash},
|
|
}
|
|
|
|
var blk1Buf bytes.Buffer
|
|
blk1Enc := wire.NewEncoder(&blk1Buf)
|
|
wire.EncodeBlock(blk1Enc, &block1)
|
|
block1Blob := hex.EncodeToString(blk1Buf.Bytes())
|
|
block1Hash := wire.BlockHash(&block1)
|
|
|
|
// Override genesis hash for this test.
|
|
orig := GenesisHash
|
|
GenesisHash = genesisHash.String()
|
|
t.Cleanup(func() { GenesisHash = orig })
|
|
|
|
// Track which batch the server has served (to handle the sync loop).
|
|
callCount := 0
|
|
|
|
// Mock RPC server returning 2 blocks.
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if r.URL.Path == "/getheight" {
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"height": 2,
|
|
"status": "OK",
|
|
})
|
|
return
|
|
}
|
|
|
|
// JSON-RPC dispatcher.
|
|
var req struct {
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params"`
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
|
|
switch req.Method {
|
|
case "get_blocks_details":
|
|
var params struct {
|
|
HeightStart uint64 `json:"height_start"`
|
|
Count uint64 `json:"count"`
|
|
}
|
|
json.Unmarshal(req.Params, ¶ms)
|
|
|
|
var blocks []map[string]any
|
|
|
|
// Return blocks based on the requested start height.
|
|
if params.HeightStart == 0 {
|
|
callCount++
|
|
blocks = []map[string]any{
|
|
{
|
|
"height": uint64(0),
|
|
"timestamp": uint64(1770897600),
|
|
"base_reward": uint64(1000000000000),
|
|
"id": genesisHash.String(),
|
|
"difficulty": "1",
|
|
"type": uint64(1),
|
|
"blob": genesisBlob,
|
|
"transactions_details": []any{},
|
|
},
|
|
{
|
|
"height": uint64(1),
|
|
"timestamp": uint64(1770897720),
|
|
"base_reward": uint64(1000000),
|
|
"id": block1Hash.String(),
|
|
"difficulty": "100",
|
|
"type": uint64(1),
|
|
"blob": block1Blob,
|
|
"transactions_details": []map[string]any{
|
|
{
|
|
"id": regularTxHash.String(),
|
|
"blob": regularTxBlob,
|
|
"fee": uint64(100000000000),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
result := map[string]any{
|
|
"blocks": blocks,
|
|
"status": "OK",
|
|
}
|
|
resultBytes, _ := json.Marshal(result)
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": "0",
|
|
"result": json.RawMessage(resultBytes),
|
|
})
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s, _ := store.New(":memory:")
|
|
defer s.Close()
|
|
c := New(s)
|
|
|
|
client := rpc.NewClient(srv.URL)
|
|
|
|
err := c.Sync(context.Background(), client, DefaultSyncOptions())
|
|
if err != nil {
|
|
t.Fatalf("Sync: %v", err)
|
|
}
|
|
|
|
// Verify height is 2.
|
|
h, _ := c.Height()
|
|
if h != 2 {
|
|
t.Errorf("height after sync: got %d, want 2", h)
|
|
}
|
|
|
|
// Verify block 1 can be retrieved by height.
|
|
gotBlk, gotMeta, err := c.GetBlockByHeight(1)
|
|
if err != nil {
|
|
t.Fatalf("GetBlockByHeight(1): %v", err)
|
|
}
|
|
if gotBlk.MajorVersion != 1 {
|
|
t.Errorf("block 1 major_version: got %d, want 1", gotBlk.MajorVersion)
|
|
}
|
|
if gotMeta.Hash != block1Hash {
|
|
t.Errorf("block 1 hash: got %s, want %s", gotMeta.Hash, block1Hash)
|
|
}
|
|
if gotMeta.CumulativeDiff != 101 { // genesis(1) + block1(100)
|
|
t.Errorf("block 1 cumulative_diff: got %d, want 101", gotMeta.CumulativeDiff)
|
|
}
|
|
|
|
// Verify block 1 can be retrieved by hash.
|
|
_, gotMeta2, err := c.GetBlockByHash(block1Hash)
|
|
if err != nil {
|
|
t.Fatalf("GetBlockByHash(block1): %v", err)
|
|
}
|
|
if gotMeta2.Height != 1 {
|
|
t.Errorf("block 1 height from hash lookup: got %d, want 1", gotMeta2.Height)
|
|
}
|
|
|
|
// Verify the regular transaction was stored.
|
|
gotTx, gotTxMeta, err := c.GetTransaction(regularTxHash)
|
|
if err != nil {
|
|
t.Fatalf("GetTransaction(regularTx): %v", err)
|
|
}
|
|
if gotTx.Version != 1 {
|
|
t.Errorf("regular tx version: got %d, want 1", gotTx.Version)
|
|
}
|
|
if gotTxMeta.KeeperBlock != 1 {
|
|
t.Errorf("regular tx keeper_block: got %d, want 1", gotTxMeta.KeeperBlock)
|
|
}
|
|
if !c.HasTransaction(regularTxHash) {
|
|
t.Error("HasTransaction(regularTx): got false, want true")
|
|
}
|
|
|
|
// Verify the key image was marked as spent.
|
|
ki := types.KeyImage{0xaa, 0xbb, 0xcc}
|
|
spent, err := c.IsSpent(ki)
|
|
if err != nil {
|
|
t.Fatalf("IsSpent: %v", err)
|
|
}
|
|
if !spent {
|
|
t.Error("IsSpent(key_image): got false, want true")
|
|
}
|
|
|
|
// Verify output indexing: genesis miner tx output + block 1 miner tx output
|
|
// + regular tx output.
|
|
// Genesis miner tx has 1 output at amount 1000000000000.
|
|
// Regular tx has 1 output at amount 900000000000.
|
|
count, err := c.OutputCount(1000000000000)
|
|
if err != nil {
|
|
t.Fatalf("OutputCount(1000000000000): %v", err)
|
|
}
|
|
if count != 1 { // only genesis miner tx
|
|
t.Errorf("output count for 1000000000000: got %d, want 1", count)
|
|
}
|
|
|
|
count, err = c.OutputCount(900000000000)
|
|
if err != nil {
|
|
t.Fatalf("OutputCount(900000000000): %v", err)
|
|
}
|
|
if count != 1 { // regular tx output
|
|
t.Errorf("output count for 900000000000: got %d, want 1", count)
|
|
}
|
|
|
|
// Verify top block is block 1.
|
|
_, topMeta, err := c.TopBlock()
|
|
if err != nil {
|
|
t.Fatalf("TopBlock: %v", err)
|
|
}
|
|
if topMeta.Height != 1 {
|
|
t.Errorf("top block height: got %d, want 1", topMeta.Height)
|
|
}
|
|
}
|
|
|
|
func TestSync_Good_AlreadySynced(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"height": 0,
|
|
"status": "OK",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s, _ := store.New(":memory:")
|
|
defer s.Close()
|
|
c := New(s)
|
|
|
|
client := rpc.NewClient(srv.URL)
|
|
err := c.Sync(context.Background(), client, DefaultSyncOptions())
|
|
if err != nil {
|
|
t.Fatalf("Sync on empty: %v", err)
|
|
}
|
|
|
|
h, _ := c.Height()
|
|
if h != 0 {
|
|
t.Errorf("height: got %d, want 0", h)
|
|
}
|
|
}
|
|
|
|
func TestSync_Bad_GetHeightError(t *testing.T) {
|
|
// Server that returns HTTP 500 on /getheight.
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s, _ := store.New(":memory:")
|
|
defer s.Close()
|
|
c := New(s)
|
|
|
|
client := rpc.NewClient(srv.URL)
|
|
err := c.Sync(context.Background(), client, DefaultSyncOptions())
|
|
if err == nil {
|
|
t.Fatal("Sync: expected error from bad getheight, got nil")
|
|
}
|
|
}
|
|
|
|
func TestSync_Bad_FetchBlocksError(t *testing.T) {
|
|
// Server that succeeds on /getheight but fails on JSON-RPC.
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if r.URL.Path == "/getheight" {
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"height": 1,
|
|
"status": "OK",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Return a JSON-RPC error for get_blocks_details.
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": "0",
|
|
"error": map[string]any{
|
|
"code": -1,
|
|
"message": "internal error",
|
|
},
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s, _ := store.New(":memory:")
|
|
defer s.Close()
|
|
c := New(s)
|
|
|
|
client := rpc.NewClient(srv.URL)
|
|
err := c.Sync(context.Background(), client, DefaultSyncOptions())
|
|
if err == nil {
|
|
t.Fatal("Sync: expected error from bad get_blocks_details, got nil")
|
|
}
|
|
}
|
|
|
|
func TestSync_Bad_GenesisHashMismatch(t *testing.T) {
|
|
genesisBlob, genesisHash := makeGenesisBlockBlob()
|
|
|
|
// Deliberately do NOT override GenesisHash, so it mismatches.
|
|
// The default GenesisHash is a different value.
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if r.URL.Path == "/getheight" {
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"height": 1,
|
|
"status": "OK",
|
|
})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params"`
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
|
|
switch req.Method {
|
|
case "get_blocks_details":
|
|
result := map[string]any{
|
|
"blocks": []map[string]any{{
|
|
"height": uint64(0),
|
|
"timestamp": uint64(1770897600),
|
|
"base_reward": uint64(1000000000000),
|
|
"id": genesisHash.String(),
|
|
"difficulty": "1",
|
|
"type": uint64(1),
|
|
"blob": genesisBlob,
|
|
"transactions_details": []any{},
|
|
}},
|
|
"status": "OK",
|
|
}
|
|
resultBytes, _ := json.Marshal(result)
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": "0",
|
|
"result": json.RawMessage(resultBytes),
|
|
})
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s, _ := store.New(":memory:")
|
|
defer s.Close()
|
|
c := New(s)
|
|
|
|
client := rpc.NewClient(srv.URL)
|
|
err := c.Sync(context.Background(), client, DefaultSyncOptions())
|
|
if err == nil {
|
|
t.Fatal("Sync: expected genesis hash mismatch error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestSync_Bad_BlockHashMismatch(t *testing.T) {
|
|
genesisBlob, genesisHash := makeGenesisBlockBlob()
|
|
|
|
// Set genesis hash to a value that differs from the actual computed hash.
|
|
wrongHash := "0000000000000000000000000000000000000000000000000000000000000001"
|
|
orig := GenesisHash
|
|
GenesisHash = wrongHash
|
|
t.Cleanup(func() { GenesisHash = orig })
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if r.URL.Path == "/getheight" {
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"height": 1,
|
|
"status": "OK",
|
|
})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params"`
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
|
|
switch req.Method {
|
|
case "get_blocks_details":
|
|
result := map[string]any{
|
|
"blocks": []map[string]any{{
|
|
"height": uint64(0),
|
|
"timestamp": uint64(1770897600),
|
|
"base_reward": uint64(0),
|
|
"id": wrongHash, // wrong: doesn't match computed hash
|
|
"difficulty": "1",
|
|
"type": uint64(1),
|
|
"blob": genesisBlob,
|
|
"transactions_details": []any{},
|
|
}},
|
|
"status": "OK",
|
|
}
|
|
resultBytes, _ := json.Marshal(result)
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": "0",
|
|
"result": json.RawMessage(resultBytes),
|
|
})
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s, _ := store.New(":memory:")
|
|
defer s.Close()
|
|
c := New(s)
|
|
|
|
client := rpc.NewClient(srv.URL)
|
|
err := c.Sync(context.Background(), client, DefaultSyncOptions())
|
|
if err == nil {
|
|
t.Fatal("Sync: expected block hash mismatch error, got nil")
|
|
}
|
|
|
|
// Verify the real genesis hash is unaffected.
|
|
_ = genesisHash
|
|
}
|
|
|
|
func TestSync_Bad_InvalidRegularTxBlob(t *testing.T) {
|
|
genesisBlob, genesisHash := makeGenesisBlockBlob()
|
|
|
|
// Build block 1 with a tx hash but the RPC will return a bad tx blob.
|
|
fakeTxHash := types.Hash{0xde, 0xad}
|
|
minerTx1 := testCoinbaseTx(1)
|
|
block1 := types.Block{
|
|
BlockHeader: types.BlockHeader{
|
|
MajorVersion: 1,
|
|
Nonce: 42,
|
|
PrevID: genesisHash,
|
|
Timestamp: 1770897720,
|
|
},
|
|
MinerTx: minerTx1,
|
|
TxHashes: []types.Hash{fakeTxHash},
|
|
}
|
|
|
|
var blk1Buf bytes.Buffer
|
|
blk1Enc := wire.NewEncoder(&blk1Buf)
|
|
wire.EncodeBlock(blk1Enc, &block1)
|
|
block1Blob := hex.EncodeToString(blk1Buf.Bytes())
|
|
block1Hash := wire.BlockHash(&block1)
|
|
|
|
orig := GenesisHash
|
|
GenesisHash = genesisHash.String()
|
|
t.Cleanup(func() { GenesisHash = orig })
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if r.URL.Path == "/getheight" {
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"height": 2,
|
|
"status": "OK",
|
|
})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params"`
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
|
|
switch req.Method {
|
|
case "get_blocks_details":
|
|
result := map[string]any{
|
|
"blocks": []map[string]any{
|
|
{
|
|
"height": uint64(0),
|
|
"timestamp": uint64(1770897600),
|
|
"base_reward": uint64(1000000000000),
|
|
"id": genesisHash.String(),
|
|
"difficulty": "1",
|
|
"type": uint64(1),
|
|
"blob": genesisBlob,
|
|
"transactions_details": []any{},
|
|
},
|
|
{
|
|
"height": uint64(1),
|
|
"timestamp": uint64(1770897720),
|
|
"base_reward": uint64(1000000),
|
|
"id": block1Hash.String(),
|
|
"difficulty": "100",
|
|
"type": uint64(1),
|
|
"blob": block1Blob,
|
|
"transactions_details": []map[string]any{
|
|
{
|
|
"id": fakeTxHash.String(),
|
|
"blob": "badc0de", // invalid: odd length hex
|
|
"fee": uint64(0),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"status": "OK",
|
|
}
|
|
resultBytes, _ := json.Marshal(result)
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": "0",
|
|
"result": json.RawMessage(resultBytes),
|
|
})
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s, _ := store.New(":memory:")
|
|
defer s.Close()
|
|
c := New(s)
|
|
|
|
client := rpc.NewClient(srv.URL)
|
|
err := c.Sync(context.Background(), client, DefaultSyncOptions())
|
|
if err == nil {
|
|
t.Fatal("Sync: expected error from invalid tx blob, got nil")
|
|
}
|
|
}
|
|
|
|
func TestSync_Bad_InvalidBlockBlob(t *testing.T) {
|
|
// Override genesis hash to a value that matches a fake block ID.
|
|
fakeHash := "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
|
|
orig := GenesisHash
|
|
GenesisHash = fakeHash
|
|
t.Cleanup(func() { GenesisHash = orig })
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if r.URL.Path == "/getheight" {
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"height": 1,
|
|
"status": "OK",
|
|
})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params"`
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
|
|
switch req.Method {
|
|
case "get_blocks_details":
|
|
result := map[string]any{
|
|
"blocks": []map[string]any{{
|
|
"height": uint64(0),
|
|
"timestamp": uint64(1770897600),
|
|
"base_reward": uint64(0),
|
|
"id": fakeHash,
|
|
"difficulty": "1",
|
|
"type": uint64(1),
|
|
"blob": "deadbeef", // invalid wire data
|
|
"transactions_details": []any{},
|
|
}},
|
|
"status": "OK",
|
|
}
|
|
resultBytes, _ := json.Marshal(result)
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": "0",
|
|
"result": json.RawMessage(resultBytes),
|
|
})
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s, _ := store.New(":memory:")
|
|
defer s.Close()
|
|
c := New(s)
|
|
|
|
client := rpc.NewClient(srv.URL)
|
|
err := c.Sync(context.Background(), client, DefaultSyncOptions())
|
|
if err == nil {
|
|
t.Fatal("Sync: expected error from invalid block blob, got nil")
|
|
}
|
|
}
|
|
|
|
func TestSync_Bad_PreHardforkFreeze(t *testing.T) {
|
|
genesisBlob, genesisHash := makeGenesisBlockBlob()
|
|
genesisBytes, err := hex.DecodeString(genesisBlob)
|
|
if err != nil {
|
|
t.Fatalf("decode genesis blob: %v", err)
|
|
}
|
|
|
|
regularTx := types.Transaction{
|
|
Version: 1,
|
|
Vin: []types.TxInput{
|
|
types.TxInputToKey{
|
|
Amount: 1000000000000,
|
|
KeyOffsets: []types.TxOutRef{{
|
|
Tag: types.RefTypeGlobalIndex,
|
|
GlobalIndex: 0,
|
|
}},
|
|
KeyImage: types.KeyImage{0xaa, 0xbb, 0xcc},
|
|
EtcDetails: wire.EncodeVarint(0),
|
|
},
|
|
},
|
|
Vout: []types.TxOutput{
|
|
types.TxOutputBare{
|
|
Amount: 900000000000,
|
|
Target: types.TxOutToKey{Key: types.PublicKey{0x02}},
|
|
},
|
|
},
|
|
Extra: wire.EncodeVarint(0),
|
|
Attachment: wire.EncodeVarint(0),
|
|
}
|
|
|
|
var txBuf bytes.Buffer
|
|
txEnc := wire.NewEncoder(&txBuf)
|
|
wire.EncodeTransaction(txEnc, ®ularTx)
|
|
regularTxBlob := txBuf.Bytes()
|
|
regularTxHash := wire.TransactionHash(®ularTx)
|
|
|
|
minerTx1 := testCoinbaseTx(1)
|
|
block1 := types.Block{
|
|
BlockHeader: types.BlockHeader{
|
|
MajorVersion: 1,
|
|
Nonce: 42,
|
|
PrevID: genesisHash,
|
|
Timestamp: 1770897720,
|
|
},
|
|
MinerTx: minerTx1,
|
|
TxHashes: []types.Hash{regularTxHash},
|
|
}
|
|
|
|
var blk1Buf bytes.Buffer
|
|
blk1Enc := wire.NewEncoder(&blk1Buf)
|
|
wire.EncodeBlock(blk1Enc, &block1)
|
|
block1Blob := blk1Buf.Bytes()
|
|
|
|
orig := GenesisHash
|
|
GenesisHash = genesisHash.String()
|
|
t.Cleanup(func() { GenesisHash = orig })
|
|
|
|
s, _ := store.New(":memory:")
|
|
defer s.Close()
|
|
c := New(s)
|
|
|
|
opts := SyncOptions{
|
|
Forks: []config.HardFork{
|
|
{Version: config.HF0Initial, Height: 0, Mandatory: true},
|
|
{Version: config.HF5, Height: 2, Mandatory: true},
|
|
},
|
|
}
|
|
|
|
if err := c.processBlockBlobs(genesisBytes, nil, 0, 1, opts); err != nil {
|
|
t.Fatalf("process genesis: %v", err)
|
|
}
|
|
|
|
err = c.processBlockBlobs(block1Blob, [][]byte{regularTxBlob}, 1, 100, opts)
|
|
if err == nil {
|
|
t.Fatal("expected freeze rejection, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "freeze") {
|
|
t.Fatalf("expected freeze error, got %v", err)
|
|
}
|
|
}
|
|
|
|
// testCoinbaseTxV2 creates a v2 (post-HF4) coinbase transaction with Zarcanum outputs.
|
|
func testCoinbaseTxV2(height uint64) types.Transaction {
|
|
return types.Transaction{
|
|
Version: types.VersionPostHF4,
|
|
Vin: []types.TxInput{types.TxInputGenesis{Height: height}},
|
|
Vout: []types.TxOutput{
|
|
types.TxOutputZarcanum{
|
|
StealthAddress: types.PublicKey{0x01},
|
|
ConcealingPoint: types.PublicKey{0x02},
|
|
AmountCommitment: types.PublicKey{0x03},
|
|
BlindedAssetID: types.PublicKey{0x04},
|
|
EncryptedAmount: 1000000,
|
|
MixAttr: 0,
|
|
},
|
|
types.TxOutputZarcanum{
|
|
StealthAddress: types.PublicKey{0x05},
|
|
ConcealingPoint: types.PublicKey{0x06},
|
|
AmountCommitment: types.PublicKey{0x07},
|
|
BlindedAssetID: types.PublicKey{0x08},
|
|
EncryptedAmount: 2000000,
|
|
MixAttr: 0,
|
|
},
|
|
},
|
|
Extra: wire.EncodeVarint(0),
|
|
Attachment: wire.EncodeVarint(0),
|
|
SignaturesRaw: wire.EncodeVarint(0),
|
|
Proofs: wire.EncodeVarint(0),
|
|
}
|
|
}
|
|
|
|
func TestSync_Good_ZCInputKeyImageMarkedSpent(t *testing.T) {
|
|
// --- Build genesis block (block 0) ---
|
|
genesisBlob, genesisHash := makeGenesisBlockBlob()
|
|
|
|
// --- Build a v2 transaction with a TxInputZC for block 1 ---
|
|
zcKeyImage := types.KeyImage{0xdd, 0xee, 0xff}
|
|
zcTx := types.Transaction{
|
|
Version: types.VersionPostHF4,
|
|
Vin: []types.TxInput{
|
|
types.TxInputZC{
|
|
KeyOffsets: []types.TxOutRef{{
|
|
Tag: types.RefTypeGlobalIndex,
|
|
GlobalIndex: 0,
|
|
}},
|
|
KeyImage: zcKeyImage,
|
|
EtcDetails: wire.EncodeVarint(0),
|
|
},
|
|
},
|
|
Vout: []types.TxOutput{
|
|
types.TxOutputZarcanum{
|
|
StealthAddress: types.PublicKey{0x10},
|
|
ConcealingPoint: types.PublicKey{0x11},
|
|
AmountCommitment: types.PublicKey{0x12},
|
|
BlindedAssetID: types.PublicKey{0x13},
|
|
EncryptedAmount: 500000,
|
|
MixAttr: 0,
|
|
},
|
|
types.TxOutputZarcanum{
|
|
StealthAddress: types.PublicKey{0x14},
|
|
ConcealingPoint: types.PublicKey{0x15},
|
|
AmountCommitment: types.PublicKey{0x16},
|
|
BlindedAssetID: types.PublicKey{0x17},
|
|
EncryptedAmount: 400000,
|
|
MixAttr: 0,
|
|
},
|
|
},
|
|
Extra: wire.EncodeVarint(0),
|
|
Attachment: wire.EncodeVarint(0),
|
|
SignaturesRaw: wire.EncodeVarint(0),
|
|
Proofs: wire.EncodeVarint(0),
|
|
}
|
|
|
|
var txBuf bytes.Buffer
|
|
txEnc := wire.NewEncoder(&txBuf)
|
|
wire.EncodeTransaction(txEnc, &zcTx)
|
|
zcTxBlob := hex.EncodeToString(txBuf.Bytes())
|
|
zcTxHash := wire.TransactionHash(&zcTx)
|
|
|
|
// --- Build block 1 (v2 block) ---
|
|
minerTx1 := testCoinbaseTxV2(1)
|
|
block1 := types.Block{
|
|
BlockHeader: types.BlockHeader{
|
|
MajorVersion: 2,
|
|
Nonce: 42,
|
|
PrevID: genesisHash,
|
|
Timestamp: 1770897720,
|
|
},
|
|
MinerTx: minerTx1,
|
|
TxHashes: []types.Hash{zcTxHash},
|
|
}
|
|
|
|
var blk1Buf bytes.Buffer
|
|
blk1Enc := wire.NewEncoder(&blk1Buf)
|
|
wire.EncodeBlock(blk1Enc, &block1)
|
|
block1Blob := hex.EncodeToString(blk1Buf.Bytes())
|
|
block1Hash := wire.BlockHash(&block1)
|
|
|
|
// Override genesis hash for this test.
|
|
orig := GenesisHash
|
|
GenesisHash = genesisHash.String()
|
|
t.Cleanup(func() { GenesisHash = orig })
|
|
|
|
// Mock RPC server returning 2 blocks.
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if r.URL.Path == "/getheight" {
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"height": 2,
|
|
"status": "OK",
|
|
})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params"`
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
|
|
switch req.Method {
|
|
case "get_blocks_details":
|
|
blocks := []map[string]any{
|
|
{
|
|
"height": uint64(0),
|
|
"timestamp": uint64(1770897600),
|
|
"base_reward": uint64(1000000000000),
|
|
"id": genesisHash.String(),
|
|
"difficulty": "1",
|
|
"type": uint64(1),
|
|
"blob": genesisBlob,
|
|
"transactions_details": []any{},
|
|
},
|
|
{
|
|
"height": uint64(1),
|
|
"timestamp": uint64(1770897720),
|
|
"base_reward": uint64(1000000),
|
|
"id": block1Hash.String(),
|
|
"difficulty": "100",
|
|
"type": uint64(1),
|
|
"blob": block1Blob,
|
|
"transactions_details": []map[string]any{
|
|
{
|
|
"id": zcTxHash.String(),
|
|
"blob": zcTxBlob,
|
|
"fee": uint64(0),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := map[string]any{
|
|
"blocks": blocks,
|
|
"status": "OK",
|
|
}
|
|
resultBytes, _ := json.Marshal(result)
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": "0",
|
|
"result": json.RawMessage(resultBytes),
|
|
})
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s, _ := store.New(":memory:")
|
|
defer s.Close()
|
|
c := New(s)
|
|
|
|
client := rpc.NewClient(srv.URL)
|
|
|
|
// Use custom forks where HF4 is active from height 0 so ZC inputs pass validation.
|
|
opts := SyncOptions{
|
|
VerifySignatures: false,
|
|
Forks: []config.HardFork{
|
|
{Version: config.HF1, Height: 0, Mandatory: true},
|
|
{Version: config.HF2, Height: 0, Mandatory: true},
|
|
{Version: config.HF3, Height: 0, Mandatory: true},
|
|
{Version: config.HF4Zarcanum, Height: 0, Mandatory: true},
|
|
},
|
|
}
|
|
|
|
err := c.Sync(context.Background(), client, opts)
|
|
if err != nil {
|
|
t.Fatalf("Sync: %v", err)
|
|
}
|
|
|
|
// Verify height is 2.
|
|
h, _ := c.Height()
|
|
if h != 2 {
|
|
t.Errorf("height after sync: got %d, want 2", h)
|
|
}
|
|
|
|
// Verify the ZC key image was marked as spent.
|
|
spent, err := c.IsSpent(zcKeyImage)
|
|
if err != nil {
|
|
t.Fatalf("IsSpent: %v", err)
|
|
}
|
|
if !spent {
|
|
t.Error("IsSpent(zc_key_image): got false, want true")
|
|
}
|
|
}
|
|
|
|
func TestSync_Good_HTLCInputKeyImageMarkedSpent(t *testing.T) {
|
|
genesisBlob, genesisHash := makeGenesisBlockBlob()
|
|
|
|
htlcKeyImage := types.KeyImage{0x44, 0x55, 0x66}
|
|
htlcTx := types.Transaction{
|
|
Version: 1,
|
|
Vin: []types.TxInput{
|
|
types.TxInputHTLC{
|
|
HTLCOrigin: "contract-1",
|
|
Amount: 1000000000000,
|
|
KeyOffsets: []types.TxOutRef{{
|
|
Tag: types.RefTypeGlobalIndex,
|
|
GlobalIndex: 0,
|
|
}},
|
|
KeyImage: htlcKeyImage,
|
|
EtcDetails: wire.EncodeVarint(0),
|
|
},
|
|
},
|
|
Vout: []types.TxOutput{
|
|
types.TxOutputBare{
|
|
Amount: 900000000000,
|
|
Target: types.TxOutToKey{Key: types.PublicKey{0x21}},
|
|
},
|
|
},
|
|
Extra: wire.EncodeVarint(0),
|
|
Attachment: wire.EncodeVarint(0),
|
|
}
|
|
|
|
var txBuf bytes.Buffer
|
|
txEnc := wire.NewEncoder(&txBuf)
|
|
wire.EncodeTransaction(txEnc, &htlcTx)
|
|
htlcTxBlob := hex.EncodeToString(txBuf.Bytes())
|
|
htlcTxHash := wire.TransactionHash(&htlcTx)
|
|
|
|
minerTx1 := testCoinbaseTx(1)
|
|
block1 := types.Block{
|
|
BlockHeader: types.BlockHeader{
|
|
MajorVersion: 1,
|
|
Nonce: 42,
|
|
PrevID: genesisHash,
|
|
Timestamp: 1770897720,
|
|
},
|
|
MinerTx: minerTx1,
|
|
TxHashes: []types.Hash{htlcTxHash},
|
|
}
|
|
|
|
var blk1Buf bytes.Buffer
|
|
blk1Enc := wire.NewEncoder(&blk1Buf)
|
|
wire.EncodeBlock(blk1Enc, &block1)
|
|
block1Blob := hex.EncodeToString(blk1Buf.Bytes())
|
|
block1Hash := wire.BlockHash(&block1)
|
|
|
|
orig := GenesisHash
|
|
GenesisHash = genesisHash.String()
|
|
t.Cleanup(func() { GenesisHash = orig })
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if r.URL.Path == "/getheight" {
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"height": 2,
|
|
"status": "OK",
|
|
})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params"`
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
|
|
switch req.Method {
|
|
case "get_blocks_details":
|
|
blocks := []map[string]any{
|
|
{
|
|
"height": uint64(0),
|
|
"timestamp": uint64(1770897600),
|
|
"base_reward": uint64(1000000000000),
|
|
"id": genesisHash.String(),
|
|
"difficulty": "1",
|
|
"type": uint64(1),
|
|
"blob": genesisBlob,
|
|
"transactions_details": []any{},
|
|
},
|
|
{
|
|
"height": uint64(1),
|
|
"timestamp": uint64(1770897720),
|
|
"base_reward": uint64(1000000),
|
|
"id": block1Hash.String(),
|
|
"difficulty": "100",
|
|
"type": uint64(1),
|
|
"blob": block1Blob,
|
|
"transactions_details": []map[string]any{
|
|
{
|
|
"id": htlcTxHash.String(),
|
|
"blob": htlcTxBlob,
|
|
"fee": uint64(100000000000),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := map[string]any{
|
|
"blocks": blocks,
|
|
"status": "OK",
|
|
}
|
|
resultBytes, _ := json.Marshal(result)
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": "0",
|
|
"result": json.RawMessage(resultBytes),
|
|
})
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s, _ := store.New(":memory:")
|
|
defer s.Close()
|
|
c := New(s)
|
|
|
|
client := rpc.NewClient(srv.URL)
|
|
opts := SyncOptions{
|
|
VerifySignatures: false,
|
|
Forks: []config.HardFork{
|
|
{Version: config.HF1, Height: 0, Mandatory: true},
|
|
{Version: config.HF2, Height: 0, Mandatory: true},
|
|
},
|
|
}
|
|
|
|
err := c.Sync(context.Background(), client, opts)
|
|
if err != nil {
|
|
t.Fatalf("Sync: %v", err)
|
|
}
|
|
|
|
spent, err := c.IsSpent(htlcKeyImage)
|
|
if err != nil {
|
|
t.Fatalf("IsSpent: %v", err)
|
|
}
|
|
if !spent {
|
|
t.Error("IsSpent(htlc_key_image): got false, want true")
|
|
}
|
|
}
|