Skip to content
This repository has been archived by the owner on Apr 2, 2024. It is now read-only.

Commit

Permalink
Merge pull request #420 from BuxOrg/fix-252-beef-econding
Browse files Browse the repository at this point in the history
fix(BUX-252):  beef econding issues
  • Loading branch information
arkadiuszos4chain authored Oct 5, 2023
2 parents 8a14188 + b1ff954 commit 5ec6ada
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 40 deletions.
70 changes: 58 additions & 12 deletions beef_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ import (
"context"
"encoding/hex"
"errors"
"fmt"
)

const maxBeefVer = uint32(0xFFFF) // value from BRC-62

// ToBeefHex generates BEEF Hex for transaction
func ToBeefHex(tx *Transaction) (string, error) {
beef, err := newBeefTx(1, tx)
func ToBeefHex(ctx context.Context, tx *Transaction) (string, error) {
beef, err := newBeefTx(ctx, 1, tx)
if err != nil {
return "", err
return "", fmt.Errorf("ToBeefHex() error: %w", err)
}

beefBytes, err := beef.toBeefBytes()
if err != nil {
return "", err
return "", fmt.Errorf("ToBeefHex() error: %w", err)
}

return hex.EncodeToString(beefBytes), nil
Expand All @@ -27,19 +30,28 @@ type beefTx struct {
transactions []*Transaction
}

func newBeefTx(version uint32, tx *Transaction) (*beefTx, error) {
if version > 65_535 {
return nil, errors.New("version above 65.535")
func newBeefTx(ctx context.Context, version uint32, tx *Transaction) (*beefTx, error) {
if version > maxBeefVer {
return nil, fmt.Errorf("version above 0x%X", maxBeefVer)
}

var err error
if err = hydrateTransaction(ctx, tx); err != nil {
return nil, err
}

if err = validateCompoundMerklePathes(tx.draftTransaction.CompoundMerklePathes); err != nil {
return nil, err
}

// get inputs parent transactions
inputs := tx.draftTransaction.Configuration.Inputs
transactions := make([]*Transaction, 0, len(inputs)+1)

for _, input := range inputs {
prevTxs, err := getParentTransactionsForInput(tx.client, input)
prevTxs, err := getParentTransactionsForInput(ctx, tx.client, input)
if err != nil {
return nil, err
return nil, fmt.Errorf("retrieve input parent transaction failed: %w", err)
}

transactions = append(transactions, prevTxs...)
Expand All @@ -57,15 +69,49 @@ func newBeefTx(version uint32, tx *Transaction) (*beefTx, error) {
return beef, nil
}

func getParentTransactionsForInput(client ClientInterface, input *TransactionInput) ([]*Transaction, error) {
inputTx, err := client.GetTransactionByID(context.Background(), input.UtxoPointer.TransactionID)
func hydrateTransaction(ctx context.Context, tx *Transaction) error {
if tx.draftTransaction == nil {
dTx, err := getDraftTransactionID(
ctx, tx.XPubID, tx.DraftID, tx.GetOptions(false)...,
)

if err != nil {
return fmt.Errorf("retrieve DraftTransaction failed: %w", err)
}

tx.draftTransaction = dTx
}

return nil
}

func validateCompoundMerklePathes(compountedPaths CMPSlice) error {
if len(compountedPaths) == 0 {
return errors.New("empty compounted paths slice")
}

for _, c := range compountedPaths {
if len(c) == 0 {
return errors.New("one of compounted merkle paths is empty")
}
}

return nil
}

func getParentTransactionsForInput(ctx context.Context, client ClientInterface, input *TransactionInput) ([]*Transaction, error) {
inputTx, err := client.GetTransactionByID(ctx, input.UtxoPointer.TransactionID)
if err != nil {
return nil, err
}

if err = hydrateTransaction(ctx, inputTx); err != nil {
return nil, err
}

if inputTx.MerkleProof.TxOrID != "" {
return []*Transaction{inputTx}, nil
}

return nil, errors.New("transaction is not mined yet") // TODO: handle it in next iterration
return nil, fmt.Errorf("transaction is not mined yet (tx.ID: %s)", inputTx.ID) // TODO: handle it in next iterration
}
5 changes: 3 additions & 2 deletions beef_tx_bytes.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package bux
import (
"encoding/hex"
"errors"
"fmt"

"github.com/libsv/go-bt/v2"
)
Expand All @@ -11,7 +12,7 @@ var hasCmp = byte(0x01)
var hasNoCmp = byte(0x00)

func (beefTx *beefTx) toBeefBytes() ([]byte, error) {
if beefTx.compoundMerklePaths == nil || beefTx.transactions == nil {
if len(beefTx.compoundMerklePaths) == 0 || len(beefTx.transactions) < 2 { // valid BEEF contains at least two transactions (new transaction and one parent transaction)
return nil, errors.New("beef tx is incomplete")
}

Expand Down Expand Up @@ -63,7 +64,7 @@ func (tx *Transaction) toBeefBytes(compountedPaths CMPSlice) ([]byte, error) {
txBeefBytes, err := hex.DecodeString(tx.Hex)

if err != nil {
return nil, err
return nil, fmt.Errorf("decoding tx (ID: %s) hex failed: %w", tx.ID, err)
}

cmpIdx := tx.getCompountedMarklePathIndex(compountedPaths)
Expand Down
11 changes: 7 additions & 4 deletions beef_tx_sorting.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,19 @@ func prepareSortStructures(dag []*Transaction) (txByID map[string]*Transaction,
incomingEdgesMap[tx.ID] = 0
}

calculateIncomingEdges(incomingEdgesMap, dag)
calculateIncomingEdges(incomingEdgesMap, txByID)
zeroIncomingEdgeQueue = getTxWithZeroIncomingEdges(incomingEdgesMap)

return
}

func calculateIncomingEdges(inDegree map[string]int, transactions []*Transaction) {
for _, tx := range transactions {
func calculateIncomingEdges(inDegree map[string]int, txByID map[string]*Transaction) {
for _, tx := range txByID {
for _, input := range tx.draftTransaction.Configuration.Inputs {
inDegree[input.UtxoPointer.TransactionID]++
inputUtxoTxID := input.UtxoPointer.TransactionID
if _, ok := txByID[inputUtxoTxID]; ok { // transaction can contains inputs we are not interested in
inDegree[inputUtxoTxID]++
}
}
}
}
Expand Down
44 changes: 37 additions & 7 deletions beef_tx_sorting_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package bux

import (
"fmt"
"math/rand"
"testing"

Expand All @@ -21,16 +22,45 @@ func Test_kahnTopologicalSortTransaction(t *testing.T) {
createTx("8", "7"),
}

unsortedTxs := shuffleTransactions(txsFromOldestToNewest)
txsFromOldestToNewestWithUnnecessaryInputs := []*Transaction{
createTx("0"),
createTx("1", "0"),
createTx("2", "1", "101", "102"),
createTx("3", "2", "1"),
createTx("4", "3", "1"),
createTx("5", "3", "2", "100"),
createTx("6", "4", "2", "0"),
createTx("7", "6", "5", "3", "1", "103", "105", "106"),
createTx("8", "7"),
}

tCases := []struct {
name string
expectedSortedTransactions []*Transaction
}{{
name: "txs with neccessary data only",
expectedSortedTransactions: txsFromOldestToNewest,
},
{
name: "txs with inputs from other txs",
expectedSortedTransactions: txsFromOldestToNewestWithUnnecessaryInputs,
},
}

t.Run("kahnTopologicalSortTransaction sort from oldest to newest", func(t *testing.T) {
sortedGraph := kahnTopologicalSortTransactions(unsortedTxs)
for _, tc := range tCases {
t.Run(fmt.Sprint("sort from oldest to newest ", tc.name), func(t *testing.T) {
// given
unsortedTxs := shuffleTransactions(tc.expectedSortedTransactions)

for i, tx := range txsFromOldestToNewest {
assert.Equal(t, tx.ID, sortedGraph[i].ID)
}
// when
sortedGraph := kahnTopologicalSortTransactions(unsortedTxs)

})
// then
for i, tx := range txsFromOldestToNewest {
assert.Equal(t, tx.ID, sortedGraph[i].ID)
}
})
}
}

func createTx(txID string, inputsTxIDs ...string) *Transaction {
Expand Down
107 changes: 107 additions & 0 deletions beef_tx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package bux

import (
"context"
"testing"

"github.com/libsv/go-bc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_ToBeefHex(t *testing.T) {
t.Run("all parents txs are already mined", func(t *testing.T) {
//given
ctx, client, deferMe := initSimpleTestCase(t)
defer deferMe()

ancestorTx := addGrandpaTx(t, ctx, client)
minedParentTx := createTxWithDraft(t, ctx, client, ancestorTx, true)

newTx := createTxWithDraft(t, ctx, client, minedParentTx, false)

//when
hex, err := ToBeefHex(ctx, newTx)

//then
assert.NoError(t, err)
assert.NotEmpty(t, hex)
})

t.Run("some parents txs are not mined yet", func(t *testing.T) {
// Error expeted! this should be changed in the future. right now the test case has been written to make sure the system doesn't panic in such a situation

//given
ctx, client, deferMe := initSimpleTestCase(t)
defer deferMe()

ancestorTx := addGrandpaTx(t, ctx, client)
notMinedParentTx := createTxWithDraft(t, ctx, client, ancestorTx, false)

newTx := createTxWithDraft(t, ctx, client, notMinedParentTx, false)

//when
hex, err := ToBeefHex(ctx, newTx)

//then
assert.Error(t, err)
assert.Empty(t, hex)
})
}

func addGrandpaTx(t *testing.T, ctx context.Context, client ClientInterface) *Transaction {
// great ancestor
grandpaTx := newTransaction(testTx2Hex, append(client.DefaultModelOptions(), New())...)
grandpaTx.BlockHeight = 1
// mark it as mined
grandpaTxMp := bc.MerkleProof{
TxOrID: "111111111111111111111111111111111111111",
Nodes: []string{"n1", "n2"},
}
grandpaTx.MerkleProof = MerkleProof(grandpaTxMp)
err := grandpaTx.Save(ctx)
require.NoError(t, err)

return grandpaTx
}

func createTxWithDraft(t *testing.T, ctx context.Context, client ClientInterface, parentTx *Transaction, mined bool) *Transaction {
draftTransaction := newDraftTransaction(
testXPub, &TransactionConfig{
Inputs: []*TransactionInput{{Utxo: *newUtxoFromTxID(parentTx.GetID(), 0, append(client.DefaultModelOptions(), New())...)}},
Outputs: []*TransactionOutput{{
To: "1A1PjKqjWMNBzTVdcBru27EV1PHcXWc63W",
Satoshis: 1000,
}},
ChangeNumberOfDestinations: 1,
Sync: &SyncConfig{
Broadcast: true,
BroadcastInstant: false,
PaymailP2P: false,
SyncOnChain: false,
},
},
append(client.DefaultModelOptions(), New())...,
)

err := draftTransaction.Save(ctx)
require.NoError(t, err)

var transaction *Transaction
transaction, err = client.RecordTransaction(ctx, testXPub, draftTransaction.Hex, draftTransaction.ID, client.DefaultModelOptions()...)
require.NoError(t, err)
assert.NotEmpty(t, transaction)

if mined {
transaction.BlockHeight = 128
mp := bc.MerkleProof{
TxOrID: "423542156234627frafserg6gtrdsbd", Nodes: []string{"n1", "n2"},
}
transaction.MerkleProof = MerkleProof(mp)
}

err = transaction.Save(ctx)
require.NoError(t, err)

return transaction
}
1 change: 1 addition & 0 deletions model_sync_transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,7 @@ func notifyPaymailProviders(ctx context.Context, transaction *Transaction) ([]*S

// Notify each provider with the transaction
if payload, err = finalizeP2PTransaction(
ctx,
pm,
out.PaymailP4,
transaction,
Expand Down
19 changes: 4 additions & 15 deletions paymail.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,19 +155,8 @@ func startP2PTransaction(client paymail.ClientInterface,
}

// finalizeP2PTransaction will notify the paymail provider about the transaction
func finalizeP2PTransaction(client paymail.ClientInterface, p4 *PaymailP4, transaction *Transaction) (*paymail.P2PTransactionPayload, error) {

// Submit the P2P transaction
/*logger.Data(2, logger.DEBUG, "sending p2p tx...",
logger.MakeParameter("alias", alias),
logger.MakeParameter("p2pSubmitURL", p2pSubmitURL),
logger.MakeParameter("domain", domain),
logger.MakeParameter("note", note),
logger.MakeParameter("senderPaymailAddress", senderPaymailAddress),
logger.MakeParameter("referenceID", referenceID),
)*/

p2pTransaction, err := buildP2pTx(p4, transaction)
func finalizeP2PTransaction(ctx context.Context, client paymail.ClientInterface, p4 *PaymailP4, transaction *Transaction) (*paymail.P2PTransactionPayload, error) {
p2pTransaction, err := buildP2pTx(ctx, p4, transaction)
if err != nil {
return nil, err
}
Expand All @@ -180,7 +169,7 @@ func finalizeP2PTransaction(client paymail.ClientInterface, p4 *PaymailP4, trans
return &response.P2PTransactionPayload, nil
}

func buildP2pTx(p4 *PaymailP4, transaction *Transaction) (*paymail.P2PTransaction, error) {
func buildP2pTx(ctx context.Context, p4 *PaymailP4, transaction *Transaction) (*paymail.P2PTransaction, error) {
p2pTransaction := &paymail.P2PTransaction{
MetaData: &paymail.P2PMetaData{
Note: p4.Note,
Expand All @@ -192,7 +181,7 @@ func buildP2pTx(p4 *PaymailP4, transaction *Transaction) (*paymail.P2PTransactio
switch p4.Format {

case BeefPaymailPayloadFormat:
beef, err := ToBeefHex(transaction)
beef, err := ToBeefHex(ctx, transaction)

if err != nil {
return nil, err
Expand Down

0 comments on commit 5ec6ada

Please sign in to comment.