lnd.xprv/chainntnfs/txnotifier_test.go
Wilmer Paulino a0d3b7d9e3
chainntnfs: support caching confirm/spend hints for scripts
In this commit, we refactor the HeightHintCache and its underlying
interfaces to be able to manipulate hints for ConfRequests and
SpendRequests. By doing so, we'll be able to manipulate hints for
scripts if the request includes either a zero hash or a zero outpoint.
2019-01-21 13:57:43 -08:00

2026 lines
61 KiB
Go

package chainntnfs_test
import (
"sync"
"testing"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/chainntnfs"
)
var (
zeroHash chainhash.Hash
zeroOutPoint wire.OutPoint
)
type mockHintCache struct {
mu sync.Mutex
confHints map[chainntnfs.ConfRequest]uint32
spendHints map[chainntnfs.SpendRequest]uint32
}
var _ chainntnfs.SpendHintCache = (*mockHintCache)(nil)
var _ chainntnfs.ConfirmHintCache = (*mockHintCache)(nil)
func (c *mockHintCache) CommitSpendHint(heightHint uint32,
spendRequests ...chainntnfs.SpendRequest) error {
c.mu.Lock()
defer c.mu.Unlock()
for _, spendRequest := range spendRequests {
c.spendHints[spendRequest] = heightHint
}
return nil
}
func (c *mockHintCache) QuerySpendHint(spendRequest chainntnfs.SpendRequest) (uint32, error) {
c.mu.Lock()
defer c.mu.Unlock()
hint, ok := c.spendHints[spendRequest]
if !ok {
return 0, chainntnfs.ErrSpendHintNotFound
}
return hint, nil
}
func (c *mockHintCache) PurgeSpendHint(spendRequests ...chainntnfs.SpendRequest) error {
c.mu.Lock()
defer c.mu.Unlock()
for _, spendRequest := range spendRequests {
delete(c.spendHints, spendRequest)
}
return nil
}
func (c *mockHintCache) CommitConfirmHint(heightHint uint32,
confRequests ...chainntnfs.ConfRequest) error {
c.mu.Lock()
defer c.mu.Unlock()
for _, confRequest := range confRequests {
c.confHints[confRequest] = heightHint
}
return nil
}
func (c *mockHintCache) QueryConfirmHint(confRequest chainntnfs.ConfRequest) (uint32, error) {
c.mu.Lock()
defer c.mu.Unlock()
hint, ok := c.confHints[confRequest]
if !ok {
return 0, chainntnfs.ErrConfirmHintNotFound
}
return hint, nil
}
func (c *mockHintCache) PurgeConfirmHint(confRequests ...chainntnfs.ConfRequest) error {
c.mu.Lock()
defer c.mu.Unlock()
for _, confRequest := range confRequests {
delete(c.confHints, confRequest)
}
return nil
}
func newMockHintCache() *mockHintCache {
return &mockHintCache{
confHints: make(map[chainntnfs.ConfRequest]uint32),
spendHints: make(map[chainntnfs.SpendRequest]uint32),
}
}
// TestTxNotifierMaxConfs ensures that we are not able to register for more
// confirmations on a transaction than the maximum supported.
func TestTxNotifierMaxConfs(t *testing.T) {
t.Parallel()
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
10, chainntnfs.ReorgSafetyLimit, hintCache, hintCache,
)
// Registering one confirmation above the maximum should fail with
// ErrTxMaxConfs.
ntfn := &chainntnfs.ConfNtfn{
ConfID: 1,
TxID: &zeroHash,
NumConfirmations: chainntnfs.MaxNumConfs + 1,
Event: chainntnfs.NewConfirmationEvent(
chainntnfs.MaxNumConfs,
),
}
if _, err := n.RegisterConf(ntfn); err != chainntnfs.ErrTxMaxConfs {
t.Fatalf("expected chainntnfs.ErrTxMaxConfs, got %v", err)
}
ntfn.NumConfirmations--
if _, err := n.RegisterConf(ntfn); err != nil {
t.Fatalf("unable to register conf ntfn: %v", err)
}
}
// TestTxNotifierFutureConfDispatch tests that the TxNotifier dispatches
// registered notifications when a transaction confirms after registration.
func TestTxNotifierFutureConfDispatch(t *testing.T) {
t.Parallel()
const (
tx1NumConfs uint32 = 1
tx2NumConfs uint32 = 2
)
var (
tx1 = wire.MsgTx{Version: 1}
tx2 = wire.MsgTx{Version: 2}
tx3 = wire.MsgTx{Version: 3}
)
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
10, chainntnfs.ReorgSafetyLimit, hintCache, hintCache,
)
// Create the test transactions and register them with the TxNotifier
// before including them in a block to receive future
// notifications.
tx1Hash := tx1.TxHash()
ntfn1 := chainntnfs.ConfNtfn{
TxID: &tx1Hash,
NumConfirmations: tx1NumConfs,
Event: chainntnfs.NewConfirmationEvent(tx1NumConfs),
}
if _, err := n.RegisterConf(&ntfn1); err != nil {
t.Fatalf("unable to register ntfn: %v", err)
}
tx2Hash := tx2.TxHash()
ntfn2 := chainntnfs.ConfNtfn{
TxID: &tx2Hash,
NumConfirmations: tx2NumConfs,
Event: chainntnfs.NewConfirmationEvent(tx2NumConfs),
}
if _, err := n.RegisterConf(&ntfn2); err != nil {
t.Fatalf("unable to register ntfn: %v", err)
}
// We should not receive any notifications from both transactions
// since they have not been included in a block yet.
select {
case <-ntfn1.Event.Updates:
t.Fatal("Received unexpected confirmation update for tx1")
case txConf := <-ntfn1.Event.Confirmed:
t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
default:
}
select {
case <-ntfn2.Event.Updates:
t.Fatal("Received unexpected confirmation update for tx2")
case txConf := <-ntfn2.Event.Confirmed:
t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
default:
}
// Include the transactions in a block and add it to the TxNotifier.
// This should confirm tx1, but not tx2.
block1 := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{&tx1, &tx2, &tx3},
})
err := n.ConnectTip(block1.Hash(), 11, block1.Transactions())
if err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(11); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// We should only receive one update for tx1 since it only requires
// one confirmation and it already met it.
select {
case numConfsLeft := <-ntfn1.Event.Updates:
const expected = 0
if numConfsLeft != expected {
t.Fatalf("Received incorrect confirmation update: tx1 "+
"expected %d confirmations left, got %d",
expected, numConfsLeft)
}
default:
t.Fatal("Expected confirmation update for tx1")
}
// A confirmation notification for this tranaction should be dispatched,
// as it only required one confirmation.
select {
case txConf := <-ntfn1.Event.Confirmed:
expectedConf := chainntnfs.TxConfirmation{
BlockHash: block1.Hash(),
BlockHeight: 11,
TxIndex: 0,
}
assertConfDetails(t, txConf, &expectedConf)
default:
t.Fatalf("Expected confirmation for tx1")
}
// We should only receive one update for tx2 since it only has one
// confirmation so far and it requires two.
select {
case numConfsLeft := <-ntfn2.Event.Updates:
const expected = 1
if numConfsLeft != expected {
t.Fatalf("Received incorrect confirmation update: tx2 "+
"expected %d confirmations left, got %d",
expected, numConfsLeft)
}
default:
t.Fatal("Expected confirmation update for tx2")
}
// A confirmation notification for tx2 should not be dispatched yet, as
// it requires one more confirmation.
select {
case txConf := <-ntfn2.Event.Confirmed:
t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
default:
}
// Create a new block and add it to the TxNotifier at the next height.
// This should confirm tx2.
block2 := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{&tx3},
})
err = n.ConnectTip(block2.Hash(), 12, block2.Transactions())
if err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(12); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// We should not receive any event notifications for tx1 since it has
// already been confirmed.
select {
case <-ntfn1.Event.Updates:
t.Fatal("Received unexpected confirmation update for tx1")
case txConf := <-ntfn1.Event.Confirmed:
t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
default:
}
// We should only receive one update since the last at the new height,
// indicating how many confirmations are still left.
select {
case numConfsLeft := <-ntfn2.Event.Updates:
const expected = 0
if numConfsLeft != expected {
t.Fatalf("Received incorrect confirmation update: tx2 "+
"expected %d confirmations left, got %d",
expected, numConfsLeft)
}
default:
t.Fatal("Expected confirmation update for tx2")
}
// A confirmation notification for tx2 should be dispatched, since it
// now meets its required number of confirmations.
select {
case txConf := <-ntfn2.Event.Confirmed:
expectedConf := chainntnfs.TxConfirmation{
BlockHash: block1.Hash(),
BlockHeight: 11,
TxIndex: 1,
}
assertConfDetails(t, txConf, &expectedConf)
default:
t.Fatalf("Expected confirmation for tx2")
}
}
// TestTxNotifierHistoricalConfDispatch tests that the TxNotifier dispatches
// registered notifications when the transaction is confirmed before
// registration.
func TestTxNotifierHistoricalConfDispatch(t *testing.T) {
t.Parallel()
const (
tx1NumConfs uint32 = 1
tx2NumConfs uint32 = 3
)
var (
tx1 = wire.MsgTx{Version: 1}
tx2 = wire.MsgTx{Version: 2}
tx3 = wire.MsgTx{Version: 3}
)
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
10, chainntnfs.ReorgSafetyLimit, hintCache, hintCache,
)
// Create the test transactions at a height before the TxNotifier's
// starting height so that they are confirmed once registering them.
tx1Hash := tx1.TxHash()
ntfn1 := chainntnfs.ConfNtfn{
ConfID: 0,
TxID: &tx1Hash,
NumConfirmations: tx1NumConfs,
Event: chainntnfs.NewConfirmationEvent(tx1NumConfs),
}
if _, err := n.RegisterConf(&ntfn1); err != nil {
t.Fatalf("unable to register ntfn: %v", err)
}
tx2Hash := tx2.TxHash()
ntfn2 := chainntnfs.ConfNtfn{
ConfID: 1,
TxID: &tx2Hash,
NumConfirmations: tx2NumConfs,
Event: chainntnfs.NewConfirmationEvent(tx2NumConfs),
}
if _, err := n.RegisterConf(&ntfn2); err != nil {
t.Fatalf("unable to register ntfn: %v", err)
}
// Update tx1 with its confirmation details. We should only receive one
// update since it only requires one confirmation and it already met it.
txConf1 := chainntnfs.TxConfirmation{
BlockHash: &zeroHash,
BlockHeight: 9,
TxIndex: 1,
}
err := n.UpdateConfDetails(tx1Hash, &txConf1)
if err != nil {
t.Fatalf("unable to update conf details: %v", err)
}
select {
case numConfsLeft := <-ntfn1.Event.Updates:
const expected = 0
if numConfsLeft != expected {
t.Fatalf("Received incorrect confirmation update: tx1 "+
"expected %d confirmations left, got %d",
expected, numConfsLeft)
}
default:
t.Fatal("Expected confirmation update for tx1")
}
// A confirmation notification for tx1 should also be dispatched.
select {
case txConf := <-ntfn1.Event.Confirmed:
assertConfDetails(t, txConf, &txConf1)
default:
t.Fatalf("Expected confirmation for tx1")
}
// Update tx2 with its confirmation details. This should not trigger a
// confirmation notification since it hasn't reached its required number
// of confirmations, but we should receive a confirmation update
// indicating how many confirmation are left.
txConf2 := chainntnfs.TxConfirmation{
BlockHash: &zeroHash,
BlockHeight: 9,
TxIndex: 2,
}
err = n.UpdateConfDetails(tx2Hash, &txConf2)
if err != nil {
t.Fatalf("unable to update conf details: %v", err)
}
select {
case numConfsLeft := <-ntfn2.Event.Updates:
const expected = 1
if numConfsLeft != expected {
t.Fatalf("Received incorrect confirmation update: tx2 "+
"expected %d confirmations left, got %d",
expected, numConfsLeft)
}
default:
t.Fatal("Expected confirmation update for tx2")
}
select {
case txConf := <-ntfn2.Event.Confirmed:
t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
default:
}
// Create a new block and add it to the TxNotifier at the next height.
// This should confirm tx2.
block := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{&tx3},
})
err = n.ConnectTip(block.Hash(), 11, block.Transactions())
if err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(11); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// We should not receive any event notifications for tx1 since it has
// already been confirmed.
select {
case <-ntfn1.Event.Updates:
t.Fatal("Received unexpected confirmation update for tx1")
case txConf := <-ntfn1.Event.Confirmed:
t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
default:
}
// We should only receive one update for tx2 since the last one,
// indicating how many confirmations are still left.
select {
case numConfsLeft := <-ntfn2.Event.Updates:
const expected = 0
if numConfsLeft != expected {
t.Fatalf("Received incorrect confirmation update: tx2 "+
"expected %d confirmations left, got %d",
expected, numConfsLeft)
}
default:
t.Fatal("Expected confirmation update for tx2")
}
// A confirmation notification for tx2 should be dispatched, as it met
// its required number of confirmations.
select {
case txConf := <-ntfn2.Event.Confirmed:
assertConfDetails(t, txConf, &txConf2)
default:
t.Fatalf("Expected confirmation for tx2")
}
}
// TestTxNotifierFutureSpendDispatch tests that the TxNotifier dispatches
// registered notifications when an outpoint is spent after registration.
func TestTxNotifierFutureSpendDispatch(t *testing.T) {
t.Parallel()
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
10, chainntnfs.ReorgSafetyLimit, hintCache, hintCache,
)
// We'll start off by registering for a spend notification of an
// outpoint.
ntfn := &chainntnfs.SpendNtfn{
OutPoint: zeroOutPoint,
Event: chainntnfs.NewSpendEvent(nil),
}
if _, err := n.RegisterSpend(ntfn); err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
// We should not receive a notification as the outpoint has not been
// spent yet.
select {
case <-ntfn.Event.Spend:
t.Fatal("received unexpected spend notification")
default:
}
// Construct the details of the spending transaction of the outpoint
// above. We'll include it in the next block, which should trigger a
// spend notification.
spendTx := wire.NewMsgTx(2)
spendTx.AddTxIn(&wire.TxIn{PreviousOutPoint: zeroOutPoint})
spendTxHash := spendTx.TxHash()
block := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{spendTx},
})
err := n.ConnectTip(block.Hash(), 11, block.Transactions())
if err != nil {
t.Fatalf("unable to connect block: %v", err)
}
if err := n.NotifyHeight(11); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
expectedSpendDetails := &chainntnfs.SpendDetail{
SpentOutPoint: &ntfn.OutPoint,
SpenderTxHash: &spendTxHash,
SpendingTx: spendTx,
SpenderInputIndex: 0,
SpendingHeight: 11,
}
// Ensure that the details of the notification match as expected.
select {
case spendDetails := <-ntfn.Event.Spend:
assertSpendDetails(t, spendDetails, expectedSpendDetails)
default:
t.Fatal("expected to receive spend details")
}
// Finally, we'll ensure that if the spending transaction has also been
// spent, then we don't receive another spend notification.
prevOut := wire.OutPoint{Hash: spendTxHash, Index: 0}
spendOfSpend := wire.NewMsgTx(2)
spendOfSpend.AddTxIn(&wire.TxIn{PreviousOutPoint: prevOut})
block = btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{spendOfSpend},
})
err = n.ConnectTip(block.Hash(), 12, block.Transactions())
if err != nil {
t.Fatalf("unable to connect block: %v", err)
}
if err := n.NotifyHeight(12); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
select {
case <-ntfn.Event.Spend:
t.Fatal("received unexpected spend notification")
default:
}
}
// TestTxNotifierHistoricalSpendDispatch tests that the TxNotifier dispatches
// registered notifications when an outpoint is spent before registration.
func TestTxNotifierHistoricalSpendDispatch(t *testing.T) {
t.Parallel()
const startingHeight = 10
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
startingHeight, chainntnfs.ReorgSafetyLimit, hintCache,
hintCache,
)
// We'll start by constructing the spending details of the outpoint
// below.
spentOutpoint := zeroOutPoint
spendTx := wire.NewMsgTx(2)
spendTx.AddTxIn(&wire.TxIn{PreviousOutPoint: zeroOutPoint})
spendTxHash := spendTx.TxHash()
expectedSpendDetails := &chainntnfs.SpendDetail{
SpentOutPoint: &spentOutpoint,
SpenderTxHash: &spendTxHash,
SpendingTx: spendTx,
SpenderInputIndex: 0,
SpendingHeight: startingHeight - 1,
}
// We'll register for a spend notification of the outpoint and ensure
// that a notification isn't dispatched.
ntfn := &chainntnfs.SpendNtfn{
OutPoint: spentOutpoint,
Event: chainntnfs.NewSpendEvent(nil),
}
if _, err := n.RegisterSpend(ntfn); err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
select {
case <-ntfn.Event.Spend:
t.Fatal("received unexpected spend notification")
default:
}
// Because we're interested in testing the case of a historical spend,
// we'll hand off the spending details of the outpoint to the notifier
// as it is not possible for it to view historical events in the chain.
// By doing this, we replicate the functionality of the ChainNotifier.
err := n.UpdateSpendDetails(ntfn.OutPoint, expectedSpendDetails)
if err != nil {
t.Fatalf("unable to update spend details: %v", err)
}
// Now that we have the spending details, we should receive a spend
// notification. We'll ensure that the details match as intended.
select {
case spendDetails := <-ntfn.Event.Spend:
assertSpendDetails(t, spendDetails, expectedSpendDetails)
default:
t.Fatalf("expected to receive spend details")
}
// Finally, we'll ensure that if the spending transaction has also been
// spent, then we don't receive another spend notification.
prevOut := wire.OutPoint{Hash: spendTxHash, Index: 0}
spendOfSpend := wire.NewMsgTx(2)
spendOfSpend.AddTxIn(&wire.TxIn{PreviousOutPoint: prevOut})
block := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{spendOfSpend},
})
err = n.ConnectTip(block.Hash(), startingHeight+1, block.Transactions())
if err != nil {
t.Fatalf("unable to connect block: %v", err)
}
if err := n.NotifyHeight(startingHeight + 1); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
select {
case <-ntfn.Event.Spend:
t.Fatal("received unexpected spend notification")
default:
}
}
// TestTxNotifierMultipleHistoricalRescans ensures that we don't attempt to
// request multiple historical confirmation rescans per transactions.
func TestTxNotifierMultipleHistoricalConfRescans(t *testing.T) {
t.Parallel()
const startingHeight = 10
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
startingHeight, chainntnfs.ReorgSafetyLimit, hintCache,
hintCache,
)
// The first registration for a transaction in the notifier should
// request a historical confirmation rescan as it does not have a
// historical view of the chain.
confNtfn1 := &chainntnfs.ConfNtfn{
ConfID: 0,
TxID: &zeroHash,
Event: chainntnfs.NewConfirmationEvent(1),
}
historicalConfDispatch1, err := n.RegisterConf(confNtfn1)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
if historicalConfDispatch1 == nil {
t.Fatal("expected to receive historical dispatch request")
}
// We'll register another confirmation notification for the same
// transaction. This should not request a historical confirmation rescan
// since the first one is still pending.
confNtfn2 := &chainntnfs.ConfNtfn{
ConfID: 1,
TxID: &zeroHash,
Event: chainntnfs.NewConfirmationEvent(1),
}
historicalConfDispatch2, err := n.RegisterConf(confNtfn2)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
if historicalConfDispatch2 != nil {
t.Fatal("received unexpected historical rescan request")
}
// Finally, we'll mark the ongoing historical rescan as complete and
// register another notification. We should also expect not to see a
// historical rescan request since the confirmation details should be
// cached.
confDetails := &chainntnfs.TxConfirmation{
BlockHeight: startingHeight - 1,
}
if err := n.UpdateConfDetails(*confNtfn2.TxID, confDetails); err != nil {
t.Fatalf("unable to update conf details: %v", err)
}
confNtfn3 := &chainntnfs.ConfNtfn{
ConfID: 2,
TxID: &zeroHash,
Event: chainntnfs.NewConfirmationEvent(1),
}
historicalConfDispatch3, err := n.RegisterConf(confNtfn3)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
if historicalConfDispatch3 != nil {
t.Fatal("received unexpected historical rescan request")
}
}
// TestTxNotifierMultipleHistoricalRescans ensures that we don't attempt to
// request multiple historical spend rescans per outpoints.
func TestTxNotifierMultipleHistoricalSpendRescans(t *testing.T) {
t.Parallel()
const startingHeight = 10
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
startingHeight, chainntnfs.ReorgSafetyLimit, hintCache,
hintCache,
)
// The first registration for an outpoint in the notifier should request
// a historical spend rescan as it does not have a historical view of
// the chain.
ntfn1 := &chainntnfs.SpendNtfn{
SpendID: 0,
OutPoint: zeroOutPoint,
Event: chainntnfs.NewSpendEvent(nil),
}
historicalDispatch1, err := n.RegisterSpend(ntfn1)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
if historicalDispatch1 == nil {
t.Fatal("expected to receive historical dispatch request")
}
// We'll register another spend notification for the same outpoint. This
// should not request a historical spend rescan since the first one is
// still pending.
ntfn2 := &chainntnfs.SpendNtfn{
SpendID: 1,
OutPoint: zeroOutPoint,
Event: chainntnfs.NewSpendEvent(nil),
}
historicalDispatch2, err := n.RegisterSpend(ntfn2)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
if historicalDispatch2 != nil {
t.Fatal("received unexpected historical rescan request")
}
// Finally, we'll mark the ongoing historical rescan as complete and
// register another notification. We should also expect not to see a
// historical rescan request since the confirmation details should be
// cached.
spendDetails := &chainntnfs.SpendDetail{
SpentOutPoint: &ntfn2.OutPoint,
SpenderTxHash: &zeroHash,
SpendingTx: wire.NewMsgTx(2),
SpenderInputIndex: 0,
SpendingHeight: startingHeight - 1,
}
err = n.UpdateSpendDetails(ntfn2.OutPoint, spendDetails)
if err != nil {
t.Fatalf("unable to update spend details: %v", err)
}
ntfn3 := &chainntnfs.SpendNtfn{
SpendID: 2,
OutPoint: zeroOutPoint,
Event: chainntnfs.NewSpendEvent(nil),
}
historicalDispatch3, err := n.RegisterSpend(ntfn3)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
if historicalDispatch3 != nil {
t.Fatal("received unexpected historical rescan request")
}
}
// TestTxNotifierMultipleHistoricalNtfns ensures that the TxNotifier will only
// request one rescan for a transaction/outpoint when having multiple client
// registrations. Once the rescan has completed and retrieved the
// confirmation/spend details, a notification should be dispatched to _all_
// clients.
func TestTxNotifierMultipleHistoricalNtfns(t *testing.T) {
t.Parallel()
const (
numNtfns = 5
startingHeight = 10
)
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
startingHeight, chainntnfs.ReorgSafetyLimit, hintCache,
hintCache,
)
// We'll start off by registered 5 clients for a confirmation
// notification on the same transaction.
confNtfns := make([]*chainntnfs.ConfNtfn, numNtfns)
for i := uint64(0); i < numNtfns; i++ {
confNtfns[i] = &chainntnfs.ConfNtfn{
ConfID: i,
TxID: &zeroHash,
Event: chainntnfs.NewConfirmationEvent(1),
}
if _, err := n.RegisterConf(confNtfns[i]); err != nil {
t.Fatalf("unable to register conf ntfn #%d: %v", i, err)
}
}
// Ensure none of them have received the confirmation details.
for i, ntfn := range confNtfns {
select {
case <-ntfn.Event.Confirmed:
t.Fatalf("request #%d received unexpected confirmation "+
"notification", i)
default:
}
}
// We'll assume a historical rescan was dispatched and found the
// following confirmation details. We'll let the notifier know so that
// it can stop watching at tip.
expectedConfDetails := &chainntnfs.TxConfirmation{
BlockHeight: startingHeight - 1,
}
err := n.UpdateConfDetails(*confNtfns[0].TxID, expectedConfDetails)
if err != nil {
t.Fatalf("unable to update conf details: %v", err)
}
// With the confirmation details retrieved, each client should now have
// been notified of the confirmation.
for i, ntfn := range confNtfns {
select {
case confDetails := <-ntfn.Event.Confirmed:
assertConfDetails(t, confDetails, expectedConfDetails)
default:
t.Fatalf("request #%d expected to received "+
"confirmation notification", i)
}
}
// In order to ensure that the confirmation details are properly cached,
// we'll register another client for the same transaction. We should not
// see a historical rescan request and the confirmation notification
// should come through immediately.
extraConfNtfn := &chainntnfs.ConfNtfn{
ConfID: numNtfns + 1,
TxID: &zeroHash,
Event: chainntnfs.NewConfirmationEvent(1),
}
historicalConfRescan, err := n.RegisterConf(extraConfNtfn)
if err != nil {
t.Fatalf("unable to register conf ntfn: %v", err)
}
if historicalConfRescan != nil {
t.Fatal("received unexpected historical rescan request")
}
select {
case confDetails := <-extraConfNtfn.Event.Confirmed:
assertConfDetails(t, confDetails, expectedConfDetails)
default:
t.Fatal("expected to receive spend notification")
}
// Similarly, we'll do the same thing but for spend notifications.
spendNtfns := make([]*chainntnfs.SpendNtfn, numNtfns)
for i := uint64(0); i < numNtfns; i++ {
spendNtfns[i] = &chainntnfs.SpendNtfn{
SpendID: i,
OutPoint: zeroOutPoint,
Event: chainntnfs.NewSpendEvent(nil),
}
if _, err := n.RegisterSpend(spendNtfns[i]); err != nil {
t.Fatalf("unable to register spend ntfn #%d: %v", i, err)
}
}
// Ensure none of them have received the spend details.
for i, ntfn := range spendNtfns {
select {
case <-ntfn.Event.Spend:
t.Fatalf("request #%d received unexpected spend "+
"notification", i)
default:
}
}
// We'll assume a historical rescan was dispatched and found the
// following spend details. We'll let the notifier know so that it can
// stop watching at tip.
expectedSpendDetails := &chainntnfs.SpendDetail{
SpentOutPoint: &spendNtfns[0].OutPoint,
SpenderTxHash: &zeroHash,
SpendingTx: wire.NewMsgTx(2),
SpenderInputIndex: 0,
SpendingHeight: startingHeight - 1,
}
err = n.UpdateSpendDetails(spendNtfns[0].OutPoint, expectedSpendDetails)
if err != nil {
t.Fatalf("unable to update spend details: %v", err)
}
// With the spend details retrieved, each client should now have been
// notified of the spend.
for i, ntfn := range spendNtfns {
select {
case spendDetails := <-ntfn.Event.Spend:
assertSpendDetails(t, spendDetails, expectedSpendDetails)
default:
t.Fatalf("request #%d expected to received spend "+
"notification", i)
}
}
// Finally, in order to ensure that the spend details are properly
// cached, we'll register another client for the same outpoint. We
// should not see a historical rescan request and the spend notification
// should come through immediately.
extraSpendNtfn := &chainntnfs.SpendNtfn{
SpendID: numNtfns + 1,
OutPoint: zeroOutPoint,
Event: chainntnfs.NewSpendEvent(nil),
}
historicalSpendRescan, err := n.RegisterSpend(extraSpendNtfn)
if err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
if historicalSpendRescan != nil {
t.Fatal("received unexpected historical rescan request")
}
select {
case spendDetails := <-extraSpendNtfn.Event.Spend:
assertSpendDetails(t, spendDetails, expectedSpendDetails)
default:
t.Fatal("expected to receive spend notification")
}
}
// TestTxNotifierCancelSpend ensures that a spend notification after a client
// has canceled their intent to receive one.
func TestTxNotifierCancelSpend(t *testing.T) {
t.Parallel()
const startingHeight = 10
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
startingHeight, chainntnfs.ReorgSafetyLimit, hintCache,
hintCache,
)
// We'll register two notification requests. Only the second one will be
// canceled.
ntfn1 := &chainntnfs.SpendNtfn{
SpendID: 0,
OutPoint: zeroOutPoint,
Event: chainntnfs.NewSpendEvent(nil),
}
if _, err := n.RegisterSpend(ntfn1); err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
ntfn2 := &chainntnfs.SpendNtfn{
SpendID: 1,
OutPoint: zeroOutPoint,
Event: chainntnfs.NewSpendEvent(nil),
}
if _, err := n.RegisterSpend(ntfn2); err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
// Construct the spending details of the outpoint and create a dummy
// block containing it.
spendTx := wire.NewMsgTx(2)
spendTx.AddTxIn(&wire.TxIn{PreviousOutPoint: ntfn1.OutPoint})
spendTxHash := spendTx.TxHash()
expectedSpendDetails := &chainntnfs.SpendDetail{
SpentOutPoint: &ntfn1.OutPoint,
SpenderTxHash: &spendTxHash,
SpendingTx: spendTx,
SpenderInputIndex: 0,
SpendingHeight: startingHeight + 1,
}
block := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{spendTx},
})
// Before extending the notifier's tip with the dummy block above, we'll
// cancel the second request.
n.CancelSpend(ntfn2.OutPoint, ntfn2.SpendID)
err := n.ConnectTip(block.Hash(), startingHeight+1, block.Transactions())
if err != nil {
t.Fatalf("unable to connect block: %v", err)
}
if err := n.NotifyHeight(startingHeight + 1); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// The first request should still be active, so we should receive a
// spend notification with the correct spending details.
select {
case spendDetails := <-ntfn1.Event.Spend:
assertSpendDetails(t, spendDetails, expectedSpendDetails)
default:
t.Fatalf("expected to receive spend notification")
}
// The second one, however, should not have. The event's Spend channel
// must have also been closed to indicate the caller that the TxNotifier
// can no longer fulfill their canceled request.
select {
case _, ok := <-ntfn2.Event.Spend:
if ok {
t.Fatal("expected Spend channel to be closed")
}
default:
t.Fatal("expected Spend channel to be closed")
}
}
// TestTxNotifierConfReorg ensures that clients are notified of a reorg when a
// transaction for which they registered a confirmation notification has been
// reorged out of the chain.
func TestTxNotifierConfReorg(t *testing.T) {
t.Parallel()
const (
tx1NumConfs uint32 = 2
tx2NumConfs uint32 = 1
tx3NumConfs uint32 = 2
)
var (
tx1 = wire.MsgTx{Version: 1}
tx2 = wire.MsgTx{Version: 2}
tx3 = wire.MsgTx{Version: 3}
)
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
7, chainntnfs.ReorgSafetyLimit, hintCache, hintCache,
)
// Tx 1 will be confirmed in block 9 and requires 2 confs.
tx1Hash := tx1.TxHash()
ntfn1 := chainntnfs.ConfNtfn{
TxID: &tx1Hash,
NumConfirmations: tx1NumConfs,
Event: chainntnfs.NewConfirmationEvent(tx1NumConfs),
}
if _, err := n.RegisterConf(&ntfn1); err != nil {
t.Fatalf("unable to register ntfn: %v", err)
}
if err := n.UpdateConfDetails(*ntfn1.TxID, nil); err != nil {
t.Fatalf("unable to deliver conf details: %v", err)
}
// Tx 2 will be confirmed in block 10 and requires 1 conf.
tx2Hash := tx2.TxHash()
ntfn2 := chainntnfs.ConfNtfn{
TxID: &tx2Hash,
NumConfirmations: tx2NumConfs,
Event: chainntnfs.NewConfirmationEvent(tx2NumConfs),
}
if _, err := n.RegisterConf(&ntfn2); err != nil {
t.Fatalf("unable to register ntfn: %v", err)
}
if err := n.UpdateConfDetails(*ntfn2.TxID, nil); err != nil {
t.Fatalf("unable to deliver conf details: %v", err)
}
// Tx 3 will be confirmed in block 10 and requires 2 confs.
tx3Hash := tx3.TxHash()
ntfn3 := chainntnfs.ConfNtfn{
TxID: &tx3Hash,
NumConfirmations: tx3NumConfs,
Event: chainntnfs.NewConfirmationEvent(tx3NumConfs),
}
if _, err := n.RegisterConf(&ntfn3); err != nil {
t.Fatalf("unable to register ntfn: %v", err)
}
if err := n.UpdateConfDetails(*ntfn3.TxID, nil); err != nil {
t.Fatalf("unable to deliver conf details: %v", err)
}
// Sync chain to block 10. Txs 1 & 2 should be confirmed.
block1 := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{&tx1},
})
if err := n.ConnectTip(nil, 8, block1.Transactions()); err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(8); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
if err := n.ConnectTip(nil, 9, nil); err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(9); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
block2 := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{&tx2, &tx3},
})
if err := n.ConnectTip(nil, 10, block2.Transactions()); err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(10); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// We should receive two updates for tx1 since it requires two
// confirmations and it has already met them.
for i := 0; i < 2; i++ {
select {
case <-ntfn1.Event.Updates:
default:
t.Fatal("Expected confirmation update for tx1")
}
}
// A confirmation notification for tx1 should be dispatched, as it met
// its required number of confirmations.
select {
case <-ntfn1.Event.Confirmed:
default:
t.Fatalf("Expected confirmation for tx1")
}
// We should only receive one update for tx2 since it only requires
// one confirmation and it already met it.
select {
case <-ntfn2.Event.Updates:
default:
t.Fatal("Expected confirmation update for tx2")
}
// A confirmation notification for tx2 should be dispatched, as it met
// its required number of confirmations.
select {
case <-ntfn2.Event.Confirmed:
default:
t.Fatalf("Expected confirmation for tx2")
}
// We should only receive one update for tx3 since it only has one
// confirmation so far and it requires two.
select {
case <-ntfn3.Event.Updates:
default:
t.Fatal("Expected confirmation update for tx3")
}
// A confirmation notification for tx3 should not be dispatched yet, as
// it requires one more confirmation.
select {
case txConf := <-ntfn3.Event.Confirmed:
t.Fatalf("Received unexpected confirmation for tx3: %v", txConf)
default:
}
// The block that included tx2 and tx3 is disconnected and two next
// blocks without them are connected.
if err := n.DisconnectTip(10); err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.ConnectTip(nil, 10, nil); err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(10); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
if err := n.ConnectTip(nil, 11, nil); err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(11); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
select {
case reorgDepth := <-ntfn2.Event.NegativeConf:
if reorgDepth != 1 {
t.Fatalf("Incorrect value for negative conf notification: "+
"expected %d, got %d", 1, reorgDepth)
}
default:
t.Fatalf("Expected negative conf notification for tx1")
}
// We should not receive any event notifications from all of the
// transactions because tx1 has already been confirmed and tx2 and tx3
// have not been included in the chain since the reorg.
select {
case <-ntfn1.Event.Updates:
t.Fatal("Received unexpected confirmation update for tx1")
case txConf := <-ntfn1.Event.Confirmed:
t.Fatalf("Received unexpected confirmation for tx1: %v", txConf)
default:
}
select {
case <-ntfn2.Event.Updates:
t.Fatal("Received unexpected confirmation update for tx2")
case txConf := <-ntfn2.Event.Confirmed:
t.Fatalf("Received unexpected confirmation for tx2: %v", txConf)
default:
}
select {
case <-ntfn3.Event.Updates:
t.Fatal("Received unexpected confirmation update for tx3")
case txConf := <-ntfn3.Event.Confirmed:
t.Fatalf("Received unexpected confirmation for tx3: %v", txConf)
default:
}
// Now transactions 2 & 3 are re-included in a new block.
block3 := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{&tx2, &tx3},
})
block4 := btcutil.NewBlock(&wire.MsgBlock{})
err := n.ConnectTip(block3.Hash(), 12, block3.Transactions())
if err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(12); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
err = n.ConnectTip(block4.Hash(), 13, block4.Transactions())
if err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(13); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// We should only receive one update for tx2 since it only requires
// one confirmation and it already met it.
select {
case numConfsLeft := <-ntfn2.Event.Updates:
const expected = 0
if numConfsLeft != expected {
t.Fatalf("Received incorrect confirmation update: tx2 "+
"expected %d confirmations left, got %d",
expected, numConfsLeft)
}
default:
t.Fatal("Expected confirmation update for tx2")
}
// A confirmation notification for tx2 should be dispatched, as it met
// its required number of confirmations.
select {
case txConf := <-ntfn2.Event.Confirmed:
expectedConf := chainntnfs.TxConfirmation{
BlockHash: block3.Hash(),
BlockHeight: 12,
TxIndex: 0,
}
assertConfDetails(t, txConf, &expectedConf)
default:
t.Fatalf("Expected confirmation for tx2")
}
// We should receive two updates for tx3 since it requires two
// confirmations and it has already met them.
for i := uint32(1); i <= 2; i++ {
select {
case numConfsLeft := <-ntfn3.Event.Updates:
expected := tx3NumConfs - i
if numConfsLeft != expected {
t.Fatalf("Received incorrect confirmation update: tx3 "+
"expected %d confirmations left, got %d",
expected, numConfsLeft)
}
default:
t.Fatal("Expected confirmation update for tx2")
}
}
// A confirmation notification for tx3 should be dispatched, as it met
// its required number of confirmations.
select {
case txConf := <-ntfn3.Event.Confirmed:
expectedConf := chainntnfs.TxConfirmation{
BlockHash: block3.Hash(),
BlockHeight: 12,
TxIndex: 1,
}
assertConfDetails(t, txConf, &expectedConf)
default:
t.Fatalf("Expected confirmation for tx3")
}
}
// TestTxNotifierSpendReorg ensures that clients are notified of a reorg when
// the spending transaction of an outpoint for which they registered a spend
// notification for has been reorged out of the chain.
func TestTxNotifierSpendReorg(t *testing.T) {
t.Parallel()
const startingHeight = 10
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
startingHeight, chainntnfs.ReorgSafetyLimit, hintCache,
hintCache,
)
// We'll have two outpoints that will be spent throughout the test. The
// first will be spent and will not experience a reorg, while the second
// one will.
op1 := zeroOutPoint
op1.Index = 1
spendTx1 := wire.NewMsgTx(2)
spendTx1.AddTxIn(&wire.TxIn{PreviousOutPoint: op1})
spendTxHash1 := spendTx1.TxHash()
expectedSpendDetails1 := &chainntnfs.SpendDetail{
SpentOutPoint: &op1,
SpenderTxHash: &spendTxHash1,
SpendingTx: spendTx1,
SpenderInputIndex: 0,
SpendingHeight: startingHeight + 1,
}
op2 := zeroOutPoint
op2.Index = 2
spendTx2 := wire.NewMsgTx(2)
spendTx2.AddTxIn(&wire.TxIn{PreviousOutPoint: zeroOutPoint})
spendTx2.AddTxIn(&wire.TxIn{PreviousOutPoint: op2})
spendTxHash2 := spendTx2.TxHash()
// The second outpoint will experience a reorg and get re-spent at a
// different height, so we'll need to construct the spend details for
// before and after the reorg.
expectedSpendDetails2BeforeReorg := chainntnfs.SpendDetail{
SpentOutPoint: &op2,
SpenderTxHash: &spendTxHash2,
SpendingTx: spendTx2,
SpenderInputIndex: 1,
SpendingHeight: startingHeight + 2,
}
// The spend details after the reorg will be exactly the same, except
// for the spend confirming at the next height.
expectedSpendDetails2AfterReorg := expectedSpendDetails2BeforeReorg
expectedSpendDetails2AfterReorg.SpendingHeight++
// We'll register for a spend notification for each outpoint above.
ntfn1 := &chainntnfs.SpendNtfn{
SpendID: 78,
OutPoint: op1,
Event: chainntnfs.NewSpendEvent(nil),
}
if _, err := n.RegisterSpend(ntfn1); err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
ntfn2 := &chainntnfs.SpendNtfn{
SpendID: 21,
OutPoint: op2,
Event: chainntnfs.NewSpendEvent(nil),
}
if _, err := n.RegisterSpend(ntfn2); err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
// We'll extend the chain by connecting a new block at tip. This block
// will only contain the spending transaction of the first outpoint.
block1 := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{spendTx1},
})
err := n.ConnectTip(block1.Hash(), startingHeight+1, block1.Transactions())
if err != nil {
t.Fatalf("unable to connect block: %v", err)
}
if err := n.NotifyHeight(startingHeight + 1); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// We should receive a spend notification for the first outpoint with
// its correct spending details.
select {
case spendDetails := <-ntfn1.Event.Spend:
assertSpendDetails(t, spendDetails, expectedSpendDetails1)
default:
t.Fatal("expected to receive spend details")
}
// We should not, however, receive one for the second outpoint as it has
// yet to be spent.
select {
case <-ntfn2.Event.Spend:
t.Fatal("received unexpected spend notification")
default:
}
// Now, we'll extend the chain again, this time with a block containing
// the spending transaction of the second outpoint.
block2 := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{spendTx2},
})
err = n.ConnectTip(block2.Hash(), startingHeight+2, block2.Transactions())
if err != nil {
t.Fatalf("unable to connect block: %v", err)
}
if err := n.NotifyHeight(startingHeight + 2); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// We should not receive another spend notification for the first
// outpoint.
select {
case <-ntfn1.Event.Spend:
t.Fatal("received unexpected spend notification")
default:
}
// We should receive one for the second outpoint.
select {
case spendDetails := <-ntfn2.Event.Spend:
assertSpendDetails(
t, spendDetails, &expectedSpendDetails2BeforeReorg,
)
default:
t.Fatal("expected to receive spend details")
}
// Now, to replicate a chain reorg, we'll disconnect the block that
// contained the spending transaction of the second outpoint.
if err := n.DisconnectTip(startingHeight + 2); err != nil {
t.Fatalf("unable to disconnect block: %v", err)
}
// No notifications should be dispatched for the first outpoint as it
// was spent at a previous height.
select {
case <-ntfn1.Event.Spend:
t.Fatal("received unexpected spend notification")
case <-ntfn1.Event.Reorg:
t.Fatal("received unexpected spend reorg notification")
default:
}
// We should receive a reorg notification for the second outpoint.
select {
case <-ntfn2.Event.Spend:
t.Fatal("received unexpected spend notification")
case <-ntfn2.Event.Reorg:
default:
t.Fatal("expected spend reorg notification")
}
// We'll now extend the chain with an empty block, to ensure that we can
// properly detect when an outpoint has been re-spent at a later height.
emptyBlock := btcutil.NewBlock(&wire.MsgBlock{})
err = n.ConnectTip(
emptyBlock.Hash(), startingHeight+2, emptyBlock.Transactions(),
)
if err != nil {
t.Fatalf("unable to disconnect block: %v", err)
}
if err := n.NotifyHeight(startingHeight + 2); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// We shouldn't receive notifications for either of the outpoints.
select {
case <-ntfn1.Event.Spend:
t.Fatal("received unexpected spend notification")
case <-ntfn1.Event.Reorg:
t.Fatal("received unexpected spend reorg notification")
case <-ntfn2.Event.Spend:
t.Fatal("received unexpected spend notification")
case <-ntfn2.Event.Reorg:
t.Fatal("received unexpected spend reorg notification")
default:
}
// Finally, extend the chain with another block containing the same
// spending transaction of the second outpoint.
err = n.ConnectTip(
block2.Hash(), startingHeight+3, block2.Transactions(),
)
if err != nil {
t.Fatalf("unable to connect block: %v", err)
}
if err := n.NotifyHeight(startingHeight + 3); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// We should now receive a spend notification once again for the second
// outpoint containing the new spend details.
select {
case spendDetails := <-ntfn2.Event.Spend:
assertSpendDetails(
t, spendDetails, &expectedSpendDetails2AfterReorg,
)
default:
t.Fatalf("expected to receive spend notification")
}
// Once again, we should not receive one for the first outpoint.
select {
case <-ntfn1.Event.Spend:
t.Fatal("received unexpected spend notification")
default:
}
}
// TestTxNotifierConfirmHintCache ensures that the height hints for transactions
// are kept track of correctly with each new block connected/disconnected. This
// test also asserts that the height hints are not updated until the simulated
// historical dispatches have returned, and we know the transactions aren't
// already in the chain.
func TestTxNotifierConfirmHintCache(t *testing.T) {
t.Parallel()
const (
startingHeight = 200
txDummyHeight = 201
tx1Height = 202
tx2Height = 203
)
// Initialize our TxNotifier instance backed by a height hint cache.
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
startingHeight, chainntnfs.ReorgSafetyLimit, hintCache,
hintCache,
)
// Create two test transactions and register them for notifications.
tx1 := wire.MsgTx{Version: 1}
tx1Hash := tx1.TxHash()
ntfn1 := &chainntnfs.ConfNtfn{
TxID: &tx1Hash,
NumConfirmations: 1,
Event: chainntnfs.NewConfirmationEvent(1),
}
tx2 := wire.MsgTx{Version: 2}
tx2Hash := tx2.TxHash()
ntfn2 := &chainntnfs.ConfNtfn{
TxID: &tx2Hash,
NumConfirmations: 2,
Event: chainntnfs.NewConfirmationEvent(2),
}
if _, err := n.RegisterConf(ntfn1); err != nil {
t.Fatalf("unable to register tx1: %v", err)
}
if _, err := n.RegisterConf(ntfn2); err != nil {
t.Fatalf("unable to register tx2: %v", err)
}
// Both transactions should not have a height hint set, as RegisterConf
// should not alter the cache state.
_, err := hintCache.QueryConfirmHint(tx1Hash)
if err != chainntnfs.ErrConfirmHintNotFound {
t.Fatalf("unexpected error when querying for height hint "+
"want: %v, got %v",
chainntnfs.ErrConfirmHintNotFound, err)
}
_, err = hintCache.QueryConfirmHint(tx2Hash)
if err != chainntnfs.ErrConfirmHintNotFound {
t.Fatalf("unexpected error when querying for height hint "+
"want: %v, got %v",
chainntnfs.ErrConfirmHintNotFound, err)
}
// Create a new block that will include the dummy transaction and extend
// the chain.
txDummy := wire.MsgTx{Version: 3}
block1 := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{&txDummy},
})
err = n.ConnectTip(block1.Hash(), txDummyHeight, block1.Transactions())
if err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(txDummyHeight); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// Since UpdateConfDetails has not been called for either transaction,
// the height hints should remain unchanged. This simulates blocks
// confirming while the historical dispatch is processing the
// registration.
hint, err := hintCache.QueryConfirmHint(tx1Hash)
if err != chainntnfs.ErrConfirmHintNotFound {
t.Fatalf("unexpected error when querying for height hint "+
"want: %v, got %v",
chainntnfs.ErrConfirmHintNotFound, err)
}
hint, err = hintCache.QueryConfirmHint(tx2Hash)
if err != chainntnfs.ErrConfirmHintNotFound {
t.Fatalf("unexpected error when querying for height hint "+
"want: %v, got %v",
chainntnfs.ErrConfirmHintNotFound, err)
}
// Now, update the conf details reporting that the neither txn was found
// in the historical dispatch.
if err := n.UpdateConfDetails(tx1Hash, nil); err != nil {
t.Fatalf("unable to update conf details: %v", err)
}
if err := n.UpdateConfDetails(tx2Hash, nil); err != nil {
t.Fatalf("unable to update conf details: %v", err)
}
// We'll create another block that will include the first transaction
// and extend the chain.
block2 := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{&tx1},
})
err = n.ConnectTip(block2.Hash(), tx1Height, block2.Transactions())
if err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(tx1Height); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// Now that both notifications are waiting at tip for confirmations,
// they should have their height hints updated to the latest block
// height.
hint, err = hintCache.QueryConfirmHint(tx1Hash)
if err != nil {
t.Fatalf("unable to query for hint: %v", err)
}
if hint != tx1Height {
t.Fatalf("expected hint %d, got %d",
tx1Height, hint)
}
hint, err = hintCache.QueryConfirmHint(tx2Hash)
if err != nil {
t.Fatalf("unable to query for hint: %v", err)
}
if hint != tx1Height {
t.Fatalf("expected hint %d, got %d",
tx2Height, hint)
}
// Next, we'll create another block that will include the second
// transaction and extend the chain.
block3 := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{&tx2},
})
err = n.ConnectTip(block3.Hash(), tx2Height, block3.Transactions())
if err != nil {
t.Fatalf("Failed to connect block: %v", err)
}
if err := n.NotifyHeight(tx2Height); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// The height hint for the first transaction should remain the same.
hint, err = hintCache.QueryConfirmHint(tx1Hash)
if err != nil {
t.Fatalf("unable to query for hint: %v", err)
}
if hint != tx1Height {
t.Fatalf("expected hint %d, got %d",
tx1Height, hint)
}
// The height hint for the second transaction should now be updated to
// reflect its confirmation.
hint, err = hintCache.QueryConfirmHint(tx2Hash)
if err != nil {
t.Fatalf("unable to query for hint: %v", err)
}
if hint != tx2Height {
t.Fatalf("expected hint %d, got %d",
tx2Height, hint)
}
// Finally, we'll attempt do disconnect the last block in order to
// simulate a chain reorg.
if err := n.DisconnectTip(tx2Height); err != nil {
t.Fatalf("Failed to disconnect block: %v", err)
}
// This should update the second transaction's height hint within the
// cache to the previous height.
hint, err = hintCache.QueryConfirmHint(tx2Hash)
if err != nil {
t.Fatalf("unable to query for hint: %v", err)
}
if hint != tx1Height {
t.Fatalf("expected hint %d, got %d",
tx1Height, hint)
}
// The first transaction's height hint should remain at the original
// confirmation height.
hint, err = hintCache.QueryConfirmHint(tx2Hash)
if err != nil {
t.Fatalf("unable to query for hint: %v", err)
}
if hint != tx1Height {
t.Fatalf("expected hint %d, got %d",
tx1Height, hint)
}
}
// TestTxNotifierSpendHintCache ensures that the height hints for outpoints are
// kept track of correctly with each new block connected/disconnected. This test
// also asserts that the height hints are not updated until the simulated
// historical dispatches have returned, and we know the outpoints haven't
// already been spent in the chain.
func TestTxNotifierSpendHintCache(t *testing.T) {
t.Parallel()
const (
startingHeight = 200
dummyHeight = 201
op1Height = 202
op2Height = 203
)
// Intiialize our TxNotifier instance backed by a height hint cache.
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
startingHeight, chainntnfs.ReorgSafetyLimit, hintCache,
hintCache,
)
// Create two test outpoints and register them for spend notifications.
op1 := wire.OutPoint{Hash: zeroHash, Index: 1}
ntfn1 := &chainntnfs.SpendNtfn{
OutPoint: op1,
Event: chainntnfs.NewSpendEvent(nil),
}
op2 := wire.OutPoint{Hash: zeroHash, Index: 2}
ntfn2 := &chainntnfs.SpendNtfn{
OutPoint: op2,
Event: chainntnfs.NewSpendEvent(nil),
}
if _, err := n.RegisterSpend(ntfn1); err != nil {
t.Fatalf("unable to register spend for op1: %v", err)
}
if _, err := n.RegisterSpend(ntfn2); err != nil {
t.Fatalf("unable to register spend for op2: %v", err)
}
// Both outpoints should not have a spend hint set upon registration, as
// we must first determine whether they have already been spent in the
// chain.
_, err := hintCache.QuerySpendHint(op1)
if err != chainntnfs.ErrSpendHintNotFound {
t.Fatalf("unexpected error when querying for height hint "+
"expected: %v, got %v", chainntnfs.ErrSpendHintNotFound,
err)
}
_, err = hintCache.QuerySpendHint(op2)
if err != chainntnfs.ErrSpendHintNotFound {
t.Fatalf("unexpected error when querying for height hint "+
"expected: %v, got %v", chainntnfs.ErrSpendHintNotFound,
err)
}
// Create a new empty block and extend the chain.
emptyBlock := btcutil.NewBlock(&wire.MsgBlock{})
err = n.ConnectTip(
emptyBlock.Hash(), dummyHeight, emptyBlock.Transactions(),
)
if err != nil {
t.Fatalf("unable to connect block: %v", err)
}
if err := n.NotifyHeight(dummyHeight); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// Since we haven't called UpdateSpendDetails on any of the test
// outpoints, this implies that there is a still a pending historical
// rescan for them, so their spend hints should not be created/updated.
_, err = hintCache.QuerySpendHint(op1)
if err != chainntnfs.ErrSpendHintNotFound {
t.Fatalf("unexpected error when querying for height hint "+
"expected: %v, got %v", chainntnfs.ErrSpendHintNotFound,
err)
}
_, err = hintCache.QuerySpendHint(op2)
if err != chainntnfs.ErrSpendHintNotFound {
t.Fatalf("unexpected error when querying for height hint "+
"expected: %v, got %v", chainntnfs.ErrSpendHintNotFound,
err)
}
// Now, we'll simulate that their historical rescans have finished by
// calling UpdateSpendDetails. This should allow their spend hints to be
// updated upon every block connected/disconnected.
if err := n.UpdateSpendDetails(ntfn1.OutPoint, nil); err != nil {
t.Fatalf("unable to update spend details: %v", err)
}
if err := n.UpdateSpendDetails(ntfn2.OutPoint, nil); err != nil {
t.Fatalf("unable to update spend details: %v", err)
}
// We'll create a new block that only contains the spending transaction
// of the first outpoint.
spendTx1 := wire.NewMsgTx(2)
spendTx1.AddTxIn(&wire.TxIn{PreviousOutPoint: ntfn1.OutPoint})
block1 := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{spendTx1},
})
err = n.ConnectTip(block1.Hash(), op1Height, block1.Transactions())
if err != nil {
t.Fatalf("unable to connect block: %v", err)
}
if err := n.NotifyHeight(op1Height); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// Both outpoints should have their spend hints reflect the height of
// the new block being connected due to the first outpoint being spent
// at this height, and the second outpoint still being unspent.
op1Hint, err := hintCache.QuerySpendHint(ntfn1.OutPoint)
if err != nil {
t.Fatalf("unable to query for spend hint of op1: %v", err)
}
if op1Hint != op1Height {
t.Fatalf("expected hint %d, got %d", op1Height, op1Hint)
}
op2Hint, err := hintCache.QuerySpendHint(ntfn2.OutPoint)
if err != nil {
t.Fatalf("unable to query for spend hint of op2: %v", err)
}
if op2Hint != op1Height {
t.Fatalf("expected hint %d, got %d", op1Height, op2Hint)
}
// Then, we'll create another block that spends the second outpoint.
spendTx2 := wire.NewMsgTx(2)
spendTx2.AddTxIn(&wire.TxIn{PreviousOutPoint: ntfn2.OutPoint})
block2 := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{spendTx2},
})
err = n.ConnectTip(block2.Hash(), op2Height, block2.Transactions())
if err != nil {
t.Fatalf("unable to connect block: %v", err)
}
if err := n.NotifyHeight(op2Height); err != nil {
t.Fatalf("unable to dispatch notifications: %v", err)
}
// Only the second outpoint should have its spend hint updated due to
// being spent within the new block. The first outpoint's spend hint
// should remain the same as it's already been spent before.
op1Hint, err = hintCache.QuerySpendHint(ntfn1.OutPoint)
if err != nil {
t.Fatalf("unable to query for spend hint of op1: %v", err)
}
if op1Hint != op1Height {
t.Fatalf("expected hint %d, got %d", op1Height, op1Hint)
}
op2Hint, err = hintCache.QuerySpendHint(ntfn2.OutPoint)
if err != nil {
t.Fatalf("unable to query for spend hint of op2: %v", err)
}
if op2Hint != op2Height {
t.Fatalf("expected hint %d, got %d", op2Height, op2Hint)
}
// Finally, we'll attempt do disconnect the last block in order to
// simulate a chain reorg.
if err := n.DisconnectTip(op2Height); err != nil {
t.Fatalf("unable to disconnect block: %v", err)
}
// This should update the second outpoint's spend hint within the cache
// to the previous height, as that's where its spending transaction was
// included in within the chain. The first outpoint's spend hint should
// remain the same.
op1Hint, err = hintCache.QuerySpendHint(ntfn1.OutPoint)
if err != nil {
t.Fatalf("unable to query for spend hint of op1: %v", err)
}
if op1Hint != op1Height {
t.Fatalf("expected hint %d, got %d", op1Height, op1Hint)
}
op2Hint, err = hintCache.QuerySpendHint(ntfn2.OutPoint)
if err != nil {
t.Fatalf("unable to query for spend hint of op2: %v", err)
}
if op2Hint != op1Height {
t.Fatalf("expected hint %d, got %d", op1Height, op2Hint)
}
}
// TestTxNotifierTearDown ensures that the TxNotifier properly alerts clients
// that it is shutting down and will be unable to deliver notifications.
func TestTxNotifierTearDown(t *testing.T) {
t.Parallel()
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
10, chainntnfs.ReorgSafetyLimit, hintCache, hintCache,
)
// To begin the test, we'll register for a confirmation and spend
// notification.
confNtfn := &chainntnfs.ConfNtfn{
TxID: &zeroHash,
NumConfirmations: 1,
Event: chainntnfs.NewConfirmationEvent(1),
}
if _, err := n.RegisterConf(confNtfn); err != nil {
t.Fatalf("unable to register conf ntfn: %v", err)
}
spendNtfn := &chainntnfs.SpendNtfn{
OutPoint: zeroOutPoint,
Event: chainntnfs.NewSpendEvent(nil),
}
if _, err := n.RegisterSpend(spendNtfn); err != nil {
t.Fatalf("unable to register spend ntfn: %v", err)
}
// With the notifications registered, we'll now tear down the notifier.
// The notification channels should be closed for notifications, whether
// they have been dispatched or not, so we should not expect to receive
// any more updates.
n.TearDown()
select {
case _, ok := <-confNtfn.Event.Confirmed:
if ok {
t.Fatal("expected closed Confirmed channel for conf ntfn")
}
case _, ok := <-confNtfn.Event.Updates:
if ok {
t.Fatal("expected closed Updates channel for conf ntfn")
}
case _, ok := <-confNtfn.Event.NegativeConf:
if ok {
t.Fatal("expected closed NegativeConf channel for conf ntfn")
}
case _, ok := <-spendNtfn.Event.Spend:
if ok {
t.Fatal("expected closed Spend channel for spend ntfn")
}
case _, ok := <-spendNtfn.Event.Reorg:
if ok {
t.Fatalf("expected closed Reorg channel for spend ntfn")
}
default:
t.Fatalf("expected closed notification channels for all ntfns")
}
// Now that the notifier is torn down, we should no longer be able to
// register notification requests.
if _, err := n.RegisterConf(confNtfn); err == nil {
t.Fatal("expected confirmation registration to fail")
}
if _, err := n.RegisterSpend(spendNtfn); err == nil {
t.Fatal("expected spend registration to fail")
}
}
func assertConfDetails(t *testing.T, result, expected *chainntnfs.TxConfirmation) {
t.Helper()
if result.BlockHeight != expected.BlockHeight {
t.Fatalf("Incorrect block height in confirmation details: "+
"expected %d, got %d", expected.BlockHeight,
result.BlockHeight)
}
if !result.BlockHash.IsEqual(expected.BlockHash) {
t.Fatalf("Incorrect block hash in confirmation details: "+
"expected %d, got %d", expected.BlockHash,
result.BlockHash)
}
if result.TxIndex != expected.TxIndex {
t.Fatalf("Incorrect tx index in confirmation details: "+
"expected %d, got %d", expected.TxIndex, result.TxIndex)
}
}
func assertSpendDetails(t *testing.T, result, expected *chainntnfs.SpendDetail) {
t.Helper()
if *result.SpentOutPoint != *expected.SpentOutPoint {
t.Fatalf("expected spent outpoint %v, got %v",
expected.SpentOutPoint, result.SpentOutPoint)
}
if !result.SpenderTxHash.IsEqual(expected.SpenderTxHash) {
t.Fatalf("expected spender tx hash %v, got %v",
expected.SpenderTxHash, result.SpenderTxHash)
}
if result.SpenderInputIndex != expected.SpenderInputIndex {
t.Fatalf("expected spender input index %d, got %d",
expected.SpenderInputIndex, result.SpenderInputIndex)
}
if result.SpendingHeight != expected.SpendingHeight {
t.Fatalf("expected spending height %d, got %d",
expected.SpendingHeight, result.SpendingHeight)
}
}