package btcwire import ( "bytes" "encoding/hex" "errors" "io" "math/big" "testing" "git.smesh.lol/nostrino/pkg/crypto/chainhash" ) // Bitcoin mainnet genesis block header and its display-order hash. const ( genesisHeaderHex = "0100000000000000000000000000000000000000000000000000000000000000" + "000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a" + "29ab5f49ffff001d1dac2b7c" genesisHashDisplay = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" ) // mustHex decodes s or fails the test. func mustHex(t *testing.T, s string) []byte { t.Helper() b, err := hex.DecodeString(s) if err != nil { t.Fatalf("decoding hex: %v", err) } return b } // mustGenesisHeader returns the parsed mainnet genesis header. func mustGenesisHeader(t *testing.T) BlockHeader { t.Helper() raw := mustHex(t, genesisHeaderHex) if len(raw) != BlockHeaderSize { t.Fatalf("genesis header is %d bytes, want %d", len(raw), BlockHeaderSize) } var h BlockHeader if err := h.Deserialize(raw); err != nil { t.Fatalf("Deserialize: %v", err) } return h } func TestGenesisHeader(t *testing.T) { raw := mustHex(t, genesisHeaderHex) h := mustGenesisHeader(t) if h.Version != 1 { t.Fatalf("Version = %d, want 1", h.Version) } if h.PrevBlock != (chainhash.Hash{}) { t.Fatalf("PrevBlock = %s, want zero", h.PrevBlock) } if h.Timestamp != 1231006505 { t.Fatalf("Timestamp = %d, want 1231006505", h.Timestamp) } if h.Bits != 0x1d00ffff { t.Fatalf("Bits = %08x, want 1d00ffff", h.Bits) } if h.Nonce != 2083236893 { t.Fatalf("Nonce = %d, want 2083236893", h.Nonce) } serialized := h.Serialize() if !bytes.Equal(serialized[:], raw) { t.Fatalf("Serialize = %x, want %x", serialized, raw) } if got := h.BlockHash().String(); got != genesisHashDisplay { t.Fatalf("BlockHash = %s, want %s", got, genesisHashDisplay) } if !h.CheckProofOfWork() { t.Fatal("genesis header failed proof-of-work check") } target, err := h.Target() if err != nil { t.Fatalf("Target: %v", err) } wantTarget := new(big.Int).Lsh(big.NewInt(0xffff), 208) if target.Cmp(wantTarget) != 0 { t.Fatalf("Target = %x, want %x", target, wantTarget) } } func TestGenesisHeaderMutationsFailPoW(t *testing.T) { tests := []struct { name string mutate func(*BlockHeader) }{ {"nonce zero", func(h *BlockHeader) { h.Nonce = 0 }}, {"nonce incremented", func(h *BlockHeader) { h.Nonce++ }}, {"merkle root flipped", func(h *BlockHeader) { h.MerkleRoot[0] ^= 0x01 }}, {"timestamp incremented", func(h *BlockHeader) { h.Timestamp++ }}, {"version incremented", func(h *BlockHeader) { h.Version++ }}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { h := mustGenesisHeader(t) tc.mutate(&h) original := mustGenesisHeader(t) if h.BlockHash() == original.BlockHash() { t.Fatal("mutation did not change the block hash") } if h.CheckProofOfWork() { t.Fatal("mutated header passed proof-of-work check") } }) } } func TestGenesisHeaderHashToBig(t *testing.T) { h := mustGenesisHeader(t) hash := h.BlockHash() got := HashToBig(&hash) want, ok := new(big.Int).SetString(genesisHashDisplay, 16) if !ok { t.Fatal("parsing expected hash") } if got.Cmp(want) != 0 { t.Fatalf("HashToBig = %x, want %x", got, want) } // The internal hash bytes are the reverse of the display order, so the // final byte of the internal hash is zero and the first is not. if hash[chainhash.HashSize-1] != 0x00 || hash[0] == 0x00 { t.Fatalf("internal hash byte order looks wrong: %x", hash) } } func TestBlockHeaderSerializeLayout(t *testing.T) { h := BlockHeader{ Version: 0x01020304, PrevBlock: testHash(0x10), MerkleRoot: testHash(0x20), Timestamp: 0x05060708, Bits: 0x090a0b0c, Nonce: 0x0d0e0f10, } raw := h.Serialize() if got, want := raw[0:4], []byte{0x04, 0x03, 0x02, 0x01}; !bytes.Equal(got, want) { t.Fatalf("version bytes = %x, want %x", got, want) } if got := raw[4:36]; !bytes.Equal(got, h.PrevBlock[:]) { t.Fatalf("prev block bytes = %x, want %x", got, h.PrevBlock) } if got := raw[36:68]; !bytes.Equal(got, h.MerkleRoot[:]) { t.Fatalf("merkle root bytes = %x, want %x", got, h.MerkleRoot) } if got, want := raw[68:72], []byte{0x08, 0x07, 0x06, 0x05}; !bytes.Equal(got, want) { t.Fatalf("timestamp bytes = %x, want %x", got, want) } if got, want := raw[72:76], []byte{0x0c, 0x0b, 0x0a, 0x09}; !bytes.Equal(got, want) { t.Fatalf("bits bytes = %x, want %x", got, want) } if got, want := raw[76:80], []byte{0x10, 0x0f, 0x0e, 0x0d}; !bytes.Equal(got, want) { t.Fatalf("nonce bytes = %x, want %x", got, want) } var round BlockHeader if err := round.Deserialize(raw[:]); err != nil { t.Fatalf("Deserialize: %v", err) } if round != h { t.Fatalf("round trip = %+v, want %+v", round, h) } } func TestBlockHeaderDeserializeShort(t *testing.T) { raw := mustHex(t, genesisHeaderHex) var h BlockHeader for n := 0; n < BlockHeaderSize; n++ { if err := h.Deserialize(raw[:n]); !errors.Is(err, io.ErrUnexpectedEOF) { t.Fatalf("Deserialize(%d bytes) error = %v, want io.ErrUnexpectedEOF", n, err) } } // Trailing bytes beyond the header are ignored. if err := h.Deserialize(append(raw, 0xde, 0xad)); err != nil { t.Fatalf("Deserialize with trailing bytes: %v", err) } if h.BlockHash().String() != genesisHashDisplay { t.Fatalf("BlockHash = %s, want %s", h.BlockHash(), genesisHashDisplay) } } func TestCompactToBig(t *testing.T) { tests := []struct { name string bits uint32 want *big.Int wantErr error }{ { name: "mainnet pow limit", bits: 0x1d00ffff, want: new(big.Int).Lsh(big.NewInt(0xffff), 208), }, { name: "difficulty one legacy", bits: 0x1b0404cb, want: mustBigInt(t, "00000000000404cb000000000000000000000000000000000000000000000000"), }, { name: "256 bit maximum target", bits: 0x2100ffff, want: new(big.Int).Lsh(big.NewInt(0xffff), 240), }, { name: "single satoshi target", bits: 0x03000001, want: big.NewInt(1), }, { name: "zero mantissa", bits: 0x1d000000, want: new(big.Int), }, { name: "small exponent truncates", bits: 0x01003456, want: new(big.Int), }, { name: "negative", bits: 0x1d80ffff, wantErr: ErrTargetNegative, }, { name: "overflow high exponent", bits: 0xff00ffff, wantErr: ErrTargetOverflow, }, { name: "overflow one bit too wide", bits: 0x2200ffff, wantErr: ErrTargetOverflow, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got, err := CompactToBig(tc.bits) if tc.wantErr != nil { if !errors.Is(err, tc.wantErr) { t.Fatalf("error = %v, want %v", err, tc.wantErr) } return } if err != nil { t.Fatalf("CompactToBig(0x%08x): %v", tc.bits, err) } if got.Cmp(tc.want) != 0 { t.Fatalf("CompactToBig(0x%08x) = %x, want %x", tc.bits, got, tc.want) } }) } } // mustBigInt parses s as a base 16 integer or fails the test. func mustBigInt(t *testing.T, s string) *big.Int { t.Helper() v, ok := new(big.Int).SetString(s, 16) if !ok { t.Fatalf("parsing big integer %q", s) } return v } func TestHashToBigEndianness(t *testing.T) { // Internal byte order is little endian: the first byte is the least // significant. var low chainhash.Hash low[0] = 0x01 if got := HashToBig(&low); got.Cmp(big.NewInt(1)) != 0 { t.Fatalf("HashToBig({01,00,...}) = %x, want 1", got) } var high chainhash.Hash high[chainhash.HashSize-1] = 0x01 if got, want := HashToBig(&high), new(big.Int).Lsh(big.NewInt(1), 248); got.Cmp(want) != 0 { t.Fatalf("HashToBig({...,00,01}) = %x, want %x", got, want) } var z chainhash.Hash if got := HashToBig(&z); got.Sign() != 0 { t.Fatalf("HashToBig(zero) = %x, want 0", got) } } func TestCheckProofOfWorkInvalidTarget(t *testing.T) { genesis := mustGenesisHeader(t) tests := []struct { name string bits uint32 wantErr error }{ {"zero target", 0x1d000000, nil}, {"negative target", 0x1d80ffff, ErrTargetNegative}, {"overflow target", 0xff00ffff, ErrTargetOverflow}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { h := genesis h.Bits = tc.bits _, err := h.Target() if !errors.Is(err, tc.wantErr) { t.Fatalf("Target error = %v, want %v", err, tc.wantErr) } if h.CheckProofOfWork() { t.Fatal("header with invalid target passed proof-of-work check") } }) } }