From 84387992ec34b4ea2bb8a26e9ebd6281f3395f68 Mon Sep 17 00:00:00 2001 From: positiveblue Date: Wed, 1 Feb 2023 04:50:01 -0800 Subject: [PATCH 1/8] invoices: add `UpdateType` to `InvoiceUpdateDesc` Make the kind of update explicit in the `InvoiceUpdateDesc` struct. --- invoices/invoices.go | 48 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) 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. From a3f6d5c97e2ffb9765f16f2e6b0ae27a72e7d62d Mon Sep 17 00:00:00 2001 From: positiveblue Date: Wed, 1 Feb 2023 04:59:01 -0800 Subject: [PATCH 2/8] channeldb: split `cancelHTLCs` logic in the `UpdateInvoice` method Previous to this commit we were able to call `UpdateInvoice` with an updated that added and cancelled htlcs at the same time. The function returned an error if there was overlapping between the two htlc set. However, that behavior was not used in the LND code itself. Eventually we want to split this method in multiple ones, among them one for canceling htlcs and another one for adding them. For that reason, this behavior is not supported anymore. --- channeldb/invoice_test.go | 4 ++ channeldb/invoices.go | 123 +++++++++++++++++++++++------------- invoices/invoiceregistry.go | 1 + 3 files changed, 83 insertions(+), 45 deletions(-) diff --git a/channeldb/invoice_test.go b/channeldb/invoice_test.go index 38674b6be..1520fcee5 100644 --- a/channeldb/invoice_test.go +++ b/channeldb/invoice_test.go @@ -479,6 +479,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 +555,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 +618,7 @@ func TestInvoiceCancelSingleHtlcAMP(t *testing.T) { error) { return &invpkg.InvoiceUpdateDesc{ + UpdateType: invpkg.CancelHTLCsUpdate, CancelHtlcs: map[models.CircuitKey]struct{}{ {HtlcID: 1}: {}, }, @@ -643,6 +646,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}: {}, }, diff --git a/channeldb/invoices.go b/channeldb/invoices.go index b4ce3ba39..4ab378042 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -1893,9 +1893,13 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, return &invoice, nil } + switch update.UpdateType { + case invpkg.CancelHTLCsUpdate: + return d.cancelHTLCs(invoices, invoiceNum, &invoice, update) + } + var ( - newState = invoice.State - setID *[32]byte + setID *[32]byte ) // We can either get the set ID from the main state update (if the @@ -1903,7 +1907,6 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, // 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 @@ -1968,48 +1971,6 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, } } - // 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 @@ -2186,6 +2147,78 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, return &invoice, nil } +// 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, + ) + } + } + + // 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)") + } + + // 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 + } + + // 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 +} + // 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 a80a85c8d..e84f10c16 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 From cbaec4382ac6a32de3aa0011e4ce0ea4d5df5a3a Mon Sep 17 00:00:00 2001 From: positiveblue Date: Wed, 1 Feb 2023 06:15:32 -0800 Subject: [PATCH 3/8] channeldb: split `settleHodlInvoice` logic in the `UpdateInvoice` method --- channeldb/invoices.go | 84 +++++++++++++++++++++++++++++++++++++ invoices/invoiceregistry.go | 5 +-- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/channeldb/invoices.go b/channeldb/invoices.go index 4ab378042..61734f9a0 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -1896,6 +1896,12 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, switch update.UpdateType { case invpkg.CancelHTLCsUpdate: return d.cancelHTLCs(invoices, invoiceNum, &invoice, update) + + case invpkg.SettleHodlInvoiceUpdate: + return d.settleHodlInvoice( + invoices, settleIndex, invoiceNum, &invoice, hash, + update.State, + ) } var ( @@ -2219,6 +2225,84 @@ func (d *DB) cancelHTLCs(invoices kvdb.RwBucket, invoiceNum []byte, 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 + + // 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 e84f10c16..08a4db154 100644 --- a/invoices/invoiceregistry.go +++ b/invoices/invoiceregistry.go @@ -1231,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 @@ -1246,6 +1244,7 @@ func (i *InvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error { } return &InvoiceUpdateDesc{ + UpdateType: SettleHodlInvoiceUpdate, State: &InvoiceStateUpdateDesc{ NewState: ContractSettled, Preimage: &preimage, From 27fbc2f60b6d40fd81b08dcb7f8b03b81e16ed53 Mon Sep 17 00:00:00 2001 From: positiveblue Date: Wed, 1 Feb 2023 07:49:53 -0800 Subject: [PATCH 4/8] channeldb: split `cancelInvoice` logic in the `UpdateInvoice` method --- channeldb/invoices.go | 72 ++++++++++++++++++++++++++++++++++--- invoices/invoiceregistry.go | 1 + invoices/update.go | 1 + 3 files changed, 69 insertions(+), 5 deletions(-) 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, From 6ff6c45a6be80714e0d6f84af35949c2c4a758b9 Mon Sep 17 00:00:00 2001 From: positiveblue Date: Thu, 2 Feb 2023 03:43:01 -0800 Subject: [PATCH 5/8] channeldb: split `addHTLCs` logic in the `UpdateInvoice` method --- channeldb/invoice_test.go | 27 +++- channeldb/invoices.go | 258 +++++++++++++++++++++++++++++++++++++- invoices/update.go | 7 +- 3 files changed, 283 insertions(+), 9 deletions(-) diff --git a/channeldb/invoice_test.go b/channeldb/invoice_test.go index 1520fcee5..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 } @@ -1533,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, @@ -1590,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) @@ -1667,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) @@ -2132,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 @@ -2156,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, @@ -2276,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 ce43d1063..b8780600a 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) { @@ -1897,6 +1897,12 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, case invpkg.CancelHTLCsUpdate: return d.cancelHTLCs(invoices, invoiceNum, &invoice, update) + case invpkg.AddHTLCsUpdate: + return d.addHTLCs( + invoices, settleIndex, setIDIndex, invoiceNum, &invoice, + hash, update, + ) + case invpkg.SettleHodlInvoiceUpdate: return d.settleHodlInvoice( invoices, settleIndex, invoiceNum, &invoice, hash, @@ -2225,6 +2231,256 @@ func (d *DB) cancelHTLCs(invoices kvdb.RwBucket, invoiceNum []byte, return invoice, 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 + for key, htlcUpdate := range update.AddHtlcs { + if _, exists := invoice.Htlcs[key]; exists { + return nil, fmt.Errorf("duplicate add of htlc %v", key) + } + + // Force caller to supply htlc without custom records in a + // consistent way. + if htlcUpdate.CustomRecords == nil { + return nil, errors.New("nil custom records map") + } + + 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(htlcSetID[:], invoiceNum) + if err != nil { + return nil, err + } + } else if !bytes.Equal(setIDInvNum, invoiceNum) { + return nil, invpkg.ErrDuplicateSetID{ + SetID: htlcSetID, + } + } + } + + htlc := &invpkg.InvoiceHTLC{ + Amt: htlcUpdate.Amt, + MppTotalAmt: htlcUpdate.MppTotalAmt, + Expiry: htlcUpdate.Expiry, + AcceptHeight: uint32(htlcUpdate.AcceptHeight), + AcceptTime: timestamp, + State: invpkg.HtlcStateAccepted, + CustomRecords: htlcUpdate.CustomRecords, + AMP: htlcUpdate.AMP.Copy(), + } + + invoice.Htlcs[key] = htlc + + // Collect the set of new HTLCs so we can write them properly + // below, but only if this is an AMP invoice. + if invoiceIsAMP { + updateHtlcsAmp( + invoice, htlcsAmpUpdate, htlc, + htlcUpdate.AMP.Record.SetID(), key, + ) + } + } + + // 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 + // change, which depends on having an accurate view of the accepted + // HTLCs. + if update.State != nil { + newState, err := updateInvoiceState( + invoice, hash, *update.State, + ) + if err != nil { + return nil, err + } + + // If this isn't an AMP invoice, then we'll go ahead and update + // the invoice state directly here. For AMP invoices, we + // instead will keep the top-level invoice open, and instead + // update the state of each _htlc set_ instead. However, we'll + // allow the invoice to transition to the cancelled state + // regardless. + if !invoiceIsAMP || *newState == invpkg.ContractCanceled { + invoice.State = *newState + } + + // If this is a non-AMP invoice, then the state can eventually + // go to ContractSettled, so we pass in nil value as part of + // setSettleMetaFields. + isSettled := update.State.NewState == invpkg.ContractSettled + if !invoiceIsAMP && isSettled { + err := setSettleMetaFields( + settleIndex, invoiceNum, invoice, timestamp, + nil, + ) + if err != nil { + return nil, err + } + } + } + + // The set of HTLC pre-images will only be set if we were actually able + // to reconstruct all the AMP pre-images. + var settleEligibleAMP bool + if update.State != nil { + settleEligibleAMP = len(update.State.HTLCPreimages) != 0 + } + + // With any invoice level state transitions recorded, we'll now + // finalize the process by updating the state transitions for + // individual HTLCs + var ( + settledSetIDs = make(map[invpkg.SetID]struct{}) + amtPaid lnwire.MilliSatoshi + ) + for key, htlc := range invoice.Htlcs { + // Set the HTLC preimage for any AMP HTLCs. + if setID != nil && update.State != nil { + preimage, ok := update.State.HTLCPreimages[key] + switch { + // If we don't already have a preimage for this HTLC, we + // can set it now. + case ok && htlc.AMP.Preimage == nil: + htlc.AMP.Preimage = &preimage + + // Otherwise, prevent over-writing an existing + // preimage. Ignore the case where the preimage is + // identical. + case ok && *htlc.AMP.Preimage != preimage: + return nil, invpkg.ErrHTLCPreimageAlreadyExists + } + } + + // The invoice state may have changed and this could have + // implications for the states of the individual htlcs. Align + // the htlc state with the current invoice state. + // + // If we have all the pre-images for an AMP invoice, then we'll + // act as if we're able to settle the entire invoice. We need + // to do this since it's possible for us to settle AMP invoices + // while the contract state (on disk) is still in the accept + // state. + htlcContextState := invoice.State + if settleEligibleAMP { + htlcContextState = invpkg.ContractSettled + } + htlcSettled, err := updateHtlc( + timestamp, htlc, htlcContextState, setID, + ) + if err != nil { + return nil, err + } + + // If the HTLC has being settled for the first time, and this + // is an AMP invoice, then we'll need to update some additional + // meta data state. + if htlcSettled && invoiceIsAMP { + settleHtlcsAmp( + invoice, settledSetIDs, htlcsAmpUpdate, htlc, + key, + ) + } + + accepted := htlc.State == invpkg.HtlcStateAccepted + settled := htlc.State == invpkg.HtlcStateSettled + invoiceStateReady := accepted || settled + + if !invoiceIsAMP { + // Update the running amount paid to this invoice. We + // don't include accepted htlcs when the invoice is + // still open. + if invoice.State != invpkg.ContractOpen && + invoiceStateReady { + + amtPaid += htlc.Amt + } + } else { + // For AMP invoices, since we won't always be reading + // out the total invoice set each time, we'll instead + // accumulate newly added invoices to the total amount + // paid. + if _, ok := update.AddHtlcs[key]; !ok { + continue + } + + // Update the running amount paid to this invoice. AMP + // invoices never go to the settled state, so if it's + // open, then we tally the HTLC. + if invoice.State == invpkg.ContractOpen && + invoiceStateReady { + + amtPaid += htlc.Amt + } + } + } + + // For non-AMP invoices we recalculate the amount paid from scratch + // each time, while for AMP invoices, we'll accumulate only based on + // newly added HTLCs. + if !invoiceIsAMP { + invoice.AmtPaid = amtPaid + } else { + invoice.AmtPaid += amtPaid + } + + // As we don't update the settle index above for AMP invoices, we'll do + // it here for each sub-AMP invoice that was settled. + for settledSetID := range settledSetIDs { + settledSetID := settledSetID + err := setSettleMetaFields( + 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 { + 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 invoiceIsAMP { + err := updateAMPInvoices(invoices, invoiceNum, htlcsAmpUpdate) + if err != nil { + return nil, err + } + } + + return invoice, nil +} + // settleHodlInvoice marks a hodl invoice as settled. // // NOTE: Currently it is not possible to have HODL AMP invoices. diff --git a/invoices/update.go b/invoices/update.go index 2709d5f03..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 } @@ -428,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. From 65fc8d72c652a8ea4208d90c32e634fb5cc86f34 Mon Sep 17 00:00:00 2001 From: positiveblue Date: Thu, 23 Feb 2023 06:53:24 -0800 Subject: [PATCH 6/8] channeldb: clean updateInvoice func All the updates type are now catched in a switch statement, we can delete the rest of the body of this function + add a default path for not matched flows. --- channeldb/invoices.go | 247 +----------------------------------------- 1 file changed, 4 insertions(+), 243 deletions(-) diff --git a/channeldb/invoices.go b/channeldb/invoices.go index b8780600a..8f973fcba 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -1913,250 +1913,11 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, return d.cancelInvoice( invoices, invoiceNum, &invoice, hash, update.State, ) + + default: + return nil, fmt.Errorf("unknown update type: %s", + update.UpdateType) } - - var ( - setID *[32]byte - ) - - // 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 - } - - now := d.clock.Now() - - invoiceIsAMP := invoiceCopy.IsAMP() - - // Process add actions from update descriptor. - htlcsAmpUpdate := make(map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC) //nolint:lll - for key, htlcUpdate := range update.AddHtlcs { - if _, exists := invoice.Htlcs[key]; exists { - return nil, fmt.Errorf("duplicate add of htlc %v", key) - } - - // Force caller to supply htlc without custom records in a - // consistent way. - if htlcUpdate.CustomRecords == nil { - 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 setIDInvNum == nil { - err = setIDIndex.Put(setID[:], invoiceNum) - if err != nil { - return nil, err - } - } else if !bytes.Equal(setIDInvNum, invoiceNum) { - err = invpkg.ErrDuplicateSetID{SetID: setID} - return nil, err - } - } - - htlc := &invpkg.InvoiceHTLC{ - Amt: htlcUpdate.Amt, - MppTotalAmt: htlcUpdate.MppTotalAmt, - Expiry: htlcUpdate.Expiry, - AcceptHeight: uint32(htlcUpdate.AcceptHeight), - AcceptTime: now, - State: invpkg.HtlcStateAccepted, - CustomRecords: htlcUpdate.CustomRecords, - AMP: htlcUpdate.AMP.Copy(), - } - - invoice.Htlcs[key] = htlc - - // Collect the set of new HTLCs so we can write them properly - // below, but only if this is an AMP invoice. - if invoiceIsAMP { - updateHtlcsAmp( - &invoice, htlcsAmpUpdate, htlc, setID, key, - ) - } - } - - // 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 - // change, which depends on having an accurate view of the accepted - // HTLCs. - if update.State != nil { - newState, err := updateInvoiceState( - &invoice, hash, *update.State, - ) - if err != nil { - return nil, err - } - - // If this isn't an AMP invoice, then we'll go ahead and update - // the invoice state directly here. For AMP invoices, we - // instead will keep the top-level invoice open, and instead - // update the state of each _htlc set_ instead. However, we'll - // allow the invoice to transition to the cancelled state - // regardless. - if !invoiceIsAMP || *newState == invpkg.ContractCanceled { - invoice.State = *newState - } - - // If this is a non-AMP invoice, then the state can eventually - // go to ContractSettled, so we pass in nil value as part of - // setSettleMetaFields. - isSettled := update.State.NewState == invpkg.ContractSettled - if !invoiceIsAMP && isSettled { - err := setSettleMetaFields( - settleIndex, invoiceNum, &invoice, now, nil, - ) - if err != nil { - return nil, err - } - } - } - - // The set of HTLC pre-images will only be set if we were actually able - // to reconstruct all the AMP pre-images. - var settleEligibleAMP bool - if update.State != nil { - settleEligibleAMP = len(update.State.HTLCPreimages) != 0 - } - - // With any invoice level state transitions recorded, we'll now - // finalize the process by updating the state transitions for - // individual HTLCs - var ( - settledSetIDs = make(map[invpkg.SetID]struct{}) - amtPaid lnwire.MilliSatoshi - ) - for key, htlc := range invoice.Htlcs { - // Set the HTLC preimage for any AMP HTLCs. - if setID != nil && update.State != nil { - preimage, ok := update.State.HTLCPreimages[key] - switch { - // If we don't already have a preimage for this HTLC, we - // can set it now. - case ok && htlc.AMP.Preimage == nil: - htlc.AMP.Preimage = &preimage - - // Otherwise, prevent over-writing an existing - // preimage. Ignore the case where the preimage is - // identical. - case ok && *htlc.AMP.Preimage != preimage: - return nil, invpkg.ErrHTLCPreimageAlreadyExists - } - } - - // The invoice state may have changed and this could have - // implications for the states of the individual htlcs. Align - // the htlc state with the current invoice state. - // - // If we have all the pre-images for an AMP invoice, then we'll - // act as if we're able to settle the entire invoice. We need - // to do this since it's possible for us to settle AMP invoices - // while the contract state (on disk) is still in the accept - // state. - htlcContextState := invoice.State - if settleEligibleAMP { - htlcContextState = invpkg.ContractSettled - } - htlcSettled, err := updateHtlc( - now, htlc, htlcContextState, setID, - ) - if err != nil { - return nil, err - } - - // If the HTLC has being settled for the first time, and this - // is an AMP invoice, then we'll need to update some additional - // meta data state. - if htlcSettled && invoiceIsAMP { - settleHtlcsAmp( - &invoice, settledSetIDs, htlcsAmpUpdate, htlc, - key, - ) - } - - accepted := htlc.State == invpkg.HtlcStateAccepted - settled := htlc.State == invpkg.HtlcStateSettled - invoiceStateReady := accepted || settled - - if !invoiceIsAMP { - // Update the running amount paid to this invoice. We - // don't include accepted htlcs when the invoice is - // still open. - if invoice.State != invpkg.ContractOpen && - invoiceStateReady { - - amtPaid += htlc.Amt - } - } else { - // For AMP invoices, since we won't always be reading - // out the total invoice set each time, we'll instead - // accumulate newly added invoices to the total amount - // paid. - if _, ok := update.AddHtlcs[key]; !ok { - continue - } - - // Update the running amount paid to this invoice. AMP - // invoices never go to the settled state, so if it's - // open, then we tally the HTLC. - if invoice.State == invpkg.ContractOpen && - invoiceStateReady { - - amtPaid += htlc.Amt - } - } - } - - // For non-AMP invoices we recalculate the amount paid from scratch - // each time, while for AMP invoices, we'll accumulate only based on - // newly added HTLCs. - if !invoiceIsAMP { - invoice.AmtPaid = amtPaid - } else { - invoice.AmtPaid += amtPaid - } - - // As we don't update the settle index above for AMP invoices, we'll do - // it here for each sub-AMP invoice that was settled. - for settledSetID := range settledSetIDs { - settledSetID := settledSetID - err := setSettleMetaFields( - settleIndex, invoiceNum, &invoice, now, &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 { - 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 invoiceIsAMP { - err := updateAMPInvoices(invoices, invoiceNum, htlcsAmpUpdate) - if err != nil { - return nil, err - } - } - - return &invoice, nil } // cancelHTLCs tries to cancel the htlcs in the given InvoiceUpdateDesc. From 09e87ea99b2fa1989893d35979c53925d5c5ee7e Mon Sep 17 00:00:00 2001 From: positiveblue Date: Mon, 6 Mar 2023 16:23:38 -0800 Subject: [PATCH 7/8] channeldb: add `serializeAndStoreInvoice` helper func --- channeldb/invoices.go | 52 ++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/channeldb/invoices.go b/channeldb/invoices.go index 8f973fcba..215c54609 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -1969,13 +1969,8 @@ func (d *DB) cancelHTLCs(invoices kvdb.RwBucket, invoiceNum []byte, return nil, errors.New("cancel action on non-existent htlc(s)") } - // 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 } @@ -1992,6 +1987,22 @@ func (d *DB) cancelHTLCs(invoices kvdb.RwBucket, invoiceNum []byte, 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, @@ -2219,13 +2230,8 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen } } - // 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 } @@ -2307,13 +2313,8 @@ func (d *DB) settleHodlInvoice(invoices, settleIndex kvdb.RwBucket, invoice.AmtPaid = amtPaid - // 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 } @@ -2369,13 +2370,8 @@ func (d *DB) cancelInvoice(invoices kvdb.RwBucket, invoiceNum []byte, } } - // 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 } From 034853a0a8856ebaa92ecda15bd4ba8dbda0f569 Mon Sep 17 00:00:00 2001 From: positiveblue Date: Wed, 8 Feb 2023 07:36:58 -0800 Subject: [PATCH 8/8] docs: add release notes --- docs/release-notes/release-notes-0.17.0.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 docs/release-notes/release-notes-0.17.0.md 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 +