diff --git a/beef_tx.go b/beef_tx.go index 8e620f20..10ff3383 100644 --- a/beef_tx.go +++ b/beef_tx.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "errors" "fmt" + "github.com/libsv/go-bt/v2" ) diff --git a/beef_tx_bytes.go b/beef_tx_bytes.go index 65070034..2b51df5b 100644 --- a/beef_tx_bytes.go +++ b/beef_tx_bytes.go @@ -2,6 +2,7 @@ package bux import ( "errors" + "github.com/libsv/go-bt/v2" ) @@ -76,7 +77,7 @@ func getCompountedMarklePathIndex(tx *bt.Tx, compountedPaths CMPSlice) int { for i, cmp := range compountedPaths { for txID := range cmp[0] { - if txID == tx.TxID() { // TODO: perf + if txID == tx.TxID() { pathIdx = i } } diff --git a/beef_tx_sorting.go b/beef_tx_sorting.go index 7669f069..6f322bb7 100644 --- a/beef_tx_sorting.go +++ b/beef_tx_sorting.go @@ -26,7 +26,7 @@ func prepareSortStructures(dag []*bt.Tx) (txByID map[string]*bt.Tx, incomingEdge incomingEdgesMap = make(map[string]int, dagLen) for _, tx := range dag { - txByID[tx.TxID()] = tx // TODO: perf + 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 } @@ -39,7 +39,7 @@ func prepareSortStructures(dag []*bt.Tx) (txByID map[string]*bt.Tx, incomingEdge func calculateIncomingEdges(inDegree map[string]int, txByID map[string]*bt.Tx) { for _, tx := range txByID { for _, input := range tx.Inputs { - inputUtxoTxID := input.PreviousTxIDStr() // TODO: perf + 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]++ } @@ -61,7 +61,7 @@ func getTxWithZeroIncomingEdges(incomingEdgesMap map[string]int) []string { func removeTxFromIncomingEdges(tx *bt.Tx, incomingEdgesMap map[string]int, zeroIncomingEdgeQueue []string) []string { for _, input := range tx.Inputs { - neighborID := input.PreviousTxIDStr() // TODO: perf + 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 { diff --git a/beef_tx_sorting_test.go b/beef_tx_sorting_test.go index 816372cd..5a75ec0b 100644 --- a/beef_tx_sorting_test.go +++ b/beef_tx_sorting_test.go @@ -1,96 +1,132 @@ package bux -//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 -//} +import ( + "fmt" + "math/rand" + "testing" + + "github.com/libsv/go-bt/v2" + "github.com/stretchr/testify/assert" +) + +func Test_kahnTopologicalSortTransaction(t *testing.T) { + + tCases := []struct { + name string + expectedSortedTransactions []*bt.Tx + }{ + { + name: "txs with necessary data only", + expectedSortedTransactions: getTxsFromOldestToNewestWithNecessaryDataOnly(), + }, + { + name: "txs with inputs from other txs", + expectedSortedTransactions: getTxsFromOldestToNewestWithUnecessaryData(), + }, + } + + 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 tc.expectedSortedTransactions { + assert.Equal(t, tx.TxID(), sortedGraph[i].TxID()) + } + }) + } +} + +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, + } + + 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 := bt.NewTx() + transaction.Inputs = append(transaction.Inputs, inputs...) + + return transaction +} + +func shuffleTransactions(txs []*bt.Tx) []*bt.Tx { + n := len(txs) + result := make([]*bt.Tx, 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 +}