From 90b9efb72caa70df217c078c1d282e7fa53fb1c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Lewandowski?= <35259896+pawellewandowski98@users.noreply.github.com> Date: Thu, 12 Oct 2023 08:30:45 +0200 Subject: [PATCH] feat(BUX-164): verify beef tx (#22) * feat(BUX-164): verify merkle root * chore(BUX-164): add SPV method * chore(BUX-164): remove unused files * chore(BUX-164): remove logs and fix adding up outputs * chore(BUX-164): change the way of calculating input sum * chore(BUX-164): refactor SPV methods * chore(BUX-164): move CMP into separate file * chore(BUX-164): move BEEF tx SPV to separate file * chore(BUX-170): refactor verify merkle roots method * chore(BUX-170): fix example --- compound_merkle_path.go | 18 +++ examples/server/run_server/demo_interface.go | 7 ++ go.mod | 1 + go.sum | 2 + p2p_beef_tx.go | 62 +++++++++- p2p_beef_tx_test.go | 24 ++++ p2p_spv.go | 124 +++++++++++++++++++ server/capabilities.go | 2 +- server/interface.go | 5 + server/mock_test.go | 8 +- server/p2p_receive_transaction.go | 8 +- 11 files changed, 250 insertions(+), 11 deletions(-) create mode 100644 compound_merkle_path.go create mode 100644 p2p_spv.go diff --git a/compound_merkle_path.go b/compound_merkle_path.go new file mode 100644 index 0000000..a0c83cd --- /dev/null +++ b/compound_merkle_path.go @@ -0,0 +1,18 @@ +package paymail + +type CompoundMerklePath []map[string]uint64 + +type CMPSlice []CompoundMerklePath + +func (cmp CompoundMerklePath) calculateMerkleRoots() ([]string, error) { + merkleRoots := make([]string, 0) + + for tx, offset := range cmp[len(cmp)-1] { + merkleRoot, err := calculateMerkleRoot(tx, offset, cmp) + if err != nil { + return nil, err + } + merkleRoots = append(merkleRoots, merkleRoot) + } + return merkleRoots, nil +} diff --git a/examples/server/run_server/demo_interface.go b/examples/server/run_server/demo_interface.go index 675e159..0c45dbb 100644 --- a/examples/server/run_server/demo_interface.go +++ b/examples/server/run_server/demo_interface.go @@ -43,3 +43,10 @@ func (d *demoServiceProvider) RecordTransaction(ctx context.Context, // Record the tx into your datastore layer return DemoRecordTransaction(ctx, p2pTx) } + +// VerifyMerkleRoots is a demo implementation of this interface +func (d *demoServiceProvider) VerifyMerkleRoots(ctx context.Context, merkleProofs []string) error { + + // Verify the Merkle roots + return nil +} diff --git a/go.mod b/go.mod index e8203c5..53299c7 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/go-resty/resty/v2 v2.7.0 github.com/jarcoal/httpmock v1.3.1 github.com/julienschmidt/httprouter v1.3.0 + github.com/libsv/go-bc v0.1.17 github.com/libsv/go-bk v0.1.6 github.com/libsv/go-bt/v2 v2.2.5 github.com/miekg/dns v1.1.55 diff --git a/go.sum b/go.sum index c7128b1..7bdb7c4 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4d github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/libsv/go-bc v0.1.17 h1:RHlb7BhkjGatKoGGsxZXiE2xoh5aDQDhgELAD2I4n90= +github.com/libsv/go-bc v0.1.17/go.mod h1:TydCZiPUpYPTLiYFDrz2Ug+wSzADUHJux4t3DPVPg08= github.com/libsv/go-bk v0.1.6 h1:c9CiT5+64HRDbzxPl1v/oiFmbvWZTuUYqywCf+MBs/c= github.com/libsv/go-bk v0.1.6/go.mod h1:khJboDoH18FPUaZlzRFKzlVN84d4YfdmlDtdX4LAjQA= github.com/libsv/go-bt/v2 v2.2.5 h1:VoggBLMRW9NYoFujqe5bSYKqnw5y+fYfufgERSoubog= diff --git a/p2p_beef_tx.go b/p2p_beef_tx.go index e184708..a71c4df 100644 --- a/p2p_beef_tx.go +++ b/p2p_beef_tx.go @@ -4,14 +4,10 @@ import ( "encoding/hex" "errors" "fmt" - + "github.com/libsv/go-bc" "github.com/libsv/go-bt/v2" ) -type CompoundMerklePath []map[string]uint64 - -type CMPSlice []CompoundMerklePath - const ( BEEFMarkerPart1 = 0xBE BEEFMarkerPart2 = 0xEF @@ -39,6 +35,62 @@ type DecodedBEEF struct { ProcessedTxData TxData } +func (dBeef *DecodedBEEF) GetMerkleRoots() ([]string, error) { + var merkleRoots []string + for _, cmp := range dBeef.CMPSlice { + partialMerkleRoots, err := cmp.calculateMerkleRoots() + if err != nil { + return nil, err + } + merkleRoots = append(merkleRoots, partialMerkleRoots...) + } + return merkleRoots, nil +} + +func calculateMerkleRoot(baseTx string, offset uint64, cmp []map[string]uint64) (string, error) { + for i := len(cmp) - 1; i >= 0; i-- { + var leftNode, rightNode string + newOffset := offset - 1 + if offset%2 == 0 { + newOffset = offset + 1 + } + tx2 := keyByValue(cmp[i], newOffset) + if tx2 == nil { + fmt.Println("could not find pair") + return "", errors.New("could not find pair") + } + + if newOffset > offset { + leftNode = baseTx + rightNode = *tx2 + } else { + leftNode = *tx2 + rightNode = baseTx + } + + // Calculate new merkle tree parent + str, err := bc.MerkleTreeParentStr(leftNode, rightNode) + if err != nil { + return "", err + } + baseTx = str + + // Reduce offset + offset = offset / 2 + } + + return baseTx, nil +} + +func keyByValue(m map[string]uint64, value uint64) *string { + for k, v := range m { + if value == v { + return &k + } + } + return nil +} + func DecodeBEEF(beefHex string) (*DecodedBEEF, error) { beefBytes, err := extractBytesWithoutVersionAndMarker(beefHex) if err != nil { diff --git a/p2p_beef_tx_test.go b/p2p_beef_tx_test.go index d3fbf9f..b853e68 100644 --- a/p2p_beef_tx_test.go +++ b/p2p_beef_tx_test.go @@ -1,13 +1,24 @@ package paymail import ( + "context" "errors" + "github.com/stretchr/testify/require" "testing" "github.com/libsv/go-bt/v2" "github.com/stretchr/testify/assert" ) +// Mock implementation of a service provider +type mockServiceProvider struct{} + +// VerifyMerkleRoots is a mock implementation of this interface +func (m *mockServiceProvider) VerifyMerkleRoots(_ context.Context, _ []string) error { + // Verify the merkle roots + return nil +} + func TestDecodeBEEF_DecodeBEEF_HappyPaths(t *testing.T) { testCases := []struct { name string @@ -249,3 +260,16 @@ func TestDecodeBEEF_DecodeBEEF_HandlingErrors(t *testing.T) { }) } } + +// TestDecodingBeef will test methods on the DecodedBEEF struct +func TestDecodedBeef(t *testing.T) { + t.Parallel() + + const validBeefHex = "0100beef01020101cd73c0c6bb645581816fa960fd2f1636062fcbf23cb57981074ab8d708a76e3b02003470d882cf556a4b943639eba15dc795dffdbebdc98b9a98e3637fda96e3811e01c58e40f22b9e9fcd05a09689a9b19e6e62dbfd3335c5253d09a7a7cd755d9a3c04008c00bb9360e93fb822c84b2e579fa4ce75c8378ae87f67730a49552f73c56ee801da256f78ae0ad74bbf539662cdb9122aa02ba9a9d883f1d52468d96290515adb02b4c8d919190a090e77b73ffcd52b85babaaeeb62da000473102aca7f070facef03e5b331f4961d764373f3a4e2751954e75489fb17902aad583eedbb41dc165a3b020100000001d0924efc6eb21c88ec91538edfb1fa8ae73e1e2417d6fdec0119998d6042778b0a0000006a47304402205d31e8777edd5d609d3ad9b3090c37016eacf9ab3b150d8badc6d9088ed1ba99022032af2a0b7b8d9cd6a92da5972dfd9d84722e86c213497bbe5a09d30acf9893ee412102d395073f0b4866d64d10015cb016924b1f79cad522911e0b884cd362304f6fd5ffffffff09f4010000000000001976a9147568534fbfc766d05a85c0a18adf71b736c9ad6888acf4010000000000001976a914005d343495af9904df7058ca255dfc7a6271b80f88acf4010000000000001976a914bdd0a2081a29b10c66b76534de0b3c4742fbe35688acf4010000000000001976a91479e158f460cedabf2ed37793e2c2b8f39c79909688acf4010000000000001976a9141a23b7405448ddb2fc687b8479fe9ba16d83edd888ac88130000000000001976a914d107abe806862ac2afa80e77ae5bc4c38eb93a7f88ac10270000000000001976a9149138e8bc3fad2076a9335b0a1f7ea29502b13ce588ac0000000000000000fd2401006a22314d6a4a7251744a735959647a753254487872654e69524c53586548637a417778550a5361746f73656e6465720a746578742f706c61696e057574662d38017c223150755161374b36324d694b43747373534c4b79316b683536575755374d7455523503534554036170700a7361746f73656e64657204747970650c7265676973746572557365720c7061796d61696c204861736840376538303531633662306330633339373231303238656134326361333836383733323236656263396264353732323336353935376637333461353365633339350970686f6e65486173684037363339633564383239646138373935373030353133613865626338623762656361643532333336346365356335626135666436636263356239626532333261c3090000000000001976a91479dcbb510e68557c8a791e439cd9f8b0d8d3429b88ac00000000010001000000010202f24b9ae7399cc6b218053b3b0800cd48c93131fc71442921eb46e9b2ea5a060000006a4730440220338e92e521529e433a2c6b9afbe02e30602c9a553570855692b03e8cfab5b65802204196d7bf136f9768d094808d6bfd6cade3030bf812affea1d71bd51b1c2b104b412102eb33b0cbffb1e3490033348e9d47bcffbeb2e917210958c013f3260864b86c4bffffffff08f4010000000000001976a914c0b3640ed2d59b31d90f1eca2b87db733fb303db88ac88130000000000001976a91417386cd7256887615d214d3e0f70fede265b52cc88acf4010000000000001976a91402b6128d583aa1588f617e5980f4727891e71a9b88acf4010000000000001976a91449412664a4231edb2dfc03cbabff3b404ea4776588acf4010000000000001976a914cb9610a9d2bf1805022779d6f97e2cfdd7c2c8c488acf4010000000000001976a91416b6a5e45d5b2fb7d64e255504734f7c8f7762fa88ac0000000000000000fd2401006a22314d6a4a7251744a735959647a753254487872654e69524c53586548637a417778550a5361746f73656e6465720a746578742f706c61696e057574662d38017c223150755161374b36324d694b43747373534c4b79316b683536575755374d7455523503534554036170700a7361746f73656e64657204747970650c7265676973746572557365720c7061796d61696c204861736840336135343561343561306535313837666262383264663538376438656266623061336665336130363665333338373034643539396234623132343335333362650970686f6e65486173684066376661396133303131616462356364346535336135363631646564363337633564666363663836323864643130363666663737393764613130383334303261c3090000000000001976a914caaf40bc699eb34363f25e961d72f7045dbd4d2688ac0000000000" + validDecodedBeef, err := DecodeBEEF(validBeefHex) + require.Nil(t, err) + + t.Run("SPV on valid beef", func(t *testing.T) { + require.Nil(t, ExecuteSimplifiedPaymentVerification(validDecodedBeef, new(mockServiceProvider))) + }) +} diff --git a/p2p_spv.go b/p2p_spv.go new file mode 100644 index 0000000..809a81e --- /dev/null +++ b/p2p_spv.go @@ -0,0 +1,124 @@ +package paymail + +import ( + "context" + "errors" + "fmt" + "github.com/libsv/go-bt/v2" + "github.com/libsv/go-bt/v2/bscript/interpreter" +) + +type MerkleRootVerifier interface { + VerifyMerkleRoots( + ctx context.Context, + merkleRoots []string, + ) error +} + +// ExecuteSimplifiedPaymentVerification executes the SPV for decoded BEEF tx +func ExecuteSimplifiedPaymentVerification(dBeef *DecodedBEEF, provider MerkleRootVerifier) error { + err := validateSatoshisSum(dBeef) + if err != nil { + return err + } + + err = validateLockTime(dBeef) + if err != nil { + return err + } + + err = validateScripts(dBeef) + if err != nil { + return err + } + + err = verifyMerkleRoots(dBeef, provider) + if err != nil { + return err + } + + return nil +} + +func verifyMerkleRoots(dBeef *DecodedBEEF, provider MerkleRootVerifier) error { + merkleRoots, err := dBeef.GetMerkleRoots() + if err != nil { + return err + } + + err = provider.VerifyMerkleRoots(context.Background(), merkleRoots) + if err != nil { + return err + } + + return nil +} + +func validateScripts(dBeef *DecodedBEEF) error { + for _, input := range dBeef.ProcessedTxData.Transaction.Inputs { + txId := input.PreviousTxID() + for j, input2 := range dBeef.InputsTxData { + if input2.Transaction.TxID() == string(txId) { + result := verifyScripts(dBeef.ProcessedTxData.Transaction, input2.Transaction, j) + if !result { + return errors.New("invalid script") + } + break + } + } + } + return nil +} + +func validateSatoshisSum(dBeef *DecodedBEEF) error { + if len(dBeef.ProcessedTxData.Transaction.Outputs) == 0 { + return errors.New("invalid output, no outputs") + } + + if len(dBeef.ProcessedTxData.Transaction.Inputs) == 0 { + return errors.New("invalid input, no inputs") + } + + inputSum, outputSum := uint64(0), uint64(0) + for i, input := range dBeef.ProcessedTxData.Transaction.Inputs { + inputParentTx := dBeef.InputsTxData[i] + inputSum += inputParentTx.Transaction.Outputs[input.PreviousTxOutIndex].Satoshis + } + for _, output := range dBeef.ProcessedTxData.Transaction.Outputs { + outputSum += output.Satoshis + } + + if inputSum <= outputSum { + return errors.New("invalid input and output sum, outputs can not be larger than inputs") + } + return nil +} + +func validateLockTime(dBeef *DecodedBEEF) error { + if dBeef.ProcessedTxData.Transaction.LockTime == 0 { + for _, input := range dBeef.ProcessedTxData.Transaction.Inputs { + if input.SequenceNumber != 0xffffffff { + return errors.New("unexpected transaction with nSequence") + } + } + } else { + return errors.New("nexpected transaction with nLockTime") + } + return nil +} + +// Verify locking and unlocking scripts pair +func verifyScripts(tx, prevTx *bt.Tx, inputIdx int) bool { + input := tx.InputIdx(inputIdx) + prevOutput := prevTx.OutputIdx(int(input.PreviousTxOutIndex)) + + if err := interpreter.NewEngine().Execute( + interpreter.WithTx(tx, inputIdx, prevOutput), + interpreter.WithForkID(), + interpreter.WithAfterGenesis(), + ); err != nil { + fmt.Println(err) + return false + } + return true +} diff --git a/server/capabilities.go b/server/capabilities.go index aa8a9af..ecbce4f 100644 --- a/server/capabilities.go +++ b/server/capabilities.go @@ -31,7 +31,7 @@ func P2PCapabilities(bsvAliasVersion string, senderValidation bool) *paymail.Cap // BeefCapabilities will add beef capabilities to given ones func BeefCapabilities(c *paymail.CapabilitiesPayload) *paymail.CapabilitiesPayload { - c.Capabilities[paymail.BRFCBeefTransaction] = "/receive-beef-transaction/{alias}@{domain.tld}" + c.Capabilities[paymail.BRFCBeefTransaction] = "/beef/{alias}@{domain.tld}" return c } diff --git a/server/interface.go b/server/interface.go index 4f489bc..d3cd835 100644 --- a/server/interface.go +++ b/server/interface.go @@ -33,4 +33,9 @@ type PaymailServiceProvider interface { p2pTx *paymail.P2PTransaction, metaData *RequestMetadata, ) (*paymail.P2PTransactionPayload, error) + + VerifyMerkleRoots( + ctx context.Context, + merkleProofs []string, + ) error } diff --git a/server/mock_test.go b/server/mock_test.go index f4e73c9..064d76b 100644 --- a/server/mock_test.go +++ b/server/mock_test.go @@ -2,7 +2,6 @@ package server import ( "context" - "github.com/bitcoin-sv/go-paymail" ) @@ -40,3 +39,10 @@ func (m *mockServiceProvider) RecordTransaction(_ context.Context, // Record the tx into your datastore layer return nil, nil } + +// VerifyMerkleRoots is a mock implementation of this interface +func (m *mockServiceProvider) VerifyMerkleRoots(_ context.Context, _ []string) error { + + // Verify the merkle roots + return nil +} diff --git a/server/p2p_receive_transaction.go b/server/p2p_receive_transaction.go index 9dd7a7a..2e02ef9 100644 --- a/server/p2p_receive_transaction.go +++ b/server/p2p_receive_transaction.go @@ -73,7 +73,7 @@ Incoming Data Object Example: func (c *Configuration) p2pReceiveBeefTx(w http.ResponseWriter, req *http.Request, p httprouter.Params) { p2pFormat := beefP2pPayload - requestPayload, beefData, md, vErr := processP2pReceiveTxRequest(c, req, p, p2pFormat) + requestPayload, dBeef, md, vErr := processP2pReceiveTxRequest(c, req, p, p2pFormat) if vErr != nil { ErrorResponse(w, vErr.code, vErr.msg, vErr.httpResponseCode) return @@ -83,12 +83,12 @@ func (c *Configuration) p2pReceiveBeefTx(w http.ResponseWriter, req *http.Reques panic("empty hex after parsing!") } - if beefData == nil { + if dBeef == nil { panic("empty beef after parsing!") } - var err error - if err = ExecuteSimplifiedPaymentVerification(req.Context(), beefData); err != nil { + err := paymail.ExecuteSimplifiedPaymentVerification(dBeef, c.actions) + if err != nil { ErrorResponse(w, ErrorSimplifiedPaymentVerification, err.Error(), http.StatusExpectationFailed) return }