mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
827 lines
22 KiB
Go
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")
|
|
}
|
|
}
|