Skip to content

Commit

Permalink
Implement GetTransactionConfirmations function
Browse files Browse the repository at this point in the history
The function returns number of confirmations for the given transaction.

As Esplora/Electrs doesn't support verbose transactions details in the
response, that would contain the confirmations number we need to
workaround it. (See: Blockstream/electrs#36)

The workaround:
1. Get the raw transaction
2. Deserialize the raw transaction
3. Find transaction block height by finding it in a history of
   transactions for the output script included in the transaction.
4. Get the latest block height
5. Calculate number of confirmations by subtracting the transaction
   block height from the latest block height and adding one.
  • Loading branch information
nkuba committed Nov 16, 2022
1 parent a573d52 commit 3db34f0
Showing 1 changed file with 115 additions and 2 deletions.
117 changes: 115 additions & 2 deletions pkg/bitcoin/electrum/electrum.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package electrum

import (
"context"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
"strings"
"time"
Expand All @@ -14,6 +16,7 @@ import (

"github.com/keep-network/keep-common/pkg/wrappers"
"github.com/keep-network/keep-core/pkg/bitcoin"
"github.com/keep-network/keep-core/pkg/internal/byteutils"
)

var (
Expand Down Expand Up @@ -150,11 +153,121 @@ func (c *Connection) GetTransaction(
return result, nil
}

// GetTransactionConfirmations gets the number of confirmations for the
// transaction with the given transaction hash. If the transaction with the
// given hash was not found on the chain, this function returns an error.
func (c *Connection) GetTransactionConfirmations(
transactionHash bitcoin.Hash,
) (uint, error) {
// TODO: Implementation.
panic("not implemented")
txID := transactionHash.Hex(bitcoin.ReversedByteOrder)

txLogger := logger.With(
zap.String("txID", txID),
)

var rawTransaction string
err := wrappers.DoWithDefaultRetry(c.requestRetryTimeout, func(ctx context.Context) error {
// 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
rawTx, err := c.client.GetRawTransaction(c.ctx, txID)
if err != nil {
return fmt.Errorf(
"GetRawTransaction failed: [%w]",
err,
)
}
rawTransaction = rawTx
return nil
})
if err != nil {
return 0, fmt.Errorf("failed to get raw transaction [%s]: [%w]", txID, err)
}

tx, err := decodeTransaction(rawTransaction)
if err != nil {
return 0, fmt.Errorf(
"failed to decode the transaction [%s]: [%w]",
rawTransaction,
err,
)
}

// As a workaround for the problem described in https://github.com/Blockstream/electrs/pull/36
// we need to calculate the number of confirmations based on the latest
// block height and block height of the transaction.
// Electrum protocol doesn't expose a function to get the transaction's block
// height (other that the `GetTransaction` that is unsupported by Esplora/Electrs).
// To get the block height of the transaction we query the history of transactions
// for the output script hash, as the history contains the transaction's block
// height.

// Initialize txBlockHeigh with minimum int32 value to identify a problem when
// a block height was not found in a history of any of the script hashes.
//
// The history is expected to return a block height for confirmed transaction.
// If a transaction is unconfirmed (is still in the mempool) the height will
// have a value of `0` or `-1`.
txBlockHeight := int32(math.MinInt32)
txOutLoop:
for _, txOut := range tx.TxOut {
script := txOut.PkScript
scriptHash := sha256.Sum256(script)
reversedScriptHash := byteutils.Reverse(scriptHash[:])
reversedScriptHashString := hex.EncodeToString(reversedScriptHash)

var scriptHashHistory []*electrum.GetMempoolResult
err := wrappers.DoWithDefaultRetry(c.requestRetryTimeout, func(ctx context.Context) error {
history, err := c.client.GetHistory(c.ctx, reversedScriptHashString)
if err != nil {
return fmt.Errorf("GetHistory failed: [%w]", err)
}

scriptHashHistory = history

return nil
})
if err != nil {
// Don't return an error, but continue to the next TxOut entry.
txLogger.Errorf("failed to get history for script hash [%s]: [%w]", err)
continue txOutLoop
}

for _, transaction := range scriptHashHistory {
if transaction.Hash == txID {
txBlockHeight = transaction.Height
break txOutLoop
}
}
}

// History querying didn't come up with the transaction's block height. Return
// an error.
if txBlockHeight == math.MinInt32 {
return 0, fmt.Errorf(
"failed to find the transaction block height in script hashes' histories",
)
}

// If the block height is greater than `0` the transaction is confirmed.
if txBlockHeight > 0 {
latestBlockHeight, err := c.GetLatestBlockHeight()
if err != nil {
return 0, fmt.Errorf(
"failed to get the latest block height: [%w]",
err,
)
}

if latestBlockHeight >= uint(txBlockHeight) {
// Add `1` to the calculated difference as if the transaction block
// height equals the latest block height the transaction is already
// confirmed, so it has one confirmation.
return latestBlockHeight - uint(txBlockHeight) + 1, nil
}
}

return 0, nil
}

// BroadcastTransaction broadcasts the given transaction over the
Expand Down

0 comments on commit 3db34f0

Please sign in to comment.