invoices: reconstruct AMP child preimages

This commit is contained in:
Conner Fromknecht 2021-03-24 19:52:01 -07:00
parent 2a49b59f4f
commit 90a255078d
No known key found for this signature in database
GPG Key ID: E7D737B67FA592C7
3 changed files with 138 additions and 11 deletions

@ -1019,13 +1019,18 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
continue continue
} }
preimage := res.Preimage
if htlc.AMP != nil && htlc.AMP.Preimage != nil {
preimage = *htlc.AMP.Preimage
}
// Notify subscribers that the htlcs should be settled // Notify subscribers that the htlcs should be settled
// with our peer. Note that the outcome of the // with our peer. Note that the outcome of the
// resolution is set based on the outcome of the single // resolution is set based on the outcome of the single
// htlc that we just settled, so may not be accurate // htlc that we just settled, so may not be accurate
// for all htlcs. // for all htlcs.
htlcSettleResolution := NewSettleResolution( htlcSettleResolution := NewSettleResolution(
res.Preimage, key, preimage, key,
int32(htlc.AcceptHeight), res.Outcome, int32(htlc.AcceptHeight), res.Outcome,
) )

@ -108,6 +108,10 @@ const (
// ResultAmpError is returned when we receive invalid AMP parameters. // ResultAmpError is returned when we receive invalid AMP parameters.
ResultAmpError ResultAmpError
// ResultAmpReconstruction is returned when the derived child
// hash/preimage pairs were invalid for at least one HTLC in the set.
ResultAmpReconstruction
) )
// String returns a string representation of the result. // String returns a string representation of the result.
@ -168,6 +172,9 @@ func (f FailResolutionResult) FailureString() string {
case ResultAmpError: case ResultAmpError:
return "invalid amp parameters" return "invalid amp parameters"
case ResultAmpReconstruction:
return "amp reconstruction failed"
default: default:
return "unknown failure resolution result" return "unknown failure resolution result"
} }

@ -3,6 +3,7 @@ package invoices
import ( import (
"errors" "errors"
"github.com/lightningnetwork/lnd/amp"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@ -26,11 +27,16 @@ type invoiceUpdateCtx struct {
// invoiceRef returns an identifier that can be used to lookup or update the // invoiceRef returns an identifier that can be used to lookup or update the
// invoice this HTLC is targeting. // invoice this HTLC is targeting.
func (i *invoiceUpdateCtx) invoiceRef() channeldb.InvoiceRef { func (i *invoiceUpdateCtx) invoiceRef() channeldb.InvoiceRef {
if i.mpp != nil { switch {
case i.amp != nil && i.mpp != nil:
payAddr := i.mpp.PaymentAddr()
return channeldb.InvoiceRefByAddr(payAddr)
case i.mpp != nil:
payAddr := i.mpp.PaymentAddr() payAddr := i.mpp.PaymentAddr()
return channeldb.InvoiceRefByHashAndAddr(i.hash, payAddr) return channeldb.InvoiceRefByHashAndAddr(i.hash, payAddr)
default:
return channeldb.InvoiceRefByHash(i.hash)
} }
return channeldb.InvoiceRefByHash(i.hash)
} }
// setID returns an identifier that identifies other possible HTLCs that this // setID returns an identifier that identifies other possible HTLCs that this
@ -130,6 +136,14 @@ func updateMpp(ctx *invoiceUpdateCtx,
CustomRecords: ctx.customRecords, 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 // Only accept payments to open invoices. This behaviour differs from
// non-mpp payments that are accepted even after the invoice is settled. // 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 // Because non-mpp payments don't have a payment address, this is needed
@ -154,9 +168,18 @@ func updateMpp(ctx *invoiceUpdateCtx,
return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil
} }
htlcSet := inv.HTLCSet(setID)
// Check whether total amt matches other htlcs in the set. // Check whether total amt matches other htlcs in the set.
var newSetTotal lnwire.MilliSatoshi var newSetTotal lnwire.MilliSatoshi
for _, htlc := range inv.HTLCSet(setID) { 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 { if ctx.mpp.TotalMsat() != htlc.MppTotalAmt {
return nil, ctx.failRes(ResultHtlcSetTotalMismatch), nil return nil, ctx.failRes(ResultHtlcSetTotalMismatch), nil
} }
@ -206,15 +229,107 @@ func updateMpp(ctx *invoiceUpdateCtx,
return &update, ctx.acceptRes(resultAccepted), nil return &update, ctx.acceptRes(resultAccepted), nil
} }
update.State = &channeldb.InvoiceStateUpdateDesc{ var (
NewState: channeldb.ContractSettled, htlcPreimages map[channeldb.CircuitKey]lntypes.Preimage
Preimage: inv.Terms.PaymentPreimage, htlcPreimage lntypes.Preimage
SetID: setID, )
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
} }
return &update, ctx.settleRes( update.State = &channeldb.InvoiceStateUpdateDesc{
*inv.Terms.PaymentPreimage, ResultSettled, NewState: channeldb.ContractSettled,
), nil 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 // updateLegacy is a callback for DB.UpdateInvoice that contains the invoice