go-blockchain/wire/block_test.go
Claude 6a3f8829cb
feat(wire): Phase 1 wire serialisation — bit-identical to C++ daemon
Add consensus-critical binary serialisation for blocks and transactions,
verified by computing the testnet genesis block hash and matching the C++
daemon output (cb9d5455...4963). Fixes Phase 0 type mismatches (variant
tags, field widths, missing fields) and adds encoder/decoder, tree hash,
and block/transaction hashing.

Key discovery: CryptoNote's get_object_hash(blobdata) prepends
varint(length) before hashing, so BlockHash = Keccak256(varint(len) || blob).

Co-Authored-By: Charon <charon@lethean.io>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 17:16:08 +00:00

178 lines
4.5 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 wire
import (
"bytes"
"testing"
"forge.lthn.ai/core/go-blockchain/types"
)
// testnetGenesisHeader returns the genesis block header for the Lethean testnet.
func testnetGenesisHeader() types.BlockHeader {
return types.BlockHeader{
MajorVersion: 1,
Nonce: 101011010221, // CURRENCY_FORMATION_VERSION(100) + 101011010121
PrevID: types.Hash{}, // all zeros
MinorVersion: 0,
Timestamp: 1770897600, // 2026-02-12 12:00:00 UTC
Flags: 0,
}
}
func TestEncodeBlockHeader_Good(t *testing.T) {
h := testnetGenesisHeader()
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeBlockHeader(enc, &h)
if enc.Err() != nil {
t.Fatalf("encode error: %v", enc.Err())
}
data := buf.Bytes()
// Verify structure:
// byte 0: major_version = 0x01
// bytes 1-8: nonce LE
// bytes 9-40: prev_id (32 zeros)
// byte 41: minor_version varint = 0x00
// bytes 42+: timestamp varint
// last byte: flags = 0x00
if data[0] != 0x01 {
t.Errorf("major_version: got 0x%02x, want 0x01", data[0])
}
if data[len(data)-1] != 0x00 {
t.Errorf("flags: got 0x%02x, want 0x00", data[len(data)-1])
}
}
func TestBlockHeaderRoundTrip_Good(t *testing.T) {
h := testnetGenesisHeader()
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeBlockHeader(enc, &h)
if enc.Err() != nil {
t.Fatalf("encode error: %v", enc.Err())
}
dec := NewDecoder(bytes.NewReader(buf.Bytes()))
got := DecodeBlockHeader(dec)
if dec.Err() != nil {
t.Fatalf("decode error: %v", dec.Err())
}
if got.MajorVersion != h.MajorVersion {
t.Errorf("MajorVersion: got %d, want %d", got.MajorVersion, h.MajorVersion)
}
if got.Nonce != h.Nonce {
t.Errorf("Nonce: got %d, want %d", got.Nonce, h.Nonce)
}
if got.PrevID != h.PrevID {
t.Errorf("PrevID: got %x, want %x", got.PrevID, h.PrevID)
}
if got.MinorVersion != h.MinorVersion {
t.Errorf("MinorVersion: got %d, want %d", got.MinorVersion, h.MinorVersion)
}
if got.Timestamp != h.Timestamp {
t.Errorf("Timestamp: got %d, want %d", got.Timestamp, h.Timestamp)
}
if got.Flags != h.Flags {
t.Errorf("Flags: got %d, want %d", got.Flags, h.Flags)
}
}
func TestBlockRoundTrip_Good(t *testing.T) {
// Build the genesis block and round-trip it through EncodeBlock/DecodeBlock.
rawTx := testnetGenesisRawTx()
dec := NewDecoder(bytes.NewReader(rawTx))
minerTx := DecodeTransaction(dec)
if dec.Err() != nil {
t.Fatalf("decode miner tx: %v", dec.Err())
}
block := types.Block{
BlockHeader: testnetGenesisHeader(),
MinerTx: minerTx,
}
// Encode.
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeBlock(enc, &block)
if enc.Err() != nil {
t.Fatalf("encode error: %v", enc.Err())
}
// Decode.
dec2 := NewDecoder(bytes.NewReader(buf.Bytes()))
got := DecodeBlock(dec2)
if dec2.Err() != nil {
t.Fatalf("decode error: %v", dec2.Err())
}
// Re-encode and compare bytes.
var rtBuf bytes.Buffer
enc2 := NewEncoder(&rtBuf)
EncodeBlock(enc2, &got)
if enc2.Err() != nil {
t.Fatalf("re-encode error: %v", enc2.Err())
}
if !bytes.Equal(rtBuf.Bytes(), buf.Bytes()) {
t.Errorf("block round-trip mismatch")
}
// Verify block hash is unchanged after round-trip.
if BlockHash(&got) != BlockHash(&block) {
t.Errorf("block hash changed after round-trip")
}
}
func TestBlockWithTxHashesRoundTrip_Good(t *testing.T) {
block := types.Block{
BlockHeader: testnetGenesisHeader(),
MinerTx: types.Transaction{
Version: 1,
Vin: []types.TxInput{types.TxInputGenesis{Height: 0}},
Vout: []types.TxOutput{types.TxOutputBare{
Amount: 1000,
Target: types.TxOutToKey{Key: types.PublicKey{0xAA}},
}},
Extra: EncodeVarint(0),
Attachment: EncodeVarint(0),
},
TxHashes: []types.Hash{
{0x01, 0x02, 0x03},
{0xDE, 0xAD, 0xBE, 0xEF},
},
}
var buf bytes.Buffer
enc := NewEncoder(&buf)
EncodeBlock(enc, &block)
if enc.Err() != nil {
t.Fatalf("encode error: %v", enc.Err())
}
dec := NewDecoder(bytes.NewReader(buf.Bytes()))
got := DecodeBlock(dec)
if dec.Err() != nil {
t.Fatalf("decode error: %v", dec.Err())
}
if len(got.TxHashes) != 2 {
t.Fatalf("tx_hashes count: got %d, want 2", len(got.TxHashes))
}
if got.TxHashes[0] != block.TxHashes[0] {
t.Errorf("tx_hashes[0]: got %x, want %x", got.TxHashes[0], block.TxHashes[0])
}
if got.TxHashes[1] != block.TxHashes[1] {
t.Errorf("tx_hashes[1]: got %x, want %x", got.TxHashes[1], block.TxHashes[1])
}
}