Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions chainntnfs/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,20 @@ type TxConfirmation struct {
Block *wire.MsgBlock
}

// TxUpdateInfo contains information about a transaction before it has reached
// its required number of confirmations. Transactions are registered for
// notification for a specific number of "required" confirmations, this struct
// will update the caller incrementally after each new block is found as long as
// the transaction is not yet fully regarded as confirmed.
type TxUpdateInfo struct {
// BlockHeight is the height of the block that contains the transaction.
BlockHeight uint32

// NumConfsLeft is the number of confirmations left for the transaction
// to be regarded as fully confirmed.
NumConfsLeft uint32
}

// ConfirmationEvent encapsulates a confirmation notification. With this struct,
// callers can be notified of: the instance the target txid reaches the targeted
// number of confirmations, how many confirmations are left for the target txid
Expand All @@ -229,11 +243,12 @@ type ConfirmationEvent struct {

// Updates is a channel that will sent upon, at every incremental
// confirmation, how many confirmations are left to declare the
// transaction as fully confirmed.
// transaction as fully confirmed, along with the height of the block
// that contains the transaction.
//
// NOTE: This channel must be buffered with the number of required
// confirmations.
Updates chan uint32
Updates chan TxUpdateInfo

// NegativeConf is a channel that will be sent upon if the transaction
// confirms, but is later reorged out of the chain. The integer sent
Expand Down Expand Up @@ -262,7 +277,7 @@ func NewConfirmationEvent(numConfs uint32, cancel func()) *ConfirmationEvent {
// the channel so we need to create a larger buffer to avoid
// blocking the notifier.
Confirmed: make(chan *TxConfirmation, 1),
Updates: make(chan uint32, numConfs),
Updates: make(chan TxUpdateInfo, numConfs),
NegativeConf: make(chan int32, 1),
Done: make(chan struct{}, 1),
Cancel: cancel,
Expand Down
54 changes: 42 additions & 12 deletions chainntnfs/txnotifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -947,8 +947,12 @@ func (n *TxNotifier) dispatchConfDetails(

// We'll send a 0 value to the Updates channel,
// indicating that the transaction/output script has already
// been confirmed.
err := n.notifyNumConfsLeft(ntfn, 0)
// been confirmed, and include the block height at which the
// transaction was included.
err := n.notifyNumConfsLeft(ntfn, TxUpdateInfo{
NumConfsLeft: 0,
BlockHeight: details.BlockHeight,
})
if err != nil {
return err
}
Expand Down Expand Up @@ -977,7 +981,10 @@ func (n *TxNotifier) dispatchConfDetails(
// confirmations are left for the transaction/output script to
// be confirmed.
numConfsLeft := confHeight - n.currentHeight
err := n.notifyNumConfsLeft(ntfn, numConfsLeft)
err := n.notifyNumConfsLeft(ntfn, TxUpdateInfo{
NumConfsLeft: numConfsLeft,
BlockHeight: details.BlockHeight,
})
if err != nil {
return err
}
Expand Down Expand Up @@ -1731,7 +1738,10 @@ func (n *TxNotifier) NotifyHeight(height uint32) error {
for confRequest := range confRequests {
confSet := n.confNotifications[confRequest]
for _, ntfn := range confSet.ntfns {
txConfHeight := confSet.details.BlockHeight +
// blockHeight is the height of the block which
// contains the transaction.
blockHeight := confSet.details.BlockHeight
txConfHeight := blockHeight +
ntfn.NumConfirmations - 1
numConfsLeft := txConfHeight - height

Expand All @@ -1744,7 +1754,10 @@ func (n *TxNotifier) NotifyHeight(height uint32) error {
continue
}

err := n.notifyNumConfsLeft(ntfn, numConfsLeft)
err := n.notifyNumConfsLeft(ntfn, TxUpdateInfo{
NumConfsLeft: numConfsLeft,
BlockHeight: blockHeight,
})
if err != nil {
return err
}
Expand Down Expand Up @@ -2011,6 +2024,20 @@ func (n *TxNotifier) dispatchConfReorg(ntfn *ConfNtfn,
if !ntfn.dispatched {
confHeight := heightDisconnected + ntfn.NumConfirmations - 1
ntfnSet, exists := n.ntfnsByConfirmHeight[confHeight]

// We also signal the reorg to the notifier in case the
// subscriber is also interested in the reorgs before the
// transaction received its required confirmation.
//
// Because as soon as a new block is connected which has the
// transaction included again we preemptively read the buffered
// channel.
select {
case ntfn.Event.NegativeConf <- int32(n.reorgDepth):
Comment on lines +2032 to +2036
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this doesn't block because we will only send on the channel once on reorg of the funding tx's block and it will be emptied by ConnectTip -> handleConfDetailsAtTip

case <-n.quit:
return ErrTxNotifierExiting
}

if exists {
delete(ntfnSet, ntfn)
}
Expand Down Expand Up @@ -2099,25 +2126,28 @@ func (n *TxNotifier) TearDown() {
}

// notifyNumConfsLeft sends the number of confirmations left to the
// notification subscriber through the Event.Updates channel.
// notification subscriber through the Event.Updates channel, along with the
// block height in which the transaction was included.
//
// NOTE: must be used with the TxNotifier's lock held.
func (n *TxNotifier) notifyNumConfsLeft(ntfn *ConfNtfn, num uint32) error {
func (n *TxNotifier) notifyNumConfsLeft(ntfn *ConfNtfn,
info TxUpdateInfo) error {

// If the number left is no less than the recorded value, we can skip
// sending it as it means this same value has already been sent before.
if num >= ntfn.numConfsLeft {
if info.NumConfsLeft >= ntfn.numConfsLeft {
Log.Debugf("Skipped dispatched update (numConfsLeft=%v) for "+
"request %v conf_id=%v", num, ntfn.ConfRequest,
ntfn.ConfID)
"request %v conf_id=%v", info.NumConfsLeft,
ntfn.ConfRequest, ntfn.ConfID)

return nil
}

// Update the number of confirmations left to the notification.
ntfn.numConfsLeft = num
ntfn.numConfsLeft = info.NumConfsLeft

select {
case ntfn.Event.Updates <- num:
case ntfn.Event.Updates <- info:
case <-n.quit:
return ErrTxNotifierExiting
}
Expand Down
140 changes: 91 additions & 49 deletions chainntnfs/txnotifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,13 +271,12 @@ func TestTxNotifierFutureConfDispatch(t *testing.T) {
// 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)
case updDetails := <-ntfn1.Event.Updates:
expected := chainntnfs.TxUpdateInfo{
NumConfsLeft: 0,
BlockHeight: 11,
}
require.Equal(t, expected, updDetails)
default:
t.Fatal("Expected confirmation update for tx1")
}
Expand All @@ -300,13 +299,12 @@ func TestTxNotifierFutureConfDispatch(t *testing.T) {
// 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)
case updDetails := <-ntfn2.Event.Updates:
expected := chainntnfs.TxUpdateInfo{
NumConfsLeft: 1,
BlockHeight: 11,
}
require.Equal(t, expected, updDetails)
default:
t.Fatal("Expected confirmation update for tx2")
}
Expand Down Expand Up @@ -341,13 +339,12 @@ func TestTxNotifierFutureConfDispatch(t *testing.T) {
// 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)
case updDetails := <-ntfn2.Event.Updates:
expected := chainntnfs.TxUpdateInfo{
NumConfsLeft: 0,
BlockHeight: 11,
}
require.Equal(t, expected, updDetails)
default:
t.Fatal("Expected confirmation update for tx2")
}
Expand Down Expand Up @@ -411,13 +408,12 @@ func TestTxNotifierHistoricalConfDispatch(t *testing.T) {
err = n.UpdateConfDetails(ntfn1.HistoricalDispatch.ConfRequest, &txConf1)
require.NoError(t, err, "unable to update conf details")
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)
case updDetails := <-ntfn1.Event.Updates:
expected := chainntnfs.TxUpdateInfo{
NumConfsLeft: 0,
BlockHeight: 9,
}
require.Equal(t, expected, updDetails)
default:
t.Fatal("Expected confirmation update for tx1")
}
Expand All @@ -443,13 +439,12 @@ func TestTxNotifierHistoricalConfDispatch(t *testing.T) {
err = n.UpdateConfDetails(ntfn2.HistoricalDispatch.ConfRequest, &txConf2)
require.NoError(t, err, "unable to update conf details")
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)
case updDetails := <-ntfn2.Event.Updates:
expected := chainntnfs.TxUpdateInfo{
NumConfsLeft: 1,
BlockHeight: 9,
}
require.Equal(t, expected, updDetails)
default:
t.Fatal("Expected confirmation update for tx2")
}
Expand Down Expand Up @@ -485,13 +480,12 @@ func TestTxNotifierHistoricalConfDispatch(t *testing.T) {
// 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)
case updDetails := <-ntfn2.Event.Updates:
expected := chainntnfs.TxUpdateInfo{
NumConfsLeft: 0,
BlockHeight: 9,
}
require.Equal(t, expected, updDetails)
default:
t.Fatal("Expected confirmation update for tx2")
}
Expand Down Expand Up @@ -1490,13 +1484,12 @@ func TestTxNotifierConfReorg(t *testing.T) {
// 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)
case updDetails := <-ntfn2.Event.Updates:
expected := chainntnfs.TxUpdateInfo{
NumConfsLeft: 0,
BlockHeight: 12,
}
require.Equal(t, expected, updDetails)
default:
t.Fatal("Expected confirmation update for tx2")
}
Expand All @@ -1520,15 +1513,14 @@ func TestTxNotifierConfReorg(t *testing.T) {
// 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)
case updDetails := <-ntfn3.Event.Updates:
expected := chainntnfs.TxUpdateInfo{
NumConfsLeft: tx3NumConfs - i,
BlockHeight: 12,
}
require.Equal(t, expected, updDetails)
default:
t.Fatal("Expected confirmation update for tx2")
t.Fatal("Expected confirmation update for tx3")
}
}

Expand All @@ -1548,6 +1540,56 @@ func TestTxNotifierConfReorg(t *testing.T) {
}
}

// TestTxNotifierReorgPartialConfirmation ensures that a tx with intermediate
// confirmations handles a reorg correctly and emits the appropriate reorg ntfn.
func TestTxNotifierReorgPartialConfirmation(t *testing.T) {
t.Parallel()

const txNumConfs uint32 = 2
hintCache := newMockHintCache()
n := chainntnfs.NewTxNotifier(
7, chainntnfs.ReorgSafetyLimit, hintCache, hintCache,
)

// Tx will be confirmed in block 9 and requires 2 confs.
tx := wire.MsgTx{Version: 1}
tx.AddTxOut(&wire.TxOut{PkScript: testRawScript})
txHash := tx.TxHash()
ntfn, err := n.RegisterConf(&txHash, testRawScript, txNumConfs, 1)
require.NoError(t, err, "unable to register ntfn")

err = n.UpdateConfDetails(ntfn.HistoricalDispatch.ConfRequest, nil)
require.NoError(t, err, "unable to deliver conf details")

// Mine 1 block to satisfy the requirement for a partially confirmed tx.
block := btcutil.NewBlock(&wire.MsgBlock{
Transactions: []*wire.MsgTx{&tx},
})
err = n.ConnectTip(block, 8)
require.NoError(t, err, "failed to connect block")
err = n.NotifyHeight(8)
require.NoError(t, err, "unable to dispatch notifications")

// Now that the transaction is partially confirmed, reorg out those
// blocks.
err = n.DisconnectTip(8)
require.NoError(t, err, "unable to disconnect block")

// After the intermediate confirmation is reorged out, the tx should not
// trigger a confirmation ntfn, but should trigger a reorg ntfn.
select {
case <-ntfn.Event.Confirmed:
t.Fatal("unexpected confirmation after reorg")
default:
}

select {
case <-ntfn.Event.NegativeConf:
default:
t.Fatal("expected to receive reorg notification")
}
}

// 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.
Expand Down
Loading
Loading