Skip to content

Commit

Permalink
feat(BUX-164): verify beef tx (#22)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
pawellewandowski98 authored Oct 12, 2023
1 parent 92c6a65 commit 90b9efb
Show file tree
Hide file tree
Showing 11 changed files with 250 additions and 11 deletions.
18 changes: 18 additions & 0 deletions compound_merkle_path.go
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 7 additions & 0 deletions examples/server/run_server/demo_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
62 changes: 57 additions & 5 deletions p2p_beef_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
24 changes: 24 additions & 0 deletions p2p_beef_tx_test.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)))
})
}
124 changes: 124 additions & 0 deletions p2p_spv.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion server/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
5 changes: 5 additions & 0 deletions server/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,9 @@ type PaymailServiceProvider interface {
p2pTx *paymail.P2PTransaction,
metaData *RequestMetadata,
) (*paymail.P2PTransactionPayload, error)

VerifyMerkleRoots(
ctx context.Context,
merkleProofs []string,
) error
}
8 changes: 7 additions & 1 deletion server/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package server

import (
"context"

"github.com/bitcoin-sv/go-paymail"
)

Expand Down Expand Up @@ -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
}
8 changes: 4 additions & 4 deletions server/p2p_receive_transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down

0 comments on commit 90b9efb

Please sign in to comment.