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

chore(BUX-298): refactor sync method and use go-b t structures in BEEF implementation #438

Merged
merged 4 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions beef_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"encoding/hex"
"errors"
"fmt"

"github.com/libsv/go-bt/v2"
)

const maxBeefVer = uint32(0xFFFF) // value from BRC-62
Expand All @@ -27,7 +29,7 @@ func ToBeefHex(ctx context.Context, tx *Transaction) (string, error) {
type beefTx struct {
version uint32
compoundMerklePaths CMPSlice
transactions []*Transaction
transactions []*bt.Tx
}

func newBeefTx(ctx context.Context, version uint32, tx *Transaction) (*beefTx, error) {
Expand All @@ -46,7 +48,7 @@ func newBeefTx(ctx context.Context, version uint32, tx *Transaction) (*beefTx, e

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

for _, input := range inputs {
prevTxs, err := getParentTransactionsForInput(ctx, tx.client, input)
Expand All @@ -58,7 +60,11 @@ func newBeefTx(ctx context.Context, version uint32, tx *Transaction) (*beefTx, e
}

// add current transaction
transactions = append(transactions, tx)
btTx, err := bt.NewTxFromString(tx.Hex)
if err != nil {
return nil, fmt.Errorf("cannot convert new transaction to bt.Tx from hex (tx.ID: %s). Reason: %w", tx.ID, err)
}
transactions = append(transactions, btTx)

beef := &beefTx{
version: version,
Expand All @@ -75,7 +81,7 @@ func hydrateTransaction(ctx context.Context, tx *Transaction) error {
ctx, tx.XPubID, tx.DraftID, tx.GetOptions(false)...,
)

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

Expand All @@ -99,18 +105,19 @@ func validateCompoundMerklePathes(compountedPaths CMPSlice) error {
return nil
}

func getParentTransactionsForInput(ctx context.Context, client ClientInterface, input *TransactionInput) ([]*Transaction, error) {
func getParentTransactionsForInput(ctx context.Context, client ClientInterface, input *TransactionInput) ([]*bt.Tx, 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
inputBtTx, err := bt.NewTxFromString(inputTx.Hex)
if err != nil {
return nil, fmt.Errorf("cannot convert to bt.Tx from hex (tx.ID: %s). Reason: %w", inputTx.ID, err)
}

return []*bt.Tx{inputBtTx}, nil
}

return nil, fmt.Errorf("transaction is not mined yet (tx.ID: %s)", inputTx.ID) // TODO: handle it in next iterration
Expand Down
18 changes: 6 additions & 12 deletions beef_tx_bytes.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package bux

import (
"encoding/hex"
"errors"
"fmt"

"github.com/libsv/go-bt/v2"
)
Expand Down Expand Up @@ -36,7 +34,7 @@ func (beefTx *beefTx) toBeefBytes() ([]byte, error) {
transactions := make([][]byte, 0, len(beefTx.transactions))

for _, t := range beefTx.transactions {
txBytes, err := t.toBeefBytes(beefTx.compoundMerklePaths)
txBytes, err := toBeefBytes(t, beefTx.compoundMerklePaths)
if err != nil {
return nil, err
}
Expand All @@ -60,14 +58,10 @@ func (beefTx *beefTx) toBeefBytes() ([]byte, error) {
return buffer, nil
}

func (tx *Transaction) toBeefBytes(compountedPaths CMPSlice) ([]byte, error) {
txBeefBytes, err := hex.DecodeString(tx.Hex)
func toBeefBytes(tx *bt.Tx, compountedPaths CMPSlice) ([]byte, error) {
txBeefBytes := tx.Bytes()

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

cmpIdx := tx.getCompountedMarklePathIndex(compountedPaths)
cmpIdx := getCompountedMarklePathIndex(tx, compountedPaths)
if cmpIdx > -1 {
txBeefBytes = append(txBeefBytes, hasCmp)
txBeefBytes = append(txBeefBytes, bt.VarInt(cmpIdx).Bytes()...)
Expand All @@ -78,12 +72,12 @@ func (tx *Transaction) toBeefBytes(compountedPaths CMPSlice) ([]byte, error) {
return txBeefBytes, nil
}

func (tx *Transaction) getCompountedMarklePathIndex(compountedPaths CMPSlice) int {
func getCompountedMarklePathIndex(tx *bt.Tx, compountedPaths CMPSlice) int {
pathIdx := -1

for i, cmp := range compountedPaths {
for txID := range cmp[0] {
if txID == tx.ID {
if txID == tx.TxID() {
pathIdx = i
}
}
Expand Down
30 changes: 16 additions & 14 deletions beef_tx_sorting.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package bux

func kahnTopologicalSortTransactions(transactions []*Transaction) []*Transaction {
import "github.com/libsv/go-bt/v2"

func kahnTopologicalSortTransactions(transactions []*bt.Tx) []*bt.Tx {
txByID, incomingEdgesMap, zeroIncomingEdgeQueue := prepareSortStructures(transactions)
result := make([]*Transaction, 0, len(transactions))
result := make([]*bt.Tx, 0, len(transactions))

for len(zeroIncomingEdgeQueue) > 0 {
txID := zeroIncomingEdgeQueue[0]
Expand All @@ -18,14 +20,14 @@ func kahnTopologicalSortTransactions(transactions []*Transaction) []*Transaction
return result
}

func prepareSortStructures(dag []*Transaction) (txByID map[string]*Transaction, incomingEdgesMap map[string]int, zeroIncomingEdgeQueue []string) {
func prepareSortStructures(dag []*bt.Tx) (txByID map[string]*bt.Tx, incomingEdgesMap map[string]int, zeroIncomingEdgeQueue []string) {
dagLen := len(dag)
txByID = make(map[string]*Transaction, dagLen)
txByID = make(map[string]*bt.Tx, dagLen)
incomingEdgesMap = make(map[string]int, dagLen)

for _, tx := range dag {
txByID[tx.ID] = tx
incomingEdgesMap[tx.ID] = 0
txByID[tx.TxID()] = tx // TODO: perf -> In bt, the TxID is calculated every time we try to get it, which means we hash the tx bytes twice each time. It's expensive operation - try to avoid calulation each time
incomingEdgesMap[tx.TxID()] = 0
}

calculateIncomingEdges(incomingEdgesMap, txByID)
Expand All @@ -34,11 +36,11 @@ func prepareSortStructures(dag []*Transaction) (txByID map[string]*Transaction,
return
}

func calculateIncomingEdges(inDegree map[string]int, txByID map[string]*Transaction) {
func calculateIncomingEdges(inDegree map[string]int, txByID map[string]*bt.Tx) {
for _, tx := range txByID {
for _, input := range tx.draftTransaction.Configuration.Inputs {
inputUtxoTxID := input.UtxoPointer.TransactionID
if _, ok := txByID[inputUtxoTxID]; ok { // transaction can contains inputs we are not interested in
for _, input := range tx.Inputs {
inputUtxoTxID := input.PreviousTxIDStr() // TODO: perf -> In bt, the TxID is calculated every time we try to get it, which means we hash the tx bytes twice each time. It's expensive operation - try to avoid calulation each time
if _, ok := txByID[inputUtxoTxID]; ok { // transaction can contains inputs we are not interested in
inDegree[inputUtxoTxID]++
}
}
Expand All @@ -57,9 +59,9 @@ func getTxWithZeroIncomingEdges(incomingEdgesMap map[string]int) []string {
return zeroIncomingEdgeQueue
}

func removeTxFromIncomingEdges(tx *Transaction, incomingEdgesMap map[string]int, zeroIncomingEdgeQueue []string) []string {
for _, input := range tx.draftTransaction.Configuration.Inputs {
neighborID := input.UtxoPointer.TransactionID
func removeTxFromIncomingEdges(tx *bt.Tx, incomingEdgesMap map[string]int, zeroIncomingEdgeQueue []string) []string {
for _, input := range tx.Inputs {
neighborID := input.PreviousTxIDStr() // TODO: perf -> In bt, the TxID is calculated every time we try to get it, which means we hash the tx bytes twice each time. It's expensive operation - try to avoid calulation each time
incomingEdgesMap[neighborID]--

if incomingEdgesMap[neighborID] == 0 {
Expand All @@ -70,7 +72,7 @@ func removeTxFromIncomingEdges(tx *Transaction, incomingEdgesMap map[string]int,
return zeroIncomingEdgeQueue
}

func reverseInPlace(collection []*Transaction) {
func reverseInPlace(collection []*bt.Tx) {
for i, j := 0, len(collection)-1; i < j; i, j = i+1, j-1 {
collection[i], collection[j] = collection[j], collection[i]
}
Expand Down
134 changes: 81 additions & 53 deletions beef_tx_sorting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,23 @@ import (
"math/rand"
"testing"

"github.com/libsv/go-bt/v2"
"github.com/stretchr/testify/assert"
)

func Test_kahnTopologicalSortTransaction(t *testing.T) {
// create related transactions from oldest to newest
txsFromOldestToNewest := []*Transaction{
createTx("0"),
createTx("1", "0"),
createTx("2", "1"),
createTx("3", "2", "1"),
createTx("4", "3", "1"),
createTx("5", "3", "2"),
createTx("6", "4", "2", "0"),
createTx("7", "6", "5", "3", "1"),
createTx("8", "7"),
}

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 necessary data only",
expectedSortedTransactions: txsFromOldestToNewest,
},
expectedSortedTransactions []*bt.Tx
}{
{
name: "txs with necessary data only",
expectedSortedTransactions: getTxsFromOldestToNewestWithNecessaryDataOnly(),
},
{
name: "txs with inputs from other txs",
expectedSortedTransactions: txsFromOldestToNewestWithUnnecessaryInputs,
expectedSortedTransactions: getTxsFromOldestToNewestWithUnecessaryData(),
},
}

Expand All @@ -56,43 +34,93 @@ func Test_kahnTopologicalSortTransaction(t *testing.T) {
sortedGraph := kahnTopologicalSortTransactions(unsortedTxs)

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

func createTx(txID string, inputsTxIDs ...string) *Transaction {
inputs := make([]*TransactionInput, 0)
for _, inTxID := range inputsTxIDs {
in := &TransactionInput{
Utxo: Utxo{
UtxoPointer: UtxoPointer{
TransactionID: inTxID,
},
},
}

inputs = append(inputs, in)
func getTxsFromOldestToNewestWithNecessaryDataOnly() []*bt.Tx {
// create related transactions from oldest to newest
oldestTx := createTx()
secondTx := createTx(oldestTx)
thirdTx := createTx(secondTx)
fourthTx := createTx(thirdTx, secondTx)
fifthTx := createTx(fourthTx, secondTx)
sixthTx := createTx(fourthTx, thirdTx)
seventhTx := createTx(fifthTx, thirdTx, oldestTx)
eightTx := createTx(seventhTx, sixthTx, fourthTx, secondTx)

newestTx := createTx(eightTx)

txsFromOldestToNewest := []*bt.Tx{
oldestTx,
secondTx,
thirdTx,
fourthTx,
fifthTx,
sixthTx,
seventhTx,
eightTx,
newestTx,
}

transaction := &Transaction{
draftTransaction: &DraftTransaction{
Configuration: TransactionConfig{
Inputs: inputs,
},
},
return txsFromOldestToNewest
}

func getTxsFromOldestToNewestWithUnecessaryData() []*bt.Tx {
unnecessaryParentTx_1 := createTx()
unnecessaryParentTx_2 := createTx()
unnecessaryParentTx_3 := createTx()
unnecessaryParentTx_4 := createTx()

// create related transactions from oldest to newest
oldestTx := createTx()
secondTx := createTx(oldestTx)
thirdTx := createTx(secondTx)
fourthTx := createTx(thirdTx, secondTx, unnecessaryParentTx_1, unnecessaryParentTx_4)
fifthTx := createTx(fourthTx, secondTx)
sixthTx := createTx(fourthTx, thirdTx, unnecessaryParentTx_3, unnecessaryParentTx_2, unnecessaryParentTx_1)
seventhTx := createTx(fifthTx, thirdTx, oldestTx)
eightTx := createTx(seventhTx, sixthTx, fourthTx, secondTx, unnecessaryParentTx_1)

newestTx := createTx(eightTx)

txsFromOldestToNewest := []*bt.Tx{
oldestTx,
secondTx,
thirdTx,
fourthTx,
fifthTx,
sixthTx,
seventhTx,
eightTx,
newestTx,
}

return txsFromOldestToNewest
}

func createTx(inputsParents ...*bt.Tx) *bt.Tx {
inputs := make([]*bt.Input, 0)

for _, parent := range inputsParents {
in := bt.Input{}
in.PreviousTxIDAdd(parent.TxIDBytes())

inputs = append(inputs, &in)
}

transaction.ID = txID
transaction := bt.NewTx()
transaction.Inputs = append(transaction.Inputs, inputs...)

return transaction
}

func shuffleTransactions(txs []*Transaction) []*Transaction {
func shuffleTransactions(txs []*bt.Tx) []*bt.Tx {
n := len(txs)
result := make([]*Transaction, n)
result := make([]*bt.Tx, n)
copy(result, txs)

for i := n - 1; i > 0; i-- {
Expand Down
Loading
Loading