Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(BUX-164): verify beef tx #22

Merged
merged 10 commits into from
Oct 12, 2023
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
}
57 changes: 57 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ 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/bitcoin-hc v0.3.0
pawellewandowski98 marked this conversation as resolved.
Show resolved Hide resolved
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 All @@ -16,19 +18,74 @@ require (
)

require (
github.com/FZambia/eagle v0.0.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bitcoinsv/bsvd v0.0.0-20190609155523-4c29707f7173 // indirect
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/bytedance/sonic v1.8.0 // indirect
github.com/centrifugal/centrifuge v0.29.1 // indirect
github.com/centrifugal/protocol v0.10.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dchest/uniuri v1.2.0 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/igm/sockjs-go/v3 v3.0.2 // indirect
github.com/jessevdk/go-flags v1.5.0 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/newrelic/go-agent/v3 v3.24.1 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rueian/rueidis v0.0.100-go1.18 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/segmentio/encoding v0.3.6 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.3.0 // indirect
github.com/spf13/jwalterweatherman v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.7.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.10 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.12.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 // indirect
google.golang.org/grpc v1.57.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.51.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
691 changes: 691 additions & 0 deletions go.sum

Large diffs are not rendered by default.

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-- {
dorzepowski marked this conversation as resolved.
Show resolved Hide resolved
var leftNode, rightNode string
dorzepowski marked this conversation as resolved.
Show resolved Hide resolved
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
25 changes: 25 additions & 0 deletions p2p_beef_tx_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
package paymail

import (
"context"
"errors"
"github.com/libsv/bitcoin-hc/transports/http/endpoints/api/merkleroots"
"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) (*merkleroots.MerkleRootsConfirmationsResponse, error) {
// Verify the merkle roots
return &merkleroots.MerkleRootsConfirmationsResponse{AllConfirmed: true}, nil
}

func TestDecodeBEEF_DecodeBEEF_HappyPaths(t *testing.T) {
testCases := []struct {
name string
Expand Down Expand Up @@ -249,3 +261,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) {
dorzepowski marked this conversation as resolved.
Show resolved Hide resolved
require.Nil(t, ExecuteSimplifiedPaymentVerification(validDecodedBeef, new(mockServiceProvider)))
})
}
128 changes: 128 additions & 0 deletions p2p_spv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package paymail

import (
"context"
"errors"
"fmt"
"github.com/libsv/bitcoin-hc/transports/http/endpoints/api/merkleroots"
"github.com/libsv/go-bt/v2"
"github.com/libsv/go-bt/v2/bscript/interpreter"
)

type MerkleRootVerifier interface {
VerifyMerkleRoots(
ctx context.Context,
merkleRoots []string,
) (*merkleroots.MerkleRootsConfirmationsResponse, 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
}

res, err := provider.VerifyMerkleRoots(context.Background(), merkleRoots)
if err != nil {
return err
}

if !res.AllConfirmed {
return errors.New("not all merkle roots were confirmed")
}
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("invalid sequence")
pawellewandowski98 marked this conversation as resolved.
Show resolved Hide resolved
}
}
} else {
return errors.New("invalid locktime")
pawellewandowski98 marked this conversation as resolved.
Show resolved Hide resolved
}
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
6 changes: 6 additions & 0 deletions server/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/bitcoin-sv/go-paymail"
"github.com/libsv/bitcoin-hc/transports/http/endpoints/api/merkleroots"
)

// PaymailServiceProvider the paymail server interface that needs to be implemented
Expand Down Expand Up @@ -33,4 +34,9 @@ type PaymailServiceProvider interface {
p2pTx *paymail.P2PTransaction,
metaData *RequestMetadata,
) (*paymail.P2PTransactionPayload, error)

VerifyMerkleRoots(
arkadiuszos4chain marked this conversation as resolved.
Show resolved Hide resolved
ctx context.Context,
merkleProofs []string,
) (*merkleroots.MerkleRootsConfirmationsResponse, error)
}
8 changes: 8 additions & 0 deletions server/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package server

import (
"context"
"github.com/libsv/bitcoin-hc/transports/http/endpoints/api/merkleroots"

"github.com/bitcoin-sv/go-paymail"
)
Expand Down Expand Up @@ -40,3 +41,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) (*merkleroots.MerkleRootsConfirmationsResponse, error) {

// Verify the merkle roots
return &merkleroots.MerkleRootsConfirmationsResponse{AllConfirmed: true}, nil
}
Loading
Loading