From a7bf45e4f04570092413654bf04c478130ffb113 Mon Sep 17 00:00:00 2001 From: David Case Date: Sat, 19 Oct 2024 15:32:02 -0400 Subject: [PATCH] verify --- docs/examples/verify_beef/verify_beef.go | 4 +- transaction/chaintracker/chaintracker.go | 2 +- transaction/chaintracker/whatsonchain.go | 73 +++++++++++++++++++ transaction/fees.go | 8 +++ transaction/merklepath.go | 2 +- transaction/merklepath_test.go | 4 +- transaction/txinput.go | 6 +- verify/verify.go | 91 ++++++++++++++++++++++++ 8 files changed, 182 insertions(+), 8 deletions(-) create mode 100644 transaction/chaintracker/whatsonchain.go create mode 100644 verify/verify.go diff --git a/docs/examples/verify_beef/verify_beef.go b/docs/examples/verify_beef/verify_beef.go index d31b0b3..66d43ac 100644 --- a/docs/examples/verify_beef/verify_beef.go +++ b/docs/examples/verify_beef/verify_beef.go @@ -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 diff --git a/transaction/chaintracker/chaintracker.go b/transaction/chaintracker/chaintracker.go index 781fd26..2377571 100644 --- a/transaction/chaintracker/chaintracker.go +++ b/transaction/chaintracker/chaintracker.go @@ -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) } diff --git a/transaction/chaintracker/whatsonchain.go b/transaction/chaintracker/whatsonchain.go new file mode 100644 index 0000000..4c5f169 --- /dev/null +++ b/transaction/chaintracker/whatsonchain.go @@ -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() + 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 + } +} diff --git a/transaction/fees.go b/transaction/fees.go index f737547..596501a 100644 --- a/transaction/fees.go +++ b/transaction/fees.go @@ -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 + } +} diff --git a/transaction/merklepath.go b/transaction/merklepath.go index 8c89d17..c31cdeb 100644 --- a/transaction/merklepath.go +++ b/transaction/merklepath.go @@ -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) { diff --git a/transaction/merklepath_test.go b/transaction/merklepath_test.go index ca36412..cbc83de 100644 --- a/transaction/merklepath_test.go +++ b/transaction/merklepath_test.go @@ -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) { diff --git a/transaction/txinput.go b/transaction/txinput.go index e2da390..2baa219 100644 --- a/transaction/txinput.go +++ b/transaction/txinput.go @@ -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 diff --git a/verify/verify.go b/verify/verify.go new file mode 100644 index 0000000..dae33ac --- /dev/null +++ b/verify/verify.go @@ -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) + } + } + + 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 +}