From 18a86464d5904bff370c5402a9a3b9aefd36e1b2 Mon Sep 17 00:00:00 2001 From: David Case Date: Thu, 13 Nov 2025 15:58:00 -0500 Subject: [PATCH 1/3] add Header type --- block/header.go | 119 +++++++++++++++++++++++++++++++++++++++++++ block/header_test.go | 112 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 block/header.go create mode 100644 block/header_test.go diff --git a/block/header.go b/block/header.go new file mode 100644 index 00000000..8ec72516 --- /dev/null +++ b/block/header.go @@ -0,0 +1,119 @@ +package block + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "fmt" + "io" + + "github.com/bsv-blockchain/go-sdk/chainhash" +) + +const ( + // HeaderSize is the size of a Bitcoin block header in bytes (80 bytes) + HeaderSize = 80 +) + +// Header represents a Bitcoin block header (80 bytes) +type Header struct { + Version int32 // 4 bytes - Block version + PrevBlock chainhash.Hash // 32 bytes - Previous block hash + MerkleRoot chainhash.Hash // 32 bytes - Merkle root hash + Timestamp uint32 // 4 bytes - Block timestamp (Unix time) + Bits uint32 // 4 bytes - Difficulty target + Nonce uint32 // 4 bytes - Nonce +} + +// NewHeaderFromBytes creates a BlockHeader from an 80-byte slice +func NewHeaderFromBytes(data []byte) (*Header, error) { + if len(data) != HeaderSize { + return nil, fmt.Errorf("invalid header size: expected %d bytes, got %d", HeaderSize, len(data)) + } + + h := &Header{} + r := bytes.NewReader(data) + + // Read version (4 bytes, little-endian) + if err := binary.Read(r, binary.LittleEndian, &h.Version); err != nil { + return nil, fmt.Errorf("failed to read version: %w", err) + } + + // Read previous block hash (32 bytes) + if _, err := io.ReadFull(r, h.PrevBlock[:]); err != nil { + return nil, fmt.Errorf("failed to read prev block hash: %w", err) + } + + // Read merkle root (32 bytes) + if _, err := io.ReadFull(r, h.MerkleRoot[:]); err != nil { + return nil, fmt.Errorf("failed to read merkle root: %w", err) + } + + // Read timestamp (4 bytes, little-endian) + if err := binary.Read(r, binary.LittleEndian, &h.Timestamp); err != nil { + return nil, fmt.Errorf("failed to read timestamp: %w", err) + } + + // Read bits (4 bytes, little-endian) + if err := binary.Read(r, binary.LittleEndian, &h.Bits); err != nil { + return nil, fmt.Errorf("failed to read bits: %w", err) + } + + // Read nonce (4 bytes, little-endian) + if err := binary.Read(r, binary.LittleEndian, &h.Nonce); err != nil { + return nil, fmt.Errorf("failed to read nonce: %w", err) + } + + return h, nil +} + +// NewHeaderFromHex creates a BlockHeader from a hex string (160 characters) +func NewHeaderFromHex(hexStr string) (*Header, error) { + data, err := hex.DecodeString(hexStr) + if err != nil { + return nil, fmt.Errorf("failed to decode hex: %w", err) + } + return NewHeaderFromBytes(data) +} + +// Bytes serializes the block header to an 80-byte slice +func (h *Header) Bytes() []byte { + buf := new(bytes.Buffer) + buf.Grow(HeaderSize) + + // Write version (4 bytes, little-endian) + binary.Write(buf, binary.LittleEndian, h.Version) + + // Write previous block hash (32 bytes) + buf.Write(h.PrevBlock[:]) + + // Write merkle root (32 bytes) + buf.Write(h.MerkleRoot[:]) + + // Write timestamp (4 bytes, little-endian) + binary.Write(buf, binary.LittleEndian, h.Timestamp) + + // Write bits (4 bytes, little-endian) + binary.Write(buf, binary.LittleEndian, h.Bits) + + // Write nonce (4 bytes, little-endian) + binary.Write(buf, binary.LittleEndian, h.Nonce) + + return buf.Bytes() +} + +// Hex returns the block header as a hex string +func (h *Header) Hex() string { + return hex.EncodeToString(h.Bytes()) +} + +// Hash calculates the block hash (double SHA-256 of the header) +func (h *Header) Hash() chainhash.Hash { + return chainhash.DoubleHashH(h.Bytes()) +} + +// String returns a string representation of the header +func (h *Header) String() string { + return fmt.Sprintf("Header{Hash: %s, PrevBlock: %s, Height: ?, Bits: %d}", + h.Hash().String(), h.PrevBlock.String(), h.Bits) +} diff --git a/block/header_test.go b/block/header_test.go new file mode 100644 index 00000000..792d2647 --- /dev/null +++ b/block/header_test.go @@ -0,0 +1,112 @@ +package block + +import ( + "encoding/hex" + "testing" +) + +func TestNewHeaderFromBytes(t *testing.T) { + // Genesis block mainnet header + genesisHex := "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c" + genesisBytes, err := hex.DecodeString(genesisHex) + if err != nil { + t.Fatalf("Failed to decode genesis hex: %v", err) + } + + header, err := NewHeaderFromBytes(genesisBytes) + if err != nil { + t.Fatalf("NewHeaderFromBytes() error = %v", err) + } + + if header.Version != 1 { + t.Errorf("Version = %d, want 1", header.Version) + } + + if header.Timestamp != 1231006505 { + t.Errorf("Timestamp = %d, want 1231006505", header.Timestamp) + } + + if header.Bits != 0x1d00ffff { + t.Errorf("Bits = %x, want 0x1d00ffff", header.Bits) + } + + if header.Nonce != 2083236893 { + t.Errorf("Nonce = %d, want 2083236893", header.Nonce) + } +} + +func TestHeaderBytes(t *testing.T) { + genesisHex := "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c" + genesisBytes, _ := hex.DecodeString(genesisHex) + + header, err := NewHeaderFromBytes(genesisBytes) + if err != nil { + t.Fatalf("NewHeaderFromBytes() error = %v", err) + } + + serialized := header.Bytes() + + if len(serialized) != HeaderSize { + t.Errorf("Bytes() returned %d bytes, want %d", len(serialized), HeaderSize) + } + + if hex.EncodeToString(serialized) != genesisHex { + t.Errorf("Bytes() = %s, want %s", hex.EncodeToString(serialized), genesisHex) + } +} + +func TestHeaderHex(t *testing.T) { + genesisHex := "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c" + + header, err := NewHeaderFromHex(genesisHex) + if err != nil { + t.Fatalf("NewHeaderFromHex() error = %v", err) + } + + if header.Hex() != genesisHex { + t.Errorf("Hex() = %s, want %s", header.Hex(), genesisHex) + } +} + +func TestHeaderHash(t *testing.T) { + // Genesis block mainnet + genesisHex := "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c" + expectedHash := "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" + + header, err := NewHeaderFromHex(genesisHex) + if err != nil { + t.Fatalf("NewHeaderFromHex() error = %v", err) + } + + hash := header.Hash() + hashStr := hash.String() + + if hashStr != expectedHash { + t.Errorf("Hash() = %s, want %s", hashStr, expectedHash) + } +} + +func TestNewHeaderFromBytesInvalidSize(t *testing.T) { + invalidData := []byte{0x01, 0x02, 0x03} + + _, err := NewHeaderFromBytes(invalidData) + if err == nil { + t.Error("NewHeaderFromBytes() with invalid size should return error") + } +} + +func TestHeaderPrevBlockAndMerkleRoot(t *testing.T) { + // Block 1 mainnet header + block1Hex := "010000006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000982051fd1e4ba744bbbe680e1fee14677ba1a3c3540bf7b1cdb606e857233e0e61bc6649ffff001d01e36299" + expectedPrevBlock := "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" + + header, err := NewHeaderFromHex(block1Hex) + if err != nil { + t.Fatalf("NewHeaderFromHex() error = %v", err) + } + + prevBlockStr := header.PrevBlock.String() + if prevBlockStr != expectedPrevBlock { + t.Errorf("PrevBlock = %s, want %s", prevBlockStr, expectedPrevBlock) + } +} From b56b04569fb392cfe2060066b92c8b5f1fecad2a Mon Sep 17 00:00:00 2001 From: David Case Date: Wed, 19 Nov 2025 14:45:03 +0000 Subject: [PATCH 2/3] json tags and lint fix --- block/header.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/block/header.go b/block/header.go index 8ec72516..f3705459 100644 --- a/block/header.go +++ b/block/header.go @@ -17,12 +17,12 @@ const ( // Header represents a Bitcoin block header (80 bytes) type Header struct { - Version int32 // 4 bytes - Block version - PrevBlock chainhash.Hash // 32 bytes - Previous block hash - MerkleRoot chainhash.Hash // 32 bytes - Merkle root hash - Timestamp uint32 // 4 bytes - Block timestamp (Unix time) - Bits uint32 // 4 bytes - Difficulty target - Nonce uint32 // 4 bytes - Nonce + Version int32 `json:"version"` // 4 bytes - Block version + PrevBlock chainhash.Hash `json:"prevBlock"` // 32 bytes - Previous block hash + MerkleRoot chainhash.Hash `json:"merkleRoot"` // 32 bytes - Merkle root hash + Timestamp uint32 `json:"timestamp"` // 4 bytes - Block timestamp (Unix time) + Bits uint32 `json:"bits"` // 4 bytes - Difficulty target + Nonce uint32 `json:"nonce"` // 4 bytes - Nonce } // NewHeaderFromBytes creates a BlockHeader from an 80-byte slice @@ -82,7 +82,7 @@ func (h *Header) Bytes() []byte { buf.Grow(HeaderSize) // Write version (4 bytes, little-endian) - binary.Write(buf, binary.LittleEndian, h.Version) + _ = binary.Write(buf, binary.LittleEndian, h.Version) // Write previous block hash (32 bytes) buf.Write(h.PrevBlock[:]) @@ -91,13 +91,13 @@ func (h *Header) Bytes() []byte { buf.Write(h.MerkleRoot[:]) // Write timestamp (4 bytes, little-endian) - binary.Write(buf, binary.LittleEndian, h.Timestamp) + _ = binary.Write(buf, binary.LittleEndian, h.Timestamp) // Write bits (4 bytes, little-endian) - binary.Write(buf, binary.LittleEndian, h.Bits) + _ = binary.Write(buf, binary.LittleEndian, h.Bits) // Write nonce (4 bytes, little-endian) - binary.Write(buf, binary.LittleEndian, h.Nonce) + _ = binary.Write(buf, binary.LittleEndian, h.Nonce) return buf.Bytes() } From d6554738622be3e91c3355d9306378adb6d0c7d2 Mon Sep 17 00:00:00 2001 From: David Case Date: Wed, 19 Nov 2025 13:10:29 -0500 Subject: [PATCH 3/3] update block header fields --- block/header.go | 18 +++++++++--------- block/header_test.go | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/block/header.go b/block/header.go index f3705459..1700880e 100644 --- a/block/header.go +++ b/block/header.go @@ -17,12 +17,12 @@ const ( // Header represents a Bitcoin block header (80 bytes) type Header struct { - Version int32 `json:"version"` // 4 bytes - Block version - PrevBlock chainhash.Hash `json:"prevBlock"` // 32 bytes - Previous block hash - MerkleRoot chainhash.Hash `json:"merkleRoot"` // 32 bytes - Merkle root hash - Timestamp uint32 `json:"timestamp"` // 4 bytes - Block timestamp (Unix time) - Bits uint32 `json:"bits"` // 4 bytes - Difficulty target - Nonce uint32 `json:"nonce"` // 4 bytes - Nonce + Version int32 `json:"version"` // 4 bytes - Block version + PrevHash chainhash.Hash `json:"previousHash"` // 32 bytes - Previous block hash + MerkleRoot chainhash.Hash `json:"merkleRoot"` // 32 bytes - Merkle root hash + Timestamp uint32 `json:"time"` // 4 bytes - Block timestamp (Unix time) + Bits uint32 `json:"bits"` // 4 bytes - Difficulty target + Nonce uint32 `json:"nonce"` // 4 bytes - Nonce } // NewHeaderFromBytes creates a BlockHeader from an 80-byte slice @@ -40,7 +40,7 @@ func NewHeaderFromBytes(data []byte) (*Header, error) { } // Read previous block hash (32 bytes) - if _, err := io.ReadFull(r, h.PrevBlock[:]); err != nil { + if _, err := io.ReadFull(r, h.PrevHash[:]); err != nil { return nil, fmt.Errorf("failed to read prev block hash: %w", err) } @@ -85,7 +85,7 @@ func (h *Header) Bytes() []byte { _ = binary.Write(buf, binary.LittleEndian, h.Version) // Write previous block hash (32 bytes) - buf.Write(h.PrevBlock[:]) + buf.Write(h.PrevHash[:]) // Write merkle root (32 bytes) buf.Write(h.MerkleRoot[:]) @@ -115,5 +115,5 @@ func (h *Header) Hash() chainhash.Hash { // String returns a string representation of the header func (h *Header) String() string { return fmt.Sprintf("Header{Hash: %s, PrevBlock: %s, Height: ?, Bits: %d}", - h.Hash().String(), h.PrevBlock.String(), h.Bits) + h.Hash().String(), h.PrevHash.String(), h.Bits) } diff --git a/block/header_test.go b/block/header_test.go index 792d2647..edd03fad 100644 --- a/block/header_test.go +++ b/block/header_test.go @@ -105,7 +105,7 @@ func TestHeaderPrevBlockAndMerkleRoot(t *testing.T) { t.Fatalf("NewHeaderFromHex() error = %v", err) } - prevBlockStr := header.PrevBlock.String() + prevBlockStr := header.PrevHash.String() if prevBlockStr != expectedPrevBlock { t.Errorf("PrevBlock = %s, want %s", prevBlockStr, expectedPrevBlock) }