diff --git a/invoices/invoiceregistry.go b/invoices/invoiceregistry.go index 633284668a..5ca7264e6e 100644 --- a/invoices/invoiceregistry.go +++ b/invoices/invoiceregistry.go @@ -1,6 +1,7 @@ package invoices import ( + "crypto/rand" "errors" "fmt" "sync" @@ -676,6 +677,16 @@ func (i *InvoiceRegistry) processKeySend(ctx invoiceUpdateCtx) error { return errors.New("final expiry too soon") } + // Generate a random payment address for this invoice. The invoice + // databse is indexed by payment address, so we must ensure that the + // generated keysend invoices also have a unique identifier, otherwise + // insertion will fail on the second attempt to insert a keysend invoice + // or if this context is being replayed after a restart. + var payAddr [32]byte + if _, err := rand.Read(payAddr[:]); err != nil { + return err + } + // Create placeholder invoice. invoice := &channeldb.Invoice{ CreationDate: i.cfg.Clock.Now(), @@ -683,6 +694,7 @@ func (i *InvoiceRegistry) processKeySend(ctx invoiceUpdateCtx) error { FinalCltvDelta: finalCltvDelta, Value: amt, PaymentPreimage: &preimage, + PaymentAddr: payAddr, Features: features, }, } diff --git a/invoices/invoiceregistry_test.go b/invoices/invoiceregistry_test.go index fa08d5db94..a9131f8a39 100644 --- a/invoices/invoiceregistry_test.go +++ b/invoices/invoiceregistry_test.go @@ -725,29 +725,59 @@ func testKeySend(t *testing.T, keySendEnabled bool) { return } - // Otherwise we expect no error and a settle resolution for the htlc. - settleResolution, ok := resolution.(*HtlcSettleResolution) - if !ok { - t.Fatalf("expected settle resolution, got: %T", - resolution) + checkResolution := func(res HtlcResolution, pimg lntypes.Preimage) { + // Otherwise we expect no error and a settle res for the htlc. + settleResolution, ok := res.(*HtlcSettleResolution) + assert.True(t, ok) + assert.Equal(t, settleResolution.Preimage, pimg) } - if settleResolution.Preimage != preimage { - t.Fatalf("expected settle with matching preimage") + checkSubscription := func() { + // 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. - newInvoice := <-allSubscriptions.NewInvoices - if newInvoice.State != channeldb.ContractOpen { - t.Fatalf("expected state ContractOpen, but got %v", - newInvoice.State) + checkResolution(resolution, preimage) + checkSubscription() + + // Replay the same keysend payment. We expect an identical resolution, + // 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. - settledInvoice := <-allSubscriptions.SettledInvoices - if settledInvoice.State != channeldb.ContractSettled { - t.Fatalf("expected state ContractOpen, but got %v", - settledInvoice.State) + // Finally, test that we can properly fulfill a second keysend payment + // with a unique preiamge. + preimage2 := lntypes.Preimage{1, 2, 3, 4} + hash2 := preimage2.Hash() + + 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.