chainntnfs: signal reorg notification before required confirmations
What changed, and why it matters
This change fixes a notification bug in LND's transaction confirmation tracker. Previously, if a transaction had been seen in a block but had not yet reached the required number of confirmations, and then that block was reorged (removed from the chain), subscribers were not told about the reorg. Now they are notified earlier, so downstream logic can react to the lost confirmation instead of waiting for a confirmation that may never come.
Review callers of ConfNtfn.Event.NegativeConf to confirm they correctly handle early reorg notifications and do not assume a prior Confirmed event was emitted. Include this patch in normal release testing.
Security signals we found
Notification completeness fix for reorganization handling
Changes event ordering in reorg dispatch path
Adds unit test covering partial-confirmation reorg scenario
Evidence from the diff
In chainntnfs/txnotifier.go, dispatchConfReorg now sends on ntfn.Event.NegativeConf before checking whether the confirmation height entry exists and deleting it. The new select sends the reorg depth even when the transaction has not yet reached ntfn.NumConfirmations. A test, TestTxNotifierReorgPartialConfirmation, verifies that a tx registered for 2 confirmations, included in one block, then reorged out, emits a NegativeConf event without emitting a Confirmed event.
Changed components
chainntnfs/txnotifier.gochainntnfs/txnotifier_test.goInspect captured patch +64 / −0
diff --git a/chainntnfs/txnotifier.go b/chainntnfs/txnotifier.go
index 8b5b8dc..9055d85 100644
--- a/chainntnfs/txnotifier.go
+++ b/chainntnfs/txnotifier.go
@@ -2011,6 +2011,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):
+ case <-n.quit:
+ return ErrTxNotifierExiting
+ }
+
if exists {
delete(ntfnSet, ntfn)
}
diff --git a/chainntnfs/txnotifier_test.go b/chainntnfs/txnotifier_test.go
index c7bc8da..1279330 100644
--- a/chainntnfs/txnotifier_test.go
+++ b/chainntnfs/txnotifier_test.go
@@ -1548,6 +1548,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.
Why this scored 32/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.