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 #468 from BuxOrg/feat/168-parent-inputs-beef
Browse files Browse the repository at this point in the history
feat(BUX-168): beefTx for not mined inputs
  • Loading branch information
wregulski authored Nov 24, 2023
2 parents e8fe1ad + 9a7b689 commit 4b91c0f
Show file tree
Hide file tree
Showing 11 changed files with 612 additions and 167 deletions.
33 changes: 33 additions & 0 deletions action_transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,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 +511,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
}
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
}
9 changes: 9 additions & 0 deletions beef_fixtures.go

Large diffs are not rendered by default.

106 changes: 33 additions & 73 deletions beef_tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,42 @@ package bux
import (
"context"
"encoding/hex"
"errors"
"fmt"

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

const maxBeefVer = uint32(0xFFFF) // value from BRC-62

// ToBeefHex generates BEEF Hex for transaction
func ToBeefHex(ctx context.Context, tx *Transaction) (string, error) {
beef, err := newBeefTx(ctx, 1, tx)
type beefTx struct {
version uint32
bumps BUMPs
transactions []*bt.Tx
}

// ToBeef generates BEEF Hex for transaction
func ToBeef(ctx context.Context, tx *Transaction, store TransactionGetter) (string, error) {
if err := hydrateTransaction(ctx, tx); err != nil {
return "", err
}

bumpBtFactors, bumpFactors, err := prepareBEEFFactors(ctx, tx, store)
if err != nil {
return "", fmt.Errorf("prepareBUMPFactors() error: %w", err)
}

bumps, err := calculateMergedBUMP(bumpFactors)
sortedTxs := kahnTopologicalSortTransactions(bumpBtFactors)
beefHex, err := toBeefHex(ctx, bumps, sortedTxs)
if err != nil {
return "", fmt.Errorf("ToBeef() error: %w", err)
}

return beefHex, nil
}

func toBeefHex(ctx context.Context, bumps BUMPs, parentTxs []*bt.Tx) (string, error) {
beef, err := newBeefTx(ctx, 1, bumps, parentTxs)
if err != nil {
return "", fmt.Errorf("ToBeefHex() error: %w", err)
}
Expand All @@ -26,52 +51,19 @@ func ToBeefHex(ctx context.Context, tx *Transaction) (string, error) {
return hex.EncodeToString(beefBytes), nil
}

type beefTx struct {
version uint32
bumps BUMPs
transactions []*bt.Tx
}

func newBeefTx(ctx context.Context, version uint32, tx *Transaction) (*beefTx, error) {
func newBeefTx(ctx context.Context, version uint32, bumps BUMPs, parentTxs []*bt.Tx) (*beefTx, error) {
if version > maxBeefVer {
return nil, fmt.Errorf("version above 0x%X", maxBeefVer)
}

var err error
if err = hydrateTransaction(ctx, tx); err != nil {
return nil, err
}

if err = validateBumps(tx.draftTransaction.BUMPs); err != nil {
if err := validateBumps(bumps); err != nil {
return nil, err
}

// get inputs parent transactions
inputs := tx.draftTransaction.Configuration.Inputs
transactions := make([]*bt.Tx, 0, len(inputs)+1)

for _, input := range inputs {
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)
}

transactions = append(transactions, prevTxs...)
}

// add current transaction
var btTx *bt.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,
bumps: tx.draftTransaction.BUMPs,
transactions: kahnTopologicalSortTransactions(transactions),
bumps: bumps,
transactions: parentTxs,
}

return beef, nil
Expand All @@ -92,35 +84,3 @@ func hydrateTransaction(ctx context.Context, tx *Transaction) error {

return 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 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 inputTx.MerkleProof.TxOrID != "" {
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
}
29 changes: 29 additions & 0 deletions beef_tx_mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package bux

import (
"context"
)

type MockTransactionStore struct {
Transactions map[string]*Transaction
}

func NewMockTransactionStore() *MockTransactionStore {
return &MockTransactionStore{
Transactions: make(map[string]*Transaction),
}
}

func (m *MockTransactionStore) AddToStore(tx *Transaction) {
m.Transactions[tx.ID] = tx
}

func (m *MockTransactionStore) GetTransactionsByIDs(ctx context.Context, txIDs []string) ([]*Transaction, error) {
var txs []*Transaction
for _, txID := range txIDs {
if tx, exists := m.Transactions[txID]; exists {
txs = append(txs, tx)
}
}
return txs, nil
}
Loading

0 comments on commit 4b91c0f

Please sign in to comment.