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 #438 from BuxOrg/chore-refactor-sync-method
Browse files Browse the repository at this point in the history
chore(BUX-298): refactor sync method and use go-b t structures in BEEF implementation
  • Loading branch information
arkadiuszos4chain authored Oct 25, 2023
2 parents 15eb374 + 5788912 commit c19ef9d
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 99 deletions.
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

0 comments on commit c19ef9d

Please sign in to comment.