mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
chainntnfs: ensure proper fallback to scanning tx manually
In this commit, we address a bug where it's possible that we still attempt to manually scan for a transaction to determine whether it's been included in the chain even after successfully checking the txindex and not finding it there. Now, we'll short-circuit this process by exiting early if the txindex lookup was successful but the transaction in question was not found. Otherwise, we'll fall back to the manual scan.
This commit is contained in:
parent
f88f6f68d4
commit
7895f01e2e
@ -3,6 +3,7 @@ package bitcoindnotify
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@ -27,6 +28,31 @@ const (
|
||||
reorgSafetyLimit = 100
|
||||
)
|
||||
|
||||
// txConfStatus denotes the status of a transaction's lookup.
|
||||
type txConfStatus uint8
|
||||
|
||||
const (
|
||||
// txFoundMempool denotes that the transaction was found within the
|
||||
// backend node's mempool.
|
||||
txFoundMempool txConfStatus = iota
|
||||
|
||||
// txFoundIndex denotes that the transaction was found within the
|
||||
// backend node's txindex.
|
||||
txFoundIndex
|
||||
|
||||
// txFoundIndex denotes that the transaction was not found within the
|
||||
// backend node's txindex.
|
||||
txNotFoundIndex
|
||||
|
||||
// txFoundManually denotes that the transaction was found within the
|
||||
// chain by scanning for it manually.
|
||||
txFoundManually
|
||||
|
||||
// txFoundManually denotes that the transaction was not found within the
|
||||
// chain by scanning for it manually.
|
||||
txNotFoundManually
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrChainNotifierShuttingDown is used when we are trying to
|
||||
// measure a spend notification when notifier is already stopped.
|
||||
@ -268,7 +294,7 @@ out:
|
||||
go func() {
|
||||
defer b.wg.Done()
|
||||
|
||||
confDetails, err := b.historicalConfDetails(
|
||||
confDetails, _, err := b.historicalConfDetails(
|
||||
msg.TxID, msg.heightHint,
|
||||
currentHeight,
|
||||
)
|
||||
@ -447,23 +473,32 @@ func (b *BitcoindNotifier) handleRelevantTx(tx chain.RelevantTx, bestHeight int3
|
||||
// historicalConfDetails looks up whether a transaction is already included in a
|
||||
// block in the active chain and, if so, returns details about the confirmation.
|
||||
func (b *BitcoindNotifier) historicalConfDetails(txid *chainhash.Hash,
|
||||
heightHint, currentHeight uint32) (*chainntnfs.TxConfirmation, error) {
|
||||
heightHint, currentHeight uint32) (*chainntnfs.TxConfirmation,
|
||||
txConfStatus, error) {
|
||||
|
||||
// First, we'll attempt to retrieve the transaction details using the
|
||||
// backend node's transaction index.
|
||||
txConf, err := b.confDetailsFromTxIndex(txid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// We'll first attempt to retrieve the transaction using the node's
|
||||
// txindex.
|
||||
txConf, txStatus, err := b.confDetailsFromTxIndex(txid)
|
||||
|
||||
// We'll then check the status of the transaction lookup returned to
|
||||
// determine whether we should proceed with any fallback methods.
|
||||
switch {
|
||||
// The transaction was found within the node's mempool.
|
||||
case txStatus == txFoundMempool:
|
||||
|
||||
// The transaction was found within the node's txindex.
|
||||
case txStatus == txFoundIndex:
|
||||
|
||||
// The transaction was not found within the node's mempool or txindex.
|
||||
case err == nil && txStatus == txNotFoundIndex:
|
||||
|
||||
// We failed to look up the transaction within the node's mempool or
|
||||
// txindex, so we'll proceed to scan the chain manually.
|
||||
default:
|
||||
return b.confDetailsManually(txid, heightHint, currentHeight)
|
||||
}
|
||||
|
||||
if txConf != nil {
|
||||
return txConf, nil
|
||||
}
|
||||
|
||||
// If the backend node's transaction index is not enabled, then we'll
|
||||
// fall back to manually scanning the chain's blocks, looking for the
|
||||
// block where the transaction was included in.
|
||||
return b.confDetailsManually(txid, heightHint, currentHeight)
|
||||
return txConf, txStatus, nil
|
||||
}
|
||||
|
||||
// confDetailsFromTxIndex looks up whether a transaction is already included
|
||||
@ -471,26 +506,33 @@ func (b *BitcoindNotifier) historicalConfDetails(txid *chainhash.Hash,
|
||||
// If the transaction is found, its confirmation details are returned.
|
||||
// Otherwise, nil is returned.
|
||||
func (b *BitcoindNotifier) confDetailsFromTxIndex(txid *chainhash.Hash,
|
||||
) (*chainntnfs.TxConfirmation, error) {
|
||||
) (*chainntnfs.TxConfirmation, txConfStatus, error) {
|
||||
|
||||
// If the transaction has some or all of its confirmations required,
|
||||
// then we may be able to dispatch it immediately.
|
||||
tx, err := b.chainConn.GetRawTransactionVerbose(txid)
|
||||
if err != nil {
|
||||
// Avoid returning an error if the transaction index is not
|
||||
// enabled to proceed with fallback methods.
|
||||
// If the transaction lookup was succesful, but it wasn't found
|
||||
// within the index itself, then we can exit early. We'll also
|
||||
// need to look at the error message returned as the error code
|
||||
// is used for multiple errors.
|
||||
txNotFoundErr := "No such mempool or blockchain transaction"
|
||||
jsonErr, ok := err.(*btcjson.RPCError)
|
||||
if !ok || jsonErr.Code != btcjson.ErrRPCNoTxInfo {
|
||||
return nil, fmt.Errorf("unable to query for txid "+
|
||||
"%v: %v", txid, err)
|
||||
if ok && jsonErr.Code == btcjson.ErrRPCNoTxInfo &&
|
||||
strings.Contains(jsonErr.Message, txNotFoundErr) {
|
||||
|
||||
return nil, txNotFoundIndex, nil
|
||||
}
|
||||
|
||||
return nil, txNotFoundIndex, fmt.Errorf("unable to query for "+
|
||||
"txid %v: %v", txid, err)
|
||||
}
|
||||
|
||||
// Make sure we actually retrieved a transaction that is included in a
|
||||
// block. Without this, we won't be able to retrieve its confirmation
|
||||
// details.
|
||||
if tx == nil || tx.BlockHash == "" {
|
||||
return nil, nil
|
||||
return nil, txFoundMempool, nil
|
||||
}
|
||||
|
||||
// As we need to fully populate the returned TxConfirmation struct,
|
||||
@ -498,14 +540,16 @@ func (b *BitcoindNotifier) confDetailsFromTxIndex(txid *chainhash.Hash,
|
||||
// locate its exact index within the block.
|
||||
blockHash, err := chainhash.NewHashFromStr(tx.BlockHash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get block hash %v for "+
|
||||
"historical dispatch: %v", tx.BlockHash, err)
|
||||
return nil, txNotFoundIndex, fmt.Errorf("unable to get block "+
|
||||
"hash %v for historical dispatch: %v", tx.BlockHash,
|
||||
err)
|
||||
}
|
||||
|
||||
block, err := b.chainConn.GetBlockVerbose(blockHash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get block with hash %v for "+
|
||||
"historical dispatch: %v", blockHash, err)
|
||||
return nil, txNotFoundIndex, fmt.Errorf("unable to get block "+
|
||||
"with hash %v for historical dispatch: %v", blockHash,
|
||||
err)
|
||||
}
|
||||
|
||||
// If the block was obtained, locate the transaction's index within the
|
||||
@ -513,18 +557,19 @@ func (b *BitcoindNotifier) confDetailsFromTxIndex(txid *chainhash.Hash,
|
||||
targetTxidStr := txid.String()
|
||||
for txIndex, txHash := range block.Tx {
|
||||
if txHash == targetTxidStr {
|
||||
return &chainntnfs.TxConfirmation{
|
||||
details := &chainntnfs.TxConfirmation{
|
||||
BlockHash: blockHash,
|
||||
BlockHeight: uint32(block.Height),
|
||||
TxIndex: uint32(txIndex),
|
||||
}, nil
|
||||
}
|
||||
return details, txFoundIndex, nil
|
||||
}
|
||||
}
|
||||
|
||||
// We return an error because we should have found the transaction
|
||||
// within the block, but didn't.
|
||||
return nil, fmt.Errorf("unable to locate tx %v in block %v", txid,
|
||||
blockHash)
|
||||
return nil, txNotFoundIndex, fmt.Errorf("unable to locate tx %v in "+
|
||||
"block %v", txid, blockHash)
|
||||
}
|
||||
|
||||
// confDetailsManually looks up whether a transaction is already included in a
|
||||
@ -533,7 +578,7 @@ func (b *BitcoindNotifier) confDetailsFromTxIndex(txid *chainhash.Hash,
|
||||
// height in the chain. If the transaction is found, its confirmation details
|
||||
// are returned. Otherwise, nil is returned.
|
||||
func (b *BitcoindNotifier) confDetailsManually(txid *chainhash.Hash,
|
||||
heightHint, currentHeight uint32) (*chainntnfs.TxConfirmation, error) {
|
||||
heightHint, currentHeight uint32) (*chainntnfs.TxConfirmation, txConfStatus, error) {
|
||||
|
||||
targetTxidStr := txid.String()
|
||||
|
||||
@ -544,38 +589,39 @@ func (b *BitcoindNotifier) confDetailsManually(txid *chainhash.Hash,
|
||||
// processing the next height.
|
||||
select {
|
||||
case <-b.quit:
|
||||
return nil, ErrChainNotifierShuttingDown
|
||||
return nil, txNotFoundManually, ErrChainNotifierShuttingDown
|
||||
default:
|
||||
}
|
||||
|
||||
blockHash, err := b.chainConn.GetBlockHash(int64(height))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get hash from block "+
|
||||
"with height %d", height)
|
||||
return nil, txNotFoundManually, fmt.Errorf("unable to "+
|
||||
"get hash from block with height %d", height)
|
||||
}
|
||||
|
||||
block, err := b.chainConn.GetBlockVerbose(blockHash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get block with hash "+
|
||||
"%v: %v", blockHash, err)
|
||||
return nil, txNotFoundManually, fmt.Errorf("unable to "+
|
||||
"get block with hash %v: %v", blockHash, err)
|
||||
}
|
||||
|
||||
for txIndex, txHash := range block.Tx {
|
||||
// If we're able to find the transaction in this block,
|
||||
// return its confirmation details.
|
||||
if txHash == targetTxidStr {
|
||||
return &chainntnfs.TxConfirmation{
|
||||
details := &chainntnfs.TxConfirmation{
|
||||
BlockHash: blockHash,
|
||||
BlockHeight: height,
|
||||
TxIndex: uint32(txIndex),
|
||||
}, nil
|
||||
}
|
||||
return details, txFoundManually, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here, then we were not able to find the transaction
|
||||
// within a block, so we avoid returning an error.
|
||||
return nil, nil
|
||||
return nil, txNotFoundManually, nil
|
||||
}
|
||||
|
||||
// handleBlockConnected applies a chain update for a new block. Any watched
|
||||
|
204
chainntnfs/bitcoindnotify/bitcoind_test.go
Normal file
204
chainntnfs/bitcoindnotify/bitcoind_test.go
Normal file
@ -0,0 +1,204 @@
|
||||
// +build debug
|
||||
|
||||
package bitcoindnotify
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/integration/rpctest"
|
||||
"github.com/btcsuite/btcwallet/chain"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
)
|
||||
|
||||
// setUpNotifier is a helper function to start a new notifier backed by a
|
||||
// bitcoind driver.
|
||||
func setUpNotifier(t *testing.T, bitcoindConn *chain.BitcoindConn) *BitcoindNotifier {
|
||||
t.Helper()
|
||||
|
||||
notifier := New(bitcoindConn)
|
||||
if err := notifier.Start(); err != nil {
|
||||
t.Fatalf("unable to start notifier: %v", err)
|
||||
}
|
||||
|
||||
return notifier
|
||||
}
|
||||
|
||||
// syncNotifierWithMiner is a helper method that attempts to wait until the
|
||||
// notifier is synced (in terms of the chain) with the miner.
|
||||
func syncNotifierWithMiner(t *testing.T, notifier *BitcoindNotifier,
|
||||
miner *rpctest.Harness) uint32 {
|
||||
|
||||
t.Helper()
|
||||
|
||||
_, minerHeight, err := miner.Node.GetBestBlock()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to retrieve miner's current height: %v", err)
|
||||
}
|
||||
|
||||
timeout := time.After(10 * time.Second)
|
||||
for {
|
||||
_, bitcoindHeight, err := notifier.chainConn.GetBestBlock()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to retrieve bitcoind's current "+
|
||||
"height: %v", err)
|
||||
}
|
||||
|
||||
if bitcoindHeight == minerHeight {
|
||||
return uint32(bitcoindHeight)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
case <-timeout:
|
||||
t.Fatalf("timed out waiting to sync notifier")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHistoricalConfDetailsTxIndex ensures that we correctly retrieve
|
||||
// historical confirmation details using the backend node's txindex.
|
||||
func TestHistoricalConfDetailsTxIndex(t *testing.T) {
|
||||
miner, tearDown := chainntnfs.NewMiner(
|
||||
t, []string{"--txindex"}, true, 25,
|
||||
)
|
||||
defer tearDown()
|
||||
|
||||
bitcoindConn, cleanUp := chainntnfs.NewBitcoindBackend(
|
||||
t, miner.P2PAddress(), true,
|
||||
)
|
||||
defer cleanUp()
|
||||
|
||||
notifier := setUpNotifier(t, bitcoindConn)
|
||||
defer notifier.Stop()
|
||||
|
||||
syncNotifierWithMiner(t, notifier, miner)
|
||||
|
||||
// A transaction unknown to the node should not be found within the
|
||||
// txindex even if it is enabled, so we should not proceed with any
|
||||
// fallback methods.
|
||||
var zeroHash chainhash.Hash
|
||||
_, txStatus, err := notifier.historicalConfDetails(&zeroHash, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to retrieve historical conf details: %v", err)
|
||||
}
|
||||
|
||||
switch txStatus {
|
||||
case txNotFoundIndex:
|
||||
case txNotFoundManually:
|
||||
t.Fatal("should not have proceeded with fallback method, but did")
|
||||
default:
|
||||
t.Fatal("should not have found non-existent transaction, but did")
|
||||
}
|
||||
|
||||
// Now, we'll create a test transaction, confirm it, and attempt to
|
||||
// retrieve its confirmation details.
|
||||
txid, _, err := chainntnfs.GetTestTxidAndScript(miner)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tx: %v", err)
|
||||
}
|
||||
if err := chainntnfs.WaitForMempoolTx(miner, txid); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := miner.Node.Generate(1); err != nil {
|
||||
t.Fatalf("unable to generate block: %v", err)
|
||||
}
|
||||
|
||||
// Ensure the notifier and miner are synced to the same height to ensure
|
||||
// the txindex includes the transaction just mined.
|
||||
syncNotifierWithMiner(t, notifier, miner)
|
||||
|
||||
_, txStatus, err = notifier.historicalConfDetails(txid, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to retrieve historical conf details: %v", err)
|
||||
}
|
||||
|
||||
// Since the backend node's txindex is enabled and the transaction has
|
||||
// confirmed, we should be able to retrieve it using the txindex.
|
||||
switch txStatus {
|
||||
case txFoundIndex:
|
||||
default:
|
||||
t.Fatal("should have found the transaction within the " +
|
||||
"txindex, but did not")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHistoricalConfDetailsNoTxIndex ensures that we correctly retrieve
|
||||
// historical confirmation details using the set of fallback methods when the
|
||||
// backend node's txindex is disabled.
|
||||
func TestHistoricalConfDetailsNoTxIndex(t *testing.T) {
|
||||
miner, tearDown := chainntnfs.NewMiner(t, nil, true, 25)
|
||||
defer tearDown()
|
||||
|
||||
bitcoindConn, cleanUp := chainntnfs.NewBitcoindBackend(
|
||||
t, miner.P2PAddress(), false,
|
||||
)
|
||||
defer cleanUp()
|
||||
|
||||
notifier := setUpNotifier(t, bitcoindConn)
|
||||
defer notifier.Stop()
|
||||
|
||||
// Since the node has its txindex disabled, we fall back to scanning the
|
||||
// chain manually. A transaction unknown to the network should not be
|
||||
// found.
|
||||
var zeroHash chainhash.Hash
|
||||
broadcastHeight := syncNotifierWithMiner(t, notifier, miner)
|
||||
_, txStatus, err := notifier.historicalConfDetails(
|
||||
&zeroHash, uint32(broadcastHeight), uint32(broadcastHeight),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to retrieve historical conf details: %v", err)
|
||||
}
|
||||
|
||||
switch txStatus {
|
||||
case txNotFoundManually:
|
||||
case txNotFoundIndex:
|
||||
t.Fatal("should have proceeded with fallback method, but did not")
|
||||
default:
|
||||
t.Fatal("should not have found non-existent transaction, but did")
|
||||
}
|
||||
|
||||
// Now, we'll create a test transaction and attempt to retrieve its
|
||||
// confirmation details. In order to fall back to manually scanning the
|
||||
// chain, the transaction must be in the chain and not contain any
|
||||
// unspent outputs. To ensure this, we'll create a transaction with only
|
||||
// one output, which we will manually spend. The backend node's
|
||||
// transaction index should also be disabled, which we've already
|
||||
// ensured above.
|
||||
//
|
||||
// TODO(wilmer): add mempool case once trickle timeout can be specified
|
||||
// in btcd.
|
||||
output, pkScript := chainntnfs.CreateSpendableOutput(t, miner)
|
||||
spendTx := chainntnfs.CreateSpendTx(t, output, pkScript)
|
||||
spendTxHash, err := miner.Node.SendRawTransaction(spendTx, true)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to broadcast tx: %v", err)
|
||||
}
|
||||
if err := chainntnfs.WaitForMempoolTx(miner, spendTxHash); err != nil {
|
||||
t.Fatalf("tx not relayed to miner: %v", err)
|
||||
}
|
||||
if _, err := miner.Node.Generate(1); err != nil {
|
||||
t.Fatalf("unable to generate block: %v", err)
|
||||
}
|
||||
|
||||
// Ensure the notifier and miner are synced to the same height to ensure
|
||||
// we can find the transaction when manually scanning the chain.
|
||||
currentHeight := syncNotifierWithMiner(t, notifier, miner)
|
||||
_, txStatus, err = notifier.historicalConfDetails(
|
||||
&output.Hash, uint32(broadcastHeight), uint32(currentHeight),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to retrieve historical conf details: %v", err)
|
||||
}
|
||||
|
||||
// Since the backend node's txindex is disabled and the transaction has
|
||||
// confirmed, we should be able to find it by falling back to scanning
|
||||
// the chain manually.
|
||||
switch txStatus {
|
||||
case txFoundManually:
|
||||
default:
|
||||
t.Fatal("should have found the transaction by manually " +
|
||||
"scanning the chain, but did not")
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package btcdnotify
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@ -26,6 +27,31 @@ const (
|
||||
reorgSafetyLimit = 100
|
||||
)
|
||||
|
||||
// txConfStatus denotes the status of a transaction's lookup.
|
||||
type txConfStatus uint8
|
||||
|
||||
const (
|
||||
// txFoundMempool denotes that the transaction was found within the
|
||||
// backend node's mempool.
|
||||
txFoundMempool txConfStatus = iota
|
||||
|
||||
// txFoundIndex denotes that the transaction was found within the
|
||||
// backend node's txindex.
|
||||
txFoundIndex
|
||||
|
||||
// txFoundIndex denotes that the transaction was not found within the
|
||||
// backend node's txindex.
|
||||
txNotFoundIndex
|
||||
|
||||
// txFoundManually denotes that the transaction was found within the
|
||||
// chain by scanning for it manually.
|
||||
txFoundManually
|
||||
|
||||
// txFoundManually denotes that the transaction was not found within the
|
||||
// chain by scanning for it manually.
|
||||
txNotFoundManually
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrChainNotifierShuttingDown is used when we are trying to
|
||||
// measure a spend notification when notifier is already stopped.
|
||||
@ -337,7 +363,7 @@ out:
|
||||
go func() {
|
||||
defer b.wg.Done()
|
||||
|
||||
confDetails, err := b.historicalConfDetails(
|
||||
confDetails, _, err := b.historicalConfDetails(
|
||||
msg.TxID, msg.heightHint,
|
||||
bestHeight,
|
||||
)
|
||||
@ -516,23 +542,31 @@ out:
|
||||
// historicalConfDetails looks up whether a transaction is already included in a
|
||||
// block in the active chain and, if so, returns details about the confirmation.
|
||||
func (b *BtcdNotifier) historicalConfDetails(txid *chainhash.Hash,
|
||||
heightHint, currentHeight uint32) (*chainntnfs.TxConfirmation, error) {
|
||||
heightHint, currentHeight uint32) (*chainntnfs.TxConfirmation, txConfStatus, error) {
|
||||
|
||||
// First, we'll attempt to retrieve the transaction details using the
|
||||
// backend node's transaction index.
|
||||
txConf, err := b.confDetailsFromTxIndex(txid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// We'll first attempt to retrieve the transaction using the node's
|
||||
// txindex.
|
||||
txConf, txStatus, err := b.confDetailsFromTxIndex(txid)
|
||||
|
||||
// We'll then check the status of the transaction lookup returned to
|
||||
// determine whether we should proceed with any fallback methods.
|
||||
switch {
|
||||
// The transaction was found within the node's mempool.
|
||||
case txStatus == txFoundMempool:
|
||||
|
||||
// The transaction was found within the node's txindex.
|
||||
case txStatus == txFoundIndex:
|
||||
|
||||
// The transaction was not found within the node's mempool or txindex.
|
||||
case txStatus == txNotFoundIndex && err == nil:
|
||||
|
||||
// We failed to look up the transaction within the node's mempool or
|
||||
// txindex, so we'll proceed to scan the chain manually.
|
||||
default:
|
||||
return b.confDetailsManually(txid, heightHint, currentHeight)
|
||||
}
|
||||
|
||||
if txConf != nil {
|
||||
return txConf, nil
|
||||
}
|
||||
|
||||
// If the backend node's transaction index is not enabled, then we'll
|
||||
// fall back to manually scanning the chain's blocks, looking for the
|
||||
// block where the transaction was included in.
|
||||
return b.confDetailsManually(txid, heightHint, currentHeight)
|
||||
return txConf, txStatus, nil
|
||||
}
|
||||
|
||||
// confDetailsFromTxIndex looks up whether a transaction is already included
|
||||
@ -540,26 +574,33 @@ func (b *BtcdNotifier) historicalConfDetails(txid *chainhash.Hash,
|
||||
// If the transaction is found, its confirmation details are returned.
|
||||
// Otherwise, nil is returned.
|
||||
func (b *BtcdNotifier) confDetailsFromTxIndex(txid *chainhash.Hash,
|
||||
) (*chainntnfs.TxConfirmation, error) {
|
||||
) (*chainntnfs.TxConfirmation, txConfStatus, error) {
|
||||
|
||||
// If the transaction has some or all of its confirmations required,
|
||||
// then we may be able to dispatch it immediately.
|
||||
tx, err := b.chainConn.GetRawTransactionVerbose(txid)
|
||||
if err != nil {
|
||||
// Avoid returning an error if the transaction index is not
|
||||
// enabled to proceed with fallback methods.
|
||||
// If the transaction lookup was succesful, but it wasn't found
|
||||
// within the index itself, then we can exit early. We'll also
|
||||
// need to look at the error message returned as the error code
|
||||
// is used for multiple errors.
|
||||
txNotFoundErr := "No information available about transaction"
|
||||
jsonErr, ok := err.(*btcjson.RPCError)
|
||||
if !ok || jsonErr.Code != btcjson.ErrRPCNoTxInfo {
|
||||
return nil, fmt.Errorf("unable to query for txid "+
|
||||
"%v: %v", txid, err)
|
||||
if ok && jsonErr.Code == btcjson.ErrRPCNoTxInfo &&
|
||||
strings.Contains(jsonErr.Message, txNotFoundErr) {
|
||||
|
||||
return nil, txNotFoundIndex, nil
|
||||
}
|
||||
|
||||
return nil, txNotFoundIndex, fmt.Errorf("unable to query for "+
|
||||
"txid %v: %v", txid, err)
|
||||
}
|
||||
|
||||
// Make sure we actually retrieved a transaction that is included in a
|
||||
// block. Without this, we won't be able to retrieve its confirmation
|
||||
// details.
|
||||
if tx == nil || tx.BlockHash == "" {
|
||||
return nil, nil
|
||||
return nil, txFoundMempool, nil
|
||||
}
|
||||
|
||||
// As we need to fully populate the returned TxConfirmation struct,
|
||||
@ -567,14 +608,16 @@ func (b *BtcdNotifier) confDetailsFromTxIndex(txid *chainhash.Hash,
|
||||
// locate its exact index within the block.
|
||||
blockHash, err := chainhash.NewHashFromStr(tx.BlockHash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get block hash %v for "+
|
||||
"historical dispatch: %v", tx.BlockHash, err)
|
||||
return nil, txNotFoundIndex, fmt.Errorf("unable to get block "+
|
||||
"hash %v for historical dispatch: %v", tx.BlockHash,
|
||||
err)
|
||||
}
|
||||
|
||||
block, err := b.chainConn.GetBlockVerbose(blockHash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get block with hash %v for "+
|
||||
"historical dispatch: %v", blockHash, err)
|
||||
return nil, txNotFoundIndex, fmt.Errorf("unable to get block "+
|
||||
"with hash %v for historical dispatch: %v", blockHash,
|
||||
err)
|
||||
}
|
||||
|
||||
// If the block was obtained, locate the transaction's index within the
|
||||
@ -582,18 +625,19 @@ func (b *BtcdNotifier) confDetailsFromTxIndex(txid *chainhash.Hash,
|
||||
targetTxidStr := txid.String()
|
||||
for txIndex, txHash := range block.Tx {
|
||||
if txHash == targetTxidStr {
|
||||
return &chainntnfs.TxConfirmation{
|
||||
details := &chainntnfs.TxConfirmation{
|
||||
BlockHash: blockHash,
|
||||
BlockHeight: uint32(block.Height),
|
||||
TxIndex: uint32(txIndex),
|
||||
}, nil
|
||||
}
|
||||
return details, txFoundIndex, nil
|
||||
}
|
||||
}
|
||||
|
||||
// We return an error because we should have found the transaction
|
||||
// within the block, but didn't.
|
||||
return nil, fmt.Errorf("unable to locate tx %v in block %v", txid,
|
||||
blockHash)
|
||||
return nil, txNotFoundIndex, fmt.Errorf("unable to locate tx %v in "+
|
||||
"block %v", txid, blockHash)
|
||||
}
|
||||
|
||||
// confDetailsManually looks up whether a transaction is already included in a
|
||||
@ -601,8 +645,8 @@ func (b *BtcdNotifier) confDetailsFromTxIndex(txid *chainhash.Hash,
|
||||
// earliest height the transaction could have been included in, to the current
|
||||
// height in the chain. If the transaction is found, its confirmation details
|
||||
// are returned. Otherwise, nil is returned.
|
||||
func (b *BtcdNotifier) confDetailsManually(txid *chainhash.Hash,
|
||||
heightHint, currentHeight uint32) (*chainntnfs.TxConfirmation, error) {
|
||||
func (b *BtcdNotifier) confDetailsManually(txid *chainhash.Hash, heightHint,
|
||||
currentHeight uint32) (*chainntnfs.TxConfirmation, txConfStatus, error) {
|
||||
|
||||
targetTxidStr := txid.String()
|
||||
|
||||
@ -613,39 +657,40 @@ func (b *BtcdNotifier) confDetailsManually(txid *chainhash.Hash,
|
||||
// processing the next height.
|
||||
select {
|
||||
case <-b.quit:
|
||||
return nil, ErrChainNotifierShuttingDown
|
||||
return nil, txNotFoundManually, ErrChainNotifierShuttingDown
|
||||
default:
|
||||
}
|
||||
|
||||
blockHash, err := b.chainConn.GetBlockHash(int64(height))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get hash from block "+
|
||||
"with height %d", height)
|
||||
return nil, txNotFoundManually, fmt.Errorf("unable to "+
|
||||
"get hash from block with height %d", height)
|
||||
}
|
||||
|
||||
// TODO: fetch the neutrino filters instead.
|
||||
block, err := b.chainConn.GetBlockVerbose(blockHash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get block with hash "+
|
||||
"%v: %v", blockHash, err)
|
||||
return nil, txNotFoundManually, fmt.Errorf("unable to "+
|
||||
"get block with hash %v: %v", blockHash, err)
|
||||
}
|
||||
|
||||
for txIndex, txHash := range block.Tx {
|
||||
// If we're able to find the transaction in this block,
|
||||
// return its confirmation details.
|
||||
if txHash == targetTxidStr {
|
||||
return &chainntnfs.TxConfirmation{
|
||||
details := &chainntnfs.TxConfirmation{
|
||||
BlockHash: blockHash,
|
||||
BlockHeight: height,
|
||||
TxIndex: uint32(txIndex),
|
||||
}, nil
|
||||
}
|
||||
return details, txFoundManually, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here, then we were not able to find the transaction
|
||||
// within a block, so we avoid returning an error.
|
||||
return nil, nil
|
||||
return nil, txNotFoundManually, nil
|
||||
}
|
||||
|
||||
// handleBlockConnected applies a chain update for a new block. Any watched
|
||||
|
180
chainntnfs/btcdnotify/btcd_test.go
Normal file
180
chainntnfs/btcdnotify/btcd_test.go
Normal file
@ -0,0 +1,180 @@
|
||||
// +build debug
|
||||
|
||||
package btcdnotify
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/integration/rpctest"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
)
|
||||
|
||||
// setUpNotifier is a helper function to start a new notifier backed by a btcd
|
||||
// driver.
|
||||
func setUpNotifier(t *testing.T, h *rpctest.Harness) *BtcdNotifier {
|
||||
rpcCfg := h.RPCConfig()
|
||||
notifier, err := New(&rpcCfg)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create notifier: %v", err)
|
||||
}
|
||||
if err := notifier.Start(); err != nil {
|
||||
t.Fatalf("unable to start notifier: %v", err)
|
||||
}
|
||||
|
||||
return notifier
|
||||
}
|
||||
|
||||
// TestHistoricalConfDetailsTxIndex ensures that we correctly retrieve
|
||||
// historical confirmation details using the backend node's txindex.
|
||||
func TestHistoricalConfDetailsTxIndex(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
harness, tearDown := chainntnfs.NewMiner(
|
||||
t, []string{"--txindex"}, true, 25,
|
||||
)
|
||||
defer tearDown()
|
||||
|
||||
notifier := setUpNotifier(t, harness)
|
||||
defer notifier.Stop()
|
||||
|
||||
// A transaction unknown to the node should not be found within the
|
||||
// txindex even if it is enabled, so we should not proceed with any
|
||||
// fallback methods.
|
||||
var zeroHash chainhash.Hash
|
||||
_, txStatus, err := notifier.historicalConfDetails(&zeroHash, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to retrieve historical conf details: %v", err)
|
||||
}
|
||||
|
||||
switch txStatus {
|
||||
case txNotFoundIndex:
|
||||
case txNotFoundManually:
|
||||
t.Fatal("should not have proceeded with fallback method, but did")
|
||||
default:
|
||||
t.Fatal("should not have found non-existent transaction, but did")
|
||||
}
|
||||
|
||||
// Now, we'll create a test transaction and attempt to retrieve its
|
||||
// confirmation details.
|
||||
txid, _, err := chainntnfs.GetTestTxidAndScript(harness)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tx: %v", err)
|
||||
}
|
||||
if err := chainntnfs.WaitForMempoolTx(harness, txid); err != nil {
|
||||
t.Fatalf("unable to find tx in the mempool: %v", err)
|
||||
}
|
||||
|
||||
_, txStatus, err = notifier.historicalConfDetails(txid, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to retrieve historical conf details: %v", err)
|
||||
}
|
||||
|
||||
// Since it has yet to be included in a block, it should have been found
|
||||
// within the mempool.
|
||||
switch txStatus {
|
||||
case txFoundMempool:
|
||||
default:
|
||||
t.Fatal("should have found the transaction within the " +
|
||||
"mempool, but did not")
|
||||
}
|
||||
|
||||
// We'll now confirm this transaction and re-attempt to retrieve its
|
||||
// confirmation details.
|
||||
if _, err := harness.Node.Generate(1); err != nil {
|
||||
t.Fatalf("unable to generate block: %v", err)
|
||||
}
|
||||
|
||||
_, txStatus, err = notifier.historicalConfDetails(txid, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to retrieve historical conf details: %v", err)
|
||||
}
|
||||
|
||||
// Since the backend node's txindex is enabled and the transaction has
|
||||
// confirmed, we should be able to retrieve it using the txindex.
|
||||
switch txStatus {
|
||||
case txFoundIndex:
|
||||
default:
|
||||
t.Fatal("should have found the transaction within the " +
|
||||
"txindex, but did not")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHistoricalConfDetailsNoTxIndex ensures that we correctly retrieve
|
||||
// historical confirmation details using the set of fallback methods when the
|
||||
// backend node's txindex is disabled.
|
||||
func TestHistoricalConfDetailsNoTxIndex(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
harness, tearDown := chainntnfs.NewMiner(t, nil, true, 25)
|
||||
defer tearDown()
|
||||
|
||||
notifier := setUpNotifier(t, harness)
|
||||
defer notifier.Stop()
|
||||
|
||||
// Since the node has its txindex disabled, we fall back to scanning the
|
||||
// chain manually. A transaction unknown to the network should not be
|
||||
// found.
|
||||
var zeroHash chainhash.Hash
|
||||
_, txStatus, err := notifier.historicalConfDetails(&zeroHash, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to retrieve historical conf details: %v", err)
|
||||
}
|
||||
|
||||
switch txStatus {
|
||||
case txNotFoundManually:
|
||||
case txNotFoundIndex:
|
||||
t.Fatal("should have proceeded with fallback method, but did not")
|
||||
default:
|
||||
t.Fatal("should not have found non-existent transaction, but did")
|
||||
}
|
||||
|
||||
// Now, we'll create a test transaction and attempt to retrieve its
|
||||
// confirmation details. We'll note its broadcast height to use as the
|
||||
// height hint when manually scanning the chain.
|
||||
_, currentHeight, err := harness.Node.GetBestBlock()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to retrieve current height: %v", err)
|
||||
}
|
||||
|
||||
txid, _, err := chainntnfs.GetTestTxidAndScript(harness)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tx: %v", err)
|
||||
}
|
||||
if err := chainntnfs.WaitForMempoolTx(harness, txid); err != nil {
|
||||
t.Fatalf("unable to find tx in the mempool: %v", err)
|
||||
}
|
||||
|
||||
_, txStatus, err = notifier.historicalConfDetails(txid, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to retrieve historical conf details: %v", err)
|
||||
}
|
||||
|
||||
// Since it has yet to be included in a block, it should have been found
|
||||
// within the mempool.
|
||||
if txStatus != txFoundMempool {
|
||||
t.Fatal("should have found the transaction within the " +
|
||||
"mempool, but did not")
|
||||
}
|
||||
|
||||
// We'll now confirm this transaction and re-attempt to retrieve its
|
||||
// confirmation details.
|
||||
if _, err := harness.Node.Generate(1); err != nil {
|
||||
t.Fatalf("unable to generate block: %v", err)
|
||||
}
|
||||
|
||||
_, txStatus, err = notifier.historicalConfDetails(
|
||||
txid, uint32(currentHeight), uint32(currentHeight)+1,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to retrieve historical conf details: %v", err)
|
||||
}
|
||||
|
||||
// Since the backend node's txindex is disabled and the transaction has
|
||||
// confirmed, we should be able to find it by falling back to scanning
|
||||
// the chain manually.
|
||||
if txStatus != txFoundManually {
|
||||
t.Fatal("should have found the transaction by manually " +
|
||||
"scanning the chain, but did not")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user