From 5449efa7f8bcebc5a04d6e4e0e480c6f1597ab6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Lewandowski?= Date: Fri, 20 Oct 2023 09:09:37 +0200 Subject: [PATCH 1/4] chore(BUX-298): try to sync tx again if there is no merle proof --- model_sync_transactions.go | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/model_sync_transactions.go b/model_sync_transactions.go index 68405eb3..0ebdb323 100644 --- a/model_sync_transactions.go +++ b/model_sync_transactions.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/hex" + "encoding/json" "errors" "fmt" "runtime" @@ -640,6 +641,19 @@ func processSyncTransaction(ctx context.Context, syncTx *SyncTransaction, transa return err } + // Get the transaction + if transaction == nil { + if transaction, err = getTransactionByID( + ctx, "", syncTx.ID, syncTx.GetOptions(false)..., + ); err != nil { + return err + } + } + + if transaction == nil { + return ErrMissingTransaction + } + // Find on-chain var txInfo *chainstate.TransactionInfo // only mAPI currently provides merkle proof, so QueryTransaction should be used here @@ -647,6 +661,8 @@ func processSyncTransaction(ctx context.Context, syncTx *SyncTransaction, transa ctx, syncTx.ID, chainstate.RequiredOnChain, defaultQueryTxTimeout, ); err != nil { if errors.Is(err, chainstate.ErrTransactionNotFound) { + syncTx.client.Logger().Info(ctx, fmt.Sprintf("processSyncTransaction(): Transaction %s not found on-chain, will try again later", syncTx.ID)) + bailAndSaveSyncTransaction( ctx, syncTx, SyncStatusReady, syncActionSync, "all", "transaction not found on-chain", ) @@ -655,17 +671,15 @@ func processSyncTransaction(ctx context.Context, syncTx *SyncTransaction, transa return err } - // Get the transaction - if transaction == nil { - if transaction, err = getTransactionByID( - ctx, "", syncTx.ID, syncTx.GetOptions(false)..., - ); err != nil { - return err - } - } + // validate txInfo + if txInfo.BlockHash == "" || txInfo.MerkleProof == nil || txInfo.MerkleProof.TxOrID == "" || len(txInfo.MerkleProof.Nodes) == 0 { + syncTx.client.Logger().Warn(ctx, fmt.Sprintf("processSyncTransaction(): txInfo for %s is invalid, will try again later", syncTx.ID)) - if transaction == nil { - return ErrMissingTransaction + if syncTx.client.IsDebug() { + txInfoJSON, _ := json.Marshal(txInfo) //nolint:nolintlint,nilerr // error is not needed + syncTx.DebugLog(string(txInfoJSON)) + } + return nil } // Add additional information (if found on-chain) @@ -700,6 +714,7 @@ func processSyncTransaction(ctx context.Context, syncTx *SyncTransaction, transa return err } + syncTx.client.Logger().Info(ctx, fmt.Sprintf("processSyncTransaction(): Transaction %s processed successfully", syncTx.ID)) // Done! return nil } From b6d0196e7bd7e6aaf7e1dfdc27a490d4b95d8ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Lewandowski?= Date: Fri, 20 Oct 2023 09:20:20 +0200 Subject: [PATCH 2/4] chore(BUX-298): use go-bt structures in BEEF implementation --- beef_tx.go | 29 +++--- beef_tx_bytes.go | 19 ++-- beef_tx_sorting.go | 30 +++--- beef_tx_sorting_test.go | 196 +++++++++++++++++++--------------------- 4 files changed, 134 insertions(+), 140 deletions(-) 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 +//} From 321db32f2d92de58f873250af9a212674cd33e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Lewandowski?= Date: Fri, 20 Oct 2023 11:15:53 +0200 Subject: [PATCH 3/4] chore(BUX-298): remove unneeded declaration --- beef_tx.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/beef_tx.go b/beef_tx.go index aa79c3d3..8e620f20 100644 --- a/beef_tx.go +++ b/beef_tx.go @@ -50,8 +50,7 @@ func newBeefTx(ctx context.Context, version uint32, tx *Transaction) (*beefTx, e transactions := make([]*bt.Tx, 0, len(inputs)+1) for _, input := range inputs { - var prevTxs []*bt.Tx - prevTxs, err = getParentTransactionsForInput(ctx, tx.client, input) + prevTxs, err := getParentTransactionsForInput(ctx, tx.client, input) if err != nil { return nil, fmt.Errorf("retrieve input parent transaction failed: %w", err) } From 578891275e857ab023576b1ae975215b601986a0 Mon Sep 17 00:00:00 2001 From: arkadiuszos4chain Date: Mon, 23 Oct 2023 13:49:43 +0200 Subject: [PATCH 4/4] chore: bring back tests; add better comment --- beef_tx.go | 1 + beef_tx_bytes.go | 3 +- beef_tx_sorting.go | 6 +- beef_tx_sorting_test.go | 224 +++++++++++++++++++++++----------------- 4 files changed, 136 insertions(+), 98 deletions(-) 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 +}