Merge pull request #3254 from cfromknecht/stricter-confirmation-matching
chainntnfs: stricter historical confirmation matching
This commit is contained in:
commit
b60e6d6f53
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user