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

feat(BUX-168, BUX-172): BEEF v2 (unmined inputs) + complex SPV #497

Merged
merged 40 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0ec5a9b
feat: finish first part of getting parent txs
wregulski Nov 10, 2023
33ba6dd
feat: beef for parent inputs
wregulski Nov 10, 2023
3cf42ac
fix: add processed tx to working collections
wregulski Nov 15, 2023
c3b2ab2
feat: add tests for beef happy path
wregulski Nov 21, 2023
dadfd62
feat: add proper test case with topologically sorted txs
wregulski Nov 21, 2023
18d85db
feat: add test cases for happy paths
wregulski Nov 22, 2023
9d57b0d
feat: add sorting of bumps and error paths tests
wregulski Nov 22, 2023
da6d425
feat: add method to call database once for multiple transactions
wregulski Nov 23, 2023
1813b55
feat: add getting batch transactions instead of one by one
wregulski Nov 23, 2023
26293d4
fix: pull request changes
wregulski Nov 23, 2023
b4bb0d7
fix: remove redundand func call
wregulski Nov 23, 2023
3cd4b2c
fix: removes old reference
wregulski Nov 24, 2023
273ccdf
feat(BUX-172): update go-paymail dependency
arkadiuszos4chain Nov 23, 2023
92ac3ca
feat(BUX-172): use go-paymail v0.9.1
arkadiuszos4chain Nov 24, 2023
1315d4e
feat(BUX-322): save inputs from BEEF tx
pawellewandowski98 Nov 24, 2023
b4e876a
chore(BUX-322): refactor saving inputs methods
pawellewandowski98 Nov 27, 2023
e3d17d1
chore(BUX-322): remove unused method
pawellewandowski98 Nov 27, 2023
741c89b
chore(BUX-322): save BUMP in parent tx
pawellewandowski98 Nov 27, 2023
38c0bc4
chore(BUX-322): change sync tx status when bump exists
pawellewandowski98 Nov 27, 2023
9d2f8a9
chore(BUX-322): cast bumpIndex to uint
pawellewandowski98 Nov 27, 2023
6b68569
feat(BUX-322): small fixes; update go-paymail ref
arkadiuszos4chain Nov 28, 2023
de59dca
chore: update go-paymail
arkadiuszos4chain Nov 29, 2023
eec0a27
feat(BUX-166)MerkleProof from mAPI and MerklePath from Arc unified to…
kuba-4chain Nov 28, 2023
446946d
feat(BUX-166): method for getting merkle paths from arc removed
kuba-4chain Nov 29, 2023
a9825e6
refactor(BUX-166): MerkleProofToBUMP simplified
kuba-4chain Nov 29, 2023
3c1309a
refactor(BUX-166): MerkleProofToBUMP simplified
kuba-4chain Nov 29, 2023
b65f0a8
feat(BUX-358): rmv cronjob
arkadiuszos4chain Nov 29, 2023
d39525c
feat(BUX-358): create SyncTx for "raw recorded" txs
arkadiuszos4chain Nov 29, 2023
efa34fc
fat(BUX-358): remove redundant 'recordMonitoredTransaction" function
arkadiuszos4chain Nov 29, 2023
0bf9d45
feat(BUX-358): fix monitor checks
arkadiuszos4chain Nov 29, 2023
cc03c7d
feat(BUX-358): rmv "recordTxHex" from ClientInterface
arkadiuszos4chain Nov 29, 2023
83857ba
feat(BUX-358): move and renamve recordTxHex > tx.service.registerRawT…
arkadiuszos4chain Nov 29, 2023
99cbc30
fix monitor check
arkadiuszos4chain Nov 29, 2023
bb0d9ba
feat(BUX-358): small code improvements
arkadiuszos4chain Nov 29, 2023
15baa1e
feat(BUX-358): small impv
arkadiuszos4chain Nov 29, 2023
ebc3e7f
feat*BUX-358): adjust to review - remove unnecessary if statements
arkadiuszos4chain Nov 30, 2023
c400c1a
feat(BUX-358): move seting tx values to tx.procesUtxos()
arkadiuszos4chain Nov 30, 2023
b1173ce
chore(BUX-358): refactorize tx and incoming tx ctors
arkadiuszos4chain Nov 30, 2023
aece6df
chore(BUX-358): add assertion
arkadiuszos4chain Dec 7, 2023
9f93b3c
feat: update go sum
wregulski Dec 8, 2023
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
141 changes: 38 additions & 103 deletions action_transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,122 +35,24 @@ func (c *Client) RecordTransaction(ctx context.Context, xPubKey, txHex, draftID
return recordTransaction(ctx, c, rts, opts...)
}

// RecordRawTransaction will parse the transaction and save it into the Datastore directly, without any checks
//
// RecordRawTransaction will parse the transaction and save it into the Datastore directly, without any checks or broadcast but bux will ask network for information if transaction was mined
// The transaction is treat as external incoming transaction - transaction without a draft
// Only use this function when you know what you are doing!
//
// txHex is the raw transaction hex
// opts are model options and can include "metadata"
func (c *Client) RecordRawTransaction(ctx context.Context, txHex string,
opts ...ModelOps,
) (*Transaction, error) {
// Check for existing NewRelic transaction
ctx = c.GetOrStartTxn(ctx, "record_raw_transaction")

return c.recordTxHex(ctx, txHex, opts...)
}

// RecordMonitoredTransaction will parse the transaction and save it into the Datastore
//
// This function will try to record the transaction directly, without checking draft ids etc.
//
//nolint:nolintlint,unparam,gci // opts is the way, but not yet being used
func recordMonitoredTransaction(ctx context.Context, client ClientInterface, txHex string,
opts ...ModelOps,
) (*Transaction, error) {
// Check for existing NewRelic transaction
ctx = client.GetOrStartTxn(ctx, "record_monitored_transaction")

transaction, err := client.recordTxHex(ctx, txHex, opts...)
if err != nil {
return nil, err
}

if transaction.BlockHash == "" {
// Create the sync transaction model
sync := newSyncTransaction(
transaction.GetID(),
transaction.Client().DefaultSyncConfig(),
transaction.GetOptions(true)...,
)
sync.BroadcastStatus = SyncStatusSkipped
sync.P2PStatus = SyncStatusSkipped

// Use the same metadata
sync.Metadata = transaction.Metadata

// If all the options are skipped, do not make a new model (ignore the record)
if !sync.isSkipped() {
if err = sync.Save(ctx); err != nil {
return nil, err
}
}
}

return transaction, nil
}

func (c *Client) recordTxHex(ctx context.Context, txHex string, opts ...ModelOps) (*Transaction, error) {
// Create the model & set the default options (gives options from client->model)
newOpts := c.DefaultModelOptions(append(opts, New())...)
transaction := newTransaction(txHex, newOpts...)

// Ensure that we have a transaction id (created from the txHex)
id := transaction.GetID()
if len(id) == 0 {
return nil, ErrMissingTxHex
}

// Create the lock and set the release for after the function completes
unlock, err := newWriteLock(
ctx, fmt.Sprintf(lockKeyRecordTx, id), c.Cachestore(),
)
defer unlock()
if err != nil {
return nil, err
}

// Logic moved from BeforeCreating hook - should be refactorized in next iteration

// If we are external and the user disabled incoming transaction checking, check outputs
if transaction.isExternal() && !transaction.Client().IsITCEnabled() {
// Check that the transaction has >= 1 known destination
if !transaction.TransactionBase.hasOneKnownDestination(ctx, transaction.Client(), transaction.GetOptions(false)...) {
return nil, ErrNoMatchingOutputs
}
}

// Process the UTXOs
if err = transaction.processUtxos(ctx); err != nil {
return nil, err
}

// Set the values from the inputs/outputs and draft tx
transaction.TotalValue, transaction.Fee = transaction.getValues()

// Add values
transaction.NumberOfInputs = uint32(len(transaction.TransactionBase.parsedTx.Inputs))
transaction.NumberOfOutputs = uint32(len(transaction.TransactionBase.parsedTx.Outputs))

// /Logic moved from BeforeCreating hook - should be refactorized in next iteration

allowUnknown := true
monitor := c.options.chainstate.Monitor()

if monitor != nil {
// do not register transactions we have nothing to do with
allowUnknown := monitor.AllowUnknownTransactions()
if transaction.XpubInIDs == nil && transaction.XpubOutIDs == nil && !allowUnknown {
return nil, ErrTransactionUnknown
}
allowUnknown = monitor.AllowUnknownTransactions()
}

// save the transaction model
if err = transaction.Save(ctx); err != nil {
return nil, err
}

// Return the response
return transaction, nil
return saveRawTransaction(ctx, c, allowUnknown, txHex, opts...)
}

// NewTransaction will create a new draft transaction and return it
Expand Down Expand Up @@ -218,6 +120,25 @@ func (c *Client) GetTransactionByID(ctx context.Context, txID string) (*Transact
return c.GetTransaction(ctx, "", txID)
}

func (c *Client) GetTransactionsByIDs(ctx context.Context, txIDs []string) ([]*Transaction, error) {
// Check for existing NewRelic transaction
ctx = c.GetOrStartTxn(ctx, "get_transactions_by_ids")

// Create the conditions
conditions := generateTxIdFilterConditions(txIDs)

// Get the transactions by it's IDs
transactions, err := getTransactions(
ctx, nil, conditions, nil,
c.DefaultModelOptions()...,
)
if err != nil {
return nil, err
}

return transactions, nil
}

// GetTransactionByHex will get a transaction from the Datastore by its full hex string
// uses GetTransaction
func (c *Client) GetTransactionByHex(ctx context.Context, hex string) (*Transaction, error) {
Expand Down Expand Up @@ -492,3 +413,17 @@ func (c *Client) RevertTransaction(ctx context.Context, id string) error {

return err
}

func generateTxIdFilterConditions(txIDs []string) *map[string]interface{} {
orConditions := make([]map[string]interface{}, len(txIDs))

for i, txID := range txIDs {
orConditions[i] = map[string]interface{}{"id": txID}
}

conditions := &map[string]interface{}{
"$or": orConditions,
}

return conditions
}
5 changes: 4 additions & 1 deletion action_transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,14 @@ func initRevertTransactionData(t *testing.T) (context.Context, ClientInterface,
assert.NotEmpty(t, hex)

newOpts := client.DefaultModelOptions(WithXPub(testXPub), New())
transaction := newTransactionWithDraftID(
transaction, err := newTransactionWithDraftID(
hex, draftTransaction.ID, newOpts...,
)
require.NoError(t, err)

transaction.draftTransaction = draftTransaction
_hydrateOutgoingWithSync(transaction)

err = transaction.processUtxos(ctx)
require.NoError(t, err)

Expand Down
174 changes: 174 additions & 0 deletions beef_bump.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package bux

import (
"context"
"errors"
"fmt"
"sort"

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

func calculateMergedBUMP(txs []*Transaction) (BUMPs, error) {
bumps := make(map[uint64][]BUMP)
mergedBUMPs := make(BUMPs, 0)

for _, tx := range txs {
if tx.BUMP.BlockHeight == 0 || len(tx.BUMP.Path) == 0 {
continue
}

bumps[tx.BlockHeight] = append(bumps[tx.BlockHeight], tx.BUMP)
}

// ensure that BUMPs are sorted by block height and will always be put in beef in the same order
mapKeys := make([]uint64, 0, len(bumps))
for k := range bumps {
mapKeys = append(mapKeys, k)
}
sort.Slice(mapKeys, func(i, j int) bool { return mapKeys[i] < mapKeys[j] })

for _, k := range mapKeys {
bump, err := CalculateMergedBUMP(bumps[k])
if err != nil {
return nil, fmt.Errorf("Error while calculating Merged BUMP: %s", err.Error())
}
if bump == nil {
continue
}
mergedBUMPs = append(mergedBUMPs, bump)
}

return mergedBUMPs, nil
}

func validateBumps(bumps BUMPs) error {
if len(bumps) == 0 {
return errors.New("empty bump paths slice")
}

for _, p := range bumps {
if len(p.Path) == 0 {
return errors.New("one of bump path is empty")
}
}

return nil
}

func prepareBEEFFactors(ctx context.Context, tx *Transaction, store TransactionGetter) ([]*bt.Tx, []*Transaction, error) {
btTxsNeededForBUMP, txsNeededForBUMP, err := initializeRequiredTxsCollection(tx)
if err != nil {
return nil, nil, err
}

var txIDs []string
for _, input := range tx.draftTransaction.Configuration.Inputs {
txIDs = append(txIDs, input.UtxoPointer.TransactionID)
}

inputTxs, err := getRequiredTransactions(ctx, txIDs, store)
if err != nil {
return nil, nil, err
}

for _, inputTx := range inputTxs {
inputBtTx, err := bt.NewTxFromString(inputTx.Hex)
if err != nil {
return nil, nil, fmt.Errorf("cannot convert to bt.Tx from hex (tx.ID: %s). Reason: %w", inputTx.ID, err)
}

txsNeededForBUMP = append(txsNeededForBUMP, inputTx)
btTxsNeededForBUMP = append(btTxsNeededForBUMP, inputBtTx)

if inputTx.BUMP.BlockHeight == 0 && len(inputTx.BUMP.Path) == 0 {
parentBtTransactions, parentTransactions, err := checkParentTransactions(ctx, store, inputBtTx)
if err != nil {
return nil, nil, err
}

txsNeededForBUMP = append(txsNeededForBUMP, parentTransactions...)
btTxsNeededForBUMP = append(btTxsNeededForBUMP, parentBtTransactions...)
}
}

return btTxsNeededForBUMP, txsNeededForBUMP, nil
}

func checkParentTransactions(ctx context.Context, store TransactionGetter, btTx *bt.Tx) ([]*bt.Tx, []*Transaction, error) {
var parentTxIDs []string
for _, txIn := range btTx.Inputs {
parentTxIDs = append(parentTxIDs, txIn.PreviousTxIDStr())
}

parentTxs, err := getRequiredTransactions(ctx, parentTxIDs, store)
if err != nil {
return nil, nil, err
}

var validTxs []*Transaction
var validBtTxs []*bt.Tx
for _, parentTx := range parentTxs {
parentBtTx, err := bt.NewTxFromString(parentTx.Hex)
if err != nil {
return nil, nil, fmt.Errorf("cannot convert to bt.Tx from hex (tx.ID: %s). Reason: %w", parentTx.ID, err)
}
validTxs = append(validTxs, parentTx)
validBtTxs = append(validBtTxs, parentBtTx)

if parentTx.BUMP.BlockHeight == 0 && len(parentTx.BUMP.Path) == 0 {
parentValidBtTxs, parentValidTxs, err := checkParentTransactions(ctx, store, parentBtTx)
if err != nil {
return nil, nil, err
}
validTxs = append(validTxs, parentValidTxs...)
validBtTxs = append(validBtTxs, parentValidBtTxs...)
}
}

return validBtTxs, validTxs, nil
}

func getRequiredTransactions(ctx context.Context, txIds []string, store TransactionGetter) ([]*Transaction, error) {
txs, err := store.GetTransactionsByIDs(ctx, txIds)
if err != nil {
return nil, fmt.Errorf("cannot get transactions from database: %w", err)
}

if len(txs) != len(txIds) {
missingTxIDs := getMissingTxs(txIds, txs)
return nil, fmt.Errorf("required transactions not found in database: %v", missingTxIDs)
}

return txs, nil
}

func getMissingTxs(txIDs []string, foundTxs []*Transaction) []string {
foundTxIDs := make(map[string]bool)
for _, tx := range foundTxs {
foundTxIDs[tx.ID] = true
}

var missingTxIDs []string
for _, txID := range txIDs {
if !foundTxIDs[txID] {
missingTxIDs = append(missingTxIDs, txID)
}
}
return missingTxIDs
}

func initializeRequiredTxsCollection(tx *Transaction) ([]*bt.Tx, []*Transaction, error) {
var btTxsNeededForBUMP []*bt.Tx
var txsNeededForBUMP []*Transaction

processedBtTx, err := bt.NewTxFromString(tx.Hex)
if err != nil {
return nil, nil, fmt.Errorf("cannot convert processed tx to bt.Tx from hex (tx.ID: %s). Reason: %w", tx.ID, err)
}

btTxsNeededForBUMP = append(btTxsNeededForBUMP, processedBtTx)
txsNeededForBUMP = append(txsNeededForBUMP, tx)

return btTxsNeededForBUMP, txsNeededForBUMP, nil
}
Loading
Loading