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>
131 lines
4.1 KiB
Go
131 lines
4.1 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"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"testing"
|
|
|
|
"forge.lthn.ai/core/go-blockchain/types"
|
|
)
|
|
|
|
// testnetGenesisRawTx returns the raw wire bytes of the testnet genesis
|
|
// coinbase transaction, constructed from the packed C struct in
|
|
// genesis/_genesis_tn.cpp.gen.
|
|
func testnetGenesisRawTx() []byte {
|
|
u64s := [25]uint64{
|
|
0xa080800100000101, 0x03018ae3c8e0c8cf, 0x7b0287d2a2218485, 0x720c5b385edbe3dd,
|
|
0x178e7c64d18a598f, 0x98bb613ff63e6d03, 0x3814f971f9160500, 0x1c595f65f55d872e,
|
|
0x835e5fd926b1f78d, 0xf597c7f5a33b6131, 0x2074496b139c8341, 0x64612073656b6174,
|
|
0x20656761746e6176, 0x6e2065687420666f, 0x666f206572757461, 0x616d726f666e6920,
|
|
0x696562206e6f6974, 0x207973616520676e, 0x6165727073206f74, 0x6168207475622064,
|
|
0x7473206f74206472, 0x202d202e656c6669, 0x206968736f746153, 0x6f746f6d616b614e,
|
|
0x0a0e0d66020b0015,
|
|
}
|
|
u8s := [2]uint8{0x00, 0x00}
|
|
|
|
buf := make([]byte, 25*8+2)
|
|
for i, v := range u64s {
|
|
binary.LittleEndian.PutUint64(buf[i*8:], v)
|
|
}
|
|
buf[200] = u8s[0]
|
|
buf[201] = u8s[1]
|
|
return buf
|
|
}
|
|
|
|
// TestGenesisBlockHash_Good is the definitive correctness test for wire
|
|
// serialisation. It constructs the testnet genesis block, computes its
|
|
// hash, and verifies it matches the hash returned by the C++ daemon.
|
|
//
|
|
// If this test passes, the block header serialisation, transaction prefix
|
|
// serialisation, tree hash, and Keccak-256 implementation are all
|
|
// bit-identical to the C++ reference.
|
|
func TestGenesisBlockHash_Good(t *testing.T) {
|
|
wantHash := "cb9d5455ccb79451931003672c405f5e2ac51bff54021aa30bc4499b1ffc4963"
|
|
|
|
// Parse the raw genesis coinbase transaction.
|
|
rawTx := testnetGenesisRawTx()
|
|
dec := NewDecoder(bytes.NewReader(rawTx))
|
|
minerTx := DecodeTransaction(dec)
|
|
if dec.Err() != nil {
|
|
t.Fatalf("failed to decode genesis miner tx: %v", dec.Err())
|
|
}
|
|
|
|
// Verify basic transaction structure.
|
|
if minerTx.Version != 1 {
|
|
t.Fatalf("miner tx version: got %d, want 1", minerTx.Version)
|
|
}
|
|
if len(minerTx.Vin) != 1 {
|
|
t.Fatalf("miner tx vin count: got %d, want 1", len(minerTx.Vin))
|
|
}
|
|
gen, ok := minerTx.Vin[0].(types.TxInputGenesis)
|
|
if !ok {
|
|
t.Fatalf("miner tx vin[0]: got %T, want TxInputGenesis", minerTx.Vin[0])
|
|
}
|
|
if gen.Height != 0 {
|
|
t.Fatalf("miner tx genesis height: got %d, want 0", gen.Height)
|
|
}
|
|
if len(minerTx.Vout) != 1 {
|
|
t.Fatalf("miner tx vout count: got %d, want 1", len(minerTx.Vout))
|
|
}
|
|
|
|
// Verify round-trip: re-encode and compare to original bytes.
|
|
var rtBuf bytes.Buffer
|
|
enc := NewEncoder(&rtBuf)
|
|
EncodeTransaction(enc, &minerTx)
|
|
if enc.Err() != nil {
|
|
t.Fatalf("re-encode error: %v", enc.Err())
|
|
}
|
|
if !bytes.Equal(rtBuf.Bytes(), rawTx) {
|
|
t.Fatalf("round-trip mismatch:\n got: %x\n want: %x", rtBuf.Bytes(), rawTx)
|
|
}
|
|
|
|
// Construct the genesis block.
|
|
block := types.Block{
|
|
BlockHeader: testnetGenesisHeader(),
|
|
MinerTx: minerTx,
|
|
TxHashes: nil, // genesis has no other transactions
|
|
}
|
|
|
|
// Compute and verify the block hash.
|
|
gotHash := BlockHash(&block)
|
|
if hex.EncodeToString(gotHash[:]) != wantHash {
|
|
t.Errorf("genesis block hash:\n got: %x\n want: %s", gotHash, wantHash)
|
|
|
|
// Debug: dump intermediate values.
|
|
prefixHash := TransactionPrefixHash(&block.MinerTx)
|
|
t.Logf("miner tx prefix hash: %x", prefixHash)
|
|
|
|
blob := BlockHashingBlob(&block)
|
|
t.Logf("block hashing blob (%d bytes): %x", len(blob), blob)
|
|
}
|
|
}
|
|
|
|
func TestTransactionPrefixHashRoundTrip_Good(t *testing.T) {
|
|
rawTx := testnetGenesisRawTx()
|
|
|
|
// Decode.
|
|
dec := NewDecoder(bytes.NewReader(rawTx))
|
|
tx := DecodeTransaction(dec)
|
|
if dec.Err() != nil {
|
|
t.Fatalf("decode error: %v", dec.Err())
|
|
}
|
|
|
|
// Prefix hash should be deterministic.
|
|
h1 := TransactionPrefixHash(&tx)
|
|
h2 := TransactionPrefixHash(&tx)
|
|
if h1 != h2 {
|
|
t.Error("prefix hash not deterministic")
|
|
}
|
|
|
|
// The prefix hash should equal Keccak-256 of the prefix bytes (first 200 bytes).
|
|
wantPrefixHash := Keccak256(rawTx[:200])
|
|
if types.Hash(wantPrefixHash) != h1 {
|
|
t.Errorf("prefix hash mismatch:\n got: %x\n want: %x", h1, wantPrefixHash)
|
|
}
|
|
}
|