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.4 - 2025-06-30](#124---2025-06-30)
- [1.2.3 - 2025-06-30](#123---2025-06-30)
- [1.2.2 - 2025-06-27](#122---2025-06-27)
- [1.2.1 - 2025-06-12](#121---2025-06-12)
Expand Down Expand Up @@ -38,6 +39,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.4] - 2025-06-30

### Changed
- Add context parameter to ChainTracker.IsValidRootForHeight for cancellation/timeout support
- Update all ChainTracker implementations to take context (WhatsOnChain, HeadersClient, GullibleHeadersClient)
- Update MerklePath.Verify and VerifyHex to take context
- Update BEEF.Verify to take context
- Update spv.Verify and VerifyScripts to take context

## [1.2.3] - 2025-06-30

### Added
Expand Down
4 changes: 3 additions & 1 deletion docs/examples/validate_spv/validate_spv.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"encoding/base64"
"fmt"

Expand Down Expand Up @@ -32,7 +33,8 @@ func main() {
return
}

valid, err := client.IsValidRootForHeight(root, tx.MerklePath.BlockHeight)
ctx := context.Background()
valid, err := client.IsValidRootForHeight(ctx, root, tx.MerklePath.BlockHeight)
if err != nil {
fmt.Println("Error validating root for height:", err)
return
Expand Down
5 changes: 4 additions & 1 deletion docs/examples/verify_beef/verify_beef.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"context"

"github.com/bsv-blockchain/go-sdk/spv"
"github.com/bsv-blockchain/go-sdk/transaction"
)
Expand All @@ -15,6 +17,7 @@ func main() {
panic(err)
}
// This ensures the BEEF structure is legitimate
verified, _ := tx.MerklePath.Verify(tx.TxID(), &spv.GullibleHeadersClient{})
ctx := context.Background()
verified, _ := tx.MerklePath.Verify(ctx, tx.TxID(), &spv.GullibleHeadersClient{})
println(verified)
}
5 changes: 4 additions & 1 deletion docs/examples/verify_transaction/verify_transaction.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"context"

"github.com/bsv-blockchain/go-sdk/spv"
"github.com/bsv-blockchain/go-sdk/transaction"
)
Expand All @@ -16,6 +18,7 @@ func main() {
}
// This ensures the BEEF structure is legitimate, the scripts are valid, and the merkle path is correct
// Also optionally verifies fees
verified, _ := spv.Verify(tx, &spv.GullibleHeadersClient{}, nil)
ctx := context.Background()
verified, _ := spv.Verify(ctx, tx, &spv.GullibleHeadersClient{}, nil)
println(verified)
}
2 changes: 1 addition & 1 deletion spv/scripts_only.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

type GullibleHeadersClient struct{}

func (g *GullibleHeadersClient) IsValidRootForHeight(merkleRoot *chainhash.Hash, height uint32) (bool, error) {
func (g *GullibleHeadersClient) IsValidRootForHeight(ctx context.Context, merkleRoot *chainhash.Hash, height uint32) (bool, error) {
// DO NOT USE IN A REAL PROJECT due to security risks of accepting any merkle root as valid without verification
return true, nil
}
Expand Down
9 changes: 5 additions & 4 deletions spv/verify.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package spv

import (
"context"
"fmt"

"github.com/bsv-blockchain/go-sdk/script/interpreter"
"github.com/bsv-blockchain/go-sdk/transaction"
"github.com/bsv-blockchain/go-sdk/transaction/chaintracker"
)

func Verify(t *transaction.Transaction,
func Verify(ctx context.Context, t *transaction.Transaction,
chainTracker chaintracker.ChainTracker,
feeModel transaction.FeeModel) (bool, error) {
verifiedTxids := make(map[string]struct{})
Expand All @@ -28,7 +29,7 @@ func Verify(t *transaction.Transaction,
}

if tx.MerklePath != nil {
if isValid, err := tx.MerklePath.Verify(txid, chainTracker); err != nil {
if isValid, err := tx.MerklePath.Verify(ctx, txid, chainTracker); err != nil {
return false, err
} else if isValid {
verifiedTxids[txidStr] = struct{}{}
Expand Down Expand Up @@ -80,6 +81,6 @@ func Verify(t *transaction.Transaction,
return true, nil
}

func VerifyScripts(t *transaction.Transaction) (bool, error) {
return Verify(t, &GullibleHeadersClient{}, nil)
func VerifyScripts(ctx context.Context, t *transaction.Transaction) (bool, error) {
return Verify(ctx, t, &GullibleHeadersClient{}, nil)
}
12 changes: 8 additions & 4 deletions spv/verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const BEEF = "AQC+7wH+kQYNAAcCVAIKXThHm90iVbs15AIfFQEYl5xesbHCXMkYy9SqoR1vNVUAAZ
func TestSPVVerify(t *testing.T) {
tx, err := transaction.NewTransactionFromBEEFHex(BRC62Hex)
require.NoError(t, err)
verified, err := Verify(tx, &GullibleHeadersClient{}, nil)
ctx := t.Context()
verified, err := Verify(ctx, tx, &GullibleHeadersClient{}, nil)
require.NoError(t, err)
require.True(t, verified)
}
Expand All @@ -27,7 +28,8 @@ func TestSPVVerifyScripts(t *testing.T) {
require.NoError(t, err)
tx, err := transaction.NewTransactionFromBEEF(buf)
require.NoError(t, err)
verified, err := VerifyScripts(tx)
ctx := t.Context()
verified, err := VerifyScripts(ctx, tx)
require.NoError(t, err)
require.True(t, verified)
}
Expand All @@ -40,7 +42,8 @@ func TestSPVVerifyWithSufficientFee(t *testing.T) {
Satoshis: 10,
}

verified, err := Verify(tx, &GullibleHeadersClient{}, feeModel)
ctx := t.Context()
verified, err := Verify(ctx, tx, &GullibleHeadersClient{}, feeModel)
require.NoError(t, err)
require.True(t, verified)
}
Expand All @@ -53,7 +56,8 @@ func TestSPVVerifyWithInsufficientFee(t *testing.T) {
Satoshis: 1,
}

verified, err := Verify(tx, &GullibleHeadersClient{}, feeModel)
ctx := t.Context()
verified, err := Verify(ctx, tx, &GullibleHeadersClient{}, feeModel)
require.Error(t, err)
require.Contains(t, err.Error(), "fee is too low")
require.False(t, verified)
Expand Down
5 changes: 3 additions & 2 deletions transaction/beef.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package transaction

import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"fmt"
Expand Down Expand Up @@ -780,7 +781,7 @@ func (b *Beef) IsValid(allowTxidOnly bool) bool {
return r.valid
}

func (b *Beef) Verify(chainTracker chaintracker.ChainTracker, allowTxidOnly bool) (bool, error) {
func (b *Beef) Verify(ctx context.Context, chainTracker chaintracker.ChainTracker, allowTxidOnly bool) (bool, error) {
r := b.verifyValid(allowTxidOnly)
if !r.valid {
return false, nil
Expand All @@ -790,7 +791,7 @@ func (b *Beef) Verify(chainTracker chaintracker.ChainTracker, allowTxidOnly bool
if err != nil {
return false, err
}
ok, err := chainTracker.IsValidRootForHeight(h, height)
ok, err := chainTracker.IsValidRootForHeight(ctx, h, height)
if err != nil || !ok {
return false, err
}
Expand Down
2 changes: 1 addition & 1 deletion transaction/chaintracker/chaintracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ import (
)

type ChainTracker interface {
IsValidRootForHeight(root *chainhash.Hash, height uint32) (bool, error)
IsValidRootForHeight(ctx context.Context, root *chainhash.Hash, height uint32) (bool, error)
CurrentHeight(ctx context.Context) (uint32, error)
}
4 changes: 2 additions & 2 deletions transaction/chaintracker/headers_client/headers_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type Client struct {
ApiKey string
}

func (c Client) IsValidRootForHeight(root *chainhash.Hash, height uint32) (bool, error) {
func (c Client) IsValidRootForHeight(ctx context.Context, root *chainhash.Hash, height uint32) (bool, error) {
type requestBody struct {
MerkleRoot string `json:"merkleRoot"`
BlockHeight uint32 `json:"blockHeight"`
Expand All @@ -46,7 +46,7 @@ func (c Client) IsValidRootForHeight(root *chainhash.Hash, height uint32) (bool,
return false, fmt.Errorf("error marshaling JSON: %v", err)
}

req, err := http.NewRequest("POST", c.Url+"/api/v1/chain/merkleroot/verify", bytes.NewBuffer(jsonPayload))
req, err := http.NewRequestWithContext(ctx, "POST", c.Url+"/api/v1/chain/merkleroot/verify", bytes.NewBuffer(jsonPayload))
if err != nil {
return false, fmt.Errorf("error creating request: %v", err)
}
Expand Down
10 changes: 5 additions & 5 deletions transaction/chaintracker/whatsonchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ func NewWhatsOnChain(network Network, apiKey string) *WhatsOnChain {
}

// Assuming BlockHeader is defined elsewhere
func (w *WhatsOnChain) GetBlockHeader(height uint32) (header *BlockHeader, err error) {
func (w *WhatsOnChain) GetBlockHeader(ctx context.Context, height uint32) (header *BlockHeader, err error) {
url := fmt.Sprintf("%s/block/%d/header", w.baseURL, height)
req, err := http.NewRequestWithContext(context.Background(), "GET", url, nil)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -78,8 +78,8 @@ func (w *WhatsOnChain) GetBlockHeader(height uint32) (header *BlockHeader, err e
return header, nil
}

func (w *WhatsOnChain) IsValidRootForHeight(root *chainhash.Hash, height uint32) (bool, error) {
if header, err := w.GetBlockHeader(height); err != nil {
func (w *WhatsOnChain) IsValidRootForHeight(ctx context.Context, root *chainhash.Hash, height uint32) (bool, error) {
if header, err := w.GetBlockHeader(ctx, height); err != nil {
return false, err
} else {
return header.MerkleRoot.IsEqual(root), nil
Expand All @@ -89,7 +89,7 @@ func (w *WhatsOnChain) IsValidRootForHeight(root *chainhash.Hash, height uint32)
// Assuming BlockHeader is defined elsewhere
func (w *WhatsOnChain) CurrentHeight(ctx context.Context) (height uint32, err error) {
url := fmt.Sprintf("%s/chain/info", w.baseURL)
req, err := http.NewRequestWithContext(context.Background(), "GET", url, nil)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return
}
Expand Down
15 changes: 10 additions & 5 deletions transaction/chaintracker/whatsonchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ func TestWhatsOnChainGetBlockHeaderSuccess(t *testing.T) {
client: ts.Client(),
}

header, err := woc.GetBlockHeader(100)
ctx := t.Context()
header, err := woc.GetBlockHeader(ctx, 100)
if err != nil {
t.Fatalf("expected no error, got %v", err)
return // Add this return statement
Expand Down Expand Up @@ -81,7 +82,8 @@ func TestWhatsOnChainGetBlockHeaderNotFound(t *testing.T) {
client: ts.Client(),
}

header, err := woc.GetBlockHeader(100)
ctx := t.Context()
header, err := woc.GetBlockHeader(ctx, 100)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
Expand All @@ -105,7 +107,8 @@ func TestWhatsOnChainGetBlockHeaderErrorResponse(t *testing.T) {
client: ts.Client(),
}

header, err := woc.GetBlockHeader(100)
ctx := t.Context()
header, err := woc.GetBlockHeader(ctx, 100)
if err == nil {
t.Fatalf("expected error, got nil")
}
Expand Down Expand Up @@ -136,7 +139,8 @@ func TestWhatsOnChainIsValidRootForHeightSuccess(t *testing.T) {
client: ts.Client(),
}

isValid, err := woc.IsValidRootForHeight(&merkleRootHash, 100)
ctx := t.Context()
isValid, err := woc.IsValidRootForHeight(ctx, &merkleRootHash, 100)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
Expand Down Expand Up @@ -168,7 +172,8 @@ func TestWhatsOnChainIsValidRootForHeightInvalidRoot(t *testing.T) {
client: ts.Client(),
}

isValid, err := woc.IsValidRootForHeight(&differentMerkleRootHash, 100)
ctx := t.Context()
isValid, err := woc.IsValidRootForHeight(ctx, &differentMerkleRootHash, 100)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
Expand Down
9 changes: 5 additions & 4 deletions transaction/merklepath.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package transaction

import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"fmt"
Expand Down Expand Up @@ -263,20 +264,20 @@ func (mp *MerklePath) ComputeRoot(txid *chainhash.Hash) (*chainhash.Hash, error)

// Verify checks if a given transaction ID is part of the Merkle tree
// at the specified block height using a chain tracker
func (mp *MerklePath) VerifyHex(txidStr string, ct chaintracker.ChainTracker) (bool, error) {
func (mp *MerklePath) VerifyHex(ctx context.Context, txidStr string, ct chaintracker.ChainTracker) (bool, error) {
if txid, err := chainhash.NewHashFromHex(txidStr); err != nil {
return false, err
} else {
return mp.Verify(txid, ct)
return mp.Verify(ctx, txid, ct)
}
}

func (mp *MerklePath) Verify(txid *chainhash.Hash, ct chaintracker.ChainTracker) (bool, error) {
func (mp *MerklePath) Verify(ctx context.Context, txid *chainhash.Hash, ct chaintracker.ChainTracker) (bool, error) {
root, err := mp.ComputeRoot(txid)
if err != nil {
return false, err
}
return ct.IsValidRootForHeight(root, mp.BlockHeight)
return ct.IsValidRootForHeight(ctx, root, mp.BlockHeight)
}

func (m *MerklePath) Combine(other *MerklePath) (err error) {
Expand Down
5 changes: 3 additions & 2 deletions transaction/merklepath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func TestMerklePathComputeRootHex(t *testing.T) {
type MyChainTracker struct{}

// Implement the IsValidRootForHeight method on MyChainTracker.
func (mct MyChainTracker) IsValidRootForHeight(root *chainhash.Hash, height uint32) (bool, error) {
func (mct MyChainTracker) IsValidRootForHeight(ctx context.Context, root *chainhash.Hash, height uint32) (bool, error) {
// Convert BRC74Root hex string to a byte slice for comparison
// expectedRoot, _ := hex.DecodeString(BRC74Root)

Expand All @@ -118,7 +118,8 @@ func TestMerklePath_Verify(t *testing.T) {
Path: BRC74JSON.Path,
}
tracker := MyChainTracker{}
result, err := path.VerifyHex(BRC74TXID1, tracker)
ctx := t.Context()
result, err := path.VerifyHex(ctx, BRC74TXID1, tracker)
require.NoError(t, err)
require.True(t, result)
})
Expand Down
Loading