You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2234 lines
67 KiB
2234 lines
67 KiB
package chainntnfs_test |
|
|
|
import ( |
|
"bytes" |
|
"sync" |
|
"testing" |
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash" |
|
"github.com/btcsuite/btcd/wire" |
|
"github.com/btcsuite/btcutil" |
|
"github.com/lightningnetwork/lnd/chainntnfs" |
|
) |
|
|
|
var ( |
|
testRawScript = []byte{ |
|
// OP_HASH160 |
|
0xa9, |
|
// OP_DATA_20 |
|
0x14, |
|
// <20-byte script hash> |
|
0x90, 0x1c, 0x86, 0x94, 0xc0, 0x3f, 0xaf, 0xd5, |
|
0x52, 0x28, 0x10, 0xe0, 0x33, 0x0f, 0x26, 0xe6, |
|
0x7a, 0x85, 0x33, 0xcd, |
|
// OP_EQUAL |
|
0x87, |
|
} |
|
testSigScript = []byte{ |
|
// OP_DATA_16 |
|
0x16, |
|
// <22-byte redeem script> |
|
0x00, 0x14, 0x1d, 0x7c, 0xd6, 0xc7, 0x5c, 0x2e, |
|
0x86, 0xf4, 0xcb, 0xf9, 0x8e, 0xae, 0xd2, 0x21, |
|
0xb3, 0x0b, 0xd9, 0xa0, 0xb9, 0x28, |
|
} |
|
) |
|
|
|
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), |
|
} |
|
} |
|
|
|
// TestTxNotifierRegistrationValidation ensures that we are not able to register |
|
// requests with invalid parameters. |
|
func TestTxNotifierRegistrationValidation(t *testing.T) { |
|
t.Parallel() |
|
|
|
testCases := []struct { |
|
name string |
|
pkScript []byte |
|
numConfs uint32 |
|
heightHint uint32 |
|
checkSpend bool |
|
err error |
|
}{ |
|
{ |
|
name: "empty output script", |
|
pkScript: nil, |
|
numConfs: 1, |
|
heightHint: 1, |
|
checkSpend: true, |
|
err: chainntnfs.ErrNoScript, |
|
}, |
|
{ |
|
name: "zero num confs", |
|
pkScript: testRawScript, |
|
numConfs: 0, |
|
heightHint: 1, |
|
err: chainntnfs.ErrNumConfsOutOfRange, |
|
}, |
|
{ |
|
name: "exceed max num confs", |
|
pkScript: testRawScript, |
|
numConfs: chainntnfs.MaxNumConfs + 1, |
|
heightHint: 1, |
|
err: chainntnfs.ErrNumConfsOutOfRange, |
|
}, |
|
{ |
|
name: "empty height hint", |
|
pkScript: testRawScript, |
|
numConfs: 1, |
|
heightHint: 0, |
|
checkSpend: true, |
|
err: chainntnfs.ErrNoHeightHint, |
|
}, |
|
} |
|
|
|
for _, testCase := range testCases { |
|
testCase := testCase |
|
t.Run(testCase.name, func(t *testing.T) { |
|
hintCache := newMockHintCache() |
|
n := chainntnfs.NewTxNotifier( |
|
10, chainntnfs.ReorgSafetyLimit, hintCache, hintCache, |
|
) |
|
|
|
_, err := n.RegisterConf( |
|
&chainntnfs.ZeroHash, testCase.pkScript, |
|
testCase.numConfs, testCase.heightHint, |
|
) |
|
if err != testCase.err { |
|
t.Fatalf("conf registration expected error "+ |
|
"\"%v\", got \"%v\"", testCase.err, err) |
|
} |
|
|
|
if !testCase.checkSpend { |
|
return |
|
} |
|
|
|
_, err = n.RegisterSpend( |
|
&chainntnfs.ZeroOutPoint, testCase.pkScript, |
|
testCase.heightHint, |
|
) |
|
if err != testCase.err { |
|
t.Fatalf("spend registration expected error "+ |
|
"\"%v\", got \"%v\"", testCase.err, 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 |
|
) |
|
|
|
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. |
|
tx1 := wire.MsgTx{Version: 1} |
|
tx1.AddTxOut(&wire.TxOut{PkScript: testRawScript}) |
|
tx1Hash := tx1.TxHash() |
|
ntfn1, err := n.RegisterConf(&tx1Hash, testRawScript, tx1NumConfs, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register ntfn: %v", err) |
|
} |
|
|
|
tx2 := wire.MsgTx{Version: 2} |
|
tx2.AddTxOut(&wire.TxOut{PkScript: testRawScript}) |
|
tx2Hash := tx2.TxHash() |
|
ntfn2, err := n.RegisterConf(&tx2Hash, testRawScript, tx2NumConfs, 1) |
|
if 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}, |
|
}) |
|
|
|
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, |
|
Tx: &tx1, |
|
} |
|
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{}) |
|
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, |
|
Tx: &tx2, |
|
} |
|
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, err := n.RegisterConf(&tx1Hash, testRawScript, tx1NumConfs, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register ntfn: %v", err) |
|
} |
|
|
|
tx2Hash := tx2.TxHash() |
|
ntfn2, err := n.RegisterConf(&tx2Hash, testRawScript, tx2NumConfs, 1) |
|
if 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: &chainntnfs.ZeroHash, |
|
BlockHeight: 9, |
|
TxIndex: 1, |
|
Tx: &tx1, |
|
} |
|
err = n.UpdateConfDetails(ntfn1.HistoricalDispatch.ConfRequest, &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: &chainntnfs.ZeroHash, |
|
BlockHeight: 9, |
|
TxIndex: 2, |
|
Tx: &tx2, |
|
} |
|
err = n.UpdateConfDetails(ntfn2.HistoricalDispatch.ConfRequest, &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. |
|
op := wire.OutPoint{Index: 1} |
|
ntfn, err := n.RegisterSpend(&op, testRawScript, 1) |
|
if 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: op, |
|
SignatureScript: testSigScript, |
|
}) |
|
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: &op, |
|
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, |
|
SignatureScript: testSigScript, |
|
}) |
|
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 := wire.OutPoint{Index: 1} |
|
spendTx := wire.NewMsgTx(2) |
|
spendTx.AddTxIn(&wire.TxIn{ |
|
PreviousOutPoint: spentOutpoint, |
|
SignatureScript: testSigScript, |
|
}) |
|
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, err := n.RegisterSpend(&spentOutpoint, testRawScript, 1) |
|
if 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.HistoricalDispatch.SpendRequest, 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, |
|
SignatureScript: testSigScript, |
|
}) |
|
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. |
|
ntfn1, err := n.RegisterConf(&chainntnfs.ZeroHash, testRawScript, 1, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register spend ntfn: %v", err) |
|
} |
|
if ntfn1.HistoricalDispatch == 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. |
|
ntfn2, err := n.RegisterConf(&chainntnfs.ZeroHash, testRawScript, 1, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register spend ntfn: %v", err) |
|
} |
|
if ntfn2.HistoricalDispatch != 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, |
|
} |
|
err = n.UpdateConfDetails(ntfn1.HistoricalDispatch.ConfRequest, confDetails) |
|
if err != nil { |
|
t.Fatalf("unable to update conf details: %v", err) |
|
} |
|
|
|
ntfn3, err := n.RegisterConf(&chainntnfs.ZeroHash, testRawScript, 1, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register spend ntfn: %v", err) |
|
} |
|
if ntfn3.HistoricalDispatch != 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. |
|
op := wire.OutPoint{Index: 1} |
|
ntfn1, err := n.RegisterSpend(&op, testRawScript, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register spend ntfn: %v", err) |
|
} |
|
if ntfn1.HistoricalDispatch == 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, err := n.RegisterSpend(&op, testRawScript, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register spend ntfn: %v", err) |
|
} |
|
if ntfn2.HistoricalDispatch != 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: &op, |
|
SpenderTxHash: &chainntnfs.ZeroHash, |
|
SpendingTx: wire.NewMsgTx(2), |
|
SpenderInputIndex: 0, |
|
SpendingHeight: startingHeight - 1, |
|
} |
|
err = n.UpdateSpendDetails( |
|
ntfn1.HistoricalDispatch.SpendRequest, spendDetails, |
|
) |
|
if err != nil { |
|
t.Fatalf("unable to update spend details: %v", err) |
|
} |
|
|
|
ntfn3, err := n.RegisterSpend(&op, testRawScript, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register spend ntfn: %v", err) |
|
} |
|
if ntfn3.HistoricalDispatch != 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, |
|
) |
|
|
|
var txid chainhash.Hash |
|
copy(txid[:], bytes.Repeat([]byte{0x01}, 32)) |
|
|
|
// We'll start off by registered 5 clients for a confirmation |
|
// notification on the same transaction. |
|
confNtfns := make([]*chainntnfs.ConfRegistration, numNtfns) |
|
for i := uint64(0); i < numNtfns; i++ { |
|
ntfn, err := n.RegisterConf(&txid, testRawScript, 1, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register conf ntfn #%d: %v", i, err) |
|
} |
|
confNtfns[i] = ntfn |
|
} |
|
|
|
// 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, |
|
Tx: wire.NewMsgTx(1), |
|
} |
|
err := n.UpdateConfDetails( |
|
confNtfns[0].HistoricalDispatch.ConfRequest, 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, err := n.RegisterConf(&txid, testRawScript, 1, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register conf ntfn: %v", err) |
|
} |
|
if extraConfNtfn.HistoricalDispatch != 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. |
|
op := wire.OutPoint{Index: 1} |
|
spendNtfns := make([]*chainntnfs.SpendRegistration, numNtfns) |
|
for i := uint64(0); i < numNtfns; i++ { |
|
ntfn, err := n.RegisterSpend(&op, testRawScript, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register spend ntfn #%d: %v", i, err) |
|
} |
|
spendNtfns[i] = ntfn |
|
} |
|
|
|
// 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: &op, |
|
SpenderTxHash: &chainntnfs.ZeroHash, |
|
SpendingTx: wire.NewMsgTx(2), |
|
SpenderInputIndex: 0, |
|
SpendingHeight: startingHeight - 1, |
|
} |
|
err = n.UpdateSpendDetails( |
|
spendNtfns[0].HistoricalDispatch.SpendRequest, 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, err := n.RegisterSpend(&op, testRawScript, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register spend ntfn: %v", err) |
|
} |
|
if extraSpendNtfn.HistoricalDispatch != 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") |
|
} |
|
} |
|
|
|
// TestTxNotifierCancelConf ensures that a confirmation notification after a |
|
// client has canceled their intent to receive one. |
|
func TestTxNotifierCancelConf(t *testing.T) { |
|
t.Parallel() |
|
|
|
const startingHeight = 10 |
|
hintCache := newMockHintCache() |
|
n := chainntnfs.NewTxNotifier(startingHeight, 100, hintCache, hintCache) |
|
|
|
// We'll register two notification requests. Only the second one will be |
|
// canceled. |
|
tx1 := wire.NewMsgTx(1) |
|
tx1.AddTxOut(&wire.TxOut{PkScript: testRawScript}) |
|
tx1Hash := tx1.TxHash() |
|
ntfn1, err := n.RegisterConf(&tx1Hash, testRawScript, 1, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register spend ntfn: %v", err) |
|
} |
|
|
|
tx2 := wire.NewMsgTx(2) |
|
tx2.AddTxOut(&wire.TxOut{PkScript: testRawScript}) |
|
tx2Hash := tx2.TxHash() |
|
ntfn2, err := n.RegisterConf(&tx2Hash, testRawScript, 1, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register spend ntfn: %v", err) |
|
} |
|
|
|
// Construct a block that will confirm both transactions. |
|
block := btcutil.NewBlock(&wire.MsgBlock{ |
|
Transactions: []*wire.MsgTx{tx1, tx2}, |
|
}) |
|
tx1ConfDetails := &chainntnfs.TxConfirmation{ |
|
BlockHeight: startingHeight + 1, |
|
BlockHash: block.Hash(), |
|
TxIndex: 0, |
|
Tx: tx1, |
|
} |
|
|
|
// Before extending the notifier's tip with the block above, we'll |
|
// cancel the second request. |
|
n.CancelConf(ntfn2.HistoricalDispatch.ConfRequest, 2) |
|
|
|
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 |
|
// confirmation notification with the correct details. |
|
select { |
|
case confDetails := <-ntfn1.Event.Confirmed: |
|
assertConfDetails(t, confDetails, tx1ConfDetails) |
|
default: |
|
t.Fatalf("expected to receive confirmation notification") |
|
} |
|
|
|
// The second one, however, should not have. The event's Confirmed |
|
// 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.Confirmed: |
|
if ok { |
|
t.Fatal("expected Confirmed channel to be closed") |
|
} |
|
default: |
|
t.Fatal("expected Confirmed channel to be closed") |
|
} |
|
} |
|
|
|
// 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. |
|
op1 := wire.OutPoint{Index: 1} |
|
ntfn1, err := n.RegisterSpend(&op1, testRawScript, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register spend ntfn: %v", err) |
|
} |
|
|
|
op2 := wire.OutPoint{Index: 2} |
|
ntfn2, err := n.RegisterSpend(&op2, testRawScript, 1) |
|
if 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: op1, |
|
SignatureScript: testSigScript, |
|
}) |
|
spendTxHash := spendTx.TxHash() |
|
expectedSpendDetails := &chainntnfs.SpendDetail{ |
|
SpentOutPoint: &op1, |
|
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.HistoricalDispatch.SpendRequest, 2) |
|
|
|
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 |
|
) |
|
|
|
hintCache := newMockHintCache() |
|
n := chainntnfs.NewTxNotifier( |
|
7, chainntnfs.ReorgSafetyLimit, hintCache, hintCache, |
|
) |
|
|
|
// Tx 1 will be confirmed in block 9 and requires 2 confs. |
|
tx1 := wire.MsgTx{Version: 1} |
|
tx1.AddTxOut(&wire.TxOut{PkScript: testRawScript}) |
|
tx1Hash := tx1.TxHash() |
|
ntfn1, err := n.RegisterConf(&tx1Hash, testRawScript, tx1NumConfs, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register ntfn: %v", err) |
|
} |
|
|
|
err = n.UpdateConfDetails(ntfn1.HistoricalDispatch.ConfRequest, nil) |
|
if err != nil { |
|
t.Fatalf("unable to deliver conf details: %v", err) |
|
} |
|
|
|
// Tx 2 will be confirmed in block 10 and requires 1 conf. |
|
tx2 := wire.MsgTx{Version: 2} |
|
tx2.AddTxOut(&wire.TxOut{PkScript: testRawScript}) |
|
tx2Hash := tx2.TxHash() |
|
ntfn2, err := n.RegisterConf(&tx2Hash, testRawScript, tx2NumConfs, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register ntfn: %v", err) |
|
} |
|
|
|
err = n.UpdateConfDetails(ntfn2.HistoricalDispatch.ConfRequest, nil) |
|
if err != nil { |
|
t.Fatalf("unable to deliver conf details: %v", err) |
|
} |
|
|
|
// Tx 3 will be confirmed in block 10 and requires 2 confs. |
|
tx3 := wire.MsgTx{Version: 3} |
|
tx3.AddTxOut(&wire.TxOut{PkScript: testRawScript}) |
|
tx3Hash := tx3.TxHash() |
|
ntfn3, err := n.RegisterConf(&tx3Hash, testRawScript, tx3NumConfs, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register ntfn: %v", err) |
|
} |
|
|
|
err = n.UpdateConfDetails(ntfn3.HistoricalDispatch.ConfRequest, nil) |
|
if 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, |
|
Tx: &tx2, |
|
} |
|
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, |
|
Tx: &tx3, |
|
} |
|
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 := wire.OutPoint{Index: 1} |
|
spendTx1 := wire.NewMsgTx(2) |
|
spendTx1.AddTxIn(&wire.TxIn{ |
|
PreviousOutPoint: op1, |
|
SignatureScript: testSigScript, |
|
}) |
|
spendTxHash1 := spendTx1.TxHash() |
|
expectedSpendDetails1 := &chainntnfs.SpendDetail{ |
|
SpentOutPoint: &op1, |
|
SpenderTxHash: &spendTxHash1, |
|
SpendingTx: spendTx1, |
|
SpenderInputIndex: 0, |
|
SpendingHeight: startingHeight + 1, |
|
} |
|
|
|
op2 := wire.OutPoint{Index: 2} |
|
spendTx2 := wire.NewMsgTx(2) |
|
spendTx2.AddTxIn(&wire.TxIn{ |
|
PreviousOutPoint: chainntnfs.ZeroOutPoint, |
|
SignatureScript: testSigScript, |
|
}) |
|
spendTx2.AddTxIn(&wire.TxIn{ |
|
PreviousOutPoint: op2, |
|
SignatureScript: testSigScript, |
|
}) |
|
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, err := n.RegisterSpend(&op1, testRawScript, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register spend ntfn: %v", err) |
|
} |
|
|
|
ntfn2, err := n.RegisterSpend(&op2, testRawScript, 1) |
|
if 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} |
|
tx1.AddTxOut(&wire.TxOut{PkScript: testRawScript}) |
|
tx1Hash := tx1.TxHash() |
|
ntfn1, err := n.RegisterConf(&tx1Hash, testRawScript, 1, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register tx1: %v", err) |
|
} |
|
|
|
tx2 := wire.MsgTx{Version: 2} |
|
tx2.AddTxOut(&wire.TxOut{PkScript: testRawScript}) |
|
tx2Hash := tx2.TxHash() |
|
ntfn2, err := n.RegisterConf(&tx2Hash, testRawScript, 2, 1) |
|
if 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(ntfn1.HistoricalDispatch.ConfRequest) |
|
if err != chainntnfs.ErrConfirmHintNotFound { |
|
t.Fatalf("unexpected error when querying for height hint "+ |
|
"want: %v, got %v", |
|
chainntnfs.ErrConfirmHintNotFound, err) |
|
} |
|
|
|
_, err = hintCache.QueryConfirmHint(ntfn2.HistoricalDispatch.ConfRequest) |
|
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(ntfn1.HistoricalDispatch.ConfRequest) |
|
if err != chainntnfs.ErrConfirmHintNotFound { |
|
t.Fatalf("unexpected error when querying for height hint "+ |
|
"want: %v, got %v", |
|
chainntnfs.ErrConfirmHintNotFound, err) |
|
} |
|
|
|
hint, err = hintCache.QueryConfirmHint(ntfn2.HistoricalDispatch.ConfRequest) |
|
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. |
|
err = n.UpdateConfDetails(ntfn1.HistoricalDispatch.ConfRequest, nil) |
|
if err != nil { |
|
t.Fatalf("unable to update conf details: %v", err) |
|
} |
|
err = n.UpdateConfDetails(ntfn2.HistoricalDispatch.ConfRequest, nil) |
|
if 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(ntfn1.HistoricalDispatch.ConfRequest) |
|
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(ntfn2.HistoricalDispatch.ConfRequest) |
|
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(ntfn1.HistoricalDispatch.ConfRequest) |
|
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(ntfn2.HistoricalDispatch.ConfRequest) |
|
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(ntfn2.HistoricalDispatch.ConfRequest) |
|
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(ntfn2.HistoricalDispatch.ConfRequest) |
|
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{Index: 1} |
|
ntfn1, err := n.RegisterSpend(&op1, testRawScript, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register spend for op1: %v", err) |
|
} |
|
op2 := wire.OutPoint{Index: 2} |
|
ntfn2, err := n.RegisterSpend(&op2, testRawScript, 1) |
|
if 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(ntfn1.HistoricalDispatch.SpendRequest) |
|
if err != chainntnfs.ErrSpendHintNotFound { |
|
t.Fatalf("unexpected error when querying for height hint "+ |
|
"expected: %v, got %v", chainntnfs.ErrSpendHintNotFound, |
|
err) |
|
} |
|
_, err = hintCache.QuerySpendHint(ntfn2.HistoricalDispatch.SpendRequest) |
|
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(ntfn1.HistoricalDispatch.SpendRequest) |
|
if err != chainntnfs.ErrSpendHintNotFound { |
|
t.Fatalf("unexpected error when querying for height hint "+ |
|
"expected: %v, got %v", chainntnfs.ErrSpendHintNotFound, |
|
err) |
|
} |
|
_, err = hintCache.QuerySpendHint(ntfn2.HistoricalDispatch.SpendRequest) |
|
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. |
|
err = n.UpdateSpendDetails(ntfn1.HistoricalDispatch.SpendRequest, nil) |
|
if err != nil { |
|
t.Fatalf("unable to update spend details: %v", err) |
|
} |
|
err = n.UpdateSpendDetails(ntfn2.HistoricalDispatch.SpendRequest, nil) |
|
if 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: op1, |
|
SignatureScript: testSigScript, |
|
}) |
|
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.HistoricalDispatch.SpendRequest) |
|
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.HistoricalDispatch.SpendRequest) |
|
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: op2, |
|
SignatureScript: testSigScript, |
|
}) |
|
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.HistoricalDispatch.SpendRequest) |
|
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.HistoricalDispatch.SpendRequest) |
|
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.HistoricalDispatch.SpendRequest) |
|
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.HistoricalDispatch.SpendRequest) |
|
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) |
|
} |
|
} |
|
|
|
// TestTxNotifierNtfnDone ensures that a notification is sent to registered |
|
// clients through the Done channel once the notification request is no longer |
|
// under the risk of being reorged out of the chain. |
|
func TestTxNotifierNtfnDone(t *testing.T) { |
|
t.Parallel() |
|
|
|
hintCache := newMockHintCache() |
|
const reorgSafetyLimit = 100 |
|
n := chainntnfs.NewTxNotifier(10, reorgSafetyLimit, hintCache, hintCache) |
|
|
|
// We'll start by creating two notification requests: one confirmation |
|
// and one spend. |
|
confNtfn, err := n.RegisterConf(&chainntnfs.ZeroHash, testRawScript, 1, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register conf ntfn: %v", err) |
|
} |
|
spendNtfn, err := n.RegisterSpend(&chainntnfs.ZeroOutPoint, testRawScript, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register spend: %v", err) |
|
} |
|
|
|
// We'll create two transactions that will satisfy the notification |
|
// requests above and include them in the next block of the chain. |
|
tx := wire.NewMsgTx(1) |
|
tx.AddTxOut(&wire.TxOut{PkScript: testRawScript}) |
|
spendTx := wire.NewMsgTx(1) |
|
spendTx.AddTxIn(&wire.TxIn{ |
|
PreviousOutPoint: wire.OutPoint{Index: 1}, |
|
SignatureScript: testSigScript, |
|
}) |
|
block := btcutil.NewBlock(&wire.MsgBlock{ |
|
Transactions: []*wire.MsgTx{tx, 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) |
|
} |
|
|
|
// With the chain extended, we should see notifications dispatched for |
|
// both requests. |
|
select { |
|
case <-confNtfn.Event.Confirmed: |
|
default: |
|
t.Fatal("expected to receive confirmation notification") |
|
} |
|
|
|
select { |
|
case <-spendNtfn.Event.Spend: |
|
default: |
|
t.Fatal("expected to receive spend notification") |
|
} |
|
|
|
// The done notifications should not be dispatched yet as the requests |
|
// are still under the risk of being reorged out the chain. |
|
select { |
|
case <-confNtfn.Event.Done: |
|
t.Fatal("received unexpected done notification for confirmation") |
|
case <-spendNtfn.Event.Done: |
|
t.Fatal("received unexpected done notification for spend") |
|
default: |
|
} |
|
|
|
// Now, we'll disconnect the block at tip to simulate a reorg. The reorg |
|
// notifications should be dispatched to the respective clients. |
|
if err := n.DisconnectTip(11); err != nil { |
|
t.Fatalf("unable to disconnect block: %v", err) |
|
} |
|
|
|
select { |
|
case <-confNtfn.Event.NegativeConf: |
|
default: |
|
t.Fatal("expected to receive reorg notification for confirmation") |
|
} |
|
|
|
select { |
|
case <-spendNtfn.Event.Reorg: |
|
default: |
|
t.Fatal("expected to receive reorg notification for spend") |
|
} |
|
|
|
// We'll reconnect the block that satisfies both of these requests. |
|
// We should see notifications dispatched for both once again. |
|
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) |
|
} |
|
|
|
select { |
|
case <-confNtfn.Event.Confirmed: |
|
default: |
|
t.Fatal("expected to receive confirmation notification") |
|
} |
|
|
|
select { |
|
case <-spendNtfn.Event.Spend: |
|
default: |
|
t.Fatal("expected to receive spend notification") |
|
} |
|
|
|
// Finally, we'll extend the chain with blocks until the requests are no |
|
// longer under the risk of being reorged out of the chain. We should |
|
// expect the done notifications to be dispatched. |
|
nextHeight := uint32(12) |
|
for i := nextHeight; i < nextHeight+reorgSafetyLimit; i++ { |
|
dummyBlock := btcutil.NewBlock(&wire.MsgBlock{}) |
|
if err := n.ConnectTip(dummyBlock.Hash(), i, nil); err != nil { |
|
t.Fatalf("unable to connect block: %v", err) |
|
} |
|
} |
|
|
|
select { |
|
case <-confNtfn.Event.Done: |
|
default: |
|
t.Fatal("expected to receive done notification for confirmation") |
|
} |
|
|
|
select { |
|
case <-spendNtfn.Event.Done: |
|
default: |
|
t.Fatal("expected to receive done notification for spend") |
|
} |
|
} |
|
|
|
// 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, err := n.RegisterConf(&chainntnfs.ZeroHash, testRawScript, 1, 1) |
|
if err != nil { |
|
t.Fatalf("unable to register conf ntfn: %v", err) |
|
} |
|
spendNtfn, err := n.RegisterSpend(&chainntnfs.ZeroOutPoint, testRawScript, 1) |
|
if 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. |
|
_, err = n.RegisterConf(&chainntnfs.ZeroHash, testRawScript, 1, 1) |
|
if err == nil { |
|
t.Fatal("expected confirmation registration to fail") |
|
} |
|
_, err = n.RegisterSpend(&chainntnfs.ZeroOutPoint, testRawScript, 1) |
|
if 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) |
|
} |
|
if result.Tx.TxHash() != expected.Tx.TxHash() { |
|
t.Fatalf("expected tx hash %v, got %v", expected.Tx.TxHash(), |
|
result.Tx.TxHash()) |
|
} |
|
} |
|
|
|
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) |
|
} |
|
}
|
|
|