Skip to content

Commit

Permalink
feat(BUX-172): implement SPV for every unmined tx in BEEF
Browse files Browse the repository at this point in the history
  • Loading branch information
arkadiuszos4chain committed Nov 22, 2023
1 parent 13260cd commit 5416ca4
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 110 deletions.
2 changes: 1 addition & 1 deletion definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,5 @@ type AddressInformation struct {
// of Merkle Roots inclusion in the longest chain.
type MerkleRootConfirmationRequestItem struct {
MerkleRoot string `json:"merkleRoot"`
BlockHeight int32 `json:"blockHeight"`
BlockHeight uint64 `json:"blockHeight"`
}
64 changes: 33 additions & 31 deletions p2p_beef_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@ const (

type TxData struct {
Transaction *bt.Tx
PathIndex *bt.VarInt
BumpIndex *bt.VarInt

txID string
}

func (td *TxData) Unmined() bool {
return td.BumpIndex == nil
}

func (td *TxData) GetTxID() string {
if len(td.txID) == 0 {
td.txID = td.Transaction.TxID()
Expand All @@ -41,9 +45,30 @@ func (td *TxData) GetTxID() string {
}

type DecodedBEEF struct {
BUMPs BUMPs
InputsTxData []*TxData
ProcessedTxData *bt.Tx
BUMPs BUMPs
Transactions []*TxData
}

func DecodeBEEF(beefHex string) (*DecodedBEEF, error) {
beefBytes, err := extractBytesWithoutVersionAndMarker(beefHex)
if err != nil {
return nil, err
}

bumps, remainingBytes, err := decodeBUMPs(beefBytes)
if err != nil {
return nil, err
}

transactions, err := decodeTransactionsWithPathIndexes(remainingBytes)
if err != nil {
return nil, err
}

return &DecodedBEEF{
BUMPs: bumps,
Transactions: transactions,
}, nil
}

// GetMerkleRoots will calculate the merkle roots for the BUMPs in the BEEF transaction
Expand All @@ -57,7 +82,7 @@ func (dBeef *DecodedBEEF) GetMerkleRootsRequest() ([]MerkleRootConfirmationReque
}

request := MerkleRootConfirmationRequestItem{
BlockHeight: int32(bump.BlockHeight),
BlockHeight: bump.BlockHeight,
MerkleRoot: merkleRoot,
}
merkleRootsRequest = append(merkleRootsRequest, request)
Expand All @@ -66,31 +91,8 @@ func (dBeef *DecodedBEEF) GetMerkleRootsRequest() ([]MerkleRootConfirmationReque
return merkleRootsRequest, nil
}

func DecodeBEEF(beefHex string) (*DecodedBEEF, error) {
beefBytes, err := extractBytesWithoutVersionAndMarker(beefHex)
if err != nil {
return nil, err
}

bumps, remainingBytes, err := decodeBUMPs(beefBytes)
if err != nil {
return nil, err
}

transactions, err := decodeTransactionsWithPathIndexes(remainingBytes)
if err != nil {
return nil, err
}

// get the last transaction as the processed transaction - it should be the last one because of khan's ordering
processedTx := transactions[len(transactions)-1]
transactions = transactions[:len(transactions)-1]

return &DecodedBEEF{
BUMPs: bumps,
InputsTxData: transactions,
ProcessedTxData: processedTx.Transaction,
}, nil
func (d *DecodedBEEF) GetLatestTx() *bt.Tx {
return d.Transactions[len(d.Transactions)-1].Transaction // get the last transaction as the processed transaction - it should be the last one because of khan's ordering
}

func decodeBUMPs(beefBytes []byte) ([]BUMP, []byte, error) {
Expand Down Expand Up @@ -239,7 +241,7 @@ func decodeTransactionsWithPathIndexes(bytes []byte) ([]*TxData, error) {

transactions = append(transactions, &TxData{
Transaction: tx,
PathIndex: pathIndex,
BumpIndex: pathIndex,
})
}

Expand Down
49 changes: 25 additions & 24 deletions p2p_beef_tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ func TestDecodeBEEF_DecodeBEEF_HappyPaths(t *testing.T) {
hexStream string
expectedDecodedBEEF *DecodedBEEF
pathIndexForTheOldestInput *bt.VarInt
expectedError error
}{
{
name: "valid BEEF with 1 CMP and 1 input transaction",
Expand Down Expand Up @@ -50,7 +49,7 @@ func TestDecodeBEEF_DecodeBEEF_HappyPaths(t *testing.T) {
},
},
},
InputsTxData: []*TxData{
Transactions: []*TxData{
{
Transaction: &bt.Tx{
Version: 1,
Expand All @@ -70,25 +69,29 @@ func TestDecodeBEEF_DecodeBEEF_HappyPaths(t *testing.T) {
},
},
},
PathIndex: func(v bt.VarInt) *bt.VarInt { return &v }(0x0),
BumpIndex: func(v bt.VarInt) *bt.VarInt { return &v }(0x0),
},
},
ProcessedTxData: &bt.Tx{
Version: 1,
LockTime: 0,
Inputs: []*bt.Input{
{
PreviousTxSatoshis: 0,
PreviousTxOutIndex: 0,
SequenceNumber: 4294967295,
PreviousTxScript: nil,
},
},
Outputs: []*bt.Output{
{
Satoshis: 26172,
LockingScript: bscript.NewFromBytes([]byte("76a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac")),

{
Transaction: &bt.Tx{
Version: 1,
LockTime: 0,
Inputs: []*bt.Input{
{
PreviousTxSatoshis: 0,
PreviousTxOutIndex: 0,
SequenceNumber: 4294967295,
PreviousTxScript: nil,
},
},
Outputs: []*bt.Output{
{
Satoshis: 26172,
LockingScript: bscript.NewFromBytes([]byte("76a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac")),
},
},
},
BumpIndex: nil,
},
},
},
Expand All @@ -104,9 +107,9 @@ func TestDecodeBEEF_DecodeBEEF_HappyPaths(t *testing.T) {
decodedBEEF, err := DecodeBEEF(beef)

// then
assert.Equal(t, tc.expectedError, err, "expected error %v, but got %v", tc.expectedError, err)
assert.Nil(t, err)

assert.Equal(t, len(tc.expectedDecodedBEEF.InputsTxData), len(decodedBEEF.InputsTxData), "expected %v inputs, but got %v", len(tc.expectedDecodedBEEF.InputsTxData), len(decodedBEEF.InputsTxData))
assert.Equal(t, len(tc.expectedDecodedBEEF.Transactions), len(decodedBEEF.Transactions), "expected %v inputs, but got %v", len(tc.expectedDecodedBEEF.Transactions), len(decodedBEEF.Transactions))

assert.Equal(t, len(tc.expectedDecodedBEEF.BUMPs), len(decodedBEEF.BUMPs), "expected %v BUMPs, but got %v", len(tc.expectedDecodedBEEF.BUMPs), len(decodedBEEF.BUMPs))

Expand All @@ -115,9 +118,7 @@ func TestDecodeBEEF_DecodeBEEF_HappyPaths(t *testing.T) {
assert.Equal(t, bump.Path, decodedBEEF.BUMPs[i].Path, "expected equal BUMPPaths for %v BUMP, expected: %v but got %v", i, bump, len(decodedBEEF.BUMPs[i].Path))
}

assert.NotNil(t, decodedBEEF.ProcessedTxData, "expected original transaction to be not nil")

assert.Equal(t, tc.expectedDecodedBEEF.InputsTxData[0].PathIndex, decodedBEEF.InputsTxData[0].PathIndex, "expected path index for the oldest input to be %v, but got %v", tc.expectedDecodedBEEF.InputsTxData[0].PathIndex, decodedBEEF.InputsTxData[0].PathIndex)
assert.Equal(t, tc.expectedDecodedBEEF.Transactions[0].BumpIndex, decodedBEEF.Transactions[0].BumpIndex, "expected path index for the oldest input to be %v, but got %v", tc.expectedDecodedBEEF.Transactions[0].BumpIndex, decodedBEEF.Transactions[0].BumpIndex)
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion server/p2p_receive_transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (c *Configuration) p2pReceiveBeefTx(w http.ResponseWriter, req *http.Reques
panic("empty beef after parsing!")
}

err := spv.ExecuteSimplifiedPaymentVerification(dBeef, c.actions)
err := spv.ExecuteSimplifiedPaymentVerification(req.Context(), dBeef, c.actions)
if err != nil {
ErrorResponse(w, ErrorSimplifiedPaymentVerification, err.Error(), http.StatusExpectationFailed)
return
Expand Down
2 changes: 1 addition & 1 deletion server/p2p_receive_transaction_request_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func getProcessedTxData(payload *p2pReceiveTxReqPayload, format p2pPayloadFormat
return nil, nil, &processingError{&parseError{ErrorInvalidParameter, errorMsg}, http.StatusBadRequest}
}

processedTx = beefData.ProcessedTxData
processedTx = beefData.GetLatestTx()

default:
panic("Unexpected transaction format!")
Expand Down
25 changes: 15 additions & 10 deletions spv/merkle_path_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
"github.com/libsv/go-bt/v2"
)

func verifyMerkleRoots(dBeef *paymail.DecodedBEEF, provider MerkleRootVerifier) error {
if err := ensureInputTransactionsArePresentInBump(dBeef); err != nil {
func verifyMerkleRoots(ctx context.Context, dBeef *paymail.DecodedBEEF, provider MerkleRootVerifier) error {
if err := ensureInputTransactionsArePresentInBump(dBeef.GetLatestTx(), dBeef); err != nil {
return err
}

Expand All @@ -18,19 +18,19 @@ func verifyMerkleRoots(dBeef *paymail.DecodedBEEF, provider MerkleRootVerifier)
return err
}

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

return nil
}

func ensureInputTransactionsArePresentInBump(dBeef *paymail.DecodedBEEF) error {
func ensureInputTransactionsArePresentInBump(tx *bt.Tx, dBeef *paymail.DecodedBEEF) error {

for _, input := range dBeef.ProcessedTxData.Inputs {
for _, input := range tx.Inputs {

minedAcestors := findMinedAncestors(input, dBeef.InputsTxData)
minedAcestors := findMinedAncestors(input, dBeef.Transactions, 0)
if len(minedAcestors) == 0 {
return errors.New("invalid BUMP - input mined ancestor is not present in BUMPs")
}
Expand All @@ -46,28 +46,33 @@ func ensureInputTransactionsArePresentInBump(dBeef *paymail.DecodedBEEF) error {
return nil
}

func findMinedAncestors(input *bt.Input, parentTxs []*paymail.TxData) []*paymail.TxData {
func findMinedAncestors(input *bt.Input, parentTxs []*paymail.TxData, depth uint) []*paymail.TxData {
if depth > 64 {
return []*paymail.TxData{}
}
depth++

parent := findParentForInput(input, parentTxs)

if parent == nil { // oh oh- end of hierarchy
return []*paymail.TxData{}
}

if parent.PathIndex != nil { // mined parent
if !parent.Unmined() {
return []*paymail.TxData{parent}
}

ancestors := make([]*paymail.TxData, 0)

for _, in := range parent.Transaction.Inputs {
ancestors = append(ancestors, findMinedAncestors(in, parentTxs)...)
ancestors = append(ancestors, findMinedAncestors(in, parentTxs, depth)...)
}

return ancestors
}

func existsInBumps(ancestorTx *paymail.TxData, bumps paymail.BUMPs) bool {
bumpIdx := int(*ancestorTx.PathIndex) // TODO: disscuss max value of index
bumpIdx := int(*ancestorTx.BumpIndex)
parentTxID := ancestorTx.GetTxID()

if len(bumps) > bumpIdx {
Expand Down
8 changes: 4 additions & 4 deletions spv/scripts_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import (
"github.com/bitcoin-sv/go-paymail"
)

func validateScripts(dBeef *paymail.DecodedBEEF) error {
for i, input := range dBeef.ProcessedTxData.Inputs {
inputParentTx := findParentForInput(input, dBeef.InputsTxData)
func validateScripts(tx *bt.Tx, inputTxs []*paymail.TxData) error {
for i, input := range tx.Inputs {
inputParentTx := findParentForInput(input, inputTxs)
if inputParentTx == nil {
return errors.New("invalid parent transactions, no matching trasactions for input")
}

err := verifyScripts(dBeef.ProcessedTxData, inputParentTx.Transaction, i)
err := verifyScripts(tx, inputParentTx.Transaction, i)
if err != nil {
return errors.New("invalid script")
}
Expand Down
Loading

0 comments on commit 5416ca4

Please sign in to comment.