diff --git a/channeldb/invoices.go b/channeldb/invoices.go index 61734f9a0..ce43d1063 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -1902,6 +1902,11 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, invoices, settleIndex, invoiceNum, &invoice, hash, update.State, ) + + case invpkg.CancelInvoiceUpdate: + return d.cancelInvoice( + invoices, invoiceNum, &invoice, hash, update.State, + ) } var ( @@ -1913,11 +1918,6 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, // call back. if update.State != nil { setID = update.State.SetID - } else if update.SetID != nil { - // When we go to cancel HTLCs, there's no new state, but the - // set of HTLCs to be cancelled along with the setID affected - // will be passed in. - setID = (*[32]byte)(update.SetID) } now := d.clock.Now() @@ -2303,6 +2303,68 @@ func (d *DB) settleHodlInvoice(invoices, settleIndex kvdb.RwBucket, return invoice, nil } +// cancelInvoice attempts to cancel the given invoice. That includes changing +// the invoice state and the state of any relevant HTLC. +func (d *DB) cancelInvoice(invoices kvdb.RwBucket, invoiceNum []byte, + invoice *invpkg.Invoice, hash *lntypes.Hash, + update *invpkg.InvoiceStateUpdateDesc) (*invpkg.Invoice, error) { + + switch { + case update == nil: + fallthrough + + case update.NewState != invpkg.ContractCanceled: + return nil, fmt.Errorf("unable to cancel invoice: "+ + "InvoiceUpdateDesc.State not valid: %v", update) + } + + var ( + setID *[32]byte + invoiceIsAMP bool + ) + + invoiceIsAMP = invoice.IsAMP() + if invoiceIsAMP { + setID = update.SetID + } + + newState, err := updateInvoiceState(invoice, hash, *update) + if err != nil { + return nil, err + } + + if newState == nil || *newState != invpkg.ContractCanceled { + return nil, fmt.Errorf("unable to cancel invoice(%v): new "+ + "computed state is not canceled: %s", invoice.AddIndex, + newState) + } + + invoice.State = invpkg.ContractCanceled + timestamp := d.clock.Now() + + // TODO(positiveblue): this logic can be simplified. + for _, htlc := range invoice.Htlcs { + _, err := updateHtlc( + timestamp, htlc, invpkg.ContractCanceled, setID, + ) + if err != nil { + return nil, err + } + } + + // Reserialize and update invoice. + var buf bytes.Buffer + if err := serializeInvoice(&buf, invoice); err != nil { + return nil, err + } + + if err := invoices.Put(invoiceNum, buf.Bytes()); err != nil { + return nil, err + } + + return invoice, nil +} + // updateInvoiceState validates and processes an invoice state update. The new // state to transition to is returned, so the caller is able to select exactly // how the invoice state is updated. diff --git a/invoices/invoiceregistry.go b/invoices/invoiceregistry.go index 08a4db154..a638f46b1 100644 --- a/invoices/invoiceregistry.go +++ b/invoices/invoiceregistry.go @@ -1329,6 +1329,7 @@ func (i *InvoiceRegistry) cancelInvoiceImpl(payHash lntypes.Hash, // channeldb to return an error if the invoice is already // settled or canceled. return &InvoiceUpdateDesc{ + UpdateType: CancelInvoiceUpdate, State: &InvoiceStateUpdateDesc{ NewState: ContractCanceled, }, diff --git a/invoices/update.go b/invoices/update.go index e9e7be6ed..2709d5f03 100644 --- a/invoices/update.go +++ b/invoices/update.go @@ -265,6 +265,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc, var failRes *HtlcFailResolution htlcPreimages, failRes = reconstructAMPPreimages(ctx, htlcSet) if failRes != nil { + update.UpdateType = CancelInvoiceUpdate update.State = &InvoiceStateUpdateDesc{ NewState: ContractCanceled, SetID: setID,