@@ -7,13 +7,15 @@ import (
77 "time"
88
99 "github.com/btcsuite/btcd/btcec/v2"
10+ "github.com/btcsuite/btcd/wire"
1011 "github.com/lightninglabs/lndclient"
1112 "github.com/lightninglabs/loop/fsm"
1213 "github.com/lightninglabs/loop/staticaddr/address"
1314 "github.com/lightninglabs/loop/staticaddr/deposit"
1415 "github.com/lightninglabs/loop/staticaddr/script"
1516 "github.com/lightninglabs/loop/staticaddr/version"
1617 "github.com/lightninglabs/loop/test"
18+ "github.com/lightningnetwork/lnd/chainntnfs"
1719 "github.com/lightningnetwork/lnd/invoices"
1820 "github.com/lightningnetwork/lnd/lntypes"
1921 "github.com/stretchr/testify/require"
@@ -204,6 +206,122 @@ func TestMonitorInvoiceAndHtlcTxInvoiceErr(t *testing.T) {
204206 }
205207}
206208
209+ // TestMonitorInvoiceAndHtlcTxStaleConfirmation verifies we clear a stale
210+ // htlcConfirmed flag across a re-registration after a confirmation stream
211+ // error. Without the fix we would think the HTLC is confirmed and sweep on
212+ // timeout even though the confirmation was lost (e.g. due to a reorg).
213+ func TestMonitorInvoiceAndHtlcTxStaleConfirmation (t * testing.T ) {
214+ ctx , cancel := context .WithTimeout (t .Context (), 5 * time .Second )
215+ defer cancel ()
216+
217+ mockLnd := test .NewMockLnd ()
218+ // Start below the HTLC expiry so the first block notification doesn't
219+ // trigger timeout immediately.
220+ mockLnd .Height = 1
221+
222+ clientKey , err := btcec .NewPrivateKey ()
223+ require .NoError (t , err )
224+ serverKey , err := btcec .NewPrivateKey ()
225+ require .NoError (t , err )
226+
227+ swapHash := lntypes.Hash {7 , 7 , 7 }
228+
229+ loopIn := & StaticAddressLoopIn {
230+ SwapHash : swapHash ,
231+ HtlcCltvExpiry : 20 ,
232+ InitiationHeight : uint32 (mockLnd .Height ),
233+ InitiationTime : time .Now (),
234+ ProtocolVersion : version .ProtocolVersion_V0 ,
235+ ClientPubkey : clientKey .PubKey (),
236+ ServerPubkey : serverKey .PubKey (),
237+ PaymentTimeoutSeconds : 3_600 ,
238+ }
239+ loopIn .SetState (MonitorInvoiceAndHtlcTx )
240+
241+ mockLnd .Invoices [swapHash ] = & lndclient.Invoice {
242+ Hash : swapHash ,
243+ State : invoices .ContractOpen ,
244+ }
245+
246+ cfg := & Config {
247+ AddressManager : & mockAddressManager {
248+ params : & address.Parameters {
249+ ClientPubkey : clientKey .PubKey (),
250+ ServerPubkey : serverKey .PubKey (),
251+ ProtocolVersion : version .ProtocolVersion_V0 ,
252+ },
253+ },
254+ ChainNotifier : mockLnd .ChainNotifier ,
255+ DepositManager : & noopDepositManager {},
256+ InvoicesClient : mockLnd .LndServices .Invoices ,
257+ LndClient : mockLnd .Client ,
258+ ChainParams : mockLnd .ChainParams ,
259+ }
260+
261+ f , err := NewFSM (ctx , loopIn , cfg , false )
262+ require .NoError (t , err )
263+
264+ resultChan := make (chan fsm.EventType , 1 )
265+ go func () {
266+ resultChan <- f .MonitorInvoiceAndHtlcTxAction (ctx , nil )
267+ }()
268+
269+ // Wait for invoice and confirmation subscriptions to be registered.
270+ select {
271+ case <- mockLnd .SingleInvoiceSubcribeChannel :
272+ case <- ctx .Done ():
273+ t .Fatalf ("invoice subscription not registered: %v" , ctx .Err ())
274+ }
275+
276+ var firstReg * test.ConfRegistration
277+ select {
278+ case firstReg = <- mockLnd .RegisterConfChannel :
279+ case <- ctx .Done ():
280+ t .Fatalf ("htlc conf registration not received: %v" , ctx .Err ())
281+ }
282+
283+ // Deliver a confirmation directly on the registration channel to set
284+ // htlcConfirmed = true.
285+ firstReg .ConfChan <- & chainntnfs.TxConfirmation {
286+ Tx : & wire.MsgTx {
287+ TxOut : []* wire.TxOut {
288+ {
289+ PkScript : firstReg .PkScript ,
290+ },
291+ },
292+ },
293+ }
294+
295+ // Ensure the confirmation was consumed by the action before injecting
296+ // an error.
297+ require .Eventually (t , func () bool {
298+ return len (firstReg .ConfChan ) == 0
299+ }, time .Second , time .Millisecond * 10 )
300+
301+ // Error the conf stream to force a re-registration; htlcConfirmed must
302+ // be cleared, otherwise we'd wrongly sweep on timeout.
303+ firstReg .ErrChan <- errors .New ("conf stream error" )
304+
305+ var secondReg * test.ConfRegistration
306+ select {
307+ case secondReg = <- mockLnd .RegisterConfChannel :
308+ case <- ctx .Done ():
309+ t .Fatalf ("htlc conf was not re-registered: %v" , ctx .Err ())
310+ }
311+ require .NotEqual (t , firstReg , secondReg )
312+
313+ // Advance chain past the HTLC expiry. With stale htlcConfirmed this
314+ // would take the sweep branch; correct behavior is to time out.
315+ require .NoError (t , mockLnd .NotifyHeight (loopIn .HtlcCltvExpiry + 1 ))
316+
317+ select {
318+ case event := <- resultChan :
319+ require .Equal (t , OnSwapTimedOut , event )
320+ case <- time .After (time .Second ):
321+ t .Fatalf ("expected swap timeout due to stale conf, got timeout" )
322+ }
323+ }
324+
207325// mockAddressManager is a minimal AddressManager implementation used by the
208326// test FSM setup.
209327type mockAddressManager struct {
0 commit comments