diff --git a/channeldb/invoices.go b/channeldb/invoices.go index ff88b46a3..d134c03b4 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -607,7 +607,9 @@ func (d *DB) QueryInvoices(_ context.Context, q invpkg.InvoiceQuery) ( // // The update is performed inside the same database transaction that fetches the // invoice and is therefore atomic. The fields to update are controlled by the -// supplied callback. +// supplied callback. When updating an invoice, the update itself happens +// in-memory on a copy of the invoice. Once it is written successfully to the +// database, the in-memory copy is returned to the caller. func (d *DB) UpdateInvoice(_ context.Context, ref invpkg.InvoiceRef, setIDHint *invpkg.SetID, callback invpkg.InvoiceUpdateCallback) ( *invpkg.Invoice, error) { @@ -657,10 +659,22 @@ func (d *DB) UpdateInvoice(_ context.Context, ref invpkg.InvoiceRef, return err } + now := d.clock.Now() + updater := &kvInvoiceUpdater{ + db: d, + invoicesBucket: invoices, + settleIndexBucket: settleIndex, + setIDIndexBucket: setIDIndex, + updateTime: now, + invoiceNum: invoiceNum, + invoice: &invoice, + updatedAmpHtlcs: make(ampHTLCsMap), + settledSetIDs: make(map[invpkg.SetID]struct{}), + } + payHash := ref.PayHash() - updatedInvoice, err = d.updateInvoice( - payHash, invoices, settleIndex, setIDIndex, - &invoice, invoiceNum, callback, + updatedInvoice, err = updateInvoice( + payHash, updater.invoice, now, callback, updater, ) return err @@ -671,6 +685,316 @@ func (d *DB) UpdateInvoice(_ context.Context, ref invpkg.InvoiceRef, return updatedInvoice, err } +// ampHTLCsMap is a map of AMP HTLCs affected by an invoice update. +type ampHTLCsMap map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC + +// kvInvoiceUpdater is an implementation of the InvoiceUpdater interface that +// is used with the kv implementation of the invoice database. Note that this +// updater is not concurrency safe and synchronizaton is expected to be handled +// on the DB level. +type kvInvoiceUpdater struct { + db *DB + invoicesBucket kvdb.RwBucket + settleIndexBucket kvdb.RwBucket + setIDIndexBucket kvdb.RwBucket + + // updateTime is the timestamp for the update. + updateTime time.Time + + // invoiceNum is a legacy key similar to the add index that is used + // only in the kv implementation. + invoiceNum []byte + + // invoice is the invoice that we're updating. As a side effect of the + // update this invoice will be mutated. + invoice *invpkg.Invoice + + // updatedAmpHtlcs holds the set of AMP HTLCs that were added or + // cancelled as part of this update. + updatedAmpHtlcs ampHTLCsMap + + // settledSetIDs holds the set IDs that are settled with this update. + settledSetIDs map[invpkg.SetID]struct{} +} + +// NOTE: this method does nothing in the k/v implementation of InvoiceUpdater. +func (k *kvInvoiceUpdater) AddHtlc(_ models.CircuitKey, + _ *invpkg.InvoiceHTLC) error { + + return nil +} + +// NOTE: this method does nothing in the k/v implementation of InvoiceUpdater. +func (k *kvInvoiceUpdater) ResolveHtlc(_ models.CircuitKey, _ invpkg.HtlcState, + _ time.Time) error { + + return nil +} + +// NOTE: this method does nothing in the k/v implementation of InvoiceUpdater. +func (k *kvInvoiceUpdater) AddAmpHtlcPreimage(_ [32]byte, _ models.CircuitKey, + _ lntypes.Preimage) error { + + return nil +} + +// NOTE: this method does nothing in the k/v implementation of InvoiceUpdater. +func (k *kvInvoiceUpdater) UpdateInvoiceState(_ invpkg.ContractState, + _ *lntypes.Preimage) error { + + return nil +} + +// NOTE: this method does nothing in the k/v implementation of InvoiceUpdater. +func (k *kvInvoiceUpdater) UpdateInvoiceAmtPaid(_ lnwire.MilliSatoshi) error { + return nil +} + +// UpdateAmpState updates the state of the AMP invoice identified by the setID. +func (k *kvInvoiceUpdater) UpdateAmpState(setID [32]byte, + state invpkg.InvoiceStateAMP, circuitKey models.CircuitKey) error { + + if _, ok := k.updatedAmpHtlcs[setID]; !ok { + switch state.State { + case invpkg.HtlcStateAccepted: + // If we're just now creating the HTLCs for this set + // then we'll also pull in the existing HTLCs that are + // part of this set, so we can write them all to disk + // together (same value) + k.updatedAmpHtlcs[setID] = k.invoice.HTLCSet( + &setID, invpkg.HtlcStateAccepted, + ) + + case invpkg.HtlcStateCanceled: + // Only HTLCs in the accepted state, can be cancelled, + // but we also want to merge that with HTLCs that may be + // canceled as well since it can be cancelled one by + // one. + k.updatedAmpHtlcs[setID] = k.invoice.HTLCSet( + &setID, invpkg.HtlcStateAccepted, + ) + + cancelledHtlcs := k.invoice.HTLCSet( + &setID, invpkg.HtlcStateCanceled, + ) + for htlcKey, htlc := range cancelledHtlcs { + k.updatedAmpHtlcs[setID][htlcKey] = htlc + } + + case invpkg.HtlcStateSettled: + k.updatedAmpHtlcs[setID] = make( + map[models.CircuitKey]*invpkg.InvoiceHTLC, + ) + } + } + + if state.State == invpkg.HtlcStateSettled { + // Add the set ID to the set that was settled in this invoice + // update. We'll use this later to update the settle index. + k.settledSetIDs[setID] = struct{}{} + } + + k.updatedAmpHtlcs[setID][circuitKey] = k.invoice.Htlcs[circuitKey] + + return nil +} + +// Finalize finalizes the update before it is written to the database. +func (k *kvInvoiceUpdater) Finalize(updateType invpkg.UpdateType) error { + switch updateType { + case invpkg.AddHTLCsUpdate: + return k.storeAddHtlcsUpdate() + + case invpkg.CancelHTLCsUpdate: + return k.storeCancelHtlcsUpdate() + + case invpkg.SettleHodlInvoiceUpdate: + return k.storeSettleHodlInvoiceUpdate() + + case invpkg.CancelInvoiceUpdate: + return k.serializeAndStoreInvoice() + } + + return fmt.Errorf("unknown update type: %v", updateType) +} + +// storeCancelHtlcsUpdate updates the invoice in the database after cancelling a +// set of HTLCs. +func (k *kvInvoiceUpdater) storeCancelHtlcsUpdate() error { + err := k.serializeAndStoreInvoice() + if err != nil { + return 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 k.invoice.IsAMP() { + return k.updateAMPInvoices() + } + + return nil +} + +// storeAddHtlcsUpdate updates the invoice in the database after adding a set of +// HTLCs. +func (k *kvInvoiceUpdater) storeAddHtlcsUpdate() error { + invoiceIsAMP := k.invoice.IsAMP() + + for htlcSetID := range k.updatedAmpHtlcs { + // Check if this SetID already exist. + setIDInvNum := k.setIDIndexBucket.Get(htlcSetID[:]) + + if setIDInvNum == nil { + err := k.setIDIndexBucket.Put( + htlcSetID[:], k.invoiceNum, + ) + if err != nil { + return err + } + } else if !bytes.Equal(setIDInvNum, k.invoiceNum) { + return invpkg.ErrDuplicateSetID{ + SetID: htlcSetID, + } + } + } + + // 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. + if !invoiceIsAMP && k.invoice.State == invpkg.ContractSettled { + err := k.setSettleMetaFields(nil) + if err != nil { + return err + } + } + + // 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 k.settledSetIDs { + settledSetID := settledSetID + err := k.setSettleMetaFields(&settledSetID) + if err != nil { + return err + } + } + + err := k.serializeAndStoreInvoice() + if err != nil { + return 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 { + return k.updateAMPInvoices() + } + + return nil +} + +// storeSettleHodlInvoiceUpdate updates the invoice in the database after +// settling a hodl invoice. +func (k *kvInvoiceUpdater) storeSettleHodlInvoiceUpdate() error { + err := k.setSettleMetaFields(nil) + if err != nil { + return err + } + + return k.serializeAndStoreInvoice() +} + +// setSettleMetaFields updates the metadata associated with settlement of an +// invoice. If a non-nil setID is passed in, then the value will be append to +// the invoice number as well, in order to allow us to detect repeated payments +// to the same AMP invoices "across time". +func (k *kvInvoiceUpdater) setSettleMetaFields(setID *invpkg.SetID) error { + // Now that we know the invoice hasn't already been settled, we'll + // update the settle index so we can place this settle event in the + // proper location within our time series. + nextSettleSeqNo, err := k.settleIndexBucket.NextSequence() + if err != nil { + return err + } + + // Make a new byte array on the stack that can potentially store the 4 + // byte invoice number along w/ the 32 byte set ID. We capture valueLen + // here which is the number of bytes copied so we can only store the 4 + // bytes if this is a non-AMP invoice. + var indexKey [invoiceSetIDKeyLen]byte + valueLen := copy(indexKey[:], k.invoiceNum) + + if setID != nil { + valueLen += copy(indexKey[valueLen:], setID[:]) + } + + var seqNoBytes [8]byte + byteOrder.PutUint64(seqNoBytes[:], nextSettleSeqNo) + err = k.settleIndexBucket.Put(seqNoBytes[:], indexKey[:valueLen]) + if err != nil { + return err + } + + // If the setID is nil, then this means that this is a non-AMP settle, + // so we'll update the invoice settle index directly. + if setID == nil { + k.invoice.SettleDate = k.updateTime + k.invoice.SettleIndex = nextSettleSeqNo + } else { + // If the set ID isn't blank, we'll update the AMP state map + // which tracks when each of the setIDs associated with a given + // AMP invoice are settled. + ampState := k.invoice.AMPState[*setID] + + ampState.SettleDate = k.updateTime + ampState.SettleIndex = nextSettleSeqNo + + k.invoice.AMPState[*setID] = ampState + } + + return nil +} + +// updateAMPInvoices updates the set of AMP invoices in-place. For AMP, rather +// then continually write the invoices to the end of the invoice value, we +// instead write the invoices into a new key preifx that follows the main +// invoice number. This ensures that we don't need to continually decode a +// potentially massive HTLC set, and also allows us to quickly find the HLTCs +// associated with a particular HTLC set. +func (k *kvInvoiceUpdater) updateAMPInvoices() error { + for setID, htlcSet := range k.updatedAmpHtlcs { + // First write out the set of HTLCs including all the relevant + // TLV values. + var b bytes.Buffer + if err := serializeHtlcs(&b, htlcSet); err != nil { + return err + } + + // Next store each HTLC in-line, using a prefix based off the + // invoice number. + invoiceSetIDKey := makeInvoiceSetIDKey(k.invoiceNum, setID[:]) + + err := k.invoicesBucket.Put(invoiceSetIDKey[:], b.Bytes()) + if err != nil { + return err + } + } + + return nil +} + +// serializeAndStoreInvoice is a helper function used to store invoices. +func (k *kvInvoiceUpdater) serializeAndStoreInvoice() error { + var buf bytes.Buffer + if err := serializeInvoice(&buf, k.invoice); err != nil { + return err + } + + return k.invoicesBucket.Put(k.invoiceNum, buf.Bytes()) +} + // InvoicesSettledSince can be used by callers to catch up any settled invoices // they missed within the settled invoice time series. We'll return all known // settled invoice that have a settle index higher than the passed @@ -1740,43 +2064,12 @@ func makeInvoiceSetIDKey(invoiceNum, setID []byte) [invoiceSetIDKeyLen]byte { return invoiceSetIDKey } -// updateAMPInvoices updates the set of AMP invoices in-place. For AMP, rather -// then continually write the invoices to the end of the invoice value, we -// instead write the invoices into a new key preifx that follows the main -// invoice number. This ensures that we don't need to continually decode a -// potentially massive HTLC set, and also allows us to quickly find the HLTCs -// associated with a particular HTLC set. -func updateAMPInvoices(invoiceBucket kvdb.RwBucket, invoiceNum []byte, - htlcsToUpdate map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC) error { //nolint:lll - - for setID, htlcSet := range htlcsToUpdate { - // First write out the set of HTLCs including all the relevant - // TLV values. - var b bytes.Buffer - if err := serializeHtlcs(&b, htlcSet); err != nil { - return err - } - - // Next store each HTLC in-line, using a prefix based off the - // invoice number. - invoiceSetIDKey := makeInvoiceSetIDKey(invoiceNum, setID[:]) - - err := invoiceBucket.Put(invoiceSetIDKey[:], b.Bytes()) - if err != nil { - return err - } - } - - return nil -} - // updateHtlcsAmp takes an invoice, and a new HTLC to be added (along with its // set ID), and updates the internal AMP state of an invoice, and also tallies // the set of HTLCs to be updated on disk. -func updateHtlcsAmp(invoice *invpkg.Invoice, - updateMap map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC, - htlc *invpkg.InvoiceHTLC, setID invpkg.SetID, - circuitKey models.CircuitKey) error { +func acceptHtlcsAmp(invoice *invpkg.Invoice, setID invpkg.SetID, + circuitKey models.CircuitKey, htlc *invpkg.InvoiceHTLC, + updater invpkg.InvoiceUpdater) error { newAmpState, err := getUpdatedInvoiceAmpState( invoice, setID, circuitKey, invpkg.HtlcStateAccepted, htlc.Amt, @@ -1787,34 +2080,24 @@ func updateHtlcsAmp(invoice *invpkg.Invoice, invoice.AMPState[setID] = newAmpState - // Now that we've updated the invoice state, we'll inform the caller of - // the _neitre_ HTLC set they need to write for this new set ID. - if _, ok := updateMap[setID]; !ok { - // If we're just now creating the HTLCs for this set then we'll - // also pull in the existing HTLCs are part of this set, so we - // can write them all to disk together (same value) - updateMap[setID] = invoice.HTLCSet( - (*[32]byte)(&setID), invpkg.HtlcStateAccepted, - ) - } - updateMap[setID][circuitKey] = htlc - - return nil + // Mark the updates as needing to be written to disk. + return updater.UpdateAmpState(setID, newAmpState, circuitKey) } // cancelHtlcsAmp processes a cancellation of an HTLC that belongs to an AMP // HTLC set. We'll need to update the meta data in the main invoice, and also // apply the new update to the update MAP, since all the HTLCs for a given HTLC // set need to be written in-line with each other. -func cancelHtlcsAmp(invoice *invpkg.Invoice, - updateMap map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC, - htlc *invpkg.InvoiceHTLC, circuitKey models.CircuitKey) error { +func cancelHtlcsAmp(invoice *invpkg.Invoice, circuitKey models.CircuitKey, + htlc *invpkg.InvoiceHTLC, updater invpkg.InvoiceUpdater) error { setID := htlc.AMP.Record.SetID() - // First, we'll update the state of the entire HTLC set to cancelled. + // First, we'll update the state of the entire HTLC set + // to cancelled. newAmpState, err := getUpdatedInvoiceAmpState( - invoice, setID, circuitKey, invpkg.HtlcStateCanceled, htlc.Amt, + invoice, setID, circuitKey, invpkg.HtlcStateCanceled, + htlc.Amt, ) if err != nil { return err @@ -1822,30 +2105,18 @@ func cancelHtlcsAmp(invoice *invpkg.Invoice, invoice.AMPState[setID] = newAmpState - if _, ok := updateMap[setID]; !ok { - // Only HTLCs in the accepted state, can be cancelled, but we - // also want to merge that with HTLCs that may be canceled as - // well since it can be cancelled one by one. - updateMap[setID] = invoice.HTLCSet( - &setID, invpkg.HtlcStateAccepted, - ) - - cancelledHtlcs := invoice.HTLCSet( - &setID, invpkg.HtlcStateCanceled, - ) - for htlcKey, htlc := range cancelledHtlcs { - updateMap[setID][htlcKey] = htlc - } + // Mark the updates as needing to be written to disk. + err = updater.UpdateAmpState(setID, newAmpState, circuitKey) + if err != nil { + return err } - // Finally, include the newly cancelled HTLC in the set of HTLCs we - // need to cancel. - updateMap[setID][circuitKey] = htlc - // We'll only decrement the total amount paid if the invoice was // already in the accepted state. if invoice.AmtPaid != 0 { - invoice.AmtPaid -= htlc.Amt + return updateInvoiceAmtPaid( + invoice, invoice.AmtPaid-htlc.Amt, updater, + ) } return nil @@ -1854,15 +2125,10 @@ func cancelHtlcsAmp(invoice *invpkg.Invoice, // settleHtlcsAmp processes a new settle operation on an HTLC set for an AMP // invoice. We'll update some meta data in the main invoice, and also signal // that this HTLC set needs to be re-written back to disk. -func settleHtlcsAmp(invoice *invpkg.Invoice, - settledSetIDs map[invpkg.SetID]struct{}, - updateMap map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC, - htlc *invpkg.InvoiceHTLC, circuitKey models.CircuitKey) error { +func settleHtlcsAmp(invoice *invpkg.Invoice, circuitKey models.CircuitKey, + htlc *invpkg.InvoiceHTLC, updater invpkg.InvoiceUpdater) error { - // First, add the set ID to the set that was settled in this invoice - // update. We'll use this later to update the settle index. setID := htlc.AMP.Record.SetID() - settledSetIDs[setID] = struct{}{} // Next update the main AMP meta-data to indicate that this HTLC set // has been fully settled. @@ -1875,22 +2141,15 @@ func settleHtlcsAmp(invoice *invpkg.Invoice, invoice.AMPState[setID] = newAmpState - // Finally, we'll add this to the set of HTLCs that need to be updated. - if _, ok := updateMap[setID]; !ok { - mapEntry := make(map[models.CircuitKey]*invpkg.InvoiceHTLC) - updateMap[setID] = mapEntry - } - updateMap[setID][circuitKey] = htlc - - return nil + // Mark the updates as needing to be written to disk. + return updater.UpdateAmpState(setID, newAmpState, circuitKey) } // 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, invoices, - settleIndex, setIDIndex kvdb.RwBucket, invoice *invpkg.Invoice, - invoiceNum []byte, callback invpkg.InvoiceUpdateCallback) ( - *invpkg.Invoice, error) { +func updateInvoice(hash *lntypes.Hash, invoice *invpkg.Invoice, + updateTime time.Time, callback invpkg.InvoiceUpdateCallback, + updater invpkg.InvoiceUpdater) (*invpkg.Invoice, error) { // Create deep copy to prevent any accidental modification in the // callback. @@ -1912,147 +2171,106 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, invoices, switch update.UpdateType { case invpkg.CancelHTLCsUpdate: - return d.cancelHTLCs(invoices, invoiceNum, invoice, update) + err := cancelHTLCs(invoice, updateTime, update, updater) + if err != nil { + return nil, err + } case invpkg.AddHTLCsUpdate: - return d.addHTLCs( - invoices, settleIndex, setIDIndex, invoiceNum, invoice, - hash, update, - ) + err := addHTLCs(invoice, hash, updateTime, update, updater) + if err != nil { + return nil, err + } case invpkg.SettleHodlInvoiceUpdate: - return d.settleHodlInvoice( - invoices, settleIndex, invoiceNum, invoice, hash, - update.State, + err := settleHodlInvoice( + invoice, hash, updateTime, update.State, updater, ) + if err != nil { + return nil, err + } case invpkg.CancelInvoiceUpdate: - return d.cancelInvoice( - invoices, invoiceNum, invoice, hash, update.State, + err := cancelInvoice( + invoice, hash, updateTime, update.State, updater, ) + if err != nil { + return nil, err + } 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. - for key := range update.CancelHtlcs { - htlc, exists := invoice.Htlcs[key] - - // Verify that we don't get an action for htlcs that are not - // present on the invoice. - if !exists { - return nil, fmt.Errorf("cancel of non-existent htlc") - } - - err := canCancelSingleHtlc(htlc, invoice.State) - if err != nil { - return nil, err - } - - htlc.State = invpkg.HtlcStateCanceled - htlc.ResolveTime = timestamp - - // 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() { - err := cancelHtlcsAmp( - invoice, htlcsAmpUpdate, htlc, key, - ) - if err != nil { - return nil, err - } - } - } - - err := d.cancelHTLCsStoreUpdate( - invoices, invoiceNum, invoice, htlcsAmpUpdate, - ) - if err != nil { + if err := updater.Finalize(update.UpdateType); err != nil { return nil, err } return invoice, nil } -// cancelHTLCsStoreUpdate is a helper function used to store the invoice and -// AMP state after canceling HTLCs. -func (d *DB) cancelHTLCsStoreUpdate(invoices kvdb.RwBucket, invoiceNum []byte, - invoice *invpkg.Invoice, - htlcsAmpUpdate map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC) error { +// cancelHTLCs tries to cancel the htlcs in the given InvoiceUpdateDesc. +// +// NOTE: cancelHTLCs updates will only use the `CancelHtlcs` field in the +// InvoiceUpdateDesc. +func cancelHTLCs(invoice *invpkg.Invoice, updateTime time.Time, + update *invpkg.InvoiceUpdateDesc, updater invpkg.InvoiceUpdater) error { - err := d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) - if err != nil { - return err - } + for key := range update.CancelHtlcs { + htlc, exists := invoice.Htlcs[key] - // 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, + // Verify that we don't get an action for htlcs that are not + // present on the invoice. + if !exists { + return fmt.Errorf("cancel of non-existent htlc") + } + + err := canCancelSingleHtlc(htlc, invoice.State) + if err != nil { + return err + } + + err = resolveHtlc( + key, htlc, invpkg.HtlcStateCanceled, updateTime, + updater, ) if err != nil { return err } + + // 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() { + err := cancelHtlcsAmp(invoice, key, htlc, updater) + if err != nil { + return err + } + } } return 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 - } - - return invoices.Put(invoiceNum, buf.Bytes()) -} - // 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) { +func addHTLCs(invoice *invpkg.Invoice, hash *lntypes.Hash, updateTime time.Time, + update *invpkg.InvoiceUpdateDesc, updater invpkg.InvoiceUpdater) 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) + return 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") + return errors.New("nil custom records map") } htlc := &invpkg.InvoiceHTLC{ @@ -2060,14 +2278,14 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen MppTotalAmt: htlcUpdate.MppTotalAmt, Expiry: htlcUpdate.Expiry, AcceptHeight: uint32(htlcUpdate.AcceptHeight), - AcceptTime: timestamp, + AcceptTime: updateTime, State: invpkg.HtlcStateAccepted, CustomRecords: htlcUpdate.CustomRecords, } if invoiceIsAMP { if htlcUpdate.AMP == nil { - return nil, fmt.Errorf("unable to add htlc "+ + return fmt.Errorf("unable to add htlc "+ "without AMP data to AMP invoice(%v)", invoice.AddIndex) } @@ -2075,17 +2293,21 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen htlc.AMP = htlcUpdate.AMP.Copy() } + if err := updater.AddHtlc(key, htlc); err != nil { + return err + } + 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 { - err := updateHtlcsAmp( - invoice, htlcsAmpUpdate, htlc, - htlcUpdate.AMP.Record.SetID(), key, + err := acceptHtlcsAmp( + invoice, htlcUpdate.AMP.Record.SetID(), key, + htlc, updater, ) if err != nil { - return nil, err + return err } } } @@ -2100,7 +2322,7 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen invoice, hash, *update.State, ) if err != nil { - return nil, err + return err } // If this isn't an AMP invoice, then we'll go ahead and update @@ -2109,6 +2331,10 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen // each _htlc set_ instead. However, we'll allow the invoice to // transition to the cancelled state regardless. if !invoiceIsAMP || *newState == invpkg.ContractCanceled { + err := updater.UpdateInvoiceState(*newState, nil) + if err != nil { + return err + } invoice.State = *newState } } @@ -2123,10 +2349,8 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen // 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 - ) + var amtPaid lnwire.MilliSatoshi + for key, htlc := range invoice.Htlcs { // Set the HTLC preimage for any AMP HTLCs. if setID != nil && update.State != nil { @@ -2135,13 +2359,19 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen // If we don't already have a preimage for this HTLC, we // can set it now. case ok && htlc.AMP.Preimage == nil: + err := updater.AddAmpHtlcPreimage( + htlc.AMP.Record.SetID(), key, preimage, + ) + if err != nil { + return err + } 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 + return invpkg.ErrHTLCPreimageAlreadyExists } } @@ -2158,17 +2388,20 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen if settleEligibleAMP { htlcContextState = invpkg.ContractSettled } - htlcStateChanged, htlcState, err := getUpdatedHtlcState( htlc, htlcContextState, setID, ) if err != nil { - return nil, err + return err } if htlcStateChanged { - htlc.State = htlcState - htlc.ResolveTime = timestamp + err = resolveHtlc( + key, htlc, htlcState, updateTime, updater, + ) + if err != nil { + return err + } } htlcSettled := htlcStateChanged && @@ -2178,12 +2411,9 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen // is an AMP invoice, then we'll need to update some additional // meta data state. if htlcSettled && invoiceIsAMP { - err = settleHtlcsAmp( - invoice, settledSetIDs, htlcsAmpUpdate, htlc, - key, - ) + err = settleHtlcsAmp(invoice, key, htlc, updater) if err != nil { - return nil, err + return err } } @@ -2223,88 +2453,35 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen // 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 + if invoiceIsAMP { + amtPaid += invoice.AmtPaid } - err := d.addHTLCsStoreUpdate( - invoices, settleIndex, setIDIndex, invoiceNum, invoice, - settledSetIDs, htlcsAmpUpdate, timestamp, - ) - if err != nil { - return nil, err - } - - return invoice, nil + return updateInvoiceAmtPaid(invoice, amtPaid, updater) } -// addHTLCsStoreUpdate is a helper function used to store the invoice and -// AMP state after adding HTLCs. -func (d *DB) addHTLCsStoreUpdate(invoices, settleIndex, setIDIndex kvdb.RwBucket, - invoiceNum []byte, invoice *invpkg.Invoice, - settledSetIDs map[invpkg.SetID]struct{}, - htlcsAmpUpdate map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC, - timestamp time.Time) error { +func resolveHtlc(circuitKey models.CircuitKey, htlc *invpkg.InvoiceHTLC, + state invpkg.HtlcState, resolveTime time.Time, + updater invpkg.InvoiceUpdater) error { - invoiceIsAMP := invoice.IsAMP() - - for htlcSetID := range htlcsAmpUpdate { - // Check if this SetID already exist. - setIDInvNum := setIDIndex.Get(htlcSetID[:]) - - if setIDInvNum == nil { - err := setIDIndex.Put(htlcSetID[:], invoiceNum) - if err != nil { - return err - } - } else if !bytes.Equal(setIDInvNum, invoiceNum) { - return invpkg.ErrDuplicateSetID{ - SetID: htlcSetID, - } - } - } - - // 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. - if !invoiceIsAMP && invoice.State == invpkg.ContractSettled { - err := setSettleMetaFields( - settleIndex, invoiceNum, invoice, timestamp, nil, - ) - if err != nil { - return err - } - } - - // 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 err - } - } - - err := d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) + err := updater.ResolveHtlc(circuitKey, state, resolveTime) if err != nil { return err } + htlc.State = state + htlc.ResolveTime = resolveTime - // 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 err - } + return nil +} + +func updateInvoiceAmtPaid(invoice *invpkg.Invoice, amt lnwire.MilliSatoshi, + updater invpkg.InvoiceUpdater) error { + + err := updater.UpdateInvoiceAmtPaid(amt) + if err != nil { + return err } + invoice.AmtPaid = amt return nil } @@ -2312,12 +2489,13 @@ func (d *DB) addHTLCsStoreUpdate(invoices, settleIndex, setIDIndex kvdb.RwBucket // 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) { + +func settleHodlInvoice(invoice *invpkg.Invoice, hash *lntypes.Hash, + updateTime time.Time, update *invpkg.InvoiceStateUpdateDesc, + updater invpkg.InvoiceUpdater) error { if !invoice.HodlInvoice { - return nil, fmt.Errorf("unable to settle hodl invoice: %v is "+ + return fmt.Errorf("unable to settle hodl invoice: %v is "+ "not a hodl invoice", invoice.AddIndex) } @@ -2328,92 +2506,74 @@ func (d *DB) settleHodlInvoice(invoices, settleIndex kvdb.RwBucket, fallthrough case update.NewState != invpkg.ContractSettled: - return nil, fmt.Errorf("unable to settle hodl invoice: "+ + return 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: " + + return fmt.Errorf("unable to settle hodl invoice: " + "preimage is nil") } - // TODO(positiveblue): create a invoice.CanSettleHodlInvoice func. newState, err := getUpdatedInvoiceState( invoice, hash, *update, ) if err != nil { - return nil, err + return err } if newState == nil || *newState != invpkg.ContractSettled { - return nil, fmt.Errorf("unable to settle hodl invoice: "+ + return fmt.Errorf("unable to settle hodl invoice: "+ "new computed state is not settled: %s", newState) } + err = updater.UpdateInvoiceState( + invpkg.ContractSettled, update.Preimage, + ) + if err != nil { + return err + } + invoice.State = invpkg.ContractSettled invoice.Terms.PaymentPreimage = update.Preimage - timestamp := d.clock.Now() // TODO(positiveblue): this logic can be further simplified. var amtPaid lnwire.MilliSatoshi - for _, htlc := range invoice.Htlcs { + for key, htlc := range invoice.Htlcs { settled, _, err := getUpdatedHtlcState( htlc, invpkg.ContractSettled, nil, ) if err != nil { - return nil, err + return err } if settled { - htlc.State = invpkg.HtlcStateSettled - htlc.ResolveTime = timestamp + err = resolveHtlc( + key, htlc, invpkg.HtlcStateSettled, updateTime, + updater, + ) + if err != nil { + return err + } + amtPaid += htlc.Amt } } - invoice.AmtPaid = amtPaid - - err = d.settleHodlInvoiceStoreUpdate( - invoices, settleIndex, invoiceNum, invoice, timestamp, - ) - if err != nil { - return nil, err - } - - return invoice, nil -} - -// settleHodlInvoiceStoreUpdate is a helper function used to store the settled -// hodl invoice update. -func (d *DB) settleHodlInvoiceStoreUpdate(invoices, settleIndex kvdb.RwBucket, - invoiceNum []byte, invoice *invpkg.Invoice, timestamp time.Time) error { - - err := setSettleMetaFields( - settleIndex, invoiceNum, invoice, timestamp, nil, - ) - if err != nil { - return err - } - - err = d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) - if err != nil { - return err - } - - return nil + return updateInvoiceAmtPaid(invoice, amtPaid, updater) } // 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) { +func cancelInvoice(invoice *invpkg.Invoice, hash *lntypes.Hash, + updateTime time.Time, update *invpkg.InvoiceStateUpdateDesc, + updater invpkg.InvoiceUpdater) error { switch { case update == nil: fallthrough case update.NewState != invpkg.ContractCanceled: - return nil, fmt.Errorf("unable to cancel invoice: "+ + return fmt.Errorf("unable to cancel invoice: "+ "InvoiceUpdateDesc.State not valid: %v", update) } @@ -2429,47 +2589,41 @@ func (d *DB) cancelInvoice(invoices kvdb.RwBucket, invoiceNum []byte, newState, err := getUpdatedInvoiceState(invoice, hash, *update) if err != nil { - return nil, err + return err } if newState == nil || *newState != invpkg.ContractCanceled { - return nil, fmt.Errorf("unable to cancel invoice(%v): new "+ + return fmt.Errorf("unable to cancel invoice(%v): new "+ "computed state is not canceled: %s", invoice.AddIndex, newState) } + err = updater.UpdateInvoiceState(invpkg.ContractCanceled, nil) + if err != nil { + return err + } invoice.State = invpkg.ContractCanceled - timestamp := d.clock.Now() - // TODO(positiveblue): this logic can be simplified. - for _, htlc := range invoice.Htlcs { + for key, htlc := range invoice.Htlcs { canceled, _, err := getUpdatedHtlcState( htlc, invpkg.ContractCanceled, setID, ) if err != nil { - return nil, err + return err } if canceled { - htlc.State = invpkg.HtlcStateCanceled - htlc.ResolveTime = timestamp + err = resolveHtlc( + key, htlc, invpkg.HtlcStateCanceled, updateTime, + updater, + ) + if err != nil { + return err + } } } - err = d.cancelInvoiceStoreUpdate(invoices, invoiceNum, invoice) - if err != nil { - return nil, err - } - - return invoice, nil -} - -// cancelInvoiceStoreUpdate is a helper function used to store the canceled -// invoice update. -func (d *DB) cancelInvoiceStoreUpdate(invoices kvdb.RwBucket, invoiceNum []byte, - invoice *invpkg.Invoice) error { - - return d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) + return nil } // getUpdatedInvoiceState validates and processes an invoice state update. The @@ -2724,59 +2878,6 @@ func getUpdatedHtlcState(htlc *invpkg.InvoiceHTLC, } } -// setSettleMetaFields updates the metadata associated with settlement of an -// invoice. If a non-nil setID is passed in, then the value will be append to -// the invoice number as well, in order to allow us to detect repeated payments -// to the same AMP invoices "across time". -func setSettleMetaFields(settleIndex kvdb.RwBucket, invoiceNum []byte, - invoice *invpkg.Invoice, now time.Time, setID *invpkg.SetID) error { - - // Now that we know the invoice hasn't already been settled, we'll - // update the settle index so we can place this settle event in the - // proper location within our time series. - nextSettleSeqNo, err := settleIndex.NextSequence() - if err != nil { - return err - } - - // Make a new byte array on the stack that can potentially store the 4 - // byte invoice number along w/ the 32 byte set ID. We capture valueLen - // here which is the number of bytes copied so we can only store the 4 - // bytes if this is a non-AMP invoice. - var indexKey [invoiceSetIDKeyLen]byte - valueLen := copy(indexKey[:], invoiceNum) - - if setID != nil { - valueLen += copy(indexKey[valueLen:], setID[:]) - } - - var seqNoBytes [8]byte - byteOrder.PutUint64(seqNoBytes[:], nextSettleSeqNo) - err = settleIndex.Put(seqNoBytes[:], indexKey[:valueLen]) - if err != nil { - return err - } - - // If the setID is nil, then this means that this is a non-AMP settle, - // so we'll update the invoice settle index directly. - if setID == nil { - invoice.SettleDate = now - invoice.SettleIndex = nextSettleSeqNo - } else { - // If the set ID isn't blank, we'll update the AMP state map - // which tracks when each of the setIDs associated with a given - // AMP invoice are settled. - ampState := invoice.AMPState[*setID] - - ampState.SettleDate = now - ampState.SettleIndex = nextSettleSeqNo - - invoice.AMPState[*setID] = ampState - } - - return nil -} - // delAMPInvoices attempts to delete all the "sub" invoices associated with a // greater AMP invoices. We do this by deleting the set of keys that share the // invoice number as a prefix. diff --git a/invoices/interface.go b/invoices/interface.go index 73274dfb5..490db1be5 100644 --- a/invoices/interface.go +++ b/invoices/interface.go @@ -2,9 +2,11 @@ package invoices import ( "context" + "time" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" ) @@ -162,3 +164,37 @@ type InvoiceSlice struct { // CircuitKey is a tuple of channel ID and HTLC ID, used to uniquely identify // HTLCs in a circuit. type CircuitKey = models.CircuitKey + +// InvoiceUpdater is an interface to abstract away the details of updating an +// invoice in the database. The methods of this interface are called during the +// in-memory update of an invoice when the database needs to be updated or the +// updated state needs to be marked as needing to be written to the database. +type InvoiceUpdater interface { + // AddHtlc adds a new htlc to the invoice. + AddHtlc(circuitKey CircuitKey, newHtlc *InvoiceHTLC) error + + // ResolveHtlc marks an htlc as resolved with the given state. + ResolveHtlc(circuitKey CircuitKey, state HtlcState, + resolveTime time.Time) error + + // AddAmpHtlcPreimage adds a preimage of an AMP htlc to the AMP invoice + // identified by the setID. + AddAmpHtlcPreimage(setID [32]byte, circuitKey CircuitKey, + preimage lntypes.Preimage) error + + // UpdateInvoiceState updates the invoice state to the new state. + UpdateInvoiceState(newState ContractState, + preimage *lntypes.Preimage) error + + // UpdateInvoiceAmtPaid updates the invoice amount paid to the new + // amount. + UpdateInvoiceAmtPaid(amtPaid lnwire.MilliSatoshi) error + + // UpdateAmpState updates the state of the AMP invoice identified by + // the setID. + UpdateAmpState(setID [32]byte, newState InvoiceStateAMP, + circuitKey models.CircuitKey) error + + // Finalize finalizes the update before it is written to the database. + Finalize(updateType UpdateType) error +}