Skip to content

Commit

Permalink
ARCO-155: change args/var names; add missing tests and benchmark
Browse files Browse the repository at this point in the history
  • Loading branch information
arkadiuszos4chain committed Jul 11, 2024
1 parent cc99c65 commit ab78a7c
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 57 deletions.
105 changes: 53 additions & 52 deletions pkg/api/handler/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,34 +140,30 @@ func (m ArcDefaultHandler) POSTTransaction(ctx echo.Context, params api.POSTTran
return ctx.JSON(e.Status, e)
}

transactionHex, err := parseTransactionFromRequest(ctx.Request())
txHex, err := parseTransactionFromRequest(ctx.Request())
if err != nil {
e := api.NewErrorFields(api.ErrStatusBadRequest, fmt.Sprintf("error parsing transaction from request: %s", err.Error()))
return ctx.JSON(e.Status, e)
}

txs, outputs, failOutputs, e := m.processTransactions(ctx.Request().Context(), transactionHex, transactionOptions)
reqCtx := ctx.Request().Context()
txs, successes, fails, e := m.processTransactions(reqCtx, txHex, transactionOptions)

if e != nil {
// if an error is returned, the processing failed, and we should return a 500 error
// if an error is returned, the processing failed
return ctx.JSON(e.Status, e)
}

if len(failOutputs) > 0 {
// if an error is returned, the processing failed
e = failOutputs[0]
if len(fails) > 0 {
// if an fail result is returned, the processing/validation failed
e = fails[0]
return ctx.JSON(e.Status, e)
}

transaction := txs[0]
response := outputs[0]

sizingInfo := make([][]uint64, 1)
normalBytes, dataBytes, feeAmount := getSizings(transaction)
sizingInfo[0] = []uint64{normalBytes, dataBytes, feeAmount}
sizingCtx := context.WithValue(ctx.Request().Context(), ContextSizings, sizingInfo)
sizingCtx := context.WithValue(reqCtx, ContextSizings, prepareSizingInfo(txs))
ctx.SetRequest(ctx.Request().WithContext(sizingCtx))

response := successes[0]
return ctx.JSON(response.Status, response)
}

Expand Down Expand Up @@ -209,34 +205,29 @@ func (m ArcDefaultHandler) POSTTransactions(ctx echo.Context, params api.POSTTra
return ctx.JSON(e.Status, e)
}

txsHexes, err := parseTransactionsFromRequest(ctx.Request())
txsHex, err := parseTransactionsFromRequest(ctx.Request())
if err != nil {
e := api.NewErrorFields(api.ErrStatusBadRequest, fmt.Sprintf("error parsing transaction from request: %s", err.Error()))
return ctx.JSON(e.Status, e)
}

// process all txs
txs, outputs, failOutputs, e := m.processTransactions(ctx.Request().Context(), txsHexes, transactionOptions)
reqCtx := ctx.Request().Context()
txs, successes, fails, e := m.processTransactions(reqCtx, txsHex, transactionOptions)
if e != nil {
return ctx.JSON(e.Status, e)
}

sizingInfo := make([][]uint64, 0, len(txs))
for _, btTx := range txs {
normalBytes, dataBytes, feeAmount := getSizings(btTx)
sizingInfo = append(sizingInfo, []uint64{normalBytes, dataBytes, feeAmount})
}
sizingCtx := context.WithValue(ctx.Request().Context(), ContextSizings, sizingInfo)
sizingCtx := context.WithValue(reqCtx, ContextSizings, prepareSizingInfo(txs))
ctx.SetRequest(ctx.Request().WithContext(sizingCtx))
// we cannot really return any other status here
// each transaction in the slice will have the result of the transaction submission

// merge success and fail outputs
responses := make([]any, 0, len(outputs)+len(failOutputs))
for _, o := range outputs {
// merge success and fail results
responses := make([]any, 0, len(successes)+len(fails))
for _, o := range successes {
responses = append(responses, o)
}
for _, fo := range failOutputs {
for _, fo := range fails {
responses = append(responses, fo)
}

Expand Down Expand Up @@ -306,12 +297,13 @@ func getTransactionsOptions(params api.POSTTransactionsParams, rejectedCallbackU
}

// processTransactions validates all the transactions in the array and submits to metamorph for processing.
func (m ArcDefaultHandler) processTransactions(ctx context.Context, txsHex []byte, transactionOptions *metamorph.TransactionOptions) (
submitedTxs []*bt.Tx, outputs []*api.TransactionResponse, failOutputs []*api.ErrorFields, pErr *api.ErrorFields) {
func (m ArcDefaultHandler) processTransactions(ctx context.Context, txsHex []byte, options *metamorph.TransactionOptions) (
submittedTxs []*bt.Tx, successes []*api.TransactionResponse, fails []*api.ErrorFields, processingErr *api.ErrorFields) {
m.logger.Info("Starting to process transactions")

// decode and validate txs
txIds := make([]string, 0)
var txIds []string

for len(txsHex) != 0 {
hexFormat := validator.GetHexFormat(txsHex)

Expand All @@ -326,14 +318,14 @@ func (m ArcDefaultHandler) processTransactions(ctx context.Context, txsHex []byt
txsHex = remainingBytes

v := beefValidator.New(m.NodePolicy)
if arcError := m.validateBEEFTransaction(ctx, v, beefTx, transactionOptions); arcError != nil {
failOutputs = append(failOutputs, arcError)
if arcError := m.validateBEEFTransaction(ctx, v, beefTx, options); arcError != nil {
fails = append(fails, arcError)
continue
}

for _, tx := range beefTx.Transactions {
if !tx.IsMined() {
submitedTxs = append(submitedTxs, tx.Transaction)
submittedTxs = append(submittedTxs, tx.Transaction)
}
}

Expand All @@ -346,41 +338,41 @@ func (m ArcDefaultHandler) processTransactions(ctx context.Context, txsHex []byt

txsHex = txsHex[bytesUsed:]
v := defaultValidator.New(m.NodePolicy)
if arcError := m.validateEFTransaction(ctx, v, transaction, transactionOptions); arcError != nil {
failOutputs = append(failOutputs, arcError)
if arcError := m.validateEFTransaction(ctx, v, transaction, options); arcError != nil {
fails = append(fails, arcError)
continue
}

submitedTxs = append(submitedTxs, transaction)
submittedTxs = append(submittedTxs, transaction)
txIds = append(txIds, transaction.TxID())
}
}

if len(submitedTxs) == 0 {
return nil, nil, failOutputs, nil
if len(submittedTxs) == 0 {
return nil, nil, fails, nil
}

timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(transactionOptions.MaxTimeout+2)*time.Second)
timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(options.MaxTimeout+2)*time.Second)
defer cancel()

// submit validated transactions to metamorph
txStatuses, e := m.submitTransactions(timeoutCtx, submitedTxs, transactionOptions)
// submit valid transactions to metamorph
txStatuses, e := m.submitTransactions(timeoutCtx, submittedTxs, options)
if e != nil {
return nil, nil, nil, e
}

// process returned transaction statuses and return to user
// prepare success results
txStatuses = filterStatusesByTxIDs(txIds, txStatuses)

now := m.now()
outputs = make([]*api.TransactionResponse, 0, len(submitedTxs))
successes = make([]*api.TransactionResponse, 0, len(submittedTxs))

for idx, tx := range txStatuses {
txID := tx.TxID
if txID == "" {
txID = submitedTxs[idx].TxID()
txID = submittedTxs[idx].TxID()
}
outputs = append(outputs, &api.TransactionResponse{
successes = append(successes, &api.TransactionResponse{
Status: int(api.StatusOK),
Title: "OK",
BlockHash: &tx.BlockHash,
Expand All @@ -393,13 +385,12 @@ func (m ArcDefaultHandler) processTransactions(ctx context.Context, txsHex []byt
})
}

return submitedTxs, outputs, failOutputs, nil
return submittedTxs, successes, fails, nil
}

func (m ArcDefaultHandler) validateEFTransaction(ctx context.Context, txValidator validator.DefaultValidator, transaction *bt.Tx, transactionOptions *metamorph.TransactionOptions) *api.ErrorFields {
func (m ArcDefaultHandler) validateEFTransaction(ctx context.Context, txValidator validator.DefaultValidator, transaction *bt.Tx, options *metamorph.TransactionOptions) *api.ErrorFields {
// the validator expects an extended transaction
// we must enrich the transaction with the missing data
// TODO: move morphing to validator
if !txValidator.IsExtended(transaction) {
err := m.extendTransaction(ctx, transaction)
if err != nil {
Expand All @@ -409,8 +400,8 @@ func (m ArcDefaultHandler) validateEFTransaction(ctx context.Context, txValidato
}
}

if !transactionOptions.SkipTxValidation {
feeOpts, scriptOpts := toValidationOpts(transactionOptions)
if !options.SkipTxValidation {
feeOpts, scriptOpts := toValidationOpts(options)

if err := txValidator.ValidateTransaction(ctx, transaction, feeOpts, scriptOpts); err != nil {
statusCode, arcError := m.handleError(ctx, transaction, err)
Expand Down Expand Up @@ -455,8 +446,8 @@ func (m ArcDefaultHandler) extendTransaction(ctx context.Context, transaction *b
return nil
}

func (m ArcDefaultHandler) validateBEEFTransaction(ctx context.Context, txValidator validator.BeefValidator, beefTx *beef.BEEF, transactionOptions *metamorph.TransactionOptions) *api.ErrorFields {
feeOpts, scriptOpts := toValidationOpts(transactionOptions)
func (m ArcDefaultHandler) validateBEEFTransaction(ctx context.Context, txValidator validator.BeefValidator, beefTx *beef.BEEF, options *metamorph.TransactionOptions) *api.ErrorFields {
feeOpts, scriptOpts := toValidationOpts(options)

if errTx, err := txValidator.ValidateTransaction(ctx, beefTx, feeOpts, scriptOpts); err != nil {
_, arcError := m.handleError(ctx, errTx, err)
Expand Down Expand Up @@ -489,7 +480,7 @@ func (m ArcDefaultHandler) submitTransactions(ctx context.Context, txs []*bt.Tx,
if len(txs) == 1 {
tx := txs[0]

// probably we could use SubmitTransactions() as well, but right now I don't want to create potential performance issues
// SubmitTransaction() used to avoid performance issue
status, err := m.TransactionHandler.SubmitTransaction(ctx, tx, options)

if err != nil {
Expand Down Expand Up @@ -582,6 +573,16 @@ func (m ArcDefaultHandler) getTransaction(ctx context.Context, inputTxID string)
return nil, metamorph.ErrParentTransactionNotFound
}

func prepareSizingInfo(txs []*bt.Tx) [][]uint64 {
sizingInfo := make([][]uint64, 0, len(txs))
for _, btTx := range txs {
normalBytes, dataBytes, feeAmount := getSizings(btTx)
sizingInfo = append(sizingInfo, []uint64{normalBytes, dataBytes, feeAmount})
}

return sizingInfo
}

func getSizings(tx *bt.Tx) (uint64, uint64, uint64) {
var feeAmount uint64

Expand Down
12 changes: 7 additions & 5 deletions pkg/api/handler/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,13 @@ func convertMerkleRootsRequest(beefMerkleRoots []beef.MerkleRootVerificationRequ
return merkleRoots
}

func filterStatusesByTxIDs(txIDs []string, allStatuses []*metamorph.TransactionStatus) []*metamorph.TransactionStatus {
if len(txIDs) == 1 && len(allStatuses) == 1 {
if allStatuses[0].TxID == txIDs[0] {
return allStatuses
func filterStatusesByTxIDs(txIDs []string, statuses []*metamorph.TransactionStatus) []*metamorph.TransactionStatus {
if len(txIDs) == 1 && len(statuses) == 1 { // optimization for a common scenario
if statuses[0].TxID == txIDs[0] {
return statuses
}

return make([]*metamorph.TransactionStatus, 0)
}

idsMap := make(map[string]struct{})
Expand All @@ -137,7 +139,7 @@ func filterStatusesByTxIDs(txIDs []string, allStatuses []*metamorph.TransactionS
}

filteredStatuses := make([]*metamorph.TransactionStatus, 0)
for _, txStatus := range allStatuses {
for _, txStatus := range statuses {
if _, ok := idsMap[txStatus.TxID]; ok {
filteredStatuses = append(filteredStatuses, txStatus)
}
Expand Down
91 changes: 91 additions & 0 deletions pkg/api/handler/helpers_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package handler

import (
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"

"github.com/bitcoin-sv/arc/internal/beef"
"github.com/bitcoin-sv/arc/pkg/blocktx"
"github.com/bitcoin-sv/arc/pkg/metamorph"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -73,3 +76,91 @@ func TestConvertMerkleRootsRequest(t *testing.T) {
})
}
}

func TestFilterStatusesByTxIDs(t *testing.T) {
tcs := []struct {
name string
txIDs []string
statuses []*metamorph.TransactionStatus
expected []*metamorph.TransactionStatus
}{
{
name: "Single txID with matching status",
txIDs: []string{"tx1"},
statuses: []*metamorph.TransactionStatus{
{TxID: "tx1"},
},
expected: []*metamorph.TransactionStatus{
{TxID: "tx1"},
},
},
{
name: "Single txID with non-matching status",
txIDs: []string{"tx1"},
statuses: []*metamorph.TransactionStatus{
{TxID: "tx2"},
},
expected: []*metamorph.TransactionStatus{},
},
{
name: "Multiple txIDs with some matching statuses",
txIDs: []string{"tx1", "tx3"},
statuses: []*metamorph.TransactionStatus{
{TxID: "tx1"},
{TxID: "tx2"},
{TxID: "tx3"},
},
expected: []*metamorph.TransactionStatus{
{TxID: "tx1"},
{TxID: "tx3"},
},
},
{
name: "No txIDs",
txIDs: []string{},
statuses: []*metamorph.TransactionStatus{
{TxID: "tx1"},
{TxID: "tx2"},
},
expected: []*metamorph.TransactionStatus{},
},
{
name: "No statuses",
txIDs: []string{"tx1", "tx2"},
statuses: []*metamorph.TransactionStatus{},
expected: []*metamorph.TransactionStatus{},
},
}

for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
res := filterStatusesByTxIDs(tc.txIDs, tc.statuses)
if !reflect.DeepEqual(res, tc.expected) {
t.Errorf("expected %v, got %v", tc.expected, res)
}
})
}
}

func BenchmarkFilterStatusesByTxIDs(b *testing.B) {
bcs := []int{1, 10, 100, 1000, 10000}

for _, n := range bcs {
b.Run(fmt.Sprintf("txIDs-%d", n), func(b *testing.B) {
txIDs := make([]string, n)
for i := 0; i < n; i++ {
txIDs[i] = fmt.Sprintf("tx-%d", i)
}

statuses := make([]*metamorph.TransactionStatus, n)
for i := 0; i < n; i++ {
statuses[i] = &metamorph.TransactionStatus{TxID: fmt.Sprintf("tx-%d", i)}
}

b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = filterStatusesByTxIDs(txIDs, statuses)
}
})
}
}

0 comments on commit ab78a7c

Please sign in to comment.