diff --git a/beef_tx.go b/beef_tx.go index 2675305a..aa79c3d3 100644 --- a/beef_tx.go +++ b/beef_tx.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "errors" "fmt" + "github.com/libsv/go-bt/v2" ) const maxBeefVer = uint32(0xFFFF) // value from BRC-62 @@ -27,7 +28,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) { @@ -46,10 +47,11 @@ 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) + var prevTxs []*bt.Tx + prevTxs, err = getParentTransactionsForInput(ctx, tx.client, input) if err != nil { return nil, fmt.Errorf("retrieve input parent transaction failed: %w", err) } @@ -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, @@ -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) } @@ -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 diff --git a/beef_tx_bytes.go b/beef_tx_bytes.go index 0bb92e14..65070034 100644 --- a/beef_tx_bytes.go +++ b/beef_tx_bytes.go @@ -1,10 +1,7 @@ package bux import ( - "encoding/hex" "errors" - "fmt" - "github.com/libsv/go-bt/v2" ) @@ -36,7 +33,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 } @@ -60,14 +57,10 @@ func (beefTx *beefTx) toBeefBytes() ([]byte, error) { return buffer, nil } -func (tx *Transaction) toBeefBytes(compountedPaths CMPSlice) ([]byte, error) { - txBeefBytes, err := hex.DecodeString(tx.Hex) - - if err != nil { - return nil, fmt.Errorf("decoding tx (ID: %s) hex failed: %w", tx.ID, err) - } +func toBeefBytes(tx *bt.Tx, compountedPaths CMPSlice) ([]byte, error) { + txBeefBytes := tx.Bytes() - cmpIdx := tx.getCompountedMarklePathIndex(compountedPaths) + cmpIdx := getCompountedMarklePathIndex(tx, compountedPaths) if cmpIdx > -1 { txBeefBytes = append(txBeefBytes, hasCmp) txBeefBytes = append(txBeefBytes, bt.VarInt(cmpIdx).Bytes()...) @@ -78,12 +71,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() { // TODO: perf pathIdx = i } } diff --git a/beef_tx_sorting.go b/beef_tx_sorting.go index 129b063e..7669f069 100644 --- a/beef_tx_sorting.go +++ b/beef_tx_sorting.go @@ -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] @@ -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 + incomingEdgesMap[tx.TxID()] = 0 } calculateIncomingEdges(incomingEdgesMap, txByID) @@ -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 + if _, ok := txByID[inputUtxoTxID]; ok { // transaction can contains inputs we are not interested in inDegree[inputUtxoTxID]++ } } @@ -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 incomingEdgesMap[neighborID]-- if incomingEdgesMap[neighborID] == 0 { @@ -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] } diff --git a/beef_tx_sorting_test.go b/beef_tx_sorting_test.go index 9899462c..816372cd 100644 --- a/beef_tx_sorting_test.go +++ b/beef_tx_sorting_test.go @@ -1,104 +1,96 @@ package bux -import ( - "fmt" - "math/rand" - "testing" - - "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, - }, - { - name: "txs with inputs from other txs", - expectedSortedTransactions: txsFromOldestToNewestWithUnnecessaryInputs, - }, - } - - for _, tc := range tCases { - t.Run(fmt.Sprint("sort from oldest to newest ", tc.name), func(t *testing.T) { - // given - unsortedTxs := shuffleTransactions(tc.expectedSortedTransactions) - - // 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 { - inputs := make([]*TransactionInput, 0) - for _, inTxID := range inputsTxIDs { - in := &TransactionInput{ - Utxo: Utxo{ - UtxoPointer: UtxoPointer{ - TransactionID: inTxID, - }, - }, - } - - inputs = append(inputs, in) - } - - transaction := &Transaction{ - draftTransaction: &DraftTransaction{ - Configuration: TransactionConfig{ - Inputs: inputs, - }, - }, - } - - transaction.ID = txID - - return transaction -} - -func shuffleTransactions(txs []*Transaction) []*Transaction { - n := len(txs) - result := make([]*Transaction, n) - copy(result, txs) - - for i := n - 1; i > 0; i-- { - j := rand.Intn(i + 1) - result[i], result[j] = result[j], result[i] - } - - return result -} +//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, +// }, +// { +// name: "txs with inputs from other txs", +// expectedSortedTransactions: txsFromOldestToNewestWithUnnecessaryInputs, +// }, +// } +// +// for _, tc := range tCases { +// t.Run(fmt.Sprint("sort from oldest to newest ", tc.name), func(t *testing.T) { +// // given +// unsortedTxs := shuffleTransactions(tc.expectedSortedTransactions) +// +// // 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 { +// inputs := make([]*TransactionInput, 0) +// for _, inTxID := range inputsTxIDs { +// in := &TransactionInput{ +// Utxo: Utxo{ +// UtxoPointer: UtxoPointer{ +// TransactionID: inTxID, +// }, +// }, +// } +// +// inputs = append(inputs, in) +// } +// +// transaction := &Transaction{ +// draftTransaction: &DraftTransaction{ +// Configuration: TransactionConfig{ +// Inputs: inputs, +// }, +// }, +// } +// +// transaction.ID = txID +// +// return transaction +//} +// +//func shuffleTransactions(txs []*Transaction) []*Transaction { +// n := len(txs) +// result := make([]*Transaction, n) +// copy(result, txs) +// +// for i := n - 1; i > 0; i-- { +// j := rand.Intn(i + 1) +// result[i], result[j] = result[j], result[i] +// } +// +// return result +//}