Merge pull request #4338 from cfromknecht/set-id-index
channeldb+invoices: add set id index for AMP
This commit is contained in:
15 files changed
+1572
-564
No files matched your search
@@ -189,6 +189,12 @@ var (
|
|||||||
number: 21,
|
number: 21,
|
||||||
migration: migration21.MigrateDatabaseWireMessages,
|
migration: migration21.MigrateDatabaseWireMessages,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Initialize set id index so that invoices can be
|
||||||
|
// queried by individual htlc sets.
|
||||||
|
number: 22,
|
||||||
|
migration: mig.CreateTLB(setIDIndexBucket),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Big endian is the preferred byte order, due to cursor scans over
|
// Big endian is the preferred byte order, due to cursor scans over
|
||||||
@@ -319,6 +325,7 @@ var topLevelBuckets = [][]byte{
|
|||||||
fwdPackagesKey,
|
fwdPackagesKey,
|
||||||
invoiceBucket,
|
invoiceBucket,
|
||||||
payAddrIndexBucket,
|
payAddrIndexBucket,
|
||||||
|
setIDIndexBucket,
|
||||||
paymentsIndexBucket,
|
paymentsIndexBucket,
|
||||||
peersBucket,
|
peersBucket,
|
||||||
nodeInfoBucket,
|
nodeInfoBucket,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/clock"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/record"
|
"github.com/lightningnetwork/lnd/record"
|
||||||
@@ -1133,23 +1134,396 @@ func TestCustomRecords(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestInvoiceHtlcAMPFields asserts that the set id and preimage fields are
|
||||||
|
// properly recorded when updating an invoice.
|
||||||
|
func TestInvoiceHtlcAMPFields(t *testing.T) {
|
||||||
|
t.Run("amp", func(t *testing.T) {
|
||||||
|
testInvoiceHtlcAMPFields(t, true)
|
||||||
|
})
|
||||||
|
t.Run("no amp", func(t *testing.T) {
|
||||||
|
testInvoiceHtlcAMPFields(t, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInvoiceHtlcAMPFields(t *testing.T, isAMP bool) {
|
||||||
|
db, cleanUp, err := MakeTestDB()
|
||||||
|
defer cleanUp()
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
testInvoice, err := randInvoice(1000)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
payHash := testInvoice.Terms.PaymentPreimage.Hash()
|
||||||
|
_, err = db.AddInvoice(testInvoice, payHash)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// Accept an htlc with custom records on this invoice.
|
||||||
|
key := CircuitKey{ChanID: lnwire.NewShortChanIDFromInt(1), HtlcID: 4}
|
||||||
|
records := make(map[uint64][]byte)
|
||||||
|
|
||||||
|
var ampData *InvoiceHtlcAMPData
|
||||||
|
if isAMP {
|
||||||
|
amp := record.NewAMP([32]byte{1}, [32]byte{2}, 3)
|
||||||
|
preimage := &lntypes.Preimage{4}
|
||||||
|
|
||||||
|
ampData = &InvoiceHtlcAMPData{
|
||||||
|
Record: *amp,
|
||||||
|
Hash: preimage.Hash(),
|
||||||
|
Preimage: preimage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ref := InvoiceRefByHash(payHash)
|
||||||
|
_, err = db.UpdateInvoice(ref,
|
||||||
|
func(invoice *Invoice) (*InvoiceUpdateDesc, error) {
|
||||||
|
return &InvoiceUpdateDesc{
|
||||||
|
AddHtlcs: map[CircuitKey]*HtlcAcceptDesc{
|
||||||
|
key: {
|
||||||
|
Amt: 500,
|
||||||
|
AMP: ampData,
|
||||||
|
CustomRecords: records,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// Retrieve the invoice from that database and verify that the AMP
|
||||||
|
// fields are as expected.
|
||||||
|
dbInvoice, err := db.LookupInvoice(ref)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, 1, len(dbInvoice.Htlcs))
|
||||||
|
require.Equal(t, ampData, dbInvoice.Htlcs[key].AMP)
|
||||||
|
}
|
||||||
|
|
||||||
// TestInvoiceRef asserts that the proper identifiers are returned from an
|
// TestInvoiceRef asserts that the proper identifiers are returned from an
|
||||||
// InvoiceRef depending on the constructor used.
|
// InvoiceRef depending on the constructor used.
|
||||||
func TestInvoiceRef(t *testing.T) {
|
func TestInvoiceRef(t *testing.T) {
|
||||||
payHash := lntypes.Hash{0x01}
|
payHash := lntypes.Hash{0x01}
|
||||||
payAddr := [32]byte{0x02}
|
payAddr := [32]byte{0x02}
|
||||||
|
setID := [32]byte{0x03}
|
||||||
|
|
||||||
// An InvoiceRef by hash should return the provided hash and a nil
|
// An InvoiceRef by hash should return the provided hash and a nil
|
||||||
// payment addr.
|
// payment addr.
|
||||||
refByHash := InvoiceRefByHash(payHash)
|
refByHash := InvoiceRefByHash(payHash)
|
||||||
require.Equal(t, payHash, refByHash.PayHash())
|
require.Equal(t, payHash, refByHash.PayHash())
|
||||||
require.Equal(t, (*[32]byte)(nil), refByHash.PayAddr())
|
require.Equal(t, (*[32]byte)(nil), refByHash.PayAddr())
|
||||||
|
require.Equal(t, (*[32]byte)(nil), refByHash.SetID())
|
||||||
|
|
||||||
// An InvoiceRef by hash and addr should return the payment hash and
|
// An InvoiceRef by hash and addr should return the payment hash and
|
||||||
// payment addr passed to the constructor.
|
// payment addr passed to the constructor.
|
||||||
refByHashAndAddr := InvoiceRefByHashAndAddr(payHash, payAddr)
|
refByHashAndAddr := InvoiceRefByHashAndAddr(payHash, payAddr)
|
||||||
require.Equal(t, payHash, refByHashAndAddr.PayHash())
|
require.Equal(t, payHash, refByHashAndAddr.PayHash())
|
||||||
require.Equal(t, &payAddr, refByHashAndAddr.PayAddr())
|
require.Equal(t, &payAddr, refByHashAndAddr.PayAddr())
|
||||||
|
require.Equal(t, (*[32]byte)(nil), refByHashAndAddr.SetID())
|
||||||
|
|
||||||
|
// An InvoiceRef by set id should return an empty pay hash, a nil pay
|
||||||
|
// addr, and a reference to the given set id.
|
||||||
|
refBySetID := InvoiceRefBySetID(setID)
|
||||||
|
require.Equal(t, lntypes.Hash{}, refBySetID.PayHash())
|
||||||
|
require.Equal(t, (*[32]byte)(nil), refBySetID.PayAddr())
|
||||||
|
require.Equal(t, &setID, refBySetID.SetID())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHTLCSet asserts that HTLCSet returns the proper set of accepted HTLCs
|
||||||
|
// that can be considered for settlement. It asserts that MPP and AMP HTLCs do
|
||||||
|
// not comingle, and also that HTLCs with disjoint set ids appear in different
|
||||||
|
// sets.
|
||||||
|
func TestHTLCSet(t *testing.T) {
|
||||||
|
inv := &Invoice{
|
||||||
|
Htlcs: make(map[CircuitKey]*InvoiceHTLC),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct two distinct set id's, in this test we'll also track the
|
||||||
|
// nil set id as a third group.
|
||||||
|
setID1 := &[32]byte{1}
|
||||||
|
setID2 := &[32]byte{2}
|
||||||
|
|
||||||
|
// Create the expected htlc sets for each group, these will be updated
|
||||||
|
// as the invoice is modified.
|
||||||
|
expSetNil := make(map[CircuitKey]*InvoiceHTLC)
|
||||||
|
expSet1 := make(map[CircuitKey]*InvoiceHTLC)
|
||||||
|
expSet2 := make(map[CircuitKey]*InvoiceHTLC)
|
||||||
|
|
||||||
|
checkHTLCSets := func() {
|
||||||
|
require.Equal(t, expSetNil, inv.HTLCSet(nil))
|
||||||
|
require.Equal(t, expSet1, inv.HTLCSet(setID1))
|
||||||
|
require.Equal(t, expSet2, inv.HTLCSet(setID2))
|
||||||
|
}
|
||||||
|
|
||||||
|
// All HTLC sets should be empty initially.
|
||||||
|
checkHTLCSets()
|
||||||
|
|
||||||
|
// Add the following sequence of HTLCs to the invoice, sanity checking
|
||||||
|
// all three HTLC sets after each transition. This sequence asserts:
|
||||||
|
// - both nil and non-nil set ids can have multiple htlcs.
|
||||||
|
// - there may be distinct htlc sets with non-nil set ids.
|
||||||
|
// - only accepted htlcs are returned as part of the set.
|
||||||
|
htlcs := []struct {
|
||||||
|
setID *[32]byte
|
||||||
|
state HtlcState
|
||||||
|
}{
|
||||||
|
{nil, HtlcStateAccepted},
|
||||||
|
{nil, HtlcStateAccepted},
|
||||||
|
{setID1, HtlcStateAccepted},
|
||||||
|
{setID1, HtlcStateAccepted},
|
||||||
|
{setID2, HtlcStateAccepted},
|
||||||
|
{setID2, HtlcStateAccepted},
|
||||||
|
{nil, HtlcStateCanceled},
|
||||||
|
{setID1, HtlcStateCanceled},
|
||||||
|
{setID2, HtlcStateCanceled},
|
||||||
|
{nil, HtlcStateSettled},
|
||||||
|
{setID1, HtlcStateSettled},
|
||||||
|
{setID2, HtlcStateSettled},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, h := range htlcs {
|
||||||
|
var ampData *InvoiceHtlcAMPData
|
||||||
|
if h.setID != nil {
|
||||||
|
ampData = &InvoiceHtlcAMPData{
|
||||||
|
Record: *record.NewAMP([32]byte{0}, *h.setID, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the HTLC to the invoice's set of HTLCs.
|
||||||
|
key := CircuitKey{HtlcID: uint64(i)}
|
||||||
|
htlc := &InvoiceHTLC{
|
||||||
|
AMP: ampData,
|
||||||
|
State: h.state,
|
||||||
|
}
|
||||||
|
inv.Htlcs[key] = htlc
|
||||||
|
|
||||||
|
// Update our expected htlc set if the htlc is accepted,
|
||||||
|
// otherwise it shouldn't be reflected.
|
||||||
|
if h.state == HtlcStateAccepted {
|
||||||
|
switch h.setID {
|
||||||
|
case nil:
|
||||||
|
expSetNil[key] = htlc
|
||||||
|
case setID1:
|
||||||
|
expSet1[key] = htlc
|
||||||
|
case setID2:
|
||||||
|
expSet2[key] = htlc
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected set id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkHTLCSets()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAddInvoiceWithHTLCs asserts that you can't insert an invoice that already
|
||||||
|
// has HTLCs.
|
||||||
|
func TestAddInvoiceWithHTLCs(t *testing.T) {
|
||||||
|
db, cleanUp, err := MakeTestDB()
|
||||||
|
defer cleanUp()
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
testInvoice, err := randInvoice(1000)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
key := CircuitKey{HtlcID: 1}
|
||||||
|
testInvoice.Htlcs[key] = &InvoiceHTLC{}
|
||||||
|
|
||||||
|
payHash := testInvoice.Terms.PaymentPreimage.Hash()
|
||||||
|
_, err = db.AddInvoice(testInvoice, payHash)
|
||||||
|
require.Equal(t, ErrInvoiceHasHtlcs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSetIDIndex asserts that the set id index properly adds new invoices as we
|
||||||
|
// accept HTLCs, that they can be queried by their set id after accepting, and
|
||||||
|
// that invoices with duplicate set ids are disallowed.
|
||||||
|
func TestSetIDIndex(t *testing.T) {
|
||||||
|
testClock := clock.NewTestClock(testNow)
|
||||||
|
db, cleanUp, err := MakeTestDB(OptionClock(testClock))
|
||||||
|
defer cleanUp()
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// We'll start out by creating an invoice and writing it to the DB.
|
||||||
|
amt := lnwire.NewMSatFromSatoshis(1000)
|
||||||
|
invoice, err := randInvoice(amt)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
preimage := *invoice.Terms.PaymentPreimage
|
||||||
|
payHash := preimage.Hash()
|
||||||
|
_, err = db.AddInvoice(invoice, payHash)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
setID := &[32]byte{1}
|
||||||
|
|
||||||
|
// Update the invoice with an accepted HTLC that also accepts the
|
||||||
|
// invoice.
|
||||||
|
ref := InvoiceRefByHashAndAddr(payHash, invoice.Terms.PaymentAddr)
|
||||||
|
dbInvoice, err := db.UpdateInvoice(ref, updateAcceptAMPHtlc(0, amt, setID, true))
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// We'll update what we expect the accepted invoice to be so that our
|
||||||
|
// comparison below has the correct assumption.
|
||||||
|
invoice.State = ContractAccepted
|
||||||
|
invoice.AmtPaid = amt
|
||||||
|
invoice.SettleDate = dbInvoice.SettleDate
|
||||||
|
invoice.Htlcs = map[CircuitKey]*InvoiceHTLC{
|
||||||
|
{HtlcID: 0}: makeAMPInvoiceHTLC(amt, *setID, preimage),
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should get back the exact same invoice that we just inserted.
|
||||||
|
require.Equal(t, invoice, dbInvoice)
|
||||||
|
|
||||||
|
// Now lookup the invoice by set id and see that we get the same one.
|
||||||
|
refBySetID := InvoiceRefBySetID(*setID)
|
||||||
|
dbInvoiceBySetID, err := db.LookupInvoice(refBySetID)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, invoice, &dbInvoiceBySetID)
|
||||||
|
|
||||||
|
// Trying to accept an HTLC to a different invoice, but using the same
|
||||||
|
// set id should fail.
|
||||||
|
invoice2, err := randInvoice(amt)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
payHash2 := invoice2.Terms.PaymentPreimage.Hash()
|
||||||
|
_, err = db.AddInvoice(invoice2, payHash2)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
ref2 := InvoiceRefByHashAndAddr(payHash2, invoice2.Terms.PaymentAddr)
|
||||||
|
_, err = db.UpdateInvoice(ref2, updateAcceptAMPHtlc(0, amt, setID, true))
|
||||||
|
require.Equal(t, ErrDuplicateSetID{setID: *setID}, err)
|
||||||
|
|
||||||
|
// Now, begin constructing a second htlc set under a different set id.
|
||||||
|
// This set will contain two distinct HTLCs.
|
||||||
|
setID2 := &[32]byte{2}
|
||||||
|
|
||||||
|
_, err = db.UpdateInvoice(ref, updateAcceptAMPHtlc(1, amt, setID2, false))
|
||||||
|
require.Nil(t, err)
|
||||||
|
dbInvoice, err = db.UpdateInvoice(ref, updateAcceptAMPHtlc(2, amt, setID2, false))
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// We'll update what we expect the settle invoice to be so that our
|
||||||
|
// comparison below has the correct assumption.
|
||||||
|
invoice.State = ContractAccepted
|
||||||
|
invoice.AmtPaid += 2 * amt
|
||||||
|
invoice.SettleDate = dbInvoice.SettleDate
|
||||||
|
invoice.Htlcs = map[CircuitKey]*InvoiceHTLC{
|
||||||
|
{HtlcID: 0}: makeAMPInvoiceHTLC(amt, *setID, preimage),
|
||||||
|
{HtlcID: 1}: makeAMPInvoiceHTLC(amt, *setID2, preimage),
|
||||||
|
{HtlcID: 2}: makeAMPInvoiceHTLC(amt, *setID2, preimage),
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should get back the exact same invoice that we just inserted.
|
||||||
|
require.Equal(t, invoice, dbInvoice)
|
||||||
|
|
||||||
|
// Now lookup the invoice by second set id and see that we get the same
|
||||||
|
// index, including the htlcs under the first set id.
|
||||||
|
refBySetID = InvoiceRefBySetID(*setID2)
|
||||||
|
dbInvoiceBySetID, err = db.LookupInvoice(refBySetID)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, invoice, &dbInvoiceBySetID)
|
||||||
|
|
||||||
|
// Now settle the first htlc set, asserting that the two htlcs with set
|
||||||
|
// id 2 get canceled as a result.
|
||||||
|
_, err = db.UpdateInvoice(
|
||||||
|
ref, getUpdateInvoiceAMPSettle(&[32]byte{}),
|
||||||
|
)
|
||||||
|
require.Equal(t, ErrEmptyHTLCSet, err)
|
||||||
|
|
||||||
|
// Now settle the first htlc set, asserting that the two htlcs with set
|
||||||
|
// id 2 get canceled as a result.
|
||||||
|
dbInvoice, err = db.UpdateInvoice(ref, getUpdateInvoiceAMPSettle(setID))
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
invoice.State = ContractSettled
|
||||||
|
invoice.SettleDate = dbInvoice.SettleDate
|
||||||
|
invoice.SettleIndex = 1
|
||||||
|
invoice.AmtPaid = amt
|
||||||
|
invoice.Htlcs[CircuitKey{HtlcID: 0}].ResolveTime = time.Unix(1, 0)
|
||||||
|
invoice.Htlcs[CircuitKey{HtlcID: 0}].State = HtlcStateSettled
|
||||||
|
invoice.Htlcs[CircuitKey{HtlcID: 1}].ResolveTime = time.Unix(1, 0)
|
||||||
|
invoice.Htlcs[CircuitKey{HtlcID: 1}].State = HtlcStateCanceled
|
||||||
|
invoice.Htlcs[CircuitKey{HtlcID: 2}].ResolveTime = time.Unix(1, 0)
|
||||||
|
invoice.Htlcs[CircuitKey{HtlcID: 2}].State = HtlcStateCanceled
|
||||||
|
require.Equal(t, invoice, dbInvoice)
|
||||||
|
|
||||||
|
// Lastly, querying for an unknown set id should fail.
|
||||||
|
refUnknownSetID := InvoiceRefBySetID([32]byte{})
|
||||||
|
_, err = db.LookupInvoice(refUnknownSetID)
|
||||||
|
require.Equal(t, ErrInvoiceNotFound, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeAMPInvoiceHTLC(amt lnwire.MilliSatoshi, setID [32]byte,
|
||||||
|
preimage lntypes.Preimage) *InvoiceHTLC {
|
||||||
|
|
||||||
|
return &InvoiceHTLC{
|
||||||
|
Amt: amt,
|
||||||
|
AcceptTime: testNow,
|
||||||
|
ResolveTime: time.Time{},
|
||||||
|
State: HtlcStateAccepted,
|
||||||
|
CustomRecords: make(record.CustomSet),
|
||||||
|
AMP: &InvoiceHtlcAMPData{
|
||||||
|
Record: *record.NewAMP([32]byte{}, setID, 0),
|
||||||
|
Hash: preimage.Hash(),
|
||||||
|
Preimage: &preimage,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateAcceptAMPHtlc returns an invoice update callback that, when called,
|
||||||
|
// settles the invoice with the given amount.
|
||||||
|
func updateAcceptAMPHtlc(id uint64, amt lnwire.MilliSatoshi,
|
||||||
|
setID *[32]byte, accept bool) InvoiceUpdateCallback {
|
||||||
|
|
||||||
|
return func(invoice *Invoice) (*InvoiceUpdateDesc, error) {
|
||||||
|
if invoice.State == ContractSettled {
|
||||||
|
return nil, ErrInvoiceAlreadySettled
|
||||||
|
}
|
||||||
|
|
||||||
|
noRecords := make(record.CustomSet)
|
||||||
|
|
||||||
|
var state *InvoiceStateUpdateDesc
|
||||||
|
if accept {
|
||||||
|
state = &InvoiceStateUpdateDesc{
|
||||||
|
NewState: ContractAccepted,
|
||||||
|
SetID: setID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ampData := &InvoiceHtlcAMPData{
|
||||||
|
Record: *record.NewAMP([32]byte{}, *setID, 0),
|
||||||
|
Hash: invoice.Terms.PaymentPreimage.Hash(),
|
||||||
|
Preimage: invoice.Terms.PaymentPreimage,
|
||||||
|
}
|
||||||
|
update := &InvoiceUpdateDesc{
|
||||||
|
State: state,
|
||||||
|
AddHtlcs: map[CircuitKey]*HtlcAcceptDesc{
|
||||||
|
{HtlcID: id}: {
|
||||||
|
Amt: amt,
|
||||||
|
CustomRecords: noRecords,
|
||||||
|
AMP: ampData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return update, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUpdateInvoiceAMPSettle(setID *[32]byte) InvoiceUpdateCallback {
|
||||||
|
return func(invoice *Invoice) (*InvoiceUpdateDesc, error) {
|
||||||
|
if invoice.State == ContractSettled {
|
||||||
|
return nil, ErrInvoiceAlreadySettled
|
||||||
|
}
|
||||||
|
|
||||||
|
update := &InvoiceUpdateDesc{
|
||||||
|
State: &InvoiceStateUpdateDesc{
|
||||||
|
Preimage: nil,
|
||||||
|
NewState: ContractSettled,
|
||||||
|
SetID: setID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return update, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDeleteInvoices tests that deleting a list of invoices will succeed
|
// TestDeleteInvoices tests that deleting a list of invoices will succeed
|
||||||
@@ -1243,4 +1617,5 @@ func TestDeleteInvoices(t *testing.T) {
|
|||||||
// Delete should succeed with all the valid references.
|
// Delete should succeed with all the valid references.
|
||||||
require.NoError(t, db.DeleteInvoice(invoicesToDelete))
|
require.NoError(t, db.DeleteInvoice(invoicesToDelete))
|
||||||
assertInvoiceCount(0)
|
assertInvoiceCount(0)
|
||||||
|
|
||||||
}
|
}
|
||||||
+483
-114
@@ -53,6 +53,14 @@ var (
|
|||||||
// maps: payAddr => invoiceKey
|
// maps: payAddr => invoiceKey
|
||||||
payAddrIndexBucket = []byte("pay-addr-index")
|
payAddrIndexBucket = []byte("pay-addr-index")
|
||||||
|
|
||||||
|
// setIDIndexBucket is the name of the top-level bucket that maps set
|
||||||
|
// ids to their invoice number. This can be used to efficiently query or
|
||||||
|
// update AMP invoice. Note that legacy or MPP invoices will not be
|
||||||
|
// included in this index, since their HTLCs do not have a set id.
|
||||||
|
//
|
||||||
|
// maps: setID => invoiceKey
|
||||||
|
setIDIndexBucket = []byte("set-id-index")
|
||||||
|
|
||||||
// numInvoicesKey is the name of key which houses the auto-incrementing
|
// numInvoicesKey is the name of key which houses the auto-incrementing
|
||||||
// invoice ID which is essentially used as a primary key. With each
|
// invoice ID which is essentially used as a primary key. With each
|
||||||
// invoice inserted, the primary key is incremented by one. This key is
|
// invoice inserted, the primary key is incremented by one. This key is
|
||||||
@@ -106,8 +114,27 @@ var (
|
|||||||
// ErrInvoicePreimageMismatch is returned when the preimage doesn't
|
// ErrInvoicePreimageMismatch is returned when the preimage doesn't
|
||||||
// match the invoice hash.
|
// match the invoice hash.
|
||||||
ErrInvoicePreimageMismatch = errors.New("preimage does not match")
|
ErrInvoicePreimageMismatch = errors.New("preimage does not match")
|
||||||
|
|
||||||
|
// ErrInvoiceHasHtlcs is returned when attempting to insert an invoice
|
||||||
|
// that already has HTLCs.
|
||||||
|
ErrInvoiceHasHtlcs = errors.New("cannot add invoice with htlcs")
|
||||||
|
|
||||||
|
// ErrEmptyHTLCSet is returned when attempting to accept or settle and
|
||||||
|
// HTLC set that has no HTLCs.
|
||||||
|
ErrEmptyHTLCSet = errors.New("cannot settle/accept empty HTLC set")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrDuplicateSetID is an error returned when attempting to adding an AMP HTLC
|
||||||
|
// to an invoice, but another invoice is already indexed by the same set id.
|
||||||
|
type ErrDuplicateSetID struct {
|
||||||
|
setID [32]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns a human-readable description of ErrDuplicateSetID.
|
||||||
|
func (e ErrDuplicateSetID) Error() string {
|
||||||
|
return fmt.Sprintf("invoice with set_id=%x already exists", e.setID)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// MaxMemoSize is maximum size of the memo field within invoices stored
|
// MaxMemoSize is maximum size of the memo field within invoices stored
|
||||||
// in the database.
|
// in the database.
|
||||||
@@ -135,6 +162,9 @@ const (
|
|||||||
expiryHeightType tlv.Type = 13
|
expiryHeightType tlv.Type = 13
|
||||||
htlcStateType tlv.Type = 15
|
htlcStateType tlv.Type = 15
|
||||||
mppTotalAmtType tlv.Type = 17
|
mppTotalAmtType tlv.Type = 17
|
||||||
|
htlcAMPType tlv.Type = 19
|
||||||
|
htlcHashType tlv.Type = 21
|
||||||
|
htlcPreimageType tlv.Type = 23
|
||||||
|
|
||||||
// A set of tlv type definitions used to serialize invoice bodiees.
|
// A set of tlv type definitions used to serialize invoice bodiees.
|
||||||
//
|
//
|
||||||
@@ -176,6 +206,14 @@ type InvoiceRef struct {
|
|||||||
// known it will be used as the primary identifier, falling back to
|
// known it will be used as the primary identifier, falling back to
|
||||||
// payHash if no value is known.
|
// payHash if no value is known.
|
||||||
payAddr *[32]byte
|
payAddr *[32]byte
|
||||||
|
|
||||||
|
// setID is the optional set id for an AMP payment. This can be used to
|
||||||
|
// lookup or update the invoice knowing only this value. Queries by set
|
||||||
|
// id are only used to facilitate user-facing requests, e.g. lookup,
|
||||||
|
// settle or cancel an AMP invoice. The regular update flow from the
|
||||||
|
// invoice registry will always query for the invoice by
|
||||||
|
// payHash+payAddr.
|
||||||
|
setID *[32]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// InvoiceRefByHash creates an InvoiceRef that queries for an invoice only by
|
// InvoiceRefByHash creates an InvoiceRef that queries for an invoice only by
|
||||||
@@ -198,6 +236,15 @@ func InvoiceRefByHashAndAddr(payHash lntypes.Hash,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InvoiceRefBySetID creates an InvoiceRef that queries the set id index for an
|
||||||
|
// invoice with the provided setID. If the invoice is not found, the query will
|
||||||
|
// not fallback to payHash or payAddr.
|
||||||
|
func InvoiceRefBySetID(setID [32]byte) InvoiceRef {
|
||||||
|
return InvoiceRef{
|
||||||
|
setID: &setID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PayHash returns the target invoice's payment hash.
|
// PayHash returns the target invoice's payment hash.
|
||||||
func (r InvoiceRef) PayHash() lntypes.Hash {
|
func (r InvoiceRef) PayHash() lntypes.Hash {
|
||||||
return r.payHash
|
return r.payHash
|
||||||
@@ -214,6 +261,17 @@ func (r InvoiceRef) PayAddr() *[32]byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetID returns the optional set id of the target invoice.
|
||||||
|
//
|
||||||
|
// NOTE: This value may be nil.
|
||||||
|
func (r InvoiceRef) SetID() *[32]byte {
|
||||||
|
if r.setID != nil {
|
||||||
|
id := *r.setID
|
||||||
|
return &id
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// String returns a human-readable representation of an InvoiceRef.
|
// String returns a human-readable representation of an InvoiceRef.
|
||||||
func (r InvoiceRef) String() string {
|
func (r InvoiceRef) String() string {
|
||||||
if r.payAddr != nil {
|
if r.payAddr != nil {
|
||||||
@@ -359,6 +417,30 @@ type Invoice struct {
|
|||||||
HodlInvoice bool
|
HodlInvoice bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTLCSet returns the set of accepted HTLCs belonging to an invoice. Passing a
|
||||||
|
// nil setID will return all accepted HTLCs in the case of legacy or MPP, and no
|
||||||
|
// HTLCs in the case of AMP. Otherwise, the returned set will be filtered by
|
||||||
|
// the populated setID which is used to retrieve AMP HTLC sets.
|
||||||
|
func (i *Invoice) HTLCSet(setID *[32]byte) map[CircuitKey]*InvoiceHTLC {
|
||||||
|
htlcSet := make(map[CircuitKey]*InvoiceHTLC)
|
||||||
|
for key, htlc := range i.Htlcs {
|
||||||
|
// 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 != HtlcStateAccepted {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !htlc.IsInHTLCSet(setID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
htlcSet[key] = htlc
|
||||||
|
}
|
||||||
|
|
||||||
|
return htlcSet
|
||||||
|
}
|
||||||
|
|
||||||
// HtlcState defines the states an htlc paying to an invoice can be in.
|
// HtlcState defines the states an htlc paying to an invoice can be in.
|
||||||
type HtlcState uint8
|
type HtlcState uint8
|
||||||
|
|
||||||
@@ -407,6 +489,80 @@ type InvoiceHTLC struct {
|
|||||||
// CustomRecords contains the custom key/value pairs that accompanied
|
// CustomRecords contains the custom key/value pairs that accompanied
|
||||||
// the htlc.
|
// the htlc.
|
||||||
CustomRecords record.CustomSet
|
CustomRecords record.CustomSet
|
||||||
|
|
||||||
|
// AMP encapsulates additional data relevant to AMP HTLCs. This includes
|
||||||
|
// the AMP onion record, in addition to the HTLC's payment hash and
|
||||||
|
// preimage since these are unique to each AMP HTLC, and not the invoice
|
||||||
|
// as a whole.
|
||||||
|
//
|
||||||
|
// NOTE: This value will only be set for AMP HTLCs.
|
||||||
|
AMP *InvoiceHtlcAMPData
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsInHTLCSet returns true if this HTLC is part an HTLC set. If nil is passed,
|
||||||
|
// this method returns true if this is an MPP HTLC. Otherwise, it only returns
|
||||||
|
// true if the AMP HTLC's set id matches the populated setID.
|
||||||
|
func (h *InvoiceHTLC) IsInHTLCSet(setID *[32]byte) bool {
|
||||||
|
wantAMPSet := setID != nil
|
||||||
|
isAMPHtlc := h.AMP != nil
|
||||||
|
|
||||||
|
// Non-AMP HTLCs cannot be part of AMP HTLC sets, and vice versa.
|
||||||
|
if wantAMPSet != isAMPHtlc {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip AMP HTLCs that have differing set ids.
|
||||||
|
if isAMPHtlc && *setID != h.AMP.Record.SetID() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvoiceHtlcAMPData is a struct hodling the additional metadata stored for
|
||||||
|
// each received AMP HTLC. This includes the AMP onion record, in addition to
|
||||||
|
// the HTLC's payment hash and preimage.
|
||||||
|
type InvoiceHtlcAMPData struct {
|
||||||
|
// AMP is a copy of the AMP record presented in the onion payload
|
||||||
|
// containing the information necessary to correlate and settle a
|
||||||
|
// spontaneous HTLC set. Newly accepted legacy keysend payments will
|
||||||
|
// also have this field set as we automatically promote them into an AMP
|
||||||
|
// payment for internal processing.
|
||||||
|
Record record.AMP
|
||||||
|
|
||||||
|
// Hash is an HTLC-level payment hash that is stored only for AMP
|
||||||
|
// payments. This is done because an AMP HTLC will carry a different
|
||||||
|
// payment hash from the invoice it might be satisfying, so we track the
|
||||||
|
// payment hashes individually to able to compute whether or not the
|
||||||
|
// reconstructed preimage correctly matches the HTLC's hash.
|
||||||
|
Hash lntypes.Hash
|
||||||
|
|
||||||
|
// Preimage is an HTLC-level preimage that satisfies the AMP HTLC's
|
||||||
|
// Hash. The preimage will be be derived either from secret share
|
||||||
|
// reconstruction of the shares in the AMP payload.
|
||||||
|
//
|
||||||
|
// NOTE: Preimage will only be present once the HTLC is in
|
||||||
|
// HltcStateSetteled.
|
||||||
|
Preimage *lntypes.Preimage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy returns a deep copy of the InvoiceHtlcAMPData.
|
||||||
|
func (d *InvoiceHtlcAMPData) Copy() *InvoiceHtlcAMPData {
|
||||||
|
if d == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var preimage *lntypes.Preimage
|
||||||
|
if d.Preimage != nil {
|
||||||
|
pimg := *d.Preimage
|
||||||
|
preimage = &pimg
|
||||||
|
}
|
||||||
|
|
||||||
|
return &InvoiceHtlcAMPData{
|
||||||
|
Record: d.Record,
|
||||||
|
Hash: d.Hash,
|
||||||
|
Preimage: preimage,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HtlcAcceptDesc describes the details of a newly accepted htlc.
|
// HtlcAcceptDesc describes the details of a newly accepted htlc.
|
||||||
@@ -427,6 +583,14 @@ type HtlcAcceptDesc struct {
|
|||||||
// CustomRecords contains the custom key/value pairs that accompanied
|
// CustomRecords contains the custom key/value pairs that accompanied
|
||||||
// the htlc.
|
// the htlc.
|
||||||
CustomRecords record.CustomSet
|
CustomRecords record.CustomSet
|
||||||
|
|
||||||
|
// AMP encapsulates additional data relevant to AMP HTLCs. This includes
|
||||||
|
// the AMP onion record, in addition to the HTLC's payment hash and
|
||||||
|
// preimage since these are unique to each AMP HTLC, and not the invoice
|
||||||
|
// as a whole.
|
||||||
|
//
|
||||||
|
// NOTE: This value will only be set for AMP HTLCs.
|
||||||
|
AMP *InvoiceHtlcAMPData
|
||||||
}
|
}
|
||||||
|
|
||||||
// InvoiceUpdateDesc describes the changes that should be applied to the
|
// InvoiceUpdateDesc describes the changes that should be applied to the
|
||||||
@@ -451,6 +615,11 @@ type InvoiceStateUpdateDesc struct {
|
|||||||
|
|
||||||
// Preimage must be set to the preimage when NewState is settled.
|
// Preimage must be set to the preimage when NewState is settled.
|
||||||
Preimage *lntypes.Preimage
|
Preimage *lntypes.Preimage
|
||||||
|
|
||||||
|
// SetID identifies a specific set of HTLCs destined for the same
|
||||||
|
// invoice as part of a larger AMP payment. This value will be nil for
|
||||||
|
// legacy or MPP payments.
|
||||||
|
SetID *[32]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// InvoiceUpdateCallback is a callback used in the db transaction to update the
|
// InvoiceUpdateCallback is a callback used in the db transaction to update the
|
||||||
@@ -479,6 +648,11 @@ func validateInvoice(i *Invoice, paymentHash lntypes.Hash) error {
|
|||||||
if i.Terms.PaymentPreimage == nil && !i.HodlInvoice {
|
if i.Terms.PaymentPreimage == nil && !i.HodlInvoice {
|
||||||
return errors.New("non-hodl invoices must have a preimage")
|
return errors.New("non-hodl invoices must have a preimage")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(i.Htlcs) > 0 {
|
||||||
|
return ErrInvoiceHasHtlcs
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,11 +828,12 @@ func (d *DB) LookupInvoice(ref InvoiceRef) (Invoice, error) {
|
|||||||
return ErrNoInvoicesCreated
|
return ErrNoInvoicesCreated
|
||||||
}
|
}
|
||||||
payAddrIndex := tx.ReadBucket(payAddrIndexBucket)
|
payAddrIndex := tx.ReadBucket(payAddrIndexBucket)
|
||||||
|
setIDIndex := tx.ReadBucket(setIDIndexBucket)
|
||||||
|
|
||||||
// Retrieve the invoice number for this invoice using the
|
// Retrieve the invoice number for this invoice using
|
||||||
// provided invoice reference.
|
// the provided invoice reference.
|
||||||
invoiceNum, err := fetchInvoiceNumByRef(
|
invoiceNum, err := fetchInvoiceNumByRef(
|
||||||
invoiceIndex, payAddrIndex, ref,
|
invoiceIndex, payAddrIndex, setIDIndex, ref,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -685,9 +860,22 @@ func (d *DB) LookupInvoice(ref InvoiceRef) (Invoice, error) {
|
|||||||
// reference. The payment address will be treated as the primary key, falling
|
// reference. The payment address will be treated as the primary key, falling
|
||||||
// back to the payment hash if nothing is found for the payment address. An
|
// back to the payment hash if nothing is found for the payment address. An
|
||||||
// error is returned if the invoice is not found.
|
// error is returned if the invoice is not found.
|
||||||
func fetchInvoiceNumByRef(invoiceIndex, payAddrIndex kvdb.RBucket,
|
func fetchInvoiceNumByRef(invoiceIndex, payAddrIndex, setIDIndex kvdb.RBucket,
|
||||||
ref InvoiceRef) ([]byte, error) {
|
ref InvoiceRef) ([]byte, error) {
|
||||||
|
|
||||||
|
// If the set id is present, we only consult the set id index for this
|
||||||
|
// invoice. This type of query is only used to facilitate user-facing
|
||||||
|
// requests to lookup, settle or cancel an AMP invoice.
|
||||||
|
setID := ref.SetID()
|
||||||
|
if setID != nil {
|
||||||
|
invoiceNumBySetID := setIDIndex.Get(setID[:])
|
||||||
|
if invoiceNumBySetID == nil {
|
||||||
|
return nil, ErrInvoiceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return invoiceNumBySetID, nil
|
||||||
|
}
|
||||||
|
|
||||||
payHash := ref.PayHash()
|
payHash := ref.PayHash()
|
||||||
payAddr := ref.PayAddr()
|
payAddr := ref.PayAddr()
|
||||||
|
|
||||||
@@ -935,20 +1123,21 @@ func (d *DB) UpdateInvoice(ref InvoiceRef,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
payAddrIndex := tx.ReadBucket(payAddrIndexBucket)
|
payAddrIndex := tx.ReadBucket(payAddrIndexBucket)
|
||||||
|
setIDIndex := tx.ReadWriteBucket(setIDIndexBucket)
|
||||||
|
|
||||||
// Retrieve the invoice number for this invoice using the
|
// Retrieve the invoice number for this invoice using the
|
||||||
// provided invoice reference.
|
// provided invoice reference.
|
||||||
invoiceNum, err := fetchInvoiceNumByRef(
|
invoiceNum, err := fetchInvoiceNumByRef(
|
||||||
invoiceIndex, payAddrIndex, ref,
|
invoiceIndex, payAddrIndex, setIDIndex, ref,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
payHash := ref.PayHash()
|
payHash := ref.PayHash()
|
||||||
updatedInvoice, err = d.updateInvoice(
|
updatedInvoice, err = d.updateInvoice(
|
||||||
payHash, invoices, settleIndex, invoiceNum,
|
payHash, invoices, settleIndex, setIDIndex,
|
||||||
callback,
|
invoiceNum, callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
@@ -1186,8 +1375,8 @@ func serializeHtlcs(w io.Writer, htlcs map[CircuitKey]*InvoiceHTLC) error {
|
|||||||
chanID := key.ChanID.ToUint64()
|
chanID := key.ChanID.ToUint64()
|
||||||
amt := uint64(htlc.Amt)
|
amt := uint64(htlc.Amt)
|
||||||
mppTotalAmt := uint64(htlc.MppTotalAmt)
|
mppTotalAmt := uint64(htlc.MppTotalAmt)
|
||||||
acceptTime := uint64(htlc.AcceptTime.UnixNano())
|
acceptTime := putNanoTime(htlc.AcceptTime)
|
||||||
resolveTime := uint64(htlc.ResolveTime.UnixNano())
|
resolveTime := putNanoTime(htlc.ResolveTime)
|
||||||
state := uint8(htlc.State)
|
state := uint8(htlc.State)
|
||||||
|
|
||||||
var records []tlv.Record
|
var records []tlv.Record
|
||||||
@@ -1205,6 +1394,29 @@ func serializeHtlcs(w io.Writer, htlcs map[CircuitKey]*InvoiceHTLC) error {
|
|||||||
tlv.MakePrimitiveRecord(mppTotalAmtType, &mppTotalAmt),
|
tlv.MakePrimitiveRecord(mppTotalAmtType, &mppTotalAmt),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if htlc.AMP != nil {
|
||||||
|
setIDRecord := tlv.MakeDynamicRecord(
|
||||||
|
htlcAMPType, &htlc.AMP.Record,
|
||||||
|
htlc.AMP.Record.PayloadSize,
|
||||||
|
record.AMPEncoder, record.AMPDecoder,
|
||||||
|
)
|
||||||
|
records = append(records, setIDRecord)
|
||||||
|
|
||||||
|
hash32 := [32]byte(htlc.AMP.Hash)
|
||||||
|
hashRecord := tlv.MakePrimitiveRecord(
|
||||||
|
htlcHashType, &hash32,
|
||||||
|
)
|
||||||
|
records = append(records, hashRecord)
|
||||||
|
|
||||||
|
if htlc.AMP.Preimage != nil {
|
||||||
|
preimage32 := [32]byte(*htlc.AMP.Preimage)
|
||||||
|
preimageRecord := tlv.MakePrimitiveRecord(
|
||||||
|
htlcPreimageType, &preimage32,
|
||||||
|
)
|
||||||
|
records = append(records, preimageRecord)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Convert the custom records to tlv.Record types that are ready
|
// Convert the custom records to tlv.Record types that are ready
|
||||||
// for serialization.
|
// for serialization.
|
||||||
customRecords := tlv.MapToRecords(htlc.CustomRecords)
|
customRecords := tlv.MapToRecords(htlc.CustomRecords)
|
||||||
@@ -1238,6 +1450,25 @@ func serializeHtlcs(w io.Writer, htlcs map[CircuitKey]*InvoiceHTLC) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// putNanoTime returns the unix nano time for the passed timestamp. A zero-value
|
||||||
|
// timestamp will be mapped to 0, since calling UnixNano in that case is
|
||||||
|
// undefined.
|
||||||
|
func putNanoTime(t time.Time) uint64 {
|
||||||
|
if t.IsZero() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return uint64(t.UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNanoTime returns a timestamp for the given number of nano seconds. If zero
|
||||||
|
// is provided, an zero-value time stamp is returned.
|
||||||
|
func getNanoTime(ns uint64) time.Time {
|
||||||
|
if ns == 0 {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return time.Unix(0, int64(ns))
|
||||||
|
}
|
||||||
|
|
||||||
func fetchInvoice(invoiceNum []byte, invoices kvdb.RBucket) (Invoice, error) {
|
func fetchInvoice(invoiceNum []byte, invoices kvdb.RBucket) (Invoice, error) {
|
||||||
invoiceBytes := invoices.Get(invoiceNum)
|
invoiceBytes := invoices.Get(invoiceNum)
|
||||||
if invoiceBytes == nil {
|
if invoiceBytes == nil {
|
||||||
@@ -1374,6 +1605,9 @@ func deserializeHtlcs(r io.Reader) (map[CircuitKey]*InvoiceHTLC, error) {
|
|||||||
state uint8
|
state uint8
|
||||||
acceptTime, resolveTime uint64
|
acceptTime, resolveTime uint64
|
||||||
amt, mppTotalAmt uint64
|
amt, mppTotalAmt uint64
|
||||||
|
amp = &record.AMP{}
|
||||||
|
hash32 = &[32]byte{}
|
||||||
|
preimage32 = &[32]byte{}
|
||||||
)
|
)
|
||||||
tlvStream, err := tlv.NewStream(
|
tlvStream, err := tlv.NewStream(
|
||||||
tlv.MakePrimitiveRecord(chanIDType, &chanID),
|
tlv.MakePrimitiveRecord(chanIDType, &chanID),
|
||||||
@@ -1387,6 +1621,12 @@ func deserializeHtlcs(r io.Reader) (map[CircuitKey]*InvoiceHTLC, error) {
|
|||||||
tlv.MakePrimitiveRecord(expiryHeightType, &htlc.Expiry),
|
tlv.MakePrimitiveRecord(expiryHeightType, &htlc.Expiry),
|
||||||
tlv.MakePrimitiveRecord(htlcStateType, &state),
|
tlv.MakePrimitiveRecord(htlcStateType, &state),
|
||||||
tlv.MakePrimitiveRecord(mppTotalAmtType, &mppTotalAmt),
|
tlv.MakePrimitiveRecord(mppTotalAmtType, &mppTotalAmt),
|
||||||
|
tlv.MakeDynamicRecord(
|
||||||
|
htlcAMPType, amp, amp.PayloadSize,
|
||||||
|
record.AMPEncoder, record.AMPDecoder,
|
||||||
|
),
|
||||||
|
tlv.MakePrimitiveRecord(htlcHashType, hash32),
|
||||||
|
tlv.MakePrimitiveRecord(htlcPreimageType, preimage32),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1397,12 +1637,35 @@ func deserializeHtlcs(r io.Reader) (map[CircuitKey]*InvoiceHTLC, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, ok := parsedTypes[htlcAMPType]; !ok {
|
||||||
|
amp = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var preimage *lntypes.Preimage
|
||||||
|
if _, ok := parsedTypes[htlcPreimageType]; ok {
|
||||||
|
pimg := lntypes.Preimage(*preimage32)
|
||||||
|
preimage = &pimg
|
||||||
|
}
|
||||||
|
|
||||||
|
var hash *lntypes.Hash
|
||||||
|
if _, ok := parsedTypes[htlcHashType]; ok {
|
||||||
|
h := lntypes.Hash(*hash32)
|
||||||
|
hash = &h
|
||||||
|
}
|
||||||
|
|
||||||
key.ChanID = lnwire.NewShortChanIDFromInt(chanID)
|
key.ChanID = lnwire.NewShortChanIDFromInt(chanID)
|
||||||
htlc.AcceptTime = time.Unix(0, int64(acceptTime))
|
htlc.AcceptTime = getNanoTime(acceptTime)
|
||||||
htlc.ResolveTime = time.Unix(0, int64(resolveTime))
|
htlc.ResolveTime = getNanoTime(resolveTime)
|
||||||
htlc.State = HtlcState(state)
|
htlc.State = HtlcState(state)
|
||||||
htlc.Amt = lnwire.MilliSatoshi(amt)
|
htlc.Amt = lnwire.MilliSatoshi(amt)
|
||||||
htlc.MppTotalAmt = lnwire.MilliSatoshi(mppTotalAmt)
|
htlc.MppTotalAmt = lnwire.MilliSatoshi(mppTotalAmt)
|
||||||
|
if amp != nil && hash != nil {
|
||||||
|
htlc.AMP = &InvoiceHtlcAMPData{
|
||||||
|
Record: *amp,
|
||||||
|
Hash: *hash,
|
||||||
|
Preimage: preimage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reconstruct the custom records fields from the parsed types
|
// Reconstruct the custom records fields from the parsed types
|
||||||
// map return from the tlv parser.
|
// map return from the tlv parser.
|
||||||
@@ -1431,6 +1694,8 @@ func copyInvoiceHTLC(src *InvoiceHTLC) *InvoiceHTLC {
|
|||||||
result.CustomRecords[k] = v
|
result.CustomRecords[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.AMP = src.AMP.Copy()
|
||||||
|
|
||||||
return &result
|
return &result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1468,8 +1733,9 @@ func copyInvoice(src *Invoice) *Invoice {
|
|||||||
|
|
||||||
// updateInvoice fetches the invoice, obtains the update descriptor from the
|
// updateInvoice fetches the invoice, obtains the update descriptor from the
|
||||||
// callback and applies the updates in a single db transaction.
|
// callback and applies the updates in a single db transaction.
|
||||||
func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex kvdb.RwBucket,
|
func (d *DB) updateInvoice(hash lntypes.Hash, invoices,
|
||||||
invoiceNum []byte, callback InvoiceUpdateCallback) (*Invoice, error) {
|
settleIndex, setIDIndex kvdb.RwBucket, invoiceNum []byte,
|
||||||
|
callback InvoiceUpdateCallback) (*Invoice, error) {
|
||||||
|
|
||||||
invoice, err := fetchInvoice(invoiceNum, invoices)
|
invoice, err := fetchInvoice(invoiceNum, invoices)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1491,10 +1757,96 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex kvdb.RwBucke
|
|||||||
return &invoice, nil
|
return &invoice, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
newState = invoice.State
|
||||||
|
setID *[32]byte
|
||||||
|
)
|
||||||
|
if update.State != nil {
|
||||||
|
setID = update.State.SetID
|
||||||
|
newState = update.State.NewState
|
||||||
|
}
|
||||||
|
|
||||||
now := d.clock.Now()
|
now := d.clock.Now()
|
||||||
|
|
||||||
// Update invoice state if the update descriptor indicates an invoice
|
// Process add actions from update descriptor.
|
||||||
// state change.
|
for key, htlcUpdate := range update.AddHtlcs {
|
||||||
|
if _, exists := invoice.Htlcs[key]; exists {
|
||||||
|
return nil, 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a newly added HTLC has an associated set id, use it to
|
||||||
|
// index this invoice in the set id index. An error is returned
|
||||||
|
// if we find the index already points to a different invoice.
|
||||||
|
if htlcUpdate.AMP != nil {
|
||||||
|
setID := htlcUpdate.AMP.Record.SetID()
|
||||||
|
setIDInvNum := setIDIndex.Get(setID[:])
|
||||||
|
if setIDInvNum == nil {
|
||||||
|
err = setIDIndex.Put(setID[:], invoiceNum)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if !bytes.Equal(setIDInvNum, invoiceNum) {
|
||||||
|
return nil, ErrDuplicateSetID{setID: setID}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
htlc := &InvoiceHTLC{
|
||||||
|
Amt: htlcUpdate.Amt,
|
||||||
|
MppTotalAmt: htlcUpdate.MppTotalAmt,
|
||||||
|
Expiry: htlcUpdate.Expiry,
|
||||||
|
AcceptHeight: uint32(htlcUpdate.AcceptHeight),
|
||||||
|
AcceptTime: now,
|
||||||
|
State: HtlcStateAccepted,
|
||||||
|
CustomRecords: htlcUpdate.CustomRecords,
|
||||||
|
AMP: htlcUpdate.AMP.Copy(),
|
||||||
|
}
|
||||||
|
|
||||||
|
invoice.Htlcs[key] = htlc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process cancel actions from update descriptor.
|
||||||
|
cancelHtlcs := update.CancelHtlcs
|
||||||
|
for key, htlc := range invoice.Htlcs {
|
||||||
|
// Check whether this htlc needs to be canceled. If it does,
|
||||||
|
// update the htlc state to Canceled.
|
||||||
|
_, cancel := cancelHtlcs[key]
|
||||||
|
if !cancel {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consistency check to verify that there is no overlap between
|
||||||
|
// the add and cancel sets.
|
||||||
|
if _, added := update.AddHtlcs[key]; added {
|
||||||
|
return nil, fmt.Errorf("added htlc %v canceled", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cancelSingleHtlc(now, htlc, newState)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete processed cancel action, so that we can check later
|
||||||
|
// that there are no actions left.
|
||||||
|
delete(cancelHtlcs, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that we didn't get an action for htlcs that are not present on
|
||||||
|
// the invoice.
|
||||||
|
if len(cancelHtlcs) > 0 {
|
||||||
|
return nil, errors.New("cancel action on non-existent htlc(s)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
if update.State != nil {
|
||||||
err := updateInvoiceState(&invoice, hash, *update.State)
|
err := updateInvoiceState(&invoice, hash, *update.State)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1511,64 +1863,15 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex kvdb.RwBucke
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process add actions from update descriptor.
|
// With any invoice level state transitions recorded, we'll now finalize
|
||||||
for key, htlcUpdate := range update.AddHtlcs {
|
// the process by updating the state transitions for individual HTLCs
|
||||||
if _, exists := invoice.Htlcs[key]; exists {
|
// and recalculate the total amount paid to the invoice.
|
||||||
return nil, fmt.Errorf("duplicate add of htlc %v", key)
|
var amtPaid lnwire.MilliSatoshi
|
||||||
}
|
for _, htlc := range invoice.Htlcs {
|
||||||
|
|
||||||
// Force caller to supply htlc without custom records in a
|
|
||||||
// consistent way.
|
|
||||||
if htlcUpdate.CustomRecords == nil {
|
|
||||||
return nil, errors.New("nil custom records map")
|
|
||||||
}
|
|
||||||
|
|
||||||
htlc := &InvoiceHTLC{
|
|
||||||
Amt: htlcUpdate.Amt,
|
|
||||||
MppTotalAmt: htlcUpdate.MppTotalAmt,
|
|
||||||
Expiry: htlcUpdate.Expiry,
|
|
||||||
AcceptHeight: uint32(htlcUpdate.AcceptHeight),
|
|
||||||
AcceptTime: now,
|
|
||||||
State: HtlcStateAccepted,
|
|
||||||
CustomRecords: htlcUpdate.CustomRecords,
|
|
||||||
}
|
|
||||||
|
|
||||||
invoice.Htlcs[key] = htlc
|
|
||||||
}
|
|
||||||
|
|
||||||
// Align htlc states with invoice state and recalculate amount paid.
|
|
||||||
var (
|
|
||||||
amtPaid lnwire.MilliSatoshi
|
|
||||||
cancelHtlcs = update.CancelHtlcs
|
|
||||||
)
|
|
||||||
for key, htlc := range invoice.Htlcs {
|
|
||||||
// Check whether this htlc needs to be canceled. If it does,
|
|
||||||
// update the htlc state to Canceled.
|
|
||||||
_, cancel := cancelHtlcs[key]
|
|
||||||
if cancel {
|
|
||||||
// Consistency check to verify that there is no overlap
|
|
||||||
// between the add and cancel sets.
|
|
||||||
if _, added := update.AddHtlcs[key]; added {
|
|
||||||
return nil, fmt.Errorf("added htlc %v canceled",
|
|
||||||
key)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := cancelSingleHtlc(now, htlc, invoice.State)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete processed cancel action, so that we can check
|
|
||||||
// later that there are no actions left.
|
|
||||||
delete(cancelHtlcs, key)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// The invoice state may have changed and this could have
|
// The invoice state may have changed and this could have
|
||||||
// implications for the states of the individual htlcs. Align
|
// implications for the states of the individual htlcs. Align
|
||||||
// the htlc state with the current invoice state.
|
// the htlc state with the current invoice state.
|
||||||
err := updateHtlc(now, htlc, invoice.State)
|
err := updateHtlc(now, htlc, invoice.State, setID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1584,12 +1887,6 @@ func (d *DB) updateInvoice(hash lntypes.Hash, invoices, settleIndex kvdb.RwBucke
|
|||||||
}
|
}
|
||||||
invoice.AmtPaid = amtPaid
|
invoice.AmtPaid = amtPaid
|
||||||
|
|
||||||
// Verify that we didn't get an action for htlcs that are not present on
|
|
||||||
// the invoice.
|
|
||||||
if len(cancelHtlcs) > 0 {
|
|
||||||
return nil, errors.New("cancel action on non-existent htlc(s)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reserialize and update invoice.
|
// Reserialize and update invoice.
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := serializeInvoice(&buf, &invoice); err != nil {
|
if err := serializeInvoice(&buf, &invoice); err != nil {
|
||||||
@@ -1629,20 +1926,54 @@ func updateInvoiceState(invoice *Invoice, hash lntypes.Hash,
|
|||||||
// or canceled. The only restriction is on transitioning to settled
|
// or canceled. The only restriction is on transitioning to settled
|
||||||
// where we ensure the preimage is valid.
|
// where we ensure the preimage is valid.
|
||||||
case ContractOpen:
|
case ContractOpen:
|
||||||
if update.NewState == ContractSettled {
|
if update.NewState == ContractCanceled {
|
||||||
// Validate preimage.
|
invoice.State = update.NewState
|
||||||
switch {
|
return nil
|
||||||
case update.Preimage != nil:
|
|
||||||
if update.Preimage.Hash() != hash {
|
|
||||||
return ErrInvoicePreimageMismatch
|
|
||||||
}
|
|
||||||
invoice.Terms.PaymentPreimage = update.Preimage
|
|
||||||
|
|
||||||
case invoice.Terms.PaymentPreimage == nil:
|
|
||||||
return errors.New("unknown preimage")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanity check that the user isn't trying to settle or accept a
|
||||||
|
// non-existent HTLC set.
|
||||||
|
if len(invoice.HTLCSet(update.SetID)) == 0 {
|
||||||
|
return 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 errors.New("AMP set cannot have preimage")
|
||||||
|
}
|
||||||
|
invoice.State = update.NewState
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
|
||||||
|
// Validate the supplied preimage for non-AMP invoices.
|
||||||
|
case update.Preimage != nil:
|
||||||
|
if update.Preimage.Hash() != hash {
|
||||||
|
return ErrInvoicePreimageMismatch
|
||||||
|
}
|
||||||
|
invoice.Terms.PaymentPreimage = update.Preimage
|
||||||
|
|
||||||
|
// 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 errors.New("unknown preimage")
|
||||||
|
}
|
||||||
|
|
||||||
|
invoice.State = update.NewState
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
// Once settled, we are in a terminal state.
|
// Once settled, we are in a terminal state.
|
||||||
case ContractSettled:
|
case ContractSettled:
|
||||||
return ErrInvoiceAlreadySettled
|
return ErrInvoiceAlreadySettled
|
||||||
@@ -1654,10 +1985,6 @@ func updateInvoiceState(invoice *Invoice, hash lntypes.Hash,
|
|||||||
default:
|
default:
|
||||||
return errors.New("unknown state transition")
|
return errors.New("unknown state transition")
|
||||||
}
|
}
|
||||||
|
|
||||||
invoice.State = update.NewState
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cancelSingleHtlc validates cancelation of a single htlc and update its state.
|
// cancelSingleHtlc validates cancelation of a single htlc and update its state.
|
||||||
@@ -1684,39 +2011,81 @@ func cancelSingleHtlc(resolveTime time.Time, htlc *InvoiceHTLC,
|
|||||||
|
|
||||||
// updateHtlc aligns the state of an htlc with the given invoice state.
|
// updateHtlc aligns the state of an htlc with the given invoice state.
|
||||||
func updateHtlc(resolveTime time.Time, htlc *InvoiceHTLC,
|
func updateHtlc(resolveTime time.Time, htlc *InvoiceHTLC,
|
||||||
invState ContractState) error {
|
invState ContractState, setID *[32]byte) error {
|
||||||
|
|
||||||
|
trySettle := func(persist bool) error {
|
||||||
|
if htlc.State != HtlcStateAccepted {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settle the HTLC if it matches the settled set id. Since we
|
||||||
|
// only allow settling of one HTLC set (for now) we cancel any
|
||||||
|
// that do not match the set id.
|
||||||
|
var htlcState HtlcState
|
||||||
|
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.
|
||||||
|
if setID != nil && !htlc.AMP.Preimage.Matches(htlc.AMP.Hash) {
|
||||||
|
return fmt.Errorf("AMP preimage mismatch, "+
|
||||||
|
"preimage=%v hash=%v", *htlc.AMP.Preimage,
|
||||||
|
htlc.AMP.Hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
htlcState = HtlcStateSettled
|
||||||
|
} else {
|
||||||
|
htlcState = HtlcStateCanceled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only persist the changes if the invoice is moving to the
|
||||||
|
// settled state.
|
||||||
|
if persist {
|
||||||
|
htlc.State = htlcState
|
||||||
|
htlc.ResolveTime = resolveTime
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if invState == 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 fmt.Errorf("cannot have a settled htlc with "+
|
||||||
|
"invoice in state %v", invState)
|
||||||
|
}
|
||||||
|
|
||||||
switch invState {
|
switch invState {
|
||||||
|
|
||||||
case ContractSettled:
|
|
||||||
if htlc.State == HtlcStateAccepted {
|
|
||||||
htlc.State = HtlcStateSettled
|
|
||||||
htlc.ResolveTime = resolveTime
|
|
||||||
}
|
|
||||||
|
|
||||||
case ContractCanceled:
|
case ContractCanceled:
|
||||||
switch htlc.State {
|
if htlc.State == HtlcStateAccepted {
|
||||||
|
|
||||||
case HtlcStateAccepted:
|
|
||||||
htlc.State = HtlcStateCanceled
|
htlc.State = HtlcStateCanceled
|
||||||
htlc.ResolveTime = resolveTime
|
htlc.ResolveTime = resolveTime
|
||||||
|
|
||||||
case HtlcStateSettled:
|
|
||||||
return fmt.Errorf("cannot have a settled htlc with " +
|
|
||||||
"invoice in state canceled")
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
case ContractOpen, ContractAccepted:
|
case ContractAccepted:
|
||||||
if htlc.State == HtlcStateSettled {
|
// Check that we can settle the HTLCs. For legacy and MPP HTLCs
|
||||||
return fmt.Errorf("cannot have a settled htlc with "+
|
// this will be a NOP, but for AMP HTLCs this asserts that we
|
||||||
"invoice in state %v", invState)
|
// 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 nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return errors.New("unknown state transition")
|
return errors.New("unknown state transition")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// setSettleMetaFields updates the metadata associated with settlement of an
|
// setSettleMetaFields updates the metadata associated with settlement of an
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ type Payload struct {
|
|||||||
// a TLV onion payload.
|
// a TLV onion payload.
|
||||||
MPP *record.MPP
|
MPP *record.MPP
|
||||||
|
|
||||||
|
// AMP holds the info provided in an option_amp record when parsed from
|
||||||
|
// a TLV onion payload.
|
||||||
|
AMP *record.AMP
|
||||||
|
|
||||||
// customRecords are user-defined records in the custom type range that
|
// customRecords are user-defined records in the custom type range that
|
||||||
// were included in the payload.
|
// were included in the payload.
|
||||||
customRecords record.CustomSet
|
customRecords record.CustomSet
|
||||||
@@ -250,6 +254,12 @@ func (h *Payload) MultiPath() *record.MPP {
|
|||||||
return h.MPP
|
return h.MPP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AMPRecord returns the record corresponding with option_amp parsed from the
|
||||||
|
// onion payload.
|
||||||
|
func (h *Payload) AMPRecord() *record.AMP {
|
||||||
|
return h.AMP
|
||||||
|
}
|
||||||
|
|
||||||
// CustomRecords returns the custom tlv type records that were parsed from the
|
// CustomRecords returns the custom tlv type records that were parsed from the
|
||||||
// payload.
|
// payload.
|
||||||
func (h *Payload) CustomRecords() record.CustomSet {
|
func (h *Payload) CustomRecords() record.CustomSet {
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ type Payload interface {
|
|||||||
// the onion payload.
|
// the onion payload.
|
||||||
MultiPath() *record.MPP
|
MultiPath() *record.MPP
|
||||||
|
|
||||||
|
// AMPRecord returns the record corresponding to the option_amp record
|
||||||
|
// parsed from the onion payload.
|
||||||
|
AMPRecord() *record.AMP
|
||||||
|
|
||||||
// CustomRecords returns the custom tlv type records that were parsed
|
// CustomRecords returns the custom tlv type records that were parsed
|
||||||
// from the payload.
|
// from the payload.
|
||||||
CustomRecords() record.CustomSet
|
CustomRecords() record.CustomSet
|
||||||
|
|||||||
@@ -707,8 +707,12 @@ func (i *InvoiceRegistry) processKeySend(ctx invoiceUpdateCtx) error {
|
|||||||
|
|
||||||
// Cancel htlc is preimage is invalid.
|
// Cancel htlc is preimage is invalid.
|
||||||
preimage, err := lntypes.MakePreimage(preimageSlice)
|
preimage, err := lntypes.MakePreimage(preimageSlice)
|
||||||
if err != nil || preimage.Hash() != ctx.hash {
|
if err != nil {
|
||||||
return errors.New("invalid keysend preimage")
|
return err
|
||||||
|
}
|
||||||
|
if preimage.Hash() != ctx.hash {
|
||||||
|
return fmt.Errorf("invalid keysend preimage %v for hash %v",
|
||||||
|
preimage, ctx.hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only allow keysend for non-mpp payments.
|
// Only allow keysend for non-mpp payments.
|
||||||
@@ -802,6 +806,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
|||||||
finalCltvRejectDelta: i.cfg.FinalCltvRejectDelta,
|
finalCltvRejectDelta: i.cfg.FinalCltvRejectDelta,
|
||||||
customRecords: payload.CustomRecords(),
|
customRecords: payload.CustomRecords(),
|
||||||
mpp: payload.MultiPath(),
|
mpp: payload.MultiPath(),
|
||||||
|
amp: payload.AMPRecord(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process keysend if present. Do this outside of the lock, because
|
// Process keysend if present. Do this outside of the lock, because
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/record"
|
"github.com/lightningnetwork/lnd/record"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,7 +19,7 @@ func TestSettleInvoice(t *testing.T) {
|
|||||||
defer ctx.cleanup()
|
defer ctx.cleanup()
|
||||||
|
|
||||||
allSubscriptions, err := ctx.registry.SubscribeNotifications(0, 0)
|
allSubscriptions, err := ctx.registry.SubscribeNotifications(0, 0)
|
||||||
assert.Nil(t, err)
|
require.Nil(t, err)
|
||||||
defer allSubscriptions.Cancel()
|
defer allSubscriptions.Cancel()
|
||||||
|
|
||||||
// Subscribe to the not yet existing invoice.
|
// Subscribe to the not yet existing invoice.
|
||||||
@@ -229,7 +228,7 @@ func testCancelInvoice(t *testing.T, gc bool) {
|
|||||||
ctx.registry.cfg.GcCanceledInvoicesOnTheFly = gc
|
ctx.registry.cfg.GcCanceledInvoicesOnTheFly = gc
|
||||||
|
|
||||||
allSubscriptions, err := ctx.registry.SubscribeNotifications(0, 0)
|
allSubscriptions, err := ctx.registry.SubscribeNotifications(0, 0)
|
||||||
assert.Nil(t, err)
|
require.Nil(t, err)
|
||||||
defer allSubscriptions.Cancel()
|
defer allSubscriptions.Cancel()
|
||||||
|
|
||||||
// Try to cancel the not yet existing invoice. This should fail.
|
// Try to cancel the not yet existing invoice. This should fail.
|
||||||
@@ -395,7 +394,7 @@ func TestSettleHoldInvoice(t *testing.T) {
|
|||||||
defer registry.Stop()
|
defer registry.Stop()
|
||||||
|
|
||||||
allSubscriptions, err := registry.SubscribeNotifications(0, 0)
|
allSubscriptions, err := registry.SubscribeNotifications(0, 0)
|
||||||
assert.Nil(t, err)
|
require.Nil(t, err)
|
||||||
defer allSubscriptions.Cancel()
|
defer allSubscriptions.Cancel()
|
||||||
|
|
||||||
// Subscribe to the not yet existing invoice.
|
// Subscribe to the not yet existing invoice.
|
||||||
@@ -695,7 +694,7 @@ func testKeySend(t *testing.T, keySendEnabled bool) {
|
|||||||
ctx.registry.cfg.AcceptKeySend = keySendEnabled
|
ctx.registry.cfg.AcceptKeySend = keySendEnabled
|
||||||
|
|
||||||
allSubscriptions, err := ctx.registry.SubscribeNotifications(0, 0)
|
allSubscriptions, err := ctx.registry.SubscribeNotifications(0, 0)
|
||||||
assert.Nil(t, err)
|
require.Nil(t, err)
|
||||||
defer allSubscriptions.Cancel()
|
defer allSubscriptions.Cancel()
|
||||||
|
|
||||||
hodlChan := make(chan interface{}, 1)
|
hodlChan := make(chan interface{}, 1)
|
||||||
@@ -767,17 +766,17 @@ func testKeySend(t *testing.T, keySendEnabled bool) {
|
|||||||
checkResolution := func(res HtlcResolution, pimg lntypes.Preimage) {
|
checkResolution := func(res HtlcResolution, pimg lntypes.Preimage) {
|
||||||
// Otherwise we expect no error and a settle res for the htlc.
|
// Otherwise we expect no error and a settle res for the htlc.
|
||||||
settleResolution, ok := res.(*HtlcSettleResolution)
|
settleResolution, ok := res.(*HtlcSettleResolution)
|
||||||
assert.True(t, ok)
|
require.True(t, ok)
|
||||||
assert.Equal(t, settleResolution.Preimage, pimg)
|
require.Equal(t, settleResolution.Preimage, pimg)
|
||||||
}
|
}
|
||||||
checkSubscription := func() {
|
checkSubscription := func() {
|
||||||
// We expect a new invoice notification to be sent out.
|
// We expect a new invoice notification to be sent out.
|
||||||
newInvoice := <-allSubscriptions.NewInvoices
|
newInvoice := <-allSubscriptions.NewInvoices
|
||||||
assert.Equal(t, newInvoice.State, channeldb.ContractOpen)
|
require.Equal(t, newInvoice.State, channeldb.ContractOpen)
|
||||||
|
|
||||||
// We expect a settled notification to be sent out.
|
// We expect a settled notification to be sent out.
|
||||||
settledInvoice := <-allSubscriptions.SettledInvoices
|
settledInvoice := <-allSubscriptions.SettledInvoices
|
||||||
assert.Equal(t, settledInvoice.State, channeldb.ContractSettled)
|
require.Equal(t, settledInvoice.State, channeldb.ContractSettled)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkResolution(resolution, preimage)
|
checkResolution(resolution, preimage)
|
||||||
@@ -789,7 +788,7 @@ func testKeySend(t *testing.T, keySendEnabled bool) {
|
|||||||
hash, amt, expiry,
|
hash, amt, expiry,
|
||||||
testCurrentHeight, getCircuitKey(10), hodlChan, keySendPayload,
|
testCurrentHeight, getCircuitKey(10), hodlChan, keySendPayload,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.Nil(t, err)
|
||||||
checkResolution(resolution, preimage)
|
checkResolution(resolution, preimage)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -813,7 +812,7 @@ func testKeySend(t *testing.T, keySendEnabled bool) {
|
|||||||
hash2, amt, expiry,
|
hash2, amt, expiry,
|
||||||
testCurrentHeight, getCircuitKey(20), hodlChan, keySendPayload2,
|
testCurrentHeight, getCircuitKey(20), hodlChan, keySendPayload2,
|
||||||
)
|
)
|
||||||
assert.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
checkResolution(resolution, preimage2)
|
checkResolution(resolution, preimage2)
|
||||||
checkSubscription()
|
checkSubscription()
|
||||||
@@ -842,7 +841,7 @@ func testHoldKeysend(t *testing.T, timeoutKeysend bool) {
|
|||||||
ctx.registry.cfg.KeysendHoldTime = holdDuration
|
ctx.registry.cfg.KeysendHoldTime = holdDuration
|
||||||
|
|
||||||
allSubscriptions, err := ctx.registry.SubscribeNotifications(0, 0)
|
allSubscriptions, err := ctx.registry.SubscribeNotifications(0, 0)
|
||||||
assert.Nil(t, err)
|
require.Nil(t, err)
|
||||||
defer allSubscriptions.Cancel()
|
defer allSubscriptions.Cancel()
|
||||||
|
|
||||||
hodlChan := make(chan interface{}, 1)
|
hodlChan := make(chan interface{}, 1)
|
||||||
@@ -915,7 +914,7 @@ func testHoldKeysend(t *testing.T, timeoutKeysend bool) {
|
|||||||
|
|
||||||
// We expect a settled notification to be sent out.
|
// We expect a settled notification to be sent out.
|
||||||
settledInvoice := <-allSubscriptions.SettledInvoices
|
settledInvoice := <-allSubscriptions.SettledInvoices
|
||||||
assert.Equal(t, settledInvoice.State, channeldb.ContractSettled)
|
require.Equal(t, settledInvoice.State, channeldb.ContractSettled)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMppPayment tests settling of an invoice with multiple partial payments.
|
// TestMppPayment tests settling of an invoice with multiple partial payments.
|
||||||
@@ -1201,7 +1200,7 @@ func TestSettleInvoicePaymentAddrRequired(t *testing.T) {
|
|||||||
defer ctx.cleanup()
|
defer ctx.cleanup()
|
||||||
|
|
||||||
allSubscriptions, err := ctx.registry.SubscribeNotifications(0, 0)
|
allSubscriptions, err := ctx.registry.SubscribeNotifications(0, 0)
|
||||||
assert.Nil(t, err)
|
require.Nil(t, err)
|
||||||
defer allSubscriptions.Cancel()
|
defer allSubscriptions.Cancel()
|
||||||
|
|
||||||
// Subscribe to the not yet existing invoice.
|
// Subscribe to the not yet existing invoice.
|
||||||
@@ -1277,7 +1276,7 @@ func TestSettleInvoicePaymentAddrRequiredOptionalGrace(t *testing.T) {
|
|||||||
defer ctx.cleanup()
|
defer ctx.cleanup()
|
||||||
|
|
||||||
allSubscriptions, err := ctx.registry.SubscribeNotifications(0, 0)
|
allSubscriptions, err := ctx.registry.SubscribeNotifications(0, 0)
|
||||||
assert.Nil(t, err)
|
require.Nil(t, err)
|
||||||
defer allSubscriptions.Cancel()
|
defer allSubscriptions.Cancel()
|
||||||
|
|
||||||
// Subscribe to the not yet existing invoice.
|
// Subscribe to the not yet existing invoice.
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
|
|
||||||
type mockPayload struct {
|
type mockPayload struct {
|
||||||
mpp *record.MPP
|
mpp *record.MPP
|
||||||
|
amp *record.AMP
|
||||||
customRecords record.CustomSet
|
customRecords record.CustomSet
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +31,10 @@ func (p *mockPayload) MultiPath() *record.MPP {
|
|||||||
return p.mpp
|
return p.mpp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *mockPayload) AMPRecord() *record.AMP {
|
||||||
|
return p.amp
|
||||||
|
}
|
||||||
|
|
||||||
func (p *mockPayload) CustomRecords() record.CustomSet {
|
func (p *mockPayload) CustomRecords() record.CustomSet {
|
||||||
// This function should always return a map instance, but for mock
|
// This function should always return a map instance, but for mock
|
||||||
// configuration we do accept nil.
|
// configuration we do accept nil.
|
||||||
|
|||||||
+22
-14
@@ -20,6 +20,7 @@ type invoiceUpdateCtx struct {
|
|||||||
finalCltvRejectDelta int32
|
finalCltvRejectDelta int32
|
||||||
customRecords record.CustomSet
|
customRecords record.CustomSet
|
||||||
mpp *record.MPP
|
mpp *record.MPP
|
||||||
|
amp *record.AMP
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -32,10 +33,22 @@ func (i *invoiceUpdateCtx) invoiceRef() channeldb.InvoiceRef {
|
|||||||
return channeldb.InvoiceRefByHash(i.hash)
|
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.
|
// log logs a message specific to this update context.
|
||||||
func (i *invoiceUpdateCtx) log(s string) {
|
func (i *invoiceUpdateCtx) log(s string) {
|
||||||
log.Debugf("Invoice%v: %v, amt=%v, expiry=%v, circuit=%v, mpp=%v",
|
log.Debugf("Invoice%v: %v, amt=%v, expiry=%v, circuit=%v, mpp=%v, "+
|
||||||
i.invoiceRef, s, i.amtPaid, i.expiry, i.circuitKey, i.mpp)
|
"amp=%v", i.hash[:], s, i.amtPaid, i.expiry, i.circuitKey,
|
||||||
|
i.mpp, i.amp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// failRes is a helper function which creates a failure resolution with
|
// failRes is a helper function which creates a failure resolution with
|
||||||
@@ -106,6 +119,8 @@ func updateMpp(ctx *invoiceUpdateCtx,
|
|||||||
inv *channeldb.Invoice) (*channeldb.InvoiceUpdateDesc,
|
inv *channeldb.Invoice) (*channeldb.InvoiceUpdateDesc,
|
||||||
HtlcResolution, error) {
|
HtlcResolution, error) {
|
||||||
|
|
||||||
|
setID := ctx.setID()
|
||||||
|
|
||||||
// Start building the accept descriptor.
|
// Start building the accept descriptor.
|
||||||
acceptDesc := &channeldb.HtlcAcceptDesc{
|
acceptDesc := &channeldb.HtlcAcceptDesc{
|
||||||
Amt: ctx.amtPaid,
|
Amt: ctx.amtPaid,
|
||||||
@@ -141,14 +156,7 @@ func updateMpp(ctx *invoiceUpdateCtx,
|
|||||||
|
|
||||||
// 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.Htlcs {
|
for _, htlc := range inv.HTLCSet(setID) {
|
||||||
// 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
|
||||||
}
|
}
|
||||||
@@ -193,6 +201,7 @@ func updateMpp(ctx *invoiceUpdateCtx,
|
|||||||
if inv.HodlInvoice {
|
if inv.HodlInvoice {
|
||||||
update.State = &channeldb.InvoiceStateUpdateDesc{
|
update.State = &channeldb.InvoiceStateUpdateDesc{
|
||||||
NewState: channeldb.ContractAccepted,
|
NewState: channeldb.ContractAccepted,
|
||||||
|
SetID: setID,
|
||||||
}
|
}
|
||||||
return &update, ctx.acceptRes(resultAccepted), nil
|
return &update, ctx.acceptRes(resultAccepted), nil
|
||||||
}
|
}
|
||||||
@@ -200,6 +209,7 @@ func updateMpp(ctx *invoiceUpdateCtx,
|
|||||||
update.State = &channeldb.InvoiceStateUpdateDesc{
|
update.State = &channeldb.InvoiceStateUpdateDesc{
|
||||||
NewState: channeldb.ContractSettled,
|
NewState: channeldb.ContractSettled,
|
||||||
Preimage: inv.Terms.PaymentPreimage,
|
Preimage: inv.Terms.PaymentPreimage,
|
||||||
|
SetID: setID,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &update, ctx.settleRes(
|
return &update, ctx.settleRes(
|
||||||
@@ -248,10 +258,8 @@ func updateLegacy(ctx *invoiceUpdateCtx,
|
|||||||
// Don't allow settling the invoice with an old style
|
// Don't allow settling the invoice with an old style
|
||||||
// htlc if we are already in the process of gathering an
|
// htlc if we are already in the process of gathering an
|
||||||
// mpp set.
|
// mpp set.
|
||||||
for _, htlc := range inv.Htlcs {
|
for _, htlc := range inv.HTLCSet(nil) {
|
||||||
if htlc.State == channeldb.HtlcStateAccepted &&
|
if htlc.MppTotalAmt > 0 {
|
||||||
htlc.MppTotalAmt > 0 {
|
|
||||||
|
|
||||||
return nil, ctx.failRes(ResultMppInProgress), nil
|
return nil, ctx.failRes(ResultMppInProgress), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,6 +256,37 @@
|
|||||||
"invoicesrpcSettleInvoiceResp": {
|
"invoicesrpcSettleInvoiceResp": {
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"lnrpcAMP": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"root_share": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "byte",
|
||||||
|
"description": "An n-of-n secret share of the root seed from which child payment hashes\nand preimages are derived."
|
||||||
|
},
|
||||||
|
"set_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "byte",
|
||||||
|
"description": "An identifier for the HTLC set that this HTLC belongs to."
|
||||||
|
},
|
||||||
|
"child_index": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "A nonce used to randomize the child preimage and child hash from a given\nroot_share."
|
||||||
|
},
|
||||||
|
"hash": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "byte",
|
||||||
|
"description": "The payment hash of the AMP HTLC."
|
||||||
|
},
|
||||||
|
"preimage": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "byte",
|
||||||
|
"description": "The preimage used to settle this AMP htlc. This field will only be\npopulated if the invoice is in InvoiceState_ACCEPTED or\nInvoiceState_SETTLED."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Details specific to AMP HTLCs."
|
||||||
|
},
|
||||||
"lnrpcFeature": {
|
"lnrpcFeature": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -489,6 +520,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "uint64",
|
"format": "uint64",
|
||||||
"description": "The total amount of the mpp payment in msat."
|
"description": "The total amount of the mpp payment in msat."
|
||||||
|
},
|
||||||
|
"amp": {
|
||||||
|
"$ref": "#/definitions/lnrpcAMP",
|
||||||
|
"description": "Details relevant to AMP HTLCs, only populated if this is an AMP HTLC."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Details of an HTLC that paid to an invoice"
|
"title": "Details of an HTLC that paid to an invoice"
|
||||||
|
|||||||
@@ -116,6 +116,25 @@ func CreateRPCInvoice(invoice *channeldb.Invoice,
|
|||||||
MppTotalAmtMsat: uint64(htlc.MppTotalAmt),
|
MppTotalAmtMsat: uint64(htlc.MppTotalAmt),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Populate any fields relevant to AMP payments.
|
||||||
|
if htlc.AMP != nil {
|
||||||
|
rootShare := htlc.AMP.Record.RootShare()
|
||||||
|
setID := htlc.AMP.Record.SetID()
|
||||||
|
|
||||||
|
var preimage []byte
|
||||||
|
if htlc.AMP.Preimage != nil {
|
||||||
|
preimage = htlc.AMP.Preimage[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
rpcHtlc.Amp = &lnrpc.AMP{
|
||||||
|
RootShare: rootShare[:],
|
||||||
|
SetId: setID[:],
|
||||||
|
ChildIndex: uint32(htlc.AMP.Record.ChildIndex()),
|
||||||
|
Hash: htlc.AMP.Hash[:],
|
||||||
|
Preimage: preimage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Only report resolved times if htlc is resolved.
|
// Only report resolved times if htlc is resolved.
|
||||||
if htlc.State != channeldb.HtlcStateAccepted {
|
if htlc.State != channeldb.HtlcStateAccepted {
|
||||||
rpcHtlc.ResolveTime = htlc.ResolveTime.Unix()
|
rpcHtlc.ResolveTime = htlc.ResolveTime.Unix()
|
||||||
|
|||||||
+515
-419
File diff suppressed because it is too large.
Load diff
@@ -2954,6 +2954,31 @@ message InvoiceHTLC {
|
|||||||
|
|
||||||
// The total amount of the mpp payment in msat.
|
// The total amount of the mpp payment in msat.
|
||||||
uint64 mpp_total_amt_msat = 10;
|
uint64 mpp_total_amt_msat = 10;
|
||||||
|
|
||||||
|
// Details relevant to AMP HTLCs, only populated if this is an AMP HTLC.
|
||||||
|
AMP amp = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Details specific to AMP HTLCs.
|
||||||
|
message AMP {
|
||||||
|
// An n-of-n secret share of the root seed from which child payment hashes
|
||||||
|
// and preimages are derived.
|
||||||
|
bytes root_share = 1;
|
||||||
|
|
||||||
|
// An identifier for the HTLC set that this HTLC belongs to.
|
||||||
|
bytes set_id = 2;
|
||||||
|
|
||||||
|
// A nonce used to randomize the child preimage and child hash from a given
|
||||||
|
// root_share.
|
||||||
|
uint32 child_index = 3;
|
||||||
|
|
||||||
|
// The payment hash of the AMP HTLC.
|
||||||
|
bytes hash = 4;
|
||||||
|
|
||||||
|
// The preimage used to settle this AMP htlc. This field will only be
|
||||||
|
// populated if the invoice is in InvoiceState_ACCEPTED or
|
||||||
|
// InvoiceState_SETTLED.
|
||||||
|
bytes preimage = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AddInvoiceResponse {
|
message AddInvoiceResponse {
|
||||||
|
|||||||
@@ -2468,6 +2468,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lnrpcAMP": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"root_share": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "byte",
|
||||||
|
"description": "An n-of-n secret share of the root seed from which child payment hashes\nand preimages are derived."
|
||||||
|
},
|
||||||
|
"set_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "byte",
|
||||||
|
"description": "An identifier for the HTLC set that this HTLC belongs to."
|
||||||
|
},
|
||||||
|
"child_index": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "A nonce used to randomize the child preimage and child hash from a given\nroot_share."
|
||||||
|
},
|
||||||
|
"hash": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "byte",
|
||||||
|
"description": "The payment hash of the AMP HTLC."
|
||||||
|
},
|
||||||
|
"preimage": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "byte",
|
||||||
|
"description": "The preimage used to settle this AMP htlc. This field will only be\npopulated if the invoice is in InvoiceState_ACCEPTED or\nInvoiceState_SETTLED."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Details specific to AMP HTLCs."
|
||||||
|
},
|
||||||
"lnrpcAbandonChannelResponse": {
|
"lnrpcAbandonChannelResponse": {
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
@@ -4186,6 +4217,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "uint64",
|
"format": "uint64",
|
||||||
"description": "The total amount of the mpp payment in msat."
|
"description": "The total amount of the mpp payment in msat."
|
||||||
|
},
|
||||||
|
"amp": {
|
||||||
|
"$ref": "#/definitions/lnrpcAMP",
|
||||||
|
"description": "Details relevant to AMP HTLCs, only populated if this is an AMP HTLC."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Details of an HTLC that paid to an invoice"
|
"title": "Details of an HTLC that paid to an invoice"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/record"
|
"github.com/lightningnetwork/lnd/record"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testSingleHopInvoice(net *lntest.NetworkHarness, t *harnessTest) {
|
func testSingleHopInvoice(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
@@ -162,6 +163,21 @@ func testSingleHopInvoice(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
t.Fatalf(err.Error())
|
t.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assert that the invoice has the proper AMP fields set, since the
|
||||||
|
// legacy keysend payment should have been promoted into an AMP payment
|
||||||
|
// internally.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
keysendInvoice, err := net.Bob.LookupInvoice(
|
||||||
|
ctxt, &lnrpc.PaymentHash{
|
||||||
|
RHash: keySendHash[:],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
require.Equal(t.t, 1, len(keysendInvoice.Htlcs))
|
||||||
|
htlc := keysendInvoice.Htlcs[0]
|
||||||
|
require.Equal(t.t, uint64(0), htlc.MppTotalAmtMsat)
|
||||||
|
require.Nil(t.t, htlc.Amp)
|
||||||
|
|
||||||
// Now create an invoice and specify routing hints.
|
// Now create an invoice and specify routing hints.
|
||||||
// We will test that the routing hints are encoded properly.
|
// We will test that the routing hints are encoded properly.
|
||||||
hintChannel := lnwire.ShortChannelID{BlockHeight: 10}
|
hintChannel := lnwire.ShortChannelID{BlockHeight: 10}
|
||||||
|
|||||||
Reference in new issue
Block a user