diff --git a/chainntnfs/bitcoindnotify/bitcoind.go b/chainntnfs/bitcoindnotify/bitcoind.go index 134c591b..16addc42 100644 --- a/chainntnfs/bitcoindnotify/bitcoind.go +++ b/chainntnfs/bitcoindnotify/bitcoind.go @@ -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 diff --git a/chainntnfs/bitcoindnotify/bitcoind_test.go b/chainntnfs/bitcoindnotify/bitcoind_test.go new file mode 100644 index 00000000..c3f863ba --- /dev/null +++ b/chainntnfs/bitcoindnotify/bitcoind_test.go @@ -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") + } +} diff --git a/chainntnfs/btcdnotify/btcd.go b/chainntnfs/btcdnotify/btcd.go index 22febdd2..66c5747d 100644 --- a/chainntnfs/btcdnotify/btcd.go +++ b/chainntnfs/btcdnotify/btcd.go @@ -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 diff --git a/chainntnfs/btcdnotify/btcd_test.go b/chainntnfs/btcdnotify/btcd_test.go new file mode 100644 index 00000000..64b7fadd --- /dev/null +++ b/chainntnfs/btcdnotify/btcd_test.go @@ -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") + } +}