Skip to content

Commit d9d85cf

Browse files
committed
contractcourt: add sync dispatch fast-path for single confirmation closes
In this commit, we add a fast-path optimization to the chain watcher's closeObserver that immediately dispatches close events when only a single confirmation is required (numConfs == 1). This addresses a timing issue with integration tests that were designed around the old synchronous blockbeat behavior, where close events were dispatched immediately upon spend detection. The recent async confirmation architecture (introduced in commit f6f716a) properly handles reorgs by waiting for N confirmations before dispatching close events. However, this created a race condition in integration tests that mine blocks synchronously and expect immediate close notifications. With the build tag setting numConfs to 1 for itests, the async confirmation notification could arrive after the test already started waiting for the close event, causing timeouts. We introduce a new handleSpendDispatch method that checks if numConfs == 1 and, if so, immediately calls handleCommitSpend to dispatch the close event synchronously, then returns true to skip the async state machine. This preserves the old behavior for integration tests while maintaining the full async reorg protection for production (where numConfs >= 3). The implementation adds the fast-path check in both spend detection paths (blockbeat and spend notification) to ensure consistent behavior regardless of which detects the spend first. We also update the affected unit tests to remove their expectation of confirmation registration, since the fast-path bypasses that step entirely. This approach optimizes for the integration test scenario without compromising production safety, as the fast-path only activates when a single confirmation is sufficient - a configuration that only exists in the controlled test environment.
1 parent 0f1eea1 commit d9d85cf

File tree

2 files changed

+52
-8
lines changed

2 files changed

+52
-8
lines changed

contractcourt/chain_watcher.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,13 @@ func newChainSet(chanState *channeldb.OpenChannel) (*chainSet, error) {
693693
// - Pending (confNtfn != nil): Spend detected, waiting for N confirmations
694694
//
695695
// - Confirmed: Spend confirmed with N blocks, close has been processed
696+
//
697+
// For single-confirmation scenarios (numConfs == 1), we bypass the async state
698+
// machine and immediately dispatch close events upon spend detection. This
699+
// provides synchronous behavior for integration tests which expect immediate
700+
// notifications. For multi-confirmation scenarios (production with
701+
// numConfs >= 3),
702+
// we use the full async state machine with reorg protection.
696703
func (c *chainWatcher) closeObserver() {
697704
defer c.wg.Done()
698705

@@ -744,6 +751,7 @@ func (c *chainWatcher) closeObserver() {
744751
"duplicate spend detection for tx %v",
745752
c.cfg.chanState.FundingOutpoint,
746753
spend.SpenderTxHash)
754+
747755
return confNtfn, nil
748756
}
749757

@@ -803,6 +811,13 @@ func (c *chainWatcher) closeObserver() {
803811
continue
804812
}
805813

814+
// FAST PATH: Check if we should dispatch immediately
815+
// for single-confirmation scenarios.
816+
if c.handleSpendDispatch(spend, "blockbeat") {
817+
continue
818+
}
819+
820+
// ASYNC PATH: Multiple confirmations (production).
806821
// STATE TRANSITION: None -> Pending (from blockbeat).
807822
log.Infof("ChannelPoint(%v): detected spend from "+
808823
"blockbeat, transitioning to %v",
@@ -826,6 +841,13 @@ func (c *chainWatcher) closeObserver() {
826841
return
827842
}
828843

844+
// FAST PATH: Check if we should dispatch immediately
845+
// for single-confirmation scenarios.
846+
if c.handleSpendDispatch(spend, "spend notification") {
847+
continue
848+
}
849+
850+
// ASYNC PATH: Multiple confirmations (production).
829851
log.Infof("ChannelPoint(%v): detected spend from "+
830852
"notification, transitioning to %v",
831853
c.cfg.chanState.FundingOutpoint,
@@ -1584,6 +1606,30 @@ func deriveFundingPkScript(chanState *channeldb.OpenChannel) ([]byte, error) {
15841606
return fundingPkScript, nil
15851607
}
15861608

1609+
// handleSpendDispatch processes a detected spend. For single-confirmation
1610+
// scenarios (numConfs == 1), it immediately dispatches the close event and
1611+
// returns true. For multi-confirmation scenarios, it returns false, indicating
1612+
// the caller should proceed with the async state machine.
1613+
func (c *chainWatcher) handleSpendDispatch(spend *chainntnfs.SpendDetail,
1614+
source string) bool {
1615+
1616+
numConfs := c.requiredConfsForSpend()
1617+
if numConfs == 1 {
1618+
log.Infof("ChannelPoint(%v): single confirmation mode, "+
1619+
"dispatching immediately from %s",
1620+
c.cfg.chanState.FundingOutpoint, source)
1621+
1622+
err := c.handleCommitSpend(spend)
1623+
if err != nil {
1624+
log.Errorf("Failed to handle commit spend: %v", err)
1625+
}
1626+
1627+
return true
1628+
}
1629+
1630+
return false
1631+
}
1632+
15871633
// handleCommitSpend takes a spending tx of the funding output and handles the
15881634
// channel close based on the closure type.
15891635
func (c *chainWatcher) handleCommitSpend(

contractcourt/chain_watcher_test.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,9 @@ func TestChainWatcherRemoteUnilateralClose(t *testing.T) {
9494
t.Fatalf("unable to send blockbeat")
9595
}
9696

97-
// Wait for the chain watcher to register for confirmations and send
98-
// the confirmation. Since we set chanCloseConfs to 1, one confirmation
99-
// is sufficient.
100-
aliceNotifier.WaitForConfRegistrationAndSend(t)
97+
// With chanCloseConfs set to 1, the fast-path dispatches immediately
98+
// without confirmation registration. The close event should arrive
99+
// directly after processing the blockbeat.
101100

102101
// We should get a new spend event over the remote unilateral close
103102
// event channel.
@@ -231,10 +230,9 @@ func TestChainWatcherRemoteUnilateralClosePendingCommit(t *testing.T) {
231230
t.Fatalf("unable to send blockbeat")
232231
}
233232

234-
// Wait for the chain watcher to register for confirmations and send
235-
// the confirmation. Since we set chanCloseConfs to 1, one confirmation
236-
// is sufficient.
237-
aliceNotifier.WaitForConfRegistrationAndSend(t)
233+
// With chanCloseConfs set to 1, the fast-path dispatches immediately
234+
// without confirmation registration. The close event should arrive
235+
// directly after processing the blockbeat.
238236

239237
// We should get a new spend event over the remote unilateral close
240238
// event channel.

0 commit comments

Comments
 (0)