diff --git a/channeldb/invoice_test.go b/channeldb/invoice_test.go index 38674b6be..ce2d960b6 100644 --- a/channeldb/invoice_test.go +++ b/channeldb/invoice_test.go @@ -461,7 +461,8 @@ func TestInvoiceCancelSingleHtlc(t *testing.T) { } return &invpkg.InvoiceUpdateDesc{ - AddHtlcs: htlcs, + UpdateType: invpkg.AddHTLCsUpdate, + AddHtlcs: htlcs, }, nil } @@ -479,6 +480,7 @@ func TestInvoiceCancelSingleHtlc(t *testing.T) { invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) { return &invpkg.InvoiceUpdateDesc{ + UpdateType: invpkg.CancelHTLCsUpdate, CancelHtlcs: map[models.CircuitKey]struct{}{ key: {}, }, @@ -554,6 +556,7 @@ func TestInvoiceCancelSingleHtlcAMP(t *testing.T) { invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) { return &invpkg.InvoiceUpdateDesc{ + UpdateType: invpkg.CancelHTLCsUpdate, CancelHtlcs: map[models.CircuitKey]struct{}{ {HtlcID: 0}: {}, }, @@ -616,6 +619,7 @@ func TestInvoiceCancelSingleHtlcAMP(t *testing.T) { error) { return &invpkg.InvoiceUpdateDesc{ + UpdateType: invpkg.CancelHTLCsUpdate, CancelHtlcs: map[models.CircuitKey]struct{}{ {HtlcID: 1}: {}, }, @@ -643,6 +647,7 @@ func TestInvoiceCancelSingleHtlcAMP(t *testing.T) { invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) { return &invpkg.InvoiceUpdateDesc{ + UpdateType: invpkg.CancelHTLCsUpdate, CancelHtlcs: map[models.CircuitKey]struct{}{ {HtlcID: 2}: {}, }, @@ -1529,6 +1534,7 @@ func getUpdateInvoice(amt lnwire.MilliSatoshi) invpkg.InvoiceUpdateCallback { }, } update := &invpkg.InvoiceUpdateDesc{ + UpdateType: invpkg.AddHTLCsUpdate, State: &invpkg.InvoiceStateUpdateDesc{ Preimage: invoice.Terms.PaymentPreimage, NewState: invpkg.ContractSettled, @@ -1586,7 +1592,10 @@ func TestCustomRecords(t *testing.T) { }, } - return &invpkg.InvoiceUpdateDesc{AddHtlcs: htlcs}, nil + return &invpkg.InvoiceUpdateDesc{ + AddHtlcs: htlcs, + UpdateType: invpkg.AddHTLCsUpdate, + }, nil } _, err = db.UpdateInvoice(ref, nil, callback) @@ -1663,7 +1672,10 @@ func testInvoiceHtlcAMPFields(t *testing.T, isAMP bool) { }, } - return &invpkg.InvoiceUpdateDesc{AddHtlcs: htlcs}, nil + return &invpkg.InvoiceUpdateDesc{ + AddHtlcs: htlcs, + UpdateType: invpkg.AddHTLCsUpdate, + }, nil } ref := invpkg.InvoiceRefByHash(payHash) @@ -2128,8 +2140,9 @@ func updateAcceptAMPHtlc(id uint64, amt lnwire.MilliSatoshi, } update := &invpkg.InvoiceUpdateDesc{ - State: state, - AddHtlcs: htlcs, + State: state, + AddHtlcs: htlcs, + UpdateType: invpkg.AddHTLCsUpdate, } return update, nil @@ -2152,6 +2165,10 @@ func getUpdateInvoiceAMPSettle(setID *[32]byte, preimage [32]byte, } update := &invpkg.InvoiceUpdateDesc{ + // TODO(positiveblue): this would be an invalid update + // because tires to settle an AMP invoice without adding + // any new htlc. + UpdateType: invpkg.AddHTLCsUpdate, State: &invpkg.InvoiceStateUpdateDesc{ Preimage: nil, NewState: invpkg.ContractSettled, @@ -2272,12 +2289,16 @@ func testUpdateHTLCPreimages(t *testing.T, test updateHTLCPreimageTestCase) { invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) { update := &invpkg.InvoiceUpdateDesc{ + // TODO(positiveblue): this would be an invalid update + // because tires to settle an AMP invoice without adding + // any new htlc. State: &invpkg.InvoiceStateUpdateDesc{ Preimage: nil, NewState: invpkg.ContractSettled, HTLCPreimages: htlcPreimages, SetID: setID, }, + UpdateType: invpkg.AddHTLCsUpdate, } return update, nil diff --git a/channeldb/invoices.go b/channeldb/invoices.go index b4ce3ba39..215c54609 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -1858,7 +1858,7 @@ func settleHtlcsAmp(invoice *invpkg.Invoice, // updateInvoice fetches the invoice, obtains the update descriptor from the // callback and applies the updates in a single db transaction. -func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, //nolint:lll,funlen +func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, settleIndex, setIDIndex kvdb.RwBucket, invoiceNum []byte, callback invpkg.InvoiceUpdateCallback) (*invpkg.Invoice, error) { @@ -1893,27 +1893,128 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, return &invoice, nil } - var ( - newState = invoice.State - setID *[32]byte - ) + switch update.UpdateType { + case invpkg.CancelHTLCsUpdate: + return d.cancelHTLCs(invoices, invoiceNum, &invoice, update) - // We can either get the set ID from the main state update (if the - // state is changing), or via the hint passed in returned by the update - // call back. - if update.State != nil { - setID = update.State.SetID - newState = update.State.NewState - } 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) + case invpkg.AddHTLCsUpdate: + return d.addHTLCs( + invoices, settleIndex, setIDIndex, invoiceNum, &invoice, + hash, update, + ) + + case invpkg.SettleHodlInvoiceUpdate: + return d.settleHodlInvoice( + invoices, settleIndex, invoiceNum, &invoice, hash, + update.State, + ) + + case invpkg.CancelInvoiceUpdate: + return d.cancelInvoice( + invoices, invoiceNum, &invoice, hash, update.State, + ) + + default: + return nil, fmt.Errorf("unknown update type: %s", + update.UpdateType) + } +} + +// cancelHTLCs tries to cancel the htlcs in the given InvoiceUpdateDesc. +// +// NOTE: cancelHTLCs updates will only use the `CancelHtlcs` field in the +// InvoiceUpdateDesc. +func (d *DB) cancelHTLCs(invoices kvdb.RwBucket, invoiceNum []byte, + invoice *invpkg.Invoice, + update *invpkg.InvoiceUpdateDesc) (*invpkg.Invoice, error) { + + timestamp := d.clock.Now() + + // Process add actions from update descriptor. + htlcsAmpUpdate := make(map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC) //nolint:lll + + // Process cancel actions from update descriptor. + cancelHtlcs := update.CancelHtlcs + for key, htlc := range invoice.Htlcs { + htlc := htlc + + // Check whether this htlc needs to be canceled. If it does, + // update the htlc state to Canceled. + _, cancel := cancelHtlcs[key] + if !cancel { + continue + } + + err := cancelSingleHtlc(timestamp, htlc, invoice.State) + if err != nil { + return nil, err + } + + // Delete processed cancel action, so that we can check later + // that there are no actions left. + delete(cancelHtlcs, key) + + // Tally this into the set of HTLCs that need to be updated on + // disk, but once again, only if this is an AMP invoice. + if invoice.IsAMP() { + cancelHtlcsAmp( + invoice, htlcsAmpUpdate, htlc, key, + ) + } } - now := d.clock.Now() + // Verify that we didn't get an action for htlcs that are not present on + // the invoice. + if len(cancelHtlcs) > 0 { + return nil, errors.New("cancel action on non-existent htlc(s)") + } - invoiceIsAMP := invoiceCopy.IsAMP() + err := d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) + if err != nil { + return nil, err + } + + // If this is an AMP invoice, then we'll actually store the rest of the + // HTLCs in-line with the invoice, using the invoice ID as a prefix, + // and the AMP key as a suffix: invoiceNum || setID. + if invoice.IsAMP() { + err := updateAMPInvoices(invoices, invoiceNum, htlcsAmpUpdate) + if err != nil { + return nil, err + } + } + + return invoice, nil +} + +// serializeAndStoreInvoice is a helper function used to store invoices. +func (d *DB) serializeAndStoreInvoice(invoices kvdb.RwBucket, invoiceNum []byte, + invoice *invpkg.Invoice) error { + + var buf bytes.Buffer + if err := serializeInvoice(&buf, invoice); err != nil { + return err + } + + if err := invoices.Put(invoiceNum, buf.Bytes()); err != nil { + return err + } + + return nil +} + +// addHTLCs tries to add the htlcs in the given InvoiceUpdateDesc. +func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen + setIDIndex kvdb.RwBucket, invoiceNum []byte, invoice *invpkg.Invoice, + hash *lntypes.Hash, update *invpkg.InvoiceUpdateDesc) (*invpkg.Invoice, + error) { + + var setID *[32]byte + invoiceIsAMP := invoice.IsAMP() + if invoiceIsAMP && update.State != nil { + setID = update.State.SetID + } + timestamp := d.clock.Now() // Process add actions from update descriptor. htlcsAmpUpdate := make(map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC) //nolint:lll @@ -1928,21 +2029,26 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, return nil, errors.New("nil custom records map") } - // If a newly added HTLC has an associated set id, use it to - // index this invoice in the set id index. An error is returned - // if we find the index already points to a different invoice. - var setID [32]byte - if htlcUpdate.AMP != nil { - setID = htlcUpdate.AMP.Record.SetID() - setIDInvNum := setIDIndex.Get(setID[:]) + if invoiceIsAMP { + if htlcUpdate.AMP == nil { + return nil, fmt.Errorf("unable to add htlc "+ + "without AMP data to AMP invoice(%v)", + invoice.AddIndex) + } + + // Check if this SetID already exist. + htlcSetID := htlcUpdate.AMP.Record.SetID() + setIDInvNum := setIDIndex.Get(htlcSetID[:]) + if setIDInvNum == nil { - err = setIDIndex.Put(setID[:], invoiceNum) + err := setIDIndex.Put(htlcSetID[:], invoiceNum) if err != nil { return nil, err } } else if !bytes.Equal(setIDInvNum, invoiceNum) { - err = invpkg.ErrDuplicateSetID{SetID: setID} - return nil, err + return nil, invpkg.ErrDuplicateSetID{ + SetID: htlcSetID, + } } } @@ -1951,7 +2057,7 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, MppTotalAmt: htlcUpdate.MppTotalAmt, Expiry: htlcUpdate.Expiry, AcceptHeight: uint32(htlcUpdate.AcceptHeight), - AcceptTime: now, + AcceptTime: timestamp, State: invpkg.HtlcStateAccepted, CustomRecords: htlcUpdate.CustomRecords, AMP: htlcUpdate.AMP.Copy(), @@ -1963,53 +2069,12 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, // below, but only if this is an AMP invoice. if invoiceIsAMP { updateHtlcsAmp( - &invoice, htlcsAmpUpdate, htlc, setID, key, + invoice, htlcsAmpUpdate, htlc, + htlcUpdate.AMP.Record.SetID(), key, ) } } - // Process cancel actions from update descriptor. - cancelHtlcs := update.CancelHtlcs - for key, htlc := range invoice.Htlcs { - htlc := htlc - - // Check whether this htlc needs to be canceled. If it does, - // update the htlc state to Canceled. - _, cancel := cancelHtlcs[key] - if !cancel { - continue - } - - // Consistency check to verify that there is no overlap between - // the add and cancel sets. - if _, added := update.AddHtlcs[key]; added { - return nil, fmt.Errorf("added htlc %v canceled", key) - } - - err := cancelSingleHtlc(now, htlc, newState) - if err != nil { - return nil, err - } - - // Delete processed cancel action, so that we can check later - // that there are no actions left. - delete(cancelHtlcs, key) - - // Tally this into the set of HTLCs that need to be updated on - // disk, but once again, only if this is an AMP invoice. - if invoiceIsAMP { - cancelHtlcsAmp( - &invoice, htlcsAmpUpdate, htlc, key, - ) - } - } - - // Verify that we didn't get an action for htlcs that are not present on - // the invoice. - if len(cancelHtlcs) > 0 { - return nil, errors.New("cancel action on non-existent htlc(s)") - } - // At this point, the set of accepted HTLCs should be fully // populated with added HTLCs or removed of canceled ones. Update // invoice state if the update descriptor indicates an invoice state @@ -2017,7 +2082,7 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, // HTLCs. if update.State != nil { newState, err := updateInvoiceState( - &invoice, hash, *update.State, + invoice, hash, *update.State, ) if err != nil { return nil, err @@ -2039,7 +2104,8 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, isSettled := update.State.NewState == invpkg.ContractSettled if !invoiceIsAMP && isSettled { err := setSettleMetaFields( - settleIndex, invoiceNum, &invoice, now, nil, + settleIndex, invoiceNum, invoice, timestamp, + nil, ) if err != nil { return nil, err @@ -2093,7 +2159,7 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, htlcContextState = invpkg.ContractSettled } htlcSettled, err := updateHtlc( - now, htlc, htlcContextState, setID, + timestamp, htlc, htlcContextState, setID, ) if err != nil { return nil, err @@ -2104,7 +2170,7 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, // meta data state. if htlcSettled && invoiceIsAMP { settleHtlcsAmp( - &invoice, settledSetIDs, htlcsAmpUpdate, htlc, + invoice, settledSetIDs, htlcsAmpUpdate, htlc, key, ) } @@ -2156,20 +2222,16 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, for settledSetID := range settledSetIDs { settledSetID := settledSetID err := setSettleMetaFields( - settleIndex, invoiceNum, &invoice, now, &settledSetID, + settleIndex, invoiceNum, invoice, timestamp, + &settledSetID, ) 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 { + err := d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) + if err != nil { return nil, err } @@ -2183,7 +2245,137 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, } } - return &invoice, nil + return invoice, nil +} + +// settleHodlInvoice marks a hodl invoice as settled. +// +// NOTE: Currently it is not possible to have HODL AMP invoices. +func (d *DB) settleHodlInvoice(invoices, settleIndex kvdb.RwBucket, + invoiceNum []byte, invoice *invpkg.Invoice, hash *lntypes.Hash, + update *invpkg.InvoiceStateUpdateDesc) (*invpkg.Invoice, error) { + + if !invoice.HodlInvoice { + return nil, fmt.Errorf("unable to settle hodl invoice: %v is "+ + "not a hodl invoice", invoice.AddIndex) + } + + // TODO(positiveblue): because NewState can only be ContractSettled we + // can remove it from the API and set it here directly. + switch { + case update == nil: + fallthrough + + case update.NewState != invpkg.ContractSettled: + return nil, fmt.Errorf("unable to settle hodl invoice: "+ + "not valid InvoiceUpdateDesc.State: %v", update) + + case update.Preimage == nil: + return nil, fmt.Errorf("unable to settle hodl invoice: " + + "preimage is nil") + } + + // TODO(positiveblue): create a invoice.CanSettleHodlInvoice func. + newState, err := updateInvoiceState(invoice, hash, *update) + if err != nil { + return nil, err + } + + if newState == nil || *newState != invpkg.ContractSettled { + return nil, fmt.Errorf("unable to settle hodl invoice: "+ + "new computed state is not settled: %s", newState) + } + + invoice.State = invpkg.ContractSettled + timestamp := d.clock.Now() + + err = setSettleMetaFields( + settleIndex, invoiceNum, invoice, timestamp, nil, + ) + if err != nil { + return nil, err + } + + // TODO(positiveblue): this logic can be further simplified. + var amtPaid lnwire.MilliSatoshi + for _, htlc := range invoice.Htlcs { + _, err := updateHtlc( + timestamp, htlc, invpkg.ContractSettled, nil, + ) + if err != nil { + return nil, err + } + + if htlc.State == invpkg.HtlcStateSettled { + amtPaid += htlc.Amt + } + } + + invoice.AmtPaid = amtPaid + + err = d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) + if err != nil { + return nil, err + } + + 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 + } + } + + err = d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) + if err != nil { + return nil, err + } + + return invoice, nil } // updateInvoiceState validates and processes an invoice state update. The new diff --git a/docs/release-notes/release-notes-0.17.0.md b/docs/release-notes/release-notes-0.17.0.md new file mode 100644 index 000000000..e32b93157 --- /dev/null +++ b/docs/release-notes/release-notes-0.17.0.md @@ -0,0 +1,12 @@ +# Release Notes + +## DB + +* Split channeldb [`UpdateInvoice` + implementation](https://github.com/lightningnetwork/lnd/pull/7377) logic in + different update types. + +# Contributors (Alphabetical Order) + +* Jordi Montes + diff --git a/invoices/invoiceregistry.go b/invoices/invoiceregistry.go index a80a85c8d..a638f46b1 100644 --- a/invoices/invoiceregistry.go +++ b/invoices/invoiceregistry.go @@ -711,6 +711,7 @@ func (i *InvoiceRegistry) cancelSingleHtlc(invoiceRef InvoiceRef, } return &InvoiceUpdateDesc{ + UpdateType: CancelHTLCsUpdate, CancelHtlcs: canceledHtlcs, SetID: setID, }, nil @@ -1230,9 +1231,7 @@ func (i *InvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error { i.Lock() defer i.Unlock() - updateInvoice := func(invoice *Invoice) ( - *InvoiceUpdateDesc, error) { - + updateInvoice := func(invoice *Invoice) (*InvoiceUpdateDesc, error) { switch invoice.State { case ContractOpen: return nil, ErrInvoiceStillOpen @@ -1245,6 +1244,7 @@ func (i *InvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error { } return &InvoiceUpdateDesc{ + UpdateType: SettleHodlInvoiceUpdate, State: &InvoiceStateUpdateDesc{ NewState: ContractSettled, Preimage: &preimage, @@ -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/invoices.go b/invoices/invoices.go index 7a99a46c6..24447223d 100644 --- a/invoices/invoices.go +++ b/invoices/invoices.go @@ -656,6 +656,51 @@ type HtlcAcceptDesc struct { AMP *InvoiceHtlcAMPData } +// UpdateType is an enum that describes the type of update that was applied to +// an invoice. +type UpdateType uint8 + +const ( + // UnknownUpdate indicates that the UpdateType has not been set for a + // given udpate. This kind of updates are not allowed. + UnknownUpdate UpdateType = iota + + // CancelHTLCsUpdate indicates that this update cancels one or more + // HTLCs. + CancelHTLCsUpdate + + // AddHTLCsUpdate indicates that this update adds one or more HTLCs. + AddHTLCsUpdate + + // SettleHodlInvoiceUpdate indicates that this update settles one or + // more HTLCs from a hodl invoice. + SettleHodlInvoiceUpdate + + // CancelInvoiceUpdate indicates that this update is trying to cancel + // an invoice. + CancelInvoiceUpdate +) + +// String returns a human readable string for the UpdateType. +func (u UpdateType) String() string { + switch u { + case CancelHTLCsUpdate: + return "CancelHTLCsUpdate" + + case AddHTLCsUpdate: + return "AddHTLCsUpdate" + + case SettleHodlInvoiceUpdate: + return "SettleHodlInvoiceUpdate" + + case CancelInvoiceUpdate: + return "CancelInvoiceUpdate" + + default: + return fmt.Sprintf("unknown invoice update type: %d", u) + } +} + // InvoiceUpdateDesc describes the changes that should be applied to the // invoice. type InvoiceUpdateDesc struct { @@ -674,6 +719,9 @@ type InvoiceUpdateDesc struct { // to be more efficient by ensuring we don't need to read out the // entire HTLC set each timee an HTLC is to be cancelled. SetID *SetID + + // UpdateType indicates what type of update is being applied. + UpdateType UpdateType } // InvoiceStateUpdateDesc describes an invoice-level state transition. diff --git a/invoices/update.go b/invoices/update.go index e9e7be6ed..390f325a7 100644 --- a/invoices/update.go +++ b/invoices/update.go @@ -238,7 +238,8 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc, } update := InvoiceUpdateDesc{ - AddHtlcs: newHtlcs, + UpdateType: AddHTLCsUpdate, + AddHtlcs: newHtlcs, } // If the invoice cannot be settled yet, only record the htlc. @@ -252,7 +253,6 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc, if inv.HodlInvoice { update.State = &InvoiceStateUpdateDesc{ NewState: ContractAccepted, - SetID: setID, } return &update, ctx.acceptRes(resultAccepted), nil } @@ -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, @@ -427,7 +428,8 @@ func updateLegacy(ctx *invoiceUpdateCtx, } update := InvoiceUpdateDesc{ - AddHtlcs: newHtlcs, + AddHtlcs: newHtlcs, + UpdateType: AddHTLCsUpdate, } // Don't update invoice state if we are accepting a duplicate payment.