Skip to content

Commit

Permalink
Optimize handling of non-existing transactions in the Electrum client (
Browse files Browse the repository at this point in the history
…#3809)

Electrum client exposes two methods for getting entire transactions and
transaction confirmations from the Bitcoin blockchain. Those methods
retry Electrum requests in case of any error. Retries are done in the
exponential backoff scheme until a timeout of 2 minutes is hit.

However, when the given transaction does not exist on the chain, there
is no point to retry. This can be harmful for scenarios like deposit
sweep generation that need to check multiple transactions in a limited
time window. If there are multiple non-existing transactions that are
checked during the process, 2 minutes per such transaction is wasted.

Here we improve the situation by introducing an error type check. If the
error says about not found transaction, retries are not executed.
  • Loading branch information
tomaszslabon authored Apr 23, 2024
2 parents 795d284 + 4abcd8a commit efa643f
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 41 deletions.
60 changes: 57 additions & 3 deletions pkg/bitcoin/electrum/electrum.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,18 @@ func (c *Connection) GetTransaction(
// We cannot use `GetTransaction` to get the the transaction details
// as Esplora/Electrs doesn't support verbose transactions.
// See: https://github.com/Blockstream/electrs/pull/36
return client.GetRawTransaction(ctx, txID)
tx, err := client.GetRawTransaction(ctx, txID)
if err != nil {
if isTxNotFoundErr(err) {
// The transaction was not found on the chain. There is
// no point in retrying the request and losing time.
return "", nil
}

return "", err
}

return tx, nil
},
"GetRawTransaction",
)
Expand All @@ -99,6 +110,13 @@ func (c *Connection) GetTransaction(
err,
)
}
if len(rawTransaction) == 0 {
return nil, fmt.Errorf(
"failed to get raw transaction with ID [%s]: [%v]",
txID,
fmt.Errorf("not found"),
)
}

result, err := convertRawTransaction(rawTransaction)
if err != nil {
Expand All @@ -123,10 +141,21 @@ func (c *Connection) GetTransactionConfirmations(
rawTransaction, err := requestWithRetry(
c,
func(ctx context.Context, client *electrum.Client) (string, error) {
// We cannot use `GetTransaction` to get the the transaction details
// We cannot use `GetTransaction` to get the transaction details
// as Esplora/Electrs doesn't support verbose transactions.
// See: https://github.com/Blockstream/electrs/pull/36
return client.GetRawTransaction(ctx, txID)
tx, err := client.GetRawTransaction(ctx, txID)
if err != nil {
if isTxNotFoundErr(err) {
// The transaction was not found on the chain. There is
// no point in retrying the request and losing time.
return "", nil
}

return "", err
}

return tx, nil
},
"GetRawTransaction",
)
Expand All @@ -138,6 +167,13 @@ func (c *Connection) GetTransactionConfirmations(
err,
)
}
if len(rawTransaction) == 0 {
return 0, fmt.Errorf(
"failed to get raw transaction with ID [%s]: [%v]",
txID,
fmt.Errorf("not found"),
)
}

tx, err := decodeTransaction(rawTransaction)
if err != nil {
Expand Down Expand Up @@ -224,6 +260,24 @@ txOutLoop:
return 0, nil
}

func isTxNotFoundErr(err error) bool {
txNotFoundErrs := []string{
"no such mempool or blockchain transaction",
"missing transaction",
"transaction not found",
}

errStr := strings.ToLower(err.Error())

for _, txNotFoundErr := range txNotFoundErrs {
if strings.Contains(errStr, txNotFoundErr) {
return true
}
}

return false
}

// BroadcastTransaction broadcasts the given transaction over the
// network of the Bitcoin chain nodes. If the broadcast action could not be
// done, this function returns an error. This function does not give any
Expand Down
63 changes: 25 additions & 38 deletions pkg/bitcoin/electrum/electrum_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"math"
"reflect"
"sort"
"strings"
"testing"
Expand Down Expand Up @@ -175,15 +176,19 @@ func TestGetTransaction_Negative_Integration(t *testing.T) {

_, err := electrum.GetTransaction(invalidTxID)

assertMissingTransactionError(
t,
testConfig.clientConfig,
fmt.Sprintf(
"failed to get raw transaction with ID [%s]",
invalidTxID.Hex(bitcoin.ReversedByteOrder),
),
err,
expectedErr := fmt.Errorf(
"failed to get raw transaction with ID [%s]: [not found]",
invalidTxID.Hex(bitcoin.ReversedByteOrder),
)
if !reflect.DeepEqual(expectedErr, err) {
t.Errorf(
"unexpected error\n"+
"expected: %v\n"+
"actual: %v\n",
expectedErr,
err,
)
}
})
}

Expand Down Expand Up @@ -221,15 +226,19 @@ func TestGetTransactionConfirmations_Negative_Integration(t *testing.T) {

_, err := electrum.GetTransactionConfirmations(invalidTxID)

assertMissingTransactionError(
t,
testConfig.clientConfig,
fmt.Sprintf(
"failed to get raw transaction with ID [%s]",
invalidTxID.Hex(bitcoin.ReversedByteOrder),
),
err,
expectedErr := fmt.Errorf(
"failed to get raw transaction with ID [%s]: [not found]",
invalidTxID.Hex(bitcoin.ReversedByteOrder),
)
if !reflect.DeepEqual(expectedErr, err) {
t.Errorf(
"unexpected error\n"+
"expected: %v\n"+
"actual: %v\n",
expectedErr,
err,
)
}
})
}

Expand Down Expand Up @@ -607,17 +616,11 @@ func assertNumberCloseTo(t *testing.T, expected uint, actual uint, delta uint) {
}

type expectedErrorMessages struct {
missingTransaction []string
missingBlockHeader []string
missingTransactionInBlock []string
}

var expectedServerErrorMessages = expectedErrorMessages{
missingTransaction: []string{
"errNo: 0, errMsg: missing transaction",
"errNo: 2, errMsg: daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})",
"errNo: 2, errMsg: daemon error: DaemonError({'message': 'Transaction not found.', 'code': -1})",
},
missingBlockHeader: []string{
"errNo: 0, errMsg: missing header",
"errNo: 1, errMsg: height 4,294,967,295 out of range",
Expand All @@ -629,22 +632,6 @@ var expectedServerErrorMessages = expectedErrorMessages{
"errNo: 1, errMsg: No transaction matching the requested hash found at height 123456"},
}

func assertMissingTransactionError(
t *testing.T,
clientConfig electrum.Config,
clientErrorPrefix string,

actualError error,
) {
assertServerError(
t,
clientConfig,
clientErrorPrefix,
expectedServerErrorMessages.missingTransaction,
actualError,
)
}

func assertMissingBlockHeaderError(
t *testing.T,
clientConfig electrum.Config,
Expand Down

0 comments on commit efa643f

Please sign in to comment.