invoices: fix htlc timer deadlock

This commit is contained in:
Joost Jager 2020-02-04 10:07:56 +01:00
parent 51324ac7ae
commit 0042a1ffeb
No known key found for this signature in database
GPG Key ID: A61B9D4C393C59C7

@ -807,8 +807,53 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
} }
} }
// Execute locked notify exit hop logic.
i.Lock() i.Lock()
defer i.Unlock() resolution, err := i.notifyExitHopHtlcLocked(&updateCtx, hodlChan)
i.Unlock()
if err != nil {
return nil, err
}
switch r := resolution.(type) {
// A direct resolution was received for this htlc.
case *HtlcResolution:
return r, nil
// The htlc is held. Start a timer outside the lock if the htlc should
// be auto-released, because otherwise a deadlock may happen with the
// main event loop.
case *acceptResolution:
if r.autoRelease {
err := i.startHtlcTimer(rHash, circuitKey, r.acceptTime)
if err != nil {
return nil, err
}
}
return nil, nil
default:
return nil, errors.New("invalid resolution type")
}
}
// acceptResolution is returned when the htlc should be held.
type acceptResolution struct {
// autoRelease signals that the htlc should be automatically released
// after a timeout.
autoRelease bool
// acceptTime is the time at which this htlc was accepted.
acceptTime time.Time
}
// notifyExitHopHtlcLocked is the internal implementation of NotifyExitHopHtlc
// that should be executed inside the registry lock.
func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
ctx *invoiceUpdateCtx, hodlChan chan<- interface{}) (
interface{}, error) {
// We'll attempt to settle an invoice matching this rHash on disk (if // We'll attempt to settle an invoice matching this rHash on disk (if
// one exists). The callback will update the invoice state and/or htlcs. // one exists). The callback will update the invoice state and/or htlcs.
@ -817,11 +862,11 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
updateSubscribers bool updateSubscribers bool
) )
invoice, err := i.cdb.UpdateInvoice( invoice, err := i.cdb.UpdateInvoice(
rHash, ctx.hash,
func(inv *channeldb.Invoice) ( func(inv *channeldb.Invoice) (
*channeldb.InvoiceUpdateDesc, error) { *channeldb.InvoiceUpdateDesc, error) {
updateDesc, res, err := updateInvoice(&updateCtx, inv) updateDesc, res, err := updateInvoice(ctx, inv)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -841,29 +886,30 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
// If the invoice was not found, return a failure resolution // If the invoice was not found, return a failure resolution
// with an invoice not found result. // with an invoice not found result.
return NewFailureResolution( return NewFailureResolution(
circuitKey, currentHeight, ResultInvoiceNotFound, ctx.circuitKey, ctx.currentHeight,
ResultInvoiceNotFound,
), nil ), nil
case nil: case nil:
default: default:
updateCtx.log(err.Error()) ctx.log(err.Error())
return nil, err return nil, err
} }
updateCtx.log(result.String()) ctx.log(result.String())
if updateSubscribers { if updateSubscribers {
i.notifyClients(rHash, invoice, invoice.State) i.notifyClients(ctx.hash, invoice, invoice.State)
} }
// Inspect latest htlc state on the invoice. // Inspect latest htlc state on the invoice.
invoiceHtlc, ok := invoice.Htlcs[circuitKey] invoiceHtlc, ok := invoice.Htlcs[ctx.circuitKey]
// If it isn't recorded, cancel htlc. // If it isn't recorded, cancel htlc.
if !ok { if !ok {
return NewFailureResolution( return NewFailureResolution(
circuitKey, currentHeight, result, ctx.circuitKey, ctx.currentHeight, result,
), nil ), nil
} }
@ -876,7 +922,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
switch invoiceHtlc.State { switch invoiceHtlc.State {
case channeldb.HtlcStateCanceled: case channeldb.HtlcStateCanceled:
return NewFailureResolution( return NewFailureResolution(
circuitKey, acceptHeight, result, ctx.circuitKey, acceptHeight, result,
), nil ), nil
case channeldb.HtlcStateSettled: case channeldb.HtlcStateSettled:
@ -902,27 +948,25 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
} }
resolution := NewSettleResolution( resolution := NewSettleResolution(
invoice.Terms.PaymentPreimage, circuitKey, invoice.Terms.PaymentPreimage, ctx.circuitKey,
acceptHeight, result, acceptHeight, result,
) )
return resolution, nil return resolution, nil
case channeldb.HtlcStateAccepted: case channeldb.HtlcStateAccepted:
// (Re)start the htlc timer if the invoice is still open. It can var resolution acceptResolution
// Auto-release the htlc if the invoice is still open. It can
// only happen for mpp payments that there are htlcs in state // only happen for mpp payments that there are htlcs in state
// Accepted while the invoice is Open. // Accepted while the invoice is Open.
if invoice.State == channeldb.ContractOpen { if invoice.State == channeldb.ContractOpen {
err := i.startHtlcTimer( resolution.acceptTime = invoiceHtlc.AcceptTime
rHash, circuitKey, resolution.autoRelease = true
invoiceHtlc.AcceptTime,
)
if err != nil {
return nil, err
}
} }
i.hodlSubscribe(hodlChan, circuitKey) i.hodlSubscribe(hodlChan, ctx.circuitKey)
return nil, nil return &resolution, nil
default: default:
panic("unknown action") panic("unknown action")