lnd/invoices/update_invoice.go
2024-02-19 20:47:24 +01:00

827 lines
22 KiB
Go

package invoices
import (
"errors"
"fmt"
"time"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
)
// 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 acceptHtlcsAmp(invoice *Invoice, setID SetID,
circuitKey models.CircuitKey, htlc *InvoiceHTLC,
updater InvoiceUpdater) error {
newAmpState, err := getUpdatedInvoiceAmpState(
invoice, setID, circuitKey, HtlcStateAccepted, htlc.Amt,
)
if err != nil {
return err
}
invoice.AMPState[setID] = newAmpState
// 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 *Invoice, circuitKey models.CircuitKey,
htlc *InvoiceHTLC, updater InvoiceUpdater) error {
setID := htlc.AMP.Record.SetID()
// First, we'll update the state of the entire HTLC set
// to cancelled.
newAmpState, err := getUpdatedInvoiceAmpState(
invoice, setID, circuitKey, HtlcStateCanceled,
htlc.Amt,
)
if err != nil {
return err
}
invoice.AMPState[setID] = newAmpState
// Mark the updates as needing to be written to disk.
err = updater.UpdateAmpState(setID, newAmpState, circuitKey)
if err != nil {
return err
}
// We'll only decrement the total amount paid if the invoice was
// already in the accepted state.
if invoice.AmtPaid != 0 {
return updateInvoiceAmtPaid(
invoice, invoice.AmtPaid-htlc.Amt, updater,
)
}
return nil
}
// 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 *Invoice, circuitKey models.CircuitKey,
htlc *InvoiceHTLC, updater InvoiceUpdater) error {
setID := htlc.AMP.Record.SetID()
// Next update the main AMP meta-data to indicate that this HTLC set
// has been fully settled.
newAmpState, err := getUpdatedInvoiceAmpState(
invoice, setID, circuitKey, HtlcStateSettled, 0,
)
if err != nil {
return err
}
invoice.AMPState[setID] = newAmpState
// 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 UpdateInvoice(hash *lntypes.Hash, invoice *Invoice,
updateTime time.Time, callback InvoiceUpdateCallback,
updater InvoiceUpdater) (*Invoice, error) {
// Create deep copy to prevent any accidental modification in the
// callback.
invoiceCopy, err := CopyInvoice(invoice)
if err != nil {
return nil, err
}
// Call the callback and obtain the update descriptor.
update, err := callback(invoiceCopy)
if err != nil {
return invoice, err
}
// If there is nothing to update, return early.
if update == nil {
return invoice, nil
}
switch update.UpdateType {
case CancelHTLCsUpdate:
err := cancelHTLCs(invoice, updateTime, update, updater)
if err != nil {
return nil, err
}
case AddHTLCsUpdate:
err := addHTLCs(invoice, hash, updateTime, update, updater)
if err != nil {
return nil, err
}
case SettleHodlInvoiceUpdate:
err := settleHodlInvoice(
invoice, hash, updateTime, update.State, updater,
)
if err != nil {
return nil, err
}
case CancelInvoiceUpdate:
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)
}
if err := updater.Finalize(update.UpdateType); err != nil {
return nil, err
}
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 cancelHTLCs(invoice *Invoice, updateTime time.Time,
update *InvoiceUpdateDesc, updater InvoiceUpdater) error {
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 fmt.Errorf("cancel of non-existent htlc")
}
err := canCancelSingleHtlc(htlc, invoice.State)
if err != nil {
return err
}
err = resolveHtlc(
key, htlc, 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
}
// addHTLCs tries to add the htlcs in the given InvoiceUpdateDesc.
//
//nolint:funlen
func addHTLCs(invoice *Invoice, hash *lntypes.Hash, updateTime time.Time,
update *InvoiceUpdateDesc, updater InvoiceUpdater) error {
var setID *[32]byte
invoiceIsAMP := invoice.IsAMP()
if invoiceIsAMP && update.State != nil {
setID = update.State.SetID
}
for key, htlcUpdate := range update.AddHtlcs {
if _, exists := invoice.Htlcs[key]; exists {
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 errors.New("nil custom records map")
}
htlc := &InvoiceHTLC{
Amt: htlcUpdate.Amt,
MppTotalAmt: htlcUpdate.MppTotalAmt,
Expiry: htlcUpdate.Expiry,
AcceptHeight: uint32(htlcUpdate.AcceptHeight),
AcceptTime: updateTime,
State: HtlcStateAccepted,
CustomRecords: htlcUpdate.CustomRecords,
}
if invoiceIsAMP {
if htlcUpdate.AMP == nil {
return fmt.Errorf("unable to add htlc "+
"without AMP data to AMP invoice(%v)",
invoice.AddIndex)
}
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 := acceptHtlcsAmp(
invoice, htlcUpdate.AMP.Record.SetID(), key,
htlc, updater,
)
if err != nil {
return err
}
}
}
// 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 := getUpdatedInvoiceState(
invoice, hash, *update.State,
)
if err != nil {
return 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 update the state of
// each _htlc set_ instead. However, we'll allow the invoice to
// transition to the cancelled state regardless.
if !invoiceIsAMP || *newState == ContractCanceled {
err := updater.UpdateInvoiceState(*newState, nil)
if err != nil {
return err
}
invoice.State = *newState
}
}
// 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 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:
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 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 = ContractSettled
}
htlcStateChanged, htlcState, err := getUpdatedHtlcState(
htlc, htlcContextState, setID,
)
if err != nil {
return err
}
if htlcStateChanged {
err = resolveHtlc(
key, htlc, htlcState, updateTime, updater,
)
if err != nil {
return err
}
}
htlcSettled := htlcStateChanged &&
htlcState == HtlcStateSettled
// 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 {
err = settleHtlcsAmp(invoice, key, htlc, updater)
if err != nil {
return err
}
}
accepted := htlc.State == HtlcStateAccepted
settled := htlc.State == 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 != 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 == 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 {
amtPaid += invoice.AmtPaid
}
return updateInvoiceAmtPaid(invoice, amtPaid, updater)
}
func resolveHtlc(circuitKey models.CircuitKey, htlc *InvoiceHTLC,
state HtlcState, resolveTime time.Time,
updater InvoiceUpdater) error {
err := updater.ResolveHtlc(circuitKey, state, resolveTime)
if err != nil {
return err
}
htlc.State = state
htlc.ResolveTime = resolveTime
return nil
}
func updateInvoiceAmtPaid(invoice *Invoice, amt lnwire.MilliSatoshi,
updater InvoiceUpdater) error {
err := updater.UpdateInvoiceAmtPaid(amt)
if err != nil {
return err
}
invoice.AmtPaid = amt
return nil
}
// settleHodlInvoice marks a hodl invoice as settled.
//
// NOTE: Currently it is not possible to have HODL AMP invoices.
func settleHodlInvoice(invoice *Invoice, hash *lntypes.Hash,
updateTime time.Time, update *InvoiceStateUpdateDesc,
updater InvoiceUpdater) error {
if !invoice.HodlInvoice {
return 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 != ContractSettled:
return fmt.Errorf("unable to settle hodl invoice: "+
"not valid InvoiceUpdateDesc.State: %v", update)
case update.Preimage == nil:
return fmt.Errorf("unable to settle hodl invoice: " +
"preimage is nil")
}
newState, err := getUpdatedInvoiceState(
invoice, hash, *update,
)
if err != nil {
return err
}
if newState == nil || *newState != ContractSettled {
return fmt.Errorf("unable to settle hodl invoice: "+
"new computed state is not settled: %s", newState)
}
err = updater.UpdateInvoiceState(
ContractSettled, update.Preimage,
)
if err != nil {
return err
}
invoice.State = ContractSettled
invoice.Terms.PaymentPreimage = update.Preimage
// TODO(positiveblue): this logic can be further simplified.
var amtPaid lnwire.MilliSatoshi
for key, htlc := range invoice.Htlcs {
settled, _, err := getUpdatedHtlcState(
htlc, ContractSettled, nil,
)
if err != nil {
return err
}
if settled {
err = resolveHtlc(
key, htlc, HtlcStateSettled, updateTime,
updater,
)
if err != nil {
return err
}
amtPaid += htlc.Amt
}
}
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 cancelInvoice(invoice *Invoice, hash *lntypes.Hash,
updateTime time.Time, update *InvoiceStateUpdateDesc,
updater InvoiceUpdater) error {
switch {
case update == nil:
fallthrough
case update.NewState != ContractCanceled:
return 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 := getUpdatedInvoiceState(invoice, hash, *update)
if err != nil {
return err
}
if newState == nil || *newState != ContractCanceled {
return fmt.Errorf("unable to cancel invoice(%v): new "+
"computed state is not canceled: %s", invoice.AddIndex,
newState)
}
err = updater.UpdateInvoiceState(ContractCanceled, nil)
if err != nil {
return err
}
invoice.State = ContractCanceled
for key, htlc := range invoice.Htlcs {
canceled, _, err := getUpdatedHtlcState(
htlc, ContractCanceled, setID,
)
if err != nil {
return err
}
if canceled {
err = resolveHtlc(
key, htlc, HtlcStateCanceled, updateTime,
updater,
)
if err != nil {
return err
}
}
}
return nil
}
// getUpdatedInvoiceState 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. Note that for AMP invoices this
// function is only used to validate the state transition if we're cancelling
// the invoice.
func getUpdatedInvoiceState(invoice *Invoice, hash *lntypes.Hash,
update InvoiceStateUpdateDesc) (*ContractState, error) {
// Returning to open is never allowed from any state.
if update.NewState == ContractOpen {
return nil, ErrInvoiceCannotOpen
}
switch invoice.State {
// Once a contract is accepted, we can only transition to settled or
// canceled. Forbid transitioning back into this state. Otherwise this
// state is identical to ContractOpen, so we fallthrough to apply the
// same checks that we apply to open invoices.
case ContractAccepted:
if update.NewState == ContractAccepted {
return nil, ErrInvoiceCannotAccept
}
fallthrough
// If a contract is open, permit a state transition to accepted, settled
// or canceled. The only restriction is on transitioning to settled
// where we ensure the preimage is valid.
case ContractOpen:
if update.NewState == ContractCanceled {
return &update.NewState, nil
}
// Sanity check that the user isn't trying to settle or accept a
// non-existent HTLC set.
set := invoice.HTLCSet(update.SetID, HtlcStateAccepted)
if len(set) == 0 {
return nil, ErrEmptyHTLCSet
}
// For AMP invoices, there are no invoice-level preimage checks.
// However, we still sanity check that we aren't trying to
// settle an AMP invoice with a preimage.
if update.SetID != nil {
if update.Preimage != nil {
return nil, errors.New("AMP set cannot have " +
"preimage")
}
return &update.NewState, nil
}
switch {
// If an invoice-level preimage was supplied, but the InvoiceRef
// doesn't specify a hash (e.g. AMP invoices) we fail.
case update.Preimage != nil && hash == nil:
return nil, ErrUnexpectedInvoicePreimage
// Validate the supplied preimage for non-AMP invoices.
case update.Preimage != nil:
if update.Preimage.Hash() != *hash {
return nil, ErrInvoicePreimageMismatch
}
// Permit non-AMP invoices to be accepted without knowing the
// preimage. When trying to settle we'll have to pass through
// the above check in order to not hit the one below.
case update.NewState == ContractAccepted:
// Fail if we still don't have a preimage when transitioning to
// settle the non-AMP invoice.
case update.NewState == ContractSettled &&
invoice.Terms.PaymentPreimage == nil:
return nil, errors.New("unknown preimage")
}
return &update.NewState, nil
// Once settled, we are in a terminal state.
case ContractSettled:
return nil, ErrInvoiceAlreadySettled
// Once canceled, we are in a terminal state.
case ContractCanceled:
return nil, ErrInvoiceAlreadyCanceled
default:
return nil, errors.New("unknown state transition")
}
}
// getUpdatedInvoiceAmpState returns the AMP state of an invoice (without
// applying it), given the new state, and the amount of the HTLC that is
// being updated.
func getUpdatedInvoiceAmpState(invoice *Invoice, setID SetID,
circuitKey models.CircuitKey, state HtlcState,
amt lnwire.MilliSatoshi) (InvoiceStateAMP, error) {
// Retrieve the AMP state for this set ID.
ampState, ok := invoice.AMPState[setID]
// If the state is accepted then we may need to create a new entry for
// this set ID, otherwise we expect that the entry already exists and
// we can update it.
if !ok && state != HtlcStateAccepted {
return InvoiceStateAMP{},
fmt.Errorf("unable to update AMP state for setID=%x ",
setID)
}
switch state {
case HtlcStateAccepted:
if !ok {
// If an entry for this set ID doesn't already exist,
// then we'll need to create it.
ampState = InvoiceStateAMP{
State: HtlcStateAccepted,
InvoiceKeys: make(
map[models.CircuitKey]struct{},
),
}
}
ampState.AmtPaid += amt
case HtlcStateCanceled:
ampState.State = HtlcStateCanceled
ampState.AmtPaid -= amt
case HtlcStateSettled:
ampState.State = HtlcStateSettled
}
ampState.InvoiceKeys[circuitKey] = struct{}{}
return ampState, nil
}
// canCancelSingleHtlc validates cancellation of a single HTLC. If nil is
// returned, then the HTLC can be cancelled.
func canCancelSingleHtlc(htlc *InvoiceHTLC,
invoiceState ContractState) error {
// It is only possible to cancel individual htlcs on an open invoice.
if invoiceState != ContractOpen {
return fmt.Errorf("htlc canceled on invoice in state %v",
invoiceState)
}
// It is only possible if the htlc is still pending.
if htlc.State != HtlcStateAccepted {
return fmt.Errorf("htlc canceled in state %v", htlc.State)
}
return nil
}
// getUpdatedHtlcState aligns the state of an htlc with the given invoice state.
// A boolean indicating whether the HTLCs state need to be updated, along with
// the new state (or old state if no change is needed) is returned.
func getUpdatedHtlcState(htlc *InvoiceHTLC,
invoiceState ContractState, setID *[32]byte) (
bool, HtlcState, error) {
trySettle := func(persist bool) (bool, HtlcState, error) {
if htlc.State != HtlcStateAccepted {
return false, htlc.State, nil
}
// Settle the HTLC if it matches the settled set id. If
// there're other HTLCs with distinct setIDs, then we'll leave
// them, as they may eventually be settled as we permit
// multiple settles to a single pay_addr for AMP.
settled := false
if htlc.IsInHTLCSet(setID) {
// Non-AMP HTLCs can be settled immediately since we
// already know the preimage is valid due to checks at
// the invoice level. For AMP HTLCs, verify that the
// per-HTLC preimage-hash pair is valid.
switch {
// Non-AMP HTLCs can be settle immediately since we
// already know the preimage is valid due to checks at
// the invoice level.
case setID == nil:
// At this point, the setID is non-nil, meaning this is
// an AMP HTLC. We know that htlc.AMP cannot be nil,
// otherwise IsInHTLCSet would have returned false.
//
// Fail if an accepted AMP HTLC has no preimage.
case htlc.AMP.Preimage == nil:
return false, htlc.State,
ErrHTLCPreimageMissing
// Fail if the accepted AMP HTLC has an invalid
// preimage.
case !htlc.AMP.Preimage.Matches(htlc.AMP.Hash):
return false, htlc.State,
ErrHTLCPreimageMismatch
}
settled = true
}
// Only persist the changes if the invoice is moving to the
// settled state, and we're actually updating the state to
// settled.
newState := htlc.State
if settled {
newState = HtlcStateSettled
}
return persist && settled, newState, nil
}
if invoiceState == ContractSettled {
// Check that we can settle the HTLCs. For legacy and MPP HTLCs
// this will be a NOP, but for AMP HTLCs this asserts that we
// have a valid hash/preimage pair. Passing true permits the
// method to update the HTLC to HtlcStateSettled.
return trySettle(true)
}
// We should never find a settled HTLC on an invoice that isn't in
// ContractSettled.
if htlc.State == HtlcStateSettled {
return false, htlc.State, ErrHTLCAlreadySettled
}
switch invoiceState {
case ContractCanceled:
htlcAlreadyCanceled := htlc.State == HtlcStateCanceled
return !htlcAlreadyCanceled, HtlcStateCanceled, nil
// TODO(roasbeef): never fully passed thru now?
case ContractAccepted:
// Check that we can settle the HTLCs. For legacy and MPP HTLCs
// this will be a NOP, but for AMP HTLCs this asserts that we
// have a valid hash/preimage pair. Passing false prevents the
// method from putting the HTLC in HtlcStateSettled, leaving it
// in HtlcStateAccepted.
return trySettle(false)
case ContractOpen:
return false, htlc.State, nil
default:
return false, htlc.State, errors.New("unknown state transition")
}
}