diff --git a/chainntnfs/bitcoindnotify/bitcoind.go b/chainntnfs/bitcoindnotify/bitcoind.go index b7e598fb..2c02167b 100644 --- a/chainntnfs/bitcoindnotify/bitcoind.go +++ b/chainntnfs/bitcoindnotify/bitcoind.go @@ -1,11 +1,8 @@ package bitcoindnotify import ( - "bytes" - "encoding/hex" "errors" "fmt" - "strings" "sync" "sync/atomic" @@ -458,7 +455,10 @@ func (b *BitcoindNotifier) historicalConfDetails(confRequest chainntnfs.ConfRequ // // We'll first attempt to retrieve the transaction using the node's // txindex. - txConf, txStatus, err := b.confDetailsFromTxIndex(&confRequest.TxID) + txNotFoundErr := "No such mempool or blockchain transaction" + txConf, txStatus, err := chainntnfs.ConfDetailsFromTxIndex( + b.chainConn, confRequest, txNotFoundErr, + ) // We'll then check the status of the transaction lookup returned to // determine whether we should proceed with any fallback methods. @@ -489,95 +489,6 @@ func (b *BitcoindNotifier) historicalConfDetails(confRequest chainntnfs.ConfRequ return txConf, txStatus, nil } -// confDetailsFromTxIndex looks up whether a transaction is already included in -// a block in the active chain by using the backend node's transaction index. -// If the transaction is found its TxConfStatus is returned. If it was found in -// the mempool this will be TxFoundMempool, if it is found in a block this will -// be TxFoundIndex. Otherwise TxNotFoundIndex is returned. If the tx is found -// in a block its confirmation details are also returned. -func (b *BitcoindNotifier) confDetailsFromTxIndex(txid *chainhash.Hash, -) (*chainntnfs.TxConfirmation, chainntnfs.TxConfStatus, error) { - - // If the transaction has some or all of its confirmations required, - // then we may be able to dispatch it immediately. - rawTxRes, err := b.chainConn.GetRawTransactionVerbose(txid) - if err != nil { - // If the transaction lookup was successful, 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 && - strings.Contains(jsonErr.Message, txNotFoundErr) { - - return nil, chainntnfs.TxNotFoundIndex, nil - } - - return nil, chainntnfs.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. If not, the transaction must be unconfirmed (in the mempool), - // and we'll return TxFoundMempool together with a nil TxConfirmation. - if rawTxRes.BlockHash == "" { - return nil, chainntnfs.TxFoundMempool, nil - } - - // As we need to fully populate the returned TxConfirmation struct, - // grab the block in which the transaction was confirmed so we can - // locate its exact index within the block. - blockHash, err := chainhash.NewHashFromStr(rawTxRes.BlockHash) - if err != nil { - return nil, chainntnfs.TxNotFoundIndex, - fmt.Errorf("unable to get block hash %v for "+ - "historical dispatch: %v", rawTxRes.BlockHash, err) - } - block, err := b.chainConn.GetBlockVerbose(blockHash) - if err != nil { - return nil, chainntnfs.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 - // block so we can give the subscriber full confirmation details. - txidStr := txid.String() - for txIndex, txHash := range block.Tx { - if txHash != txidStr { - continue - } - - // Deserialize the hex-encoded transaction to include it in the - // confirmation details. - rawTx, err := hex.DecodeString(rawTxRes.Hex) - if err != nil { - return nil, chainntnfs.TxFoundIndex, - fmt.Errorf("unable to deserialize tx %v: %v", - txHash, err) - } - var tx wire.MsgTx - if err := tx.Deserialize(bytes.NewReader(rawTx)); err != nil { - return nil, chainntnfs.TxFoundIndex, - fmt.Errorf("unable to deserialize tx %v: %v", - txHash, err) - } - - return &chainntnfs.TxConfirmation{ - Tx: &tx, - BlockHash: blockHash, - BlockHeight: uint32(block.Height), - TxIndex: uint32(txIndex), - }, chainntnfs.TxFoundIndex, nil - } - - // We return an error because we should have found the transaction - // within the block, but didn't. - return nil, chainntnfs.TxNotFoundIndex, fmt.Errorf("unable to locate "+ - "tx %v in block %v", txid, blockHash) -} - // confDetailsManually looks up whether a transaction/output script has already // been included in a block in the active chain by scanning the chain's blocks // within the given range. If the transaction/output script is found, its diff --git a/chainntnfs/btcdnotify/btcd.go b/chainntnfs/btcdnotify/btcd.go index 3de8566d..bfe42da0 100644 --- a/chainntnfs/btcdnotify/btcd.go +++ b/chainntnfs/btcdnotify/btcd.go @@ -1,11 +1,8 @@ package btcdnotify import ( - "bytes" - "encoding/hex" "errors" "fmt" - "strings" "sync" "sync/atomic" "time" @@ -480,7 +477,10 @@ func (b *BtcdNotifier) historicalConfDetails(confRequest chainntnfs.ConfRequest, // // We'll first attempt to retrieve the transaction using the node's // txindex. - txConf, txStatus, err := b.confDetailsFromTxIndex(&confRequest.TxID) + txNotFoundErr := "No information available about transaction" + txConf, txStatus, err := chainntnfs.ConfDetailsFromTxIndex( + b.chainConn, confRequest, txNotFoundErr, + ) // We'll then check the status of the transaction lookup returned to // determine whether we should proceed with any fallback methods. @@ -515,95 +515,6 @@ func (b *BtcdNotifier) historicalConfDetails(confRequest chainntnfs.ConfRequest, return txConf, txStatus, nil } -// confDetailsFromTxIndex looks up whether a transaction is already included in -// a block in the active chain by using the backend node's transaction index. -// If the transaction is found its TxConfStatus is returned. If it was found in -// the mempool this will be TxFoundMempool, if it is found in a block this will -// be TxFoundIndex. Otherwise TxNotFoundIndex is returned. If the tx is found -// in a block its confirmation details are also returned. -func (b *BtcdNotifier) confDetailsFromTxIndex(txid *chainhash.Hash, -) (*chainntnfs.TxConfirmation, chainntnfs.TxConfStatus, error) { - - // If the transaction has some or all of its confirmations required, - // then we may be able to dispatch it immediately. - rawTxRes, err := b.chainConn.GetRawTransactionVerbose(txid) - if err != nil { - // If the transaction lookup was successful, 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 && - strings.Contains(jsonErr.Message, txNotFoundErr) { - - return nil, chainntnfs.TxNotFoundIndex, nil - } - - return nil, chainntnfs.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. If not, the transaction must be unconfirmed (in the mempool), - // and we'll return TxFoundMempool together with a nil TxConfirmation. - if rawTxRes.BlockHash == "" { - return nil, chainntnfs.TxFoundMempool, nil - } - - // As we need to fully populate the returned TxConfirmation struct, - // grab the block in which the transaction was confirmed so we can - // locate its exact index within the block. - blockHash, err := chainhash.NewHashFromStr(rawTxRes.BlockHash) - if err != nil { - return nil, chainntnfs.TxNotFoundIndex, - fmt.Errorf("unable to get block hash %v for "+ - "historical dispatch: %v", rawTxRes.BlockHash, err) - } - block, err := b.chainConn.GetBlockVerbose(blockHash) - if err != nil { - return nil, chainntnfs.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 - // block so we can give the subscriber full confirmation details. - txidStr := txid.String() - for txIndex, txHash := range block.Tx { - if txHash != txidStr { - continue - } - - // Deserialize the hex-encoded transaction to include it in the - // confirmation details. - rawTx, err := hex.DecodeString(rawTxRes.Hex) - if err != nil { - return nil, chainntnfs.TxFoundIndex, - fmt.Errorf("unable to deserialize tx %v: %v", - txHash, err) - } - var tx wire.MsgTx - if err := tx.Deserialize(bytes.NewReader(rawTx)); err != nil { - return nil, chainntnfs.TxFoundIndex, - fmt.Errorf("unable to deserialize tx %v: %v", - txHash, err) - } - - return &chainntnfs.TxConfirmation{ - Tx: &tx, - BlockHash: blockHash, - BlockHeight: uint32(block.Height), - TxIndex: uint32(txIndex), - }, chainntnfs.TxFoundIndex, nil - } - - // We return an error because we should have found the transaction - // within the block, but didn't. - return nil, chainntnfs.TxNotFoundIndex, fmt.Errorf("unable to locate "+ - "tx %v in block %v", txid, blockHash) -} - // confDetailsManually looks up whether a transaction/output script has already // been included in a block in the active chain by scanning the chain's blocks // within the given range. If the transaction/output script is found, its diff --git a/chainntnfs/interface.go b/chainntnfs/interface.go index 97a62193..5e829633 100644 --- a/chainntnfs/interface.go +++ b/chainntnfs/interface.go @@ -1,8 +1,11 @@ package chainntnfs import ( + "bytes" + "encoding/hex" "errors" "fmt" + "strings" "sync" "github.com/btcsuite/btcd/btcjson" @@ -583,3 +586,111 @@ func getMissedBlocks(chainConn ChainConn, startingHeight, return missedBlocks, nil } + +// TxIndexConn abstracts an RPC backend with txindex enabled. +type TxIndexConn interface { + // GetRawTransactionVerbose returns the transaction identified by the + // passed chain hash, and returns additional information such as the + // block that the transaction confirmed. + GetRawTransactionVerbose(*chainhash.Hash) (*btcjson.TxRawResult, error) + + // GetBlockVerbose returns the block identified by the chain hash along + // with additional information such as the block's height in the chain. + GetBlockVerbose(*chainhash.Hash) (*btcjson.GetBlockVerboseResult, error) +} + +// ConfDetailsFromTxIndex looks up whether a transaction is already included in +// a block in the active chain by using the backend node's transaction index. +// If the transaction is found its TxConfStatus is returned. If it was found in +// the mempool this will be TxFoundMempool, if it is found in a block this will +// be TxFoundIndex. Otherwise TxNotFoundIndex is returned. If the tx is found +// in a block its confirmation details are also returned. +func ConfDetailsFromTxIndex(chainConn TxIndexConn, r ConfRequest, + txNotFoundErr string) (*TxConfirmation, TxConfStatus, error) { + + // If the transaction has some or all of its confirmations required, + // then we may be able to dispatch it immediately. + rawTxRes, err := chainConn.GetRawTransactionVerbose(&r.TxID) + if err != nil { + // If the transaction lookup was successful, 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. + jsonErr, ok := err.(*btcjson.RPCError) + 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", + r.TxID, err) + } + + // Deserialize the hex-encoded transaction to include it in the + // confirmation details. + rawTx, err := hex.DecodeString(rawTxRes.Hex) + if err != nil { + return nil, TxNotFoundIndex, + fmt.Errorf("unable to deserialize tx %v: %v", + r.TxID, err) + } + var tx wire.MsgTx + if err := tx.Deserialize(bytes.NewReader(rawTx)); err != nil { + return nil, TxNotFoundIndex, + fmt.Errorf("unable to deserialize tx %v: %v", + r.TxID, err) + } + + // Ensure the transaction matches our confirmation request in terms of + // txid and pkscript. + if !r.MatchesTx(&tx) { + return nil, TxNotFoundIndex, + fmt.Errorf("unable to locate tx %v", r.TxID) + } + + // Make sure we actually retrieved a transaction that is included in a + // block. If not, the transaction must be unconfirmed (in the mempool), + // and we'll return TxFoundMempool together with a nil TxConfirmation. + if rawTxRes.BlockHash == "" { + return nil, TxFoundMempool, nil + } + + // As we need to fully populate the returned TxConfirmation struct, + // grab the block in which the transaction was confirmed so we can + // locate its exact index within the block. + blockHash, err := chainhash.NewHashFromStr(rawTxRes.BlockHash) + if err != nil { + return nil, TxNotFoundIndex, + fmt.Errorf("unable to get block hash %v for "+ + "historical dispatch: %v", rawTxRes.BlockHash, err) + } + block, err := chainConn.GetBlockVerbose(blockHash) + if err != nil { + 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 + // block so we can give the subscriber full confirmation details. + txidStr := r.TxID.String() + for txIndex, txHash := range block.Tx { + if txHash != txidStr { + continue + } + + return &TxConfirmation{ + Tx: &tx, + BlockHash: blockHash, + BlockHeight: uint32(block.Height), + TxIndex: uint32(txIndex), + }, TxFoundIndex, nil + } + + // We return an error because we should have found the transaction + // within the block, but didn't. + return nil, TxNotFoundIndex, fmt.Errorf("unable to locate "+ + "tx %v in block %v", r.TxID, blockHash) +} diff --git a/chainntnfs/interface_test.go b/chainntnfs/interface_test.go index dec608a1..1c2676da 100644 --- a/chainntnfs/interface_test.go +++ b/chainntnfs/interface_test.go @@ -660,12 +660,29 @@ func testTxConfirmedBeforeNtfnRegistration(miner *rpctest.Harness, t.Fatalf("unable to register ntfn: %v", err) } + // We'll also register for a confirmation notification with the pkscript + // of a different transaction. This notification shouldn't fire since we + // match on both txid and pkscript. + var ntfn4 *chainntnfs.ConfirmationEvent + ntfn4, err = notifier.RegisterConfirmationsNtfn( + txid3, pkScript2, 1, uint32(currentHeight-1), + ) + if err != nil { + t.Fatalf("unable to register ntfn: %v", err) + } + select { case <-ntfn3.Confirmed: case <-time.After(10 * time.Second): t.Fatalf("confirmation notification never received") } + select { + case <-ntfn4.Confirmed: + t.Fatalf("confirmation notification received") + case <-time.After(5 * time.Second): + } + time.Sleep(1 * time.Second) select { diff --git a/chainntnfs/txnotifier.go b/chainntnfs/txnotifier.go index c0e88e3a..0cc6948b 100644 --- a/chainntnfs/txnotifier.go +++ b/chainntnfs/txnotifier.go @@ -192,18 +192,22 @@ func (r ConfRequest) ConfHintKey() ([]byte, error) { // the outputs of the transaction to determine if it matches. Otherwise, we'll // match on the txid. func (r ConfRequest) MatchesTx(tx *wire.MsgTx) bool { - if r.TxID != ZeroHash { - return r.TxID == tx.TxHash() - } - - pkScript := r.PkScript.Script() - for _, txOut := range tx.TxOut { - if bytes.Equal(txOut.PkScript, pkScript) { - return true + scriptMatches := func() bool { + pkScript := r.PkScript.Script() + for _, txOut := range tx.TxOut { + if bytes.Equal(txOut.PkScript, pkScript) { + return true + } } + + return false } - return false + if r.TxID != ZeroHash { + return r.TxID == tx.TxHash() && scriptMatches() + } + + return scriptMatches() } // ConfNtfn represents a notifier client's request to receive a notification