mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
staticaddr/loopin: keep htlc monitor resumable
After the client gives the server HTLC signatures, shutdown must not drive the monitor state through the generic error path. That path cancels the invoice and attempts to unlock deposits even though the server can still publish the HTLC. Return NoOp for monitor-state cancellation races and cover shutdown with a regression test that asserts no invoice cancellation or deposit unlock occurs.
This commit is contained in:
parent
0c0cee377b
commit
814af6aadf
2 changed files with 129 additions and 5 deletions
|
|
@ -565,6 +565,10 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
subscribeCtx, f.loopIn.SwapHash,
|
||||
)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return fsm.NoOp
|
||||
}
|
||||
|
||||
err = fmt.Errorf("unable to subscribe to swap "+
|
||||
"invoice: %w", err)
|
||||
|
||||
|
|
@ -592,6 +596,10 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
|
||||
htlcConfChan, htlcErrConfChan, err := registerHtlcConf()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return fsm.NoOp
|
||||
}
|
||||
|
||||
err = fmt.Errorf("unable to monitor htlc tx confirmation: %w",
|
||||
err)
|
||||
|
||||
|
|
@ -602,15 +610,23 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
registerBlocks := f.cfg.ChainNotifier.RegisterBlockEpochNtfn
|
||||
blockChan, blockChanErr, err := registerBlocks(ctx)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return fsm.NoOp
|
||||
}
|
||||
|
||||
err = fmt.Errorf("unable to subscribe to new blocks: %w", err)
|
||||
|
||||
return f.HandleError(err)
|
||||
}
|
||||
|
||||
htlcConfirmed := false
|
||||
|
||||
// Look up the current invoice state after registering subscriptions so
|
||||
// recovery can resume the payment deadline from the latest known state.
|
||||
invoice, err := f.cfg.LndClient.LookupInvoice(ctx, f.loopIn.SwapHash)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return fsm.NoOp
|
||||
}
|
||||
|
||||
err = fmt.Errorf("unable to look up invoice by swap hash: %w",
|
||||
err)
|
||||
|
||||
|
|
@ -625,8 +641,8 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
if invoice.State != invoices.ContractCanceled {
|
||||
// If the invoice is still live we set the timeout to the
|
||||
// remaining payment time. If too much time has elapsed, e.g.
|
||||
// after a restart, we set the timeout to 0 to cancel the
|
||||
// invoice and unlock the deposits immediately.
|
||||
// after a restart, we cancel the invoice immediately and keep
|
||||
// monitoring the HTLC until it can no longer confirm.
|
||||
remainingTimeSeconds := f.loopIn.RemainingPaymentTimeSeconds()
|
||||
|
||||
// If the invoice isn't cancelled yet and the payment timeout
|
||||
|
|
@ -658,6 +674,7 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
f.cancelSwapInvoice()
|
||||
}
|
||||
|
||||
htlcConfirmed := false
|
||||
for {
|
||||
select {
|
||||
case <-htlcConfChan:
|
||||
|
|
@ -666,6 +683,10 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
htlcConfirmed = true
|
||||
|
||||
case err = <-htlcErrConfChan:
|
||||
if ctx.Err() != nil {
|
||||
return fsm.NoOp
|
||||
}
|
||||
|
||||
f.Errorf("htlc tx conf chan error, re-registering: "+
|
||||
"%v", err)
|
||||
|
||||
|
|
@ -676,6 +697,10 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
// Re-register for htlc confirmation.
|
||||
htlcConfChan, htlcErrConfChan, err = registerHtlcConf()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return fsm.NoOp
|
||||
}
|
||||
|
||||
err = fmt.Errorf("unable to re-register for "+
|
||||
"htlc tx confirmation: %w", err)
|
||||
|
||||
|
|
@ -690,6 +715,10 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
|
||||
htlcConfChan, htlcErrConfChan, err = registerHtlcConf()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return fsm.NoOp
|
||||
}
|
||||
|
||||
err = fmt.Errorf("unable to monitor htlc tx "+
|
||||
"confirmation: %v", err)
|
||||
|
||||
|
|
@ -761,6 +790,10 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
return OnSweepHtlcTimeout
|
||||
|
||||
case err = <-blockChanErr:
|
||||
if ctx.Err() != nil {
|
||||
return fsm.NoOp
|
||||
}
|
||||
|
||||
f.Errorf("block subscription error: %v", err)
|
||||
|
||||
return f.HandleError(err)
|
||||
|
|
@ -784,7 +817,7 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context,
|
|||
f.Errorf("invoice subscription error: %v", err)
|
||||
|
||||
case <-ctx.Done():
|
||||
return f.HandleError(ctx.Err())
|
||||
return fsm.NoOp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -207,6 +207,97 @@ func TestMonitorInvoiceAndHtlcTxReRegistersOnConfErr(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestMonitorInvoiceAndHtlcTxNoOpOnShutdown ensures that a shutdown while the
|
||||
// client is monitoring an HTLC-signed loop-in keeps the swap resumable instead
|
||||
// of entering the generic unlock path.
|
||||
func TestMonitorInvoiceAndHtlcTxNoOpOnShutdown(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
runCtx, stop := context.WithCancel(ctx)
|
||||
|
||||
mockLnd := test.NewMockLnd()
|
||||
|
||||
clientKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
serverKey, err := btcec.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
swapHash := lntypes.Hash{4, 5, 6}
|
||||
loopIn := &StaticAddressLoopIn{
|
||||
SwapHash: swapHash,
|
||||
HtlcCltvExpiry: 2_000,
|
||||
InitiationHeight: uint32(mockLnd.Height),
|
||||
InitiationTime: time.Now(),
|
||||
ProtocolVersion: version.ProtocolVersion_V0,
|
||||
ClientPubkey: clientKey.PubKey(),
|
||||
ServerPubkey: serverKey.PubKey(),
|
||||
PaymentTimeoutSeconds: 3_600,
|
||||
}
|
||||
loopIn.SetState(MonitorInvoiceAndHtlcTx)
|
||||
|
||||
mockLnd.Invoices[swapHash] = &lndclient.Invoice{
|
||||
Hash: swapHash,
|
||||
State: invoices.ContractOpen,
|
||||
}
|
||||
|
||||
depositMgr := &recordingDepositManager{}
|
||||
cfg := &Config{
|
||||
AddressManager: &mockAddressManager{
|
||||
params: &script.Parameters{
|
||||
ClientPubkey: clientKey.PubKey(),
|
||||
ServerPubkey: serverKey.PubKey(),
|
||||
ProtocolVersion: version.ProtocolVersion_V0,
|
||||
},
|
||||
},
|
||||
ChainNotifier: mockLnd.ChainNotifier,
|
||||
DepositManager: depositMgr,
|
||||
InvoicesClient: mockLnd.LndServices.Invoices,
|
||||
LndClient: mockLnd.Client,
|
||||
ChainParams: mockLnd.ChainParams,
|
||||
}
|
||||
|
||||
f, err := NewFSM(runCtx, loopIn, cfg, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
resultChan := make(chan fsm.EventType, 1)
|
||||
go func() {
|
||||
resultChan <- f.MonitorInvoiceAndHtlcTxAction(runCtx, nil)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-mockLnd.SingleInvoiceSubcribeChannel:
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("invoice subscription not registered: %v", ctx.Err())
|
||||
}
|
||||
|
||||
select {
|
||||
case <-mockLnd.RegisterConfChannel:
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("htlc conf registration not received: %v", ctx.Err())
|
||||
}
|
||||
|
||||
stop()
|
||||
|
||||
select {
|
||||
case event := <-resultChan:
|
||||
require.Equal(t, fsm.NoOp, event)
|
||||
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("monitor action did not exit: %v", ctx.Err())
|
||||
}
|
||||
|
||||
require.Nil(t, f.LastActionError)
|
||||
require.Empty(t, depositMgr.transitions)
|
||||
|
||||
select {
|
||||
case hash := <-mockLnd.FailInvoiceChannel:
|
||||
t.Fatalf("invoice canceled on shutdown: %v", hash)
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// TestSweepHtlcTimeoutActionNoOpOnShutdown ensures that a shutdown during
|
||||
// timeout sweep publication keeps the FSM in the same state so it can resume
|
||||
// after restart.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue