diff --git a/block/header.go b/block/header.go new file mode 100644 index 00000000..1700880e --- /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 `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) +} diff --git a/block/header_test.go b/block/header_test.go new file mode 100644 index 00000000..edd03fad --- /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.PrevHash.String() + if prevBlockStr != expectedPrevBlock { + t.Errorf("PrevBlock = %s, want %s", prevBlockStr, expectedPrevBlock) + } +}