Skip to content
Merged
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. The format

## Table of Contents

- [1.2.13 - 2025-12-05](#1213---2025-12-05)
- [1.2.12 - 2025-11-12](#1212---2025-11-12)
- [1.2.11 - 2025-10-27](#1211---2025-10-27)
- [1.2.10 - 2025-09-16](#1210---2025-09-16)
Expand Down Expand Up @@ -47,6 +48,15 @@ All notable changes to this project will be documented in this file. The format
- [1.1.0 - 2024-08-19](#110---2024-08-19)
- [1.0.0 - 2024-06-06](#100---2024-06-06)

## [1.2.13] - 2025-12-05

### Added
- `Header` type in block package for 80-byte Bitcoin block header parsing
- `MerklePath.Clone()` method for deep copying merkle paths

### Fixed
- `Beef.Clone()` now performs deep copy of all nested structures (BUMPs, transactions, input references)

## [1.2.12] - 2025-11-12

### Added
Expand Down
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)
}
139 changes: 139 additions & 0 deletions block/header_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package block

import (
"encoding/hex"
"strings"
"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)
}
}

func TestHeaderString(t *testing.T) {
genesisHex := "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c"

header, err := NewHeaderFromHex(genesisHex)
if err != nil {
t.Fatalf("NewHeaderFromHex() error = %v", err)
}

str := header.String()
if str == "" {
t.Error("String() returned empty string")
}

expectedHash := "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
if !strings.Contains(str, expectedHash) {
t.Errorf("String() should contain hash %s, got %s", expectedHash, str)
}
}

func TestNewHeaderFromHexInvalid(t *testing.T) {
_, err := NewHeaderFromHex("invalid hex")
if err == nil {
t.Error("NewHeaderFromHex() with invalid hex should return error")
}
}
58 changes: 55 additions & 3 deletions transaction/beef.go
Original file line number Diff line number Diff line change
Expand Up @@ -1138,15 +1138,67 @@ txLoop:
return log
}

// Clone creates a deep copy of the Beef object.
// All nested structures are copied, so modifications to the clone
// will not affect the original.
func (b *Beef) Clone() *Beef {
c := &Beef{
Version: b.Version,
BUMPs: append([]*MerklePath(nil), b.BUMPs...),
BUMPs: make([]*MerklePath, len(b.BUMPs)),
Transactions: make(map[chainhash.Hash]*BeefTx, len(b.Transactions)),
}
for k, v := range b.Transactions {
c.Transactions[k] = v

// Deep clone BUMPs
for i, mp := range b.BUMPs {
c.BUMPs[i] = mp.Clone()
}

// First pass: ShallowClone all Transactions
for txid, beefTx := range b.Transactions {
cloned := &BeefTx{
DataFormat: beefTx.DataFormat,
BumpIndex: beefTx.BumpIndex,
}

if beefTx.KnownTxID != nil {
id := *beefTx.KnownTxID
cloned.KnownTxID = &id
}

if beefTx.InputTxids != nil {
cloned.InputTxids = make([]*chainhash.Hash, len(beefTx.InputTxids))
for i, inputTxid := range beefTx.InputTxids {
if inputTxid != nil {
id := *inputTxid
cloned.InputTxids[i] = &id
}
}
}

if beefTx.Transaction != nil {
cloned.Transaction = beefTx.Transaction.ShallowClone()
// Link to cloned BUMP
if beefTx.DataFormat == RawTxAndBumpIndex && beefTx.BumpIndex >= 0 && beefTx.BumpIndex < len(c.BUMPs) {
cloned.Transaction.MerklePath = c.BUMPs[beefTx.BumpIndex]
}
}

c.Transactions[txid] = cloned
}

// Second pass: wire up SourceTransaction references
for _, beefTx := range c.Transactions {
if beefTx.Transaction != nil {
for _, input := range beefTx.Transaction.Inputs {
if input.SourceTXID != nil {
if sourceBeefTx, ok := c.Transactions[*input.SourceTXID]; ok && sourceBeefTx.Transaction != nil {
input.SourceTransaction = sourceBeefTx.Transaction
}
}
}
}
}

return c
}

Expand Down
20 changes: 20 additions & 0 deletions transaction/beef_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,26 @@ func TestBeefClone(t *testing.T) {
original, err := NewBeefFromBytes(beefBytes)
require.NoError(t, err)

// Test cloning with nil fields by adding a BeefTx with minimal data
nilFieldsTxID := chainhash.HashH([]byte("test-nil-fields"))
original.Transactions[nilFieldsTxID] = &BeefTx{
DataFormat: TxIDOnly,
KnownTxID: nil,
InputTxids: nil,
Transaction: nil,
}

// Test cloning with InputTxids populated
inputTxidsTxID := chainhash.HashH([]byte("test-input-txids"))
knownID := chainhash.HashH([]byte("known-id"))
inputID1 := chainhash.HashH([]byte("input-1"))
original.Transactions[inputTxidsTxID] = &BeefTx{
DataFormat: TxIDOnly,
KnownTxID: &knownID,
InputTxids: []*chainhash.Hash{&inputID1, nil},
Transaction: nil,
}

// Clone the object
clone := original.Clone()

Expand Down
9 changes: 9 additions & 0 deletions transaction/merklepath.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ func (ip IndexedPath) GetOffsetLeaf(layer int, offset uint64) *PathElement {
return nil
}

// Clone creates a deep copy of the MerklePath by serializing and deserializing.
func (mp *MerklePath) Clone() *MerklePath {
if mp == nil {
return nil
}
clone, _ := NewMerklePathFromBinary(mp.Bytes())
return clone
}

// NewMerklePath creates a new MerklePath with the given block height and path
func NewMerklePath(blockHeight uint32, path [][]*PathElement) *MerklePath {
return &MerklePath{
Expand Down
Loading
Loading