Skip to content

Commit

Permalink
verify
Browse files Browse the repository at this point in the history
  • Loading branch information
shruggr committed Oct 19, 2024
1 parent 3a5dcee commit a7bf45e
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 8 deletions.
4 changes: 2 additions & 2 deletions docs/examples/verify_beef/verify_beef.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (

type GullibleHeadersClient struct{}

func (g *GullibleHeadersClient) IsValidRootForHeight(merkleRoot *chainhash.Hash, height uint32) bool {
func (g *GullibleHeadersClient) IsValidRootForHeight(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
return true, nil
}

// Replace with the BEEF structure you'd like to check
Expand Down
2 changes: 1 addition & 1 deletion transaction/chaintracker/chaintracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ package chaintracker
import "github.com/bitcoin-sv/go-sdk/chainhash"

type ChainTracker interface {
IsValidRootForHeight(root *chainhash.Hash, height uint32) bool
IsValidRootForHeight(root *chainhash.Hash, height uint32) (bool, error)
}
73 changes: 73 additions & 0 deletions transaction/chaintracker/whatsonchain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package chaintracker

import (
"bytes"
"encoding/json"
"fmt"
"net/http"

"github.com/bitcoin-sv/go-sdk/chainhash"
)

type Network string

type BlockHeader struct {
Hash *chainhash.Hash `json:"hash"`
Height uint32 `json:"height"`
Version uint32 `json:"version"`
MerkleRoot *chainhash.Hash `json:"merkleroot"`
Time uint32 `json:"time"`
Nonce uint32 `json:"nonce"`
Bits string `json:"bits"`
PrevHash *chainhash.Hash `json:"previousblockhash"`
}

var (
MainNet Network = "main"
TestNet Network = "test"
)

type WhatsOnChain struct {
Network Network
ApiKey string
}

func NewWhatsOnChain(network Network, apiKey string) *WhatsOnChain {
return &WhatsOnChain{
Network: network,
ApiKey: apiKey,
}
}

func (w *WhatsOnChain) GetBlockHeader(height uint32) (*BlockHeader, error) {
if req, err := http.NewRequest("GET", fmt.Sprintf("https://api.whatsonchain.com/v1/bsv/%s/block/%d/header", w.Network, height), bytes.NewBuffer([]byte{})); err != nil {
return nil, err
} else {
req.Header.Set("Authorization", w.ApiKey)
if resp, err := http.DefaultClient.Do(req); err != nil {
return nil, err
} else {
defer resp.Body.Close()

Check failure on line 50 in transaction/chaintracker/whatsonchain.go

View workflow job for this annotation

GitHub Actions / golangci-lint (/home/runner/work/go-sdk/go-sdk)

Error return value of `resp.Body.Close` is not checked (errcheck)
if resp.StatusCode == 404 {
return nil, nil
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to verify merkleroot for height %d because of an error: %v", height, resp.Status)
}
header := &BlockHeader{}
if err := json.NewDecoder(resp.Body).Decode(header); err != nil {
return nil, err
}

return header, nil
}
}
}

func (w *WhatsOnChain) IsValidRootForHeight(root *chainhash.Hash, height uint32) (bool, error) {
if header, err := w.GetBlockHeader(height); err != nil {
return false, err
} else {
return header.MerkleRoot.IsEqual(root), nil
}
}
8 changes: 8 additions & 0 deletions transaction/fees.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,11 @@ func (tx *Transaction) Fee(f FeeModel, changeDistribution ChangeDistribution) er
}
return nil
}

func (tx *Transaction) GetFee() (total uint64, err error) {
if totalIn, err := tx.TotalInputSatoshis(); err != nil {
return 0, err
} else {
return totalIn - tx.TotalOutputSatoshis(), nil
}
}
2 changes: 1 addition & 1 deletion transaction/merklepath.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ func (mp *MerklePath) Verify(txid *chainhash.Hash, ct chaintracker.ChainTracker)
if err != nil {
return false, err
}
return ct.IsValidRootForHeight(root, mp.BlockHeight), nil
return ct.IsValidRootForHeight(root, mp.BlockHeight)
}

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

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

// Assuming BRC74JSON.BlockHeight is of type uint64, and needs to be cast to uint64
return root.String() == BRC74Root && height == BRC74JSON.BlockHeight
return root.String() == BRC74Root && height == BRC74JSON.BlockHeight, nil
}

func TestMerklePath_Verify(t *testing.T) {
Expand Down
6 changes: 4 additions & 2 deletions transaction/txinput.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import (
)

// TotalInputSatoshis returns the total Satoshis inputted to the transaction.
func (tx *Transaction) TotalInputSatoshis() (total uint64) {
func (tx *Transaction) TotalInputSatoshis() (total uint64, err error) {
for _, in := range tx.Inputs {
prevSats := uint64(0)
if in.SourceTxSatoshis() != nil {
if in.SourceTxSatoshis() == nil {
return 0, ErrEmptyPreviousTx
} else {
prevSats = *in.SourceTxSatoshis()
}
total += prevSats
Expand Down
91 changes: 91 additions & 0 deletions verify/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package verify

import (
"fmt"

"github.com/bitcoin-sv/go-sdk/script/interpreter"
"github.com/bitcoin-sv/go-sdk/transaction"
"github.com/bitcoin-sv/go-sdk/transaction/chaintracker"
)

func Verify(t *transaction.Transaction, chainTracker chaintracker.ChainTracker, feeModel transaction.FeeModel) (bool, error) {
verifiedTxids := make(map[string]struct{})
txQueue := []*transaction.Transaction{t}
if chainTracker == nil {
chainTracker = chaintracker.NewWhatsOnChain(chaintracker.MainNet, "")
}

for len(txQueue) > 0 {
tx := txQueue[0]
txQueue = txQueue[1:]
txid := tx.TxID()
txidStr := txid.String()

if _, ok := verifiedTxids[txidStr]; ok {
continue
}

if tx.MerklePath != nil {
if isValid, err := tx.MerklePath.Verify(txid, chainTracker); err != nil {
return false, err
} else if isValid {
verifiedTxids[txidStr] = struct{}{}
}
}

if feeModel != nil {
clone := tx.ShallowClone()
clone.Outputs[0].Change = true
if err := clone.Fee(feeModel, transaction.ChangeDistributionEqual); err != nil {
return false, err
}
tx.TotalOutputSatoshis()
if txFee, err := tx.GetFee(); err != nil {
return false, err
} else if cloneFee, err := clone.GetFee(); err != nil {
return false, err
} else if txFee < cloneFee {
return false, fmt.Errorf("fee is too low")
}
}

inputTotal := uint64(0)
for vin, input := range tx.Inputs {
if input.SourceTransaction == nil {
return false, fmt.Errorf("input %d has no source transaction", vin)
}
if input.UnlockingScript == nil || len(*input.UnlockingScript) == 0 {
return false, fmt.Errorf("input %d has no unlocking script", vin)
}
sourceOutput := input.SourceTransaction.Outputs[input.SourceTxOutIndex]
inputTotal += sourceOutput.Satoshis
sourceTxid := input.SourceTransaction.TxID().String()
if _, ok := verifiedTxids[sourceTxid]; !ok {
txQueue = append(txQueue, input.SourceTransaction)
}

otherInputs := make([]*transaction.TransactionInput, 0, len(tx.Inputs)-1)
for i, otherInput := range tx.Inputs {
if i != vin {
otherInputs = append(otherInputs, otherInput)

Check failure on line 70 in verify/verify.go

View workflow job for this annotation

GitHub Actions / golangci-lint (/home/runner/work/go-sdk/go-sdk)

SA4010: this result of append is never used, except maybe in other appends (staticcheck)
}
}

if input.SourceTXID == nil {
input.SourceTXID = input.SourceTransaction.TxID()
}

if err := interpreter.NewEngine().Execute(
interpreter.WithTx(tx, vin, sourceOutput),
interpreter.WithForkID(),
interpreter.WithAfterGenesis(),
); err != nil {
fmt.Println(err)
return false, err
}

}
}

return true, nil
}

0 comments on commit a7bf45e

Please sign in to comment.