You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
450 lines
14 KiB
450 lines
14 KiB
package invoices |
|
|
|
import ( |
|
"errors" |
|
|
|
"github.com/lightningnetwork/lnd/amp" |
|
"github.com/lightningnetwork/lnd/channeldb" |
|
"github.com/lightningnetwork/lnd/lntypes" |
|
"github.com/lightningnetwork/lnd/lnwire" |
|
"github.com/lightningnetwork/lnd/record" |
|
) |
|
|
|
// invoiceUpdateCtx is an object that describes the context for the invoice |
|
// update to be carried out. |
|
type invoiceUpdateCtx struct { |
|
hash lntypes.Hash |
|
circuitKey channeldb.CircuitKey |
|
amtPaid lnwire.MilliSatoshi |
|
expiry uint32 |
|
currentHeight int32 |
|
finalCltvRejectDelta int32 |
|
customRecords record.CustomSet |
|
mpp *record.MPP |
|
amp *record.AMP |
|
} |
|
|
|
// invoiceRef returns an identifier that can be used to lookup or update the |
|
// invoice this HTLC is targeting. |
|
func (i *invoiceUpdateCtx) invoiceRef() channeldb.InvoiceRef { |
|
switch { |
|
case i.amp != nil && i.mpp != nil: |
|
payAddr := i.mpp.PaymentAddr() |
|
return channeldb.InvoiceRefByAddr(payAddr) |
|
case i.mpp != nil: |
|
payAddr := i.mpp.PaymentAddr() |
|
return channeldb.InvoiceRefByHashAndAddr(i.hash, payAddr) |
|
default: |
|
return channeldb.InvoiceRefByHash(i.hash) |
|
} |
|
} |
|
|
|
// setID returns an identifier that identifies other possible HTLCs that this |
|
// particular one is related to. If nil is returned this means the HTLC is an |
|
// MPP or legacy payment, otherwise the HTLC belongs AMP payment. |
|
func (i invoiceUpdateCtx) setID() *[32]byte { |
|
if i.amp != nil { |
|
setID := i.amp.SetID() |
|
return &setID |
|
} |
|
return nil |
|
} |
|
|
|
// log logs a message specific to this update context. |
|
func (i *invoiceUpdateCtx) log(s string) { |
|
log.Debugf("Invoice%v: %v, amt=%v, expiry=%v, circuit=%v, mpp=%v, "+ |
|
"amp=%v", i.invoiceRef(), s, i.amtPaid, i.expiry, i.circuitKey, |
|
i.mpp, i.amp) |
|
} |
|
|
|
// failRes is a helper function which creates a failure resolution with |
|
// the information contained in the invoiceUpdateCtx and the fail resolution |
|
// result provided. |
|
func (i invoiceUpdateCtx) failRes(outcome FailResolutionResult) *HtlcFailResolution { |
|
return NewFailResolution(i.circuitKey, i.currentHeight, outcome) |
|
} |
|
|
|
// settleRes is a helper function which creates a settle resolution with |
|
// the information contained in the invoiceUpdateCtx and the preimage and |
|
// the settle resolution result provided. |
|
func (i invoiceUpdateCtx) settleRes(preimage lntypes.Preimage, |
|
outcome SettleResolutionResult) *HtlcSettleResolution { |
|
|
|
return NewSettleResolution( |
|
preimage, i.circuitKey, i.currentHeight, outcome, |
|
) |
|
} |
|
|
|
// acceptRes is a helper function which creates an accept resolution with |
|
// the information contained in the invoiceUpdateCtx and the accept resolution |
|
// result provided. |
|
func (i invoiceUpdateCtx) acceptRes(outcome acceptResolutionResult) *htlcAcceptResolution { |
|
return newAcceptResolution(i.circuitKey, outcome) |
|
} |
|
|
|
// updateInvoice is a callback for DB.UpdateInvoice that contains the invoice |
|
// settlement logic. It returns a hltc resolution that indicates what the |
|
// outcome of the update was. |
|
func updateInvoice(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) ( |
|
*channeldb.InvoiceUpdateDesc, HtlcResolution, error) { |
|
|
|
// Don't update the invoice when this is a replayed htlc. |
|
htlc, ok := inv.Htlcs[ctx.circuitKey] |
|
if ok { |
|
switch htlc.State { |
|
case channeldb.HtlcStateCanceled: |
|
return nil, ctx.failRes(ResultReplayToCanceled), nil |
|
|
|
case channeldb.HtlcStateAccepted: |
|
return nil, ctx.acceptRes(resultReplayToAccepted), nil |
|
|
|
case channeldb.HtlcStateSettled: |
|
return nil, ctx.settleRes( |
|
*inv.Terms.PaymentPreimage, |
|
ResultReplayToSettled, |
|
), nil |
|
|
|
default: |
|
return nil, nil, errors.New("unknown htlc state") |
|
} |
|
} |
|
|
|
// If no MPP payload was provided, then we expect this to be a keysend, |
|
// or a payment to an invoice created before we started to require the |
|
// MPP payload. |
|
if ctx.mpp == nil { |
|
return updateLegacy(ctx, inv) |
|
} |
|
|
|
return updateMpp(ctx, inv) |
|
} |
|
|
|
// updateMpp is a callback for DB.UpdateInvoice that contains the invoice |
|
// settlement logic for mpp payments. |
|
func updateMpp(ctx *invoiceUpdateCtx, |
|
inv *channeldb.Invoice) (*channeldb.InvoiceUpdateDesc, |
|
HtlcResolution, error) { |
|
|
|
// Reject HTLCs to AMP invoices if they are missing an AMP payload, and |
|
// HTLCs to MPP invoices if they have an AMP payload. |
|
switch { |
|
|
|
case inv.Terms.Features.RequiresFeature(lnwire.AMPRequired) && |
|
ctx.amp == nil: |
|
|
|
return nil, ctx.failRes(ResultHtlcInvoiceTypeMismatch), nil |
|
|
|
case !inv.Terms.Features.RequiresFeature(lnwire.AMPRequired) && |
|
ctx.amp != nil: |
|
|
|
return nil, ctx.failRes(ResultHtlcInvoiceTypeMismatch), nil |
|
} |
|
|
|
setID := ctx.setID() |
|
|
|
// Start building the accept descriptor. |
|
acceptDesc := &channeldb.HtlcAcceptDesc{ |
|
Amt: ctx.amtPaid, |
|
Expiry: ctx.expiry, |
|
AcceptHeight: ctx.currentHeight, |
|
MppTotalAmt: ctx.mpp.TotalMsat(), |
|
CustomRecords: ctx.customRecords, |
|
} |
|
|
|
if ctx.amp != nil { |
|
acceptDesc.AMP = &channeldb.InvoiceHtlcAMPData{ |
|
Record: *ctx.amp, |
|
Hash: ctx.hash, |
|
Preimage: nil, |
|
} |
|
} |
|
|
|
// Only accept payments to open invoices. This behaviour differs from |
|
// non-mpp payments that are accepted even after the invoice is settled. |
|
// Because non-mpp payments don't have a payment address, this is needed |
|
// to thwart probing. |
|
if inv.State != channeldb.ContractOpen { |
|
return nil, ctx.failRes(ResultInvoiceNotOpen), nil |
|
} |
|
|
|
// Check the payment address that authorizes the payment. |
|
if ctx.mpp.PaymentAddr() != inv.Terms.PaymentAddr { |
|
return nil, ctx.failRes(ResultAddressMismatch), nil |
|
} |
|
|
|
// Don't accept zero-valued sets. |
|
if ctx.mpp.TotalMsat() == 0 { |
|
return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil |
|
} |
|
|
|
// Check that the total amt of the htlc set is high enough. In case this |
|
// is a zero-valued invoice, it will always be enough. |
|
if ctx.mpp.TotalMsat() < inv.Terms.Value { |
|
return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil |
|
} |
|
|
|
htlcSet := inv.HTLCSet(setID, channeldb.HtlcStateAccepted) |
|
|
|
// Check whether total amt matches other htlcs in the set. |
|
var newSetTotal lnwire.MilliSatoshi |
|
for _, htlc := range htlcSet { |
|
// Only consider accepted mpp htlcs. It is possible that there |
|
// are htlcs registered in the invoice database that previously |
|
// timed out and are in the canceled state now. |
|
if htlc.State != channeldb.HtlcStateAccepted { |
|
continue |
|
} |
|
|
|
if ctx.mpp.TotalMsat() != htlc.MppTotalAmt { |
|
return nil, ctx.failRes(ResultHtlcSetTotalMismatch), nil |
|
} |
|
|
|
newSetTotal += htlc.Amt |
|
} |
|
|
|
// Add amount of new htlc. |
|
newSetTotal += ctx.amtPaid |
|
|
|
// Make sure the communicated set total isn't overpaid. |
|
if newSetTotal > ctx.mpp.TotalMsat() { |
|
return nil, ctx.failRes(ResultHtlcSetOverpayment), nil |
|
} |
|
|
|
// The invoice is still open. Check the expiry. |
|
if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) { |
|
return nil, ctx.failRes(ResultExpiryTooSoon), nil |
|
} |
|
|
|
if ctx.expiry < uint32(ctx.currentHeight+inv.Terms.FinalCltvDelta) { |
|
return nil, ctx.failRes(ResultExpiryTooSoon), nil |
|
} |
|
|
|
// Record HTLC in the invoice database. |
|
newHtlcs := map[channeldb.CircuitKey]*channeldb.HtlcAcceptDesc{ |
|
ctx.circuitKey: acceptDesc, |
|
} |
|
|
|
update := channeldb.InvoiceUpdateDesc{ |
|
AddHtlcs: newHtlcs, |
|
} |
|
|
|
// If the invoice cannot be settled yet, only record the htlc. |
|
setComplete := newSetTotal == ctx.mpp.TotalMsat() |
|
if !setComplete { |
|
return &update, ctx.acceptRes(resultPartialAccepted), nil |
|
} |
|
|
|
// Check to see if we can settle or this is an hold invoice and |
|
// we need to wait for the preimage. |
|
if inv.HodlInvoice { |
|
update.State = &channeldb.InvoiceStateUpdateDesc{ |
|
NewState: channeldb.ContractAccepted, |
|
SetID: setID, |
|
} |
|
return &update, ctx.acceptRes(resultAccepted), nil |
|
} |
|
|
|
var ( |
|
htlcPreimages map[channeldb.CircuitKey]lntypes.Preimage |
|
htlcPreimage lntypes.Preimage |
|
) |
|
if ctx.amp != nil { |
|
var failRes *HtlcFailResolution |
|
htlcPreimages, failRes = reconstructAMPPreimages(ctx, htlcSet) |
|
if failRes != nil { |
|
update.State = &channeldb.InvoiceStateUpdateDesc{ |
|
NewState: channeldb.ContractCanceled, |
|
SetID: setID, |
|
} |
|
return &update, failRes, nil |
|
} |
|
|
|
// The preimage for _this_ HTLC will be the one with context's |
|
// circuit key. |
|
htlcPreimage = htlcPreimages[ctx.circuitKey] |
|
} else { |
|
htlcPreimage = *inv.Terms.PaymentPreimage |
|
} |
|
|
|
update.State = &channeldb.InvoiceStateUpdateDesc{ |
|
NewState: channeldb.ContractSettled, |
|
Preimage: inv.Terms.PaymentPreimage, |
|
HTLCPreimages: htlcPreimages, |
|
SetID: setID, |
|
} |
|
|
|
return &update, ctx.settleRes(htlcPreimage, ResultSettled), nil |
|
} |
|
|
|
// HTLCSet is a map of CircuitKey to InvoiceHTLC. |
|
type HTLCSet = map[channeldb.CircuitKey]*channeldb.InvoiceHTLC |
|
|
|
// HTLCPreimages is a map of CircuitKey to preimage. |
|
type HTLCPreimages = map[channeldb.CircuitKey]lntypes.Preimage |
|
|
|
// reconstructAMPPreimages reconstructs the root seed for an AMP HTLC set and |
|
// verifies that all derived child hashes match the payment hashes of the HTLCs |
|
// in the set. This method is meant to be called after receiving the full amount |
|
// committed to via mpp_total_msat. This method will return a fail resolution if |
|
// any of the child hashes fail to matche theire corresponding HTLCs. |
|
func reconstructAMPPreimages(ctx *invoiceUpdateCtx, |
|
htlcSet HTLCSet) (HTLCPreimages, *HtlcFailResolution) { |
|
|
|
// Create a slice containing all the child descriptors to be used for |
|
// reconstruction. This should include all HTLCs currently in the HTLC |
|
// set, plus the incoming HTLC. |
|
childDescs := make([]amp.ChildDesc, 0, 1+len(htlcSet)) |
|
|
|
// Add the new HTLC's child descriptor at index 0. |
|
childDescs = append(childDescs, amp.ChildDesc{ |
|
Share: ctx.amp.RootShare(), |
|
Index: ctx.amp.ChildIndex(), |
|
}) |
|
|
|
// Next, construct an index mapping the position in childDescs to a |
|
// circuit key for all preexisting HTLCs. |
|
indexToCircuitKey := make(map[int]channeldb.CircuitKey) |
|
|
|
// Add the child descriptor for each HTLC in the HTLC set, recording |
|
// it's position within the slice. |
|
var htlcSetIndex int |
|
for circuitKey, htlc := range htlcSet { |
|
childDescs = append(childDescs, amp.ChildDesc{ |
|
Share: htlc.AMP.Record.RootShare(), |
|
Index: htlc.AMP.Record.ChildIndex(), |
|
}) |
|
indexToCircuitKey[htlcSetIndex] = circuitKey |
|
htlcSetIndex++ |
|
} |
|
|
|
// Using the child descriptors, reconstruct the root seed and derive the |
|
// child hash/preimage pairs for each of the HTLCs. |
|
children := amp.ReconstructChildren(childDescs...) |
|
|
|
// Validate that the derived child preimages match the hash of each |
|
// HTLC's respective hash. |
|
if ctx.hash != children[0].Hash { |
|
return nil, ctx.failRes(ResultAmpReconstruction) |
|
} |
|
for idx, child := range children[1:] { |
|
circuitKey := indexToCircuitKey[idx] |
|
htlc := htlcSet[circuitKey] |
|
if htlc.AMP.Hash != child.Hash { |
|
return nil, ctx.failRes(ResultAmpReconstruction) |
|
} |
|
} |
|
|
|
// Finally, construct the map of learned preimages indexed by circuit |
|
// key, so that they can be persisted along with each HTLC when updating |
|
// the invoice. |
|
htlcPreimages := make(map[channeldb.CircuitKey]lntypes.Preimage) |
|
htlcPreimages[ctx.circuitKey] = children[0].Preimage |
|
for idx, child := range children[1:] { |
|
circuitKey := indexToCircuitKey[idx] |
|
htlcPreimages[circuitKey] = child.Preimage |
|
} |
|
|
|
return htlcPreimages, nil |
|
} |
|
|
|
// updateLegacy is a callback for DB.UpdateInvoice that contains the invoice |
|
// settlement logic for legacy payments. |
|
// |
|
// NOTE: This function is only kept in place in order to be able to handle key |
|
// send payments and any invoices we created in the past that are valid and |
|
// still had the optional mpp bit set. |
|
func updateLegacy(ctx *invoiceUpdateCtx, |
|
inv *channeldb.Invoice) (*channeldb.InvoiceUpdateDesc, HtlcResolution, error) { |
|
|
|
// If the invoice is already canceled, there is no further |
|
// checking to do. |
|
if inv.State == channeldb.ContractCanceled { |
|
return nil, ctx.failRes(ResultInvoiceAlreadyCanceled), nil |
|
} |
|
|
|
// If an invoice amount is specified, check that enough is paid. Also |
|
// check this for duplicate payments if the invoice is already settled |
|
// or accepted. In case this is a zero-valued invoice, it will always be |
|
// enough. |
|
if ctx.amtPaid < inv.Terms.Value { |
|
return nil, ctx.failRes(ResultAmountTooLow), nil |
|
} |
|
|
|
// If the invoice had the required feature bit set at this point, then |
|
// if we're in this method it means that the remote party didn't supply |
|
// the expected payload. However if this is a keysend payment, then |
|
// we'll permit it to pass. |
|
_, isKeySend := ctx.customRecords[record.KeySendType] |
|
invoiceFeatures := inv.Terms.Features |
|
paymentAddrRequired := invoiceFeatures.RequiresFeature( |
|
lnwire.PaymentAddrRequired, |
|
) |
|
if !isKeySend && paymentAddrRequired { |
|
log.Warnf("Payment to pay_hash=%v doesn't include MPP "+ |
|
"payload, rejecting", ctx.hash) |
|
return nil, ctx.failRes(ResultAddressMismatch), nil |
|
} |
|
|
|
// Don't allow settling the invoice with an old style |
|
// htlc if we are already in the process of gathering an |
|
// mpp set. |
|
for _, htlc := range inv.HTLCSet(nil, channeldb.HtlcStateAccepted) { |
|
if htlc.MppTotalAmt > 0 { |
|
return nil, ctx.failRes(ResultMppInProgress), nil |
|
} |
|
} |
|
|
|
// The invoice is still open. Check the expiry. |
|
if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) { |
|
return nil, ctx.failRes(ResultExpiryTooSoon), nil |
|
} |
|
|
|
if ctx.expiry < uint32(ctx.currentHeight+inv.Terms.FinalCltvDelta) { |
|
return nil, ctx.failRes(ResultExpiryTooSoon), nil |
|
} |
|
|
|
// Record HTLC in the invoice database. |
|
newHtlcs := map[channeldb.CircuitKey]*channeldb.HtlcAcceptDesc{ |
|
ctx.circuitKey: { |
|
Amt: ctx.amtPaid, |
|
Expiry: ctx.expiry, |
|
AcceptHeight: ctx.currentHeight, |
|
CustomRecords: ctx.customRecords, |
|
}, |
|
} |
|
|
|
update := channeldb.InvoiceUpdateDesc{ |
|
AddHtlcs: newHtlcs, |
|
} |
|
|
|
// Don't update invoice state if we are accepting a duplicate payment. |
|
// We do accept or settle the HTLC. |
|
switch inv.State { |
|
case channeldb.ContractAccepted: |
|
return &update, ctx.acceptRes(resultDuplicateToAccepted), nil |
|
|
|
case channeldb.ContractSettled: |
|
return &update, ctx.settleRes( |
|
*inv.Terms.PaymentPreimage, ResultDuplicateToSettled, |
|
), nil |
|
} |
|
|
|
// Check to see if we can settle or this is an hold invoice and we need |
|
// to wait for the preimage. |
|
if inv.HodlInvoice { |
|
update.State = &channeldb.InvoiceStateUpdateDesc{ |
|
NewState: channeldb.ContractAccepted, |
|
} |
|
|
|
return &update, ctx.acceptRes(resultAccepted), nil |
|
} |
|
|
|
update.State = &channeldb.InvoiceStateUpdateDesc{ |
|
NewState: channeldb.ContractSettled, |
|
Preimage: inv.Terms.PaymentPreimage, |
|
} |
|
|
|
return &update, ctx.settleRes( |
|
*inv.Terms.PaymentPreimage, ResultSettled, |
|
), nil |
|
}
|
|
|