Merge pull request #4407 from cfromknecht/gen-keysend-pay-addr
invoices/invoiceregistry: special-case payaddr index insertion for keysend
This commit is contained in:
commit
feb300f102
@ -313,6 +313,48 @@ func TestAddDuplicatePayAddr(t *testing.T) {
|
|||||||
require.Error(t, err, ErrDuplicatePayAddr)
|
require.Error(t, err, ErrDuplicatePayAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAddDuplicateKeysendPayAddr asserts that we permit duplicate payment
|
||||||
|
// addresses to be inserted if they are blank to support JIT legacy keysend
|
||||||
|
// invoices.
|
||||||
|
func TestAddDuplicateKeysendPayAddr(t *testing.T) {
|
||||||
|
db, cleanUp, err := makeTestDB()
|
||||||
|
defer cleanUp()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create two invoices with the same _blank_ payment addr.
|
||||||
|
invoice1, err := randInvoice(1000)
|
||||||
|
require.NoError(t, err)
|
||||||
|
invoice1.Terms.PaymentAddr = BlankPayAddr
|
||||||
|
|
||||||
|
invoice2, err := randInvoice(20000)
|
||||||
|
require.NoError(t, err)
|
||||||
|
invoice2.Terms.PaymentAddr = BlankPayAddr
|
||||||
|
|
||||||
|
// Inserting both should succeed without a duplicate payment address
|
||||||
|
// failure.
|
||||||
|
inv1Hash := invoice1.Terms.PaymentPreimage.Hash()
|
||||||
|
_, err = db.AddInvoice(invoice1, inv1Hash)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
inv2Hash := invoice2.Terms.PaymentPreimage.Hash()
|
||||||
|
_, err = db.AddInvoice(invoice2, inv2Hash)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Querying for each should succeed. Here we use hash+addr refs since
|
||||||
|
// the lookup will fail if the hash and addr point to different
|
||||||
|
// invoices, so if both succeed we can be assured they aren't included
|
||||||
|
// in the payment address index.
|
||||||
|
ref1 := InvoiceRefByHashAndAddr(inv1Hash, BlankPayAddr)
|
||||||
|
dbInv1, err := db.LookupInvoice(ref1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, invoice1, &dbInv1)
|
||||||
|
|
||||||
|
ref2 := InvoiceRefByHashAndAddr(inv2Hash, BlankPayAddr)
|
||||||
|
dbInv2, err := db.LookupInvoice(ref2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, invoice2, &dbInv2)
|
||||||
|
}
|
||||||
|
|
||||||
// TestInvRefEquivocation asserts that retrieving or updating an invoice using
|
// TestInvRefEquivocation asserts that retrieving or updating an invoice using
|
||||||
// an equivocating InvoiceRef results in ErrInvRefEquivocation.
|
// an equivocating InvoiceRef results in ErrInvRefEquivocation.
|
||||||
func TestInvRefEquivocation(t *testing.T) {
|
func TestInvRefEquivocation(t *testing.T) {
|
||||||
|
@ -21,6 +21,12 @@ var (
|
|||||||
// preimage for this invoice is not yet known.
|
// preimage for this invoice is not yet known.
|
||||||
unknownPreimage lntypes.Preimage
|
unknownPreimage lntypes.Preimage
|
||||||
|
|
||||||
|
// BlankPayAddr is a sentinel payment address for legacy invoices.
|
||||||
|
// Invoices with this payment address are special-cased in the insertion
|
||||||
|
// logic to prevent being indexed in the payment address index,
|
||||||
|
// otherwise they would cause collisions after the first insertion.
|
||||||
|
BlankPayAddr [32]byte
|
||||||
|
|
||||||
// invoiceBucket is the name of the bucket within the database that
|
// invoiceBucket is the name of the bucket within the database that
|
||||||
// stores all data related to invoices no matter their final state.
|
// stores all data related to invoices no matter their final state.
|
||||||
// Within the invoice bucket, each invoice is keyed by its invoice ID
|
// Within the invoice bucket, each invoice is keyed by its invoice ID
|
||||||
@ -519,9 +525,16 @@ func (d *DB) AddInvoice(newInvoice *Invoice, paymentHash lntypes.Hash) (
|
|||||||
return ErrDuplicateInvoice
|
return ErrDuplicateInvoice
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check that we aren't inserting an invoice with a duplicate
|
||||||
|
// payment address. The all-zeros payment address is
|
||||||
|
// special-cased to support legacy keysend invoices which don't
|
||||||
|
// assign one. This is safe since later we also will avoid
|
||||||
|
// indexing them and avoid collisions.
|
||||||
payAddrIndex := tx.ReadWriteBucket(payAddrIndexBucket)
|
payAddrIndex := tx.ReadWriteBucket(payAddrIndexBucket)
|
||||||
if payAddrIndex.Get(newInvoice.Terms.PaymentAddr[:]) != nil {
|
if newInvoice.Terms.PaymentAddr != BlankPayAddr {
|
||||||
return ErrDuplicatePayAddr
|
if payAddrIndex.Get(newInvoice.Terms.PaymentAddr[:]) != nil {
|
||||||
|
return ErrDuplicatePayAddr
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the current running payment ID counter hasn't yet been
|
// If the current running payment ID counter hasn't yet been
|
||||||
@ -679,7 +692,12 @@ func fetchInvoiceNumByRef(invoiceIndex, payAddrIndex kvdb.RBucket,
|
|||||||
invoiceNumByAddr []byte
|
invoiceNumByAddr []byte
|
||||||
)
|
)
|
||||||
if payAddr != nil {
|
if payAddr != nil {
|
||||||
invoiceNumByAddr = payAddrIndex.Get(payAddr[:])
|
// Only allow lookups for payment address if it is not a blank
|
||||||
|
// payment address, which is a special-cased value for legacy
|
||||||
|
// keysend invoices.
|
||||||
|
if *payAddr != BlankPayAddr {
|
||||||
|
invoiceNumByAddr = payAddrIndex.Get(payAddr[:])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@ -1047,9 +1065,15 @@ func putInvoice(invoices, invoiceIndex, payAddrIndex, addIndex kvdb.RwBucket,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
err = payAddrIndex.Put(i.Terms.PaymentAddr[:], invoiceKey[:])
|
// Add the invoice to the payment address index, but only if the invoice
|
||||||
if err != nil {
|
// has a non-zero payment address. The all-zero payment address is still
|
||||||
return 0, err
|
// in use by legacy keysend, so we special-case here to avoid
|
||||||
|
// collisions.
|
||||||
|
if i.Terms.PaymentAddr != BlankPayAddr {
|
||||||
|
err = payAddrIndex.Put(i.Terms.PaymentAddr[:], invoiceKey[:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next, we'll obtain the next add invoice index (sequence
|
// Next, we'll obtain the next add invoice index (sequence
|
||||||
|
@ -676,6 +676,15 @@ func (i *InvoiceRegistry) processKeySend(ctx invoiceUpdateCtx) error {
|
|||||||
return errors.New("final expiry too soon")
|
return errors.New("final expiry too soon")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The invoice database indexes all invoices by payment address, however
|
||||||
|
// legacy keysend payment do not have one. In order to avoid a new
|
||||||
|
// payment type on-disk wrt. to indexing, we'll continue to insert a
|
||||||
|
// blank payment address which is special cased in the insertion logic
|
||||||
|
// to not be indexed. In the future, once AMP is merged, this should be
|
||||||
|
// replaced by generating a random payment address on the behalf of the
|
||||||
|
// sender.
|
||||||
|
payAddr := channeldb.BlankPayAddr
|
||||||
|
|
||||||
// Create placeholder invoice.
|
// Create placeholder invoice.
|
||||||
invoice := &channeldb.Invoice{
|
invoice := &channeldb.Invoice{
|
||||||
CreationDate: i.cfg.Clock.Now(),
|
CreationDate: i.cfg.Clock.Now(),
|
||||||
@ -683,6 +692,7 @@ func (i *InvoiceRegistry) processKeySend(ctx invoiceUpdateCtx) error {
|
|||||||
FinalCltvDelta: finalCltvDelta,
|
FinalCltvDelta: finalCltvDelta,
|
||||||
Value: amt,
|
Value: amt,
|
||||||
PaymentPreimage: &preimage,
|
PaymentPreimage: &preimage,
|
||||||
|
PaymentAddr: payAddr,
|
||||||
Features: features,
|
Features: features,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -725,29 +725,59 @@ func testKeySend(t *testing.T, keySendEnabled bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise we expect no error and a settle resolution for the htlc.
|
checkResolution := func(res HtlcResolution, pimg lntypes.Preimage) {
|
||||||
settleResolution, ok := resolution.(*HtlcSettleResolution)
|
// Otherwise we expect no error and a settle res for the htlc.
|
||||||
if !ok {
|
settleResolution, ok := res.(*HtlcSettleResolution)
|
||||||
t.Fatalf("expected settle resolution, got: %T",
|
assert.True(t, ok)
|
||||||
resolution)
|
assert.Equal(t, settleResolution.Preimage, pimg)
|
||||||
}
|
}
|
||||||
if settleResolution.Preimage != preimage {
|
checkSubscription := func() {
|
||||||
t.Fatalf("expected settle with matching preimage")
|
// We expect a new invoice notification to be sent out.
|
||||||
|
newInvoice := <-allSubscriptions.NewInvoices
|
||||||
|
assert.Equal(t, newInvoice.State, channeldb.ContractOpen)
|
||||||
|
|
||||||
|
// We expect a settled notification to be sent out.
|
||||||
|
settledInvoice := <-allSubscriptions.SettledInvoices
|
||||||
|
assert.Equal(t, settledInvoice.State, channeldb.ContractSettled)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We expect a new invoice notification to be sent out.
|
checkResolution(resolution, preimage)
|
||||||
newInvoice := <-allSubscriptions.NewInvoices
|
checkSubscription()
|
||||||
if newInvoice.State != channeldb.ContractOpen {
|
|
||||||
t.Fatalf("expected state ContractOpen, but got %v",
|
// Replay the same keysend payment. We expect an identical resolution,
|
||||||
newInvoice.State)
|
// but no event should be generated.
|
||||||
|
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||||
|
hash, amt, expiry,
|
||||||
|
testCurrentHeight, getCircuitKey(10), hodlChan, keySendPayload,
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
checkResolution(resolution, preimage)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-allSubscriptions.NewInvoices:
|
||||||
|
t.Fatalf("replayed keysend should not generate event")
|
||||||
|
case <-time.After(time.Second):
|
||||||
}
|
}
|
||||||
|
|
||||||
// We expect a settled notification to be sent out.
|
// Finally, test that we can properly fulfill a second keysend payment
|
||||||
settledInvoice := <-allSubscriptions.SettledInvoices
|
// with a unique preiamge.
|
||||||
if settledInvoice.State != channeldb.ContractSettled {
|
preimage2 := lntypes.Preimage{1, 2, 3, 4}
|
||||||
t.Fatalf("expected state ContractOpen, but got %v",
|
hash2 := preimage2.Hash()
|
||||||
settledInvoice.State)
|
|
||||||
|
keySendPayload2 := &mockPayload{
|
||||||
|
customRecords: map[uint64][]byte{
|
||||||
|
record.KeySendType: preimage2[:],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||||
|
hash2, amt, expiry,
|
||||||
|
testCurrentHeight, getCircuitKey(20), hodlChan, keySendPayload2,
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
checkResolution(resolution, preimage2)
|
||||||
|
checkSubscription()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMppPayment tests settling of an invoice with multiple partial payments.
|
// TestMppPayment tests settling of an invoice with multiple partial payments.
|
||||||
|
Loading…
Reference in New Issue
Block a user