Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions block/header.go
Original file line number Diff line number Diff line change
@@ -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 `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
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.PrevHash[:]); 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.PrevHash[:])

// 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.PrevHash.String(), h.Bits)
}
112 changes: 112 additions & 0 deletions block/header_test.go
Original file line number Diff line number Diff line change
@@ -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.PrevHash.String()
if prevBlockStr != expectedPrevBlock {
t.Errorf("PrevBlock = %s, want %s", prevBlockStr, expectedPrevBlock)
}
}
Loading