lnd/invoices/test_utils_test.go
Elle Mouton b0d3e4dc0d
multi: extract path ID and total amt from received payment
We've covered all the logic for building a blinded path to ourselves and
putting that into an invoice - so now we start preparing to actually be
able to recognise the incoming payment as one from a blinded path we
created.

The incoming update_add_htlc will have an `encrypted_recipient_data`
blob for us that we would have put in the original invoice. From this we
extract the PathID which we wrote. We consider this the payment address
and we use this to derive the associated invoice location.

Blinded path payments will not include MPP records, so the payment
address and total payment amount must be gleaned from the pathID and new
totalAmtMsat onion field respectively.

This commit only covers the final hop payload of a hop in a blinded
path. Dummy hops will be handled in the following commit.
2024-07-26 09:54:18 +02:00

405 lines
9.8 KiB
Go

package invoices_test
import (
"crypto/rand"
"encoding/binary"
"encoding/hex"
"fmt"
"os"
"runtime/pprof"
"sync"
"testing"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/clock"
invpkg "github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/zpay32"
"github.com/stretchr/testify/require"
)
type mockPayload struct {
mpp *record.MPP
amp *record.AMP
customRecords record.CustomSet
metadata []byte
pathID *chainhash.Hash
totalAmtMsat lnwire.MilliSatoshi
}
func (p *mockPayload) MultiPath() *record.MPP {
return p.mpp
}
func (p *mockPayload) AMPRecord() *record.AMP {
return p.amp
}
func (p *mockPayload) PathID() *chainhash.Hash {
return p.pathID
}
func (p *mockPayload) TotalAmtMsat() lnwire.MilliSatoshi {
return p.totalAmtMsat
}
func (p *mockPayload) CustomRecords() record.CustomSet {
// This function should always return a map instance, but for mock
// configuration we do accept nil.
if p.customRecords == nil {
return make(record.CustomSet)
}
return p.customRecords
}
func (p *mockPayload) Metadata() []byte {
return p.metadata
}
type mockChainNotifier struct {
chainntnfs.ChainNotifier
blockChan chan *chainntnfs.BlockEpoch
}
func newMockNotifier() *mockChainNotifier {
return &mockChainNotifier{
blockChan: make(chan *chainntnfs.BlockEpoch),
}
}
// RegisterBlockEpochNtfn mocks a block epoch notification, using the mock's
// block channel to deliver blocks to the client.
func (m *mockChainNotifier) RegisterBlockEpochNtfn(*chainntnfs.BlockEpoch) (
*chainntnfs.BlockEpochEvent, error) {
return &chainntnfs.BlockEpochEvent{
Epochs: m.blockChan,
Cancel: func() {},
}, nil
}
const (
testHtlcExpiry = uint32(5)
testInvoiceCltvDelta = uint32(4)
testFinalCltvRejectDelta = int32(4)
testCurrentHeight = int32(1)
)
var (
testTimeout = 5 * time.Second
testTime = time.Date(2018, time.February, 2, 14, 0, 0, 0, time.UTC)
testInvoicePreimage = lntypes.Preimage{1}
testInvoicePaymentHash = testInvoicePreimage.Hash()
testPrivKeyBytes, _ = hex.DecodeString(
"e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2d" +
"b734",
)
testPrivKey, _ = btcec.PrivKeyFromBytes(testPrivKeyBytes)
testInvoiceDescription = "coffee"
testInvoiceAmount = lnwire.MilliSatoshi(100000)
testNetParams = &chaincfg.MainNetParams
testMessageSigner = zpay32.MessageSigner{
SignCompact: func(msg []byte) ([]byte, error) {
hash := chainhash.HashB(msg)
sig, err := ecdsa.SignCompact(testPrivKey, hash, true)
if err != nil {
return nil, fmt.Errorf("can't sign the "+
"message: %v", err)
}
return sig, nil
},
}
testFeatures = lnwire.NewFeatureVector(
nil, lnwire.Features,
)
testPayload = &mockPayload{}
testInvoiceCreationDate = testTime
)
type testContext struct {
idb invpkg.InvoiceDB
registry *invpkg.InvoiceRegistry
notifier *mockChainNotifier
clock *clock.TestClock
t *testing.T
}
func defaultRegistryConfig() invpkg.RegistryConfig {
return invpkg.RegistryConfig{
FinalCltvRejectDelta: testFinalCltvRejectDelta,
HtlcHoldDuration: 30 * time.Second,
}
}
func newTestContext(t *testing.T,
registryCfg *invpkg.RegistryConfig,
makeDB func(t *testing.T) (invpkg.InvoiceDB,
*clock.TestClock)) *testContext {
t.Helper()
idb, clock := makeDB(t)
notifier := newMockNotifier()
expiryWatcher := invpkg.NewInvoiceExpiryWatcher(
clock, 0, uint32(testCurrentHeight), nil, notifier,
)
cfg := defaultRegistryConfig()
if registryCfg != nil {
cfg = *registryCfg
}
cfg.Clock = clock
// Instantiate and start the invoice ctx.registry.
registry := invpkg.NewRegistry(idb, expiryWatcher, &cfg)
require.NoError(t, registry.Start())
t.Cleanup(func() {
require.NoError(t, registry.Stop())
})
ctx := testContext{
idb: idb,
registry: registry,
notifier: notifier,
clock: clock,
t: t,
}
return &ctx
}
func getCircuitKey(htlcID uint64) invpkg.CircuitKey {
return invpkg.CircuitKey{
ChanID: lnwire.ShortChannelID{
BlockHeight: 1, TxIndex: 2, TxPosition: 3,
},
HtlcID: htlcID,
}
}
// newInvoice returns an invoice that can be used for testing, using the
// constant values defined above (deep copied if necessary).
//
// Note that this invoice *does not* have a payment address set. It will
// create a regular invoice with a preimage is hodl is false, and a hodl
// invoice with no preimage otherwise.
func newInvoice(t *testing.T, hodl bool) *invpkg.Invoice {
invoice := &invpkg.Invoice{
Terms: invpkg.ContractTerm{
Value: testInvoiceAmount,
Expiry: time.Hour,
Features: testFeatures.Clone(),
},
CreationDate: testInvoiceCreationDate,
}
// If creating a hodl invoice, we don't include a preimage.
if hodl {
invoice.HodlInvoice = true
return invoice
}
preimage, err := lntypes.MakePreimage(
testInvoicePreimage[:],
)
require.NoError(t, err)
invoice.Terms.PaymentPreimage = &preimage
return invoice
}
// timeout implements a test level timeout.
func timeout() func() {
done := make(chan struct{})
go func() {
select {
case <-time.After(10 * time.Second):
err := pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
if err != nil {
panic(fmt.Sprintf("error writing to std out "+
"after timeout: %v", err))
}
panic("timeout")
case <-done:
}
}()
return func() {
close(done)
}
}
// invoiceExpiryTestData simply holds generated expired and pending invoices.
type invoiceExpiryTestData struct {
expiredInvoices map[lntypes.Hash]*invpkg.Invoice
pendingInvoices map[lntypes.Hash]*invpkg.Invoice
}
// generateInvoiceExpiryTestData generates the specified number of fake expired
// and pending invoices anchored to the passed now timestamp.
func generateInvoiceExpiryTestData(
t *testing.T, now time.Time,
offset, numExpired, numPending int) invoiceExpiryTestData {
var testData invoiceExpiryTestData
testData.expiredInvoices = make(map[lntypes.Hash]*invpkg.Invoice)
testData.pendingInvoices = make(map[lntypes.Hash]*invpkg.Invoice)
expiredCreationDate := now.Add(-24 * time.Hour)
for i := 1; i <= numExpired; i++ {
var preimage lntypes.Preimage
binary.BigEndian.PutUint32(preimage[:4], uint32(offset+i))
expiry := time.Duration((i+offset)%24) * time.Hour
invoice := newInvoiceExpiryTestInvoice(
t, preimage, expiredCreationDate, expiry,
)
testData.expiredInvoices[preimage.Hash()] = invoice
}
for i := 1; i <= numPending; i++ {
var preimage lntypes.Preimage
binary.BigEndian.PutUint32(preimage[4:], uint32(offset+i))
expiry := time.Duration((i+offset)%24) * time.Hour
invoice := newInvoiceExpiryTestInvoice(t, preimage, now, expiry)
testData.pendingInvoices[preimage.Hash()] = invoice
}
return testData
}
// newInvoiceExpiryTestInvoice creates a test invoice with a randomly generated
// payment address and custom preimage and expiry details. It should be used in
// the case where tests require custom invoice expiry and unique payment
// hashes.
func newInvoiceExpiryTestInvoice(t *testing.T, preimage lntypes.Preimage,
timestamp time.Time, expiry time.Duration) *invpkg.Invoice {
if expiry == 0 {
expiry = time.Hour
}
var payAddr [32]byte
if _, err := rand.Read(payAddr[:]); err != nil {
t.Fatalf("unable to generate payment addr: %v", err)
}
rawInvoice, err := zpay32.NewInvoice(
testNetParams,
preimage.Hash(),
timestamp,
zpay32.Amount(testInvoiceAmount),
zpay32.Description(testInvoiceDescription),
zpay32.Expiry(expiry),
zpay32.PaymentAddr(payAddr),
)
require.NoError(t, err, "Error while creating new invoice")
paymentRequest, err := rawInvoice.Encode(testMessageSigner)
require.NoError(t, err, "Error while encoding payment request")
return &invpkg.Invoice{
Terms: invpkg.ContractTerm{
PaymentPreimage: &preimage,
PaymentAddr: payAddr,
Value: testInvoiceAmount,
Expiry: expiry,
Features: testFeatures,
},
PaymentRequest: []byte(paymentRequest),
CreationDate: timestamp,
}
}
// checkSettleResolution asserts the resolution is a settle with the correct
// preimage. If successful, the HtlcSettleResolution is returned in case further
// checks are desired.
func checkSettleResolution(t *testing.T, res invpkg.HtlcResolution,
expPreimage lntypes.Preimage) *invpkg.HtlcSettleResolution {
t.Helper()
settleResolution, ok := res.(*invpkg.HtlcSettleResolution)
require.True(t, ok)
require.Equal(t, expPreimage, settleResolution.Preimage)
return settleResolution
}
// checkFailResolution asserts the resolution is a fail with the correct reason.
// If successful, the HtlcFailResolution is returned in case further checks are
// desired.
func checkFailResolution(t *testing.T, res invpkg.HtlcResolution,
expOutcome invpkg.FailResolutionResult) *invpkg.HtlcFailResolution {
t.Helper()
failResolution, ok := res.(*invpkg.HtlcFailResolution)
require.True(t, ok)
require.Equal(t, expOutcome, failResolution.Outcome)
return failResolution
}
type hodlExpiryTest struct {
hash lntypes.Hash
state invpkg.ContractState
stateLock sync.Mutex
mockNotifier *mockChainNotifier
mockClock *clock.TestClock
cancelChan chan lntypes.Hash
watcher *invpkg.InvoiceExpiryWatcher
}
func (h *hodlExpiryTest) announceBlock(t *testing.T, height uint32) {
t.Helper()
select {
case h.mockNotifier.blockChan <- &chainntnfs.BlockEpoch{
Height: int32(height),
}:
case <-time.After(testTimeout):
t.Fatalf("block %v not consumed", height)
}
}
func (h *hodlExpiryTest) assertCanceled(t *testing.T, expected lntypes.Hash) {
select {
case actual := <-h.cancelChan:
require.Equal(t, expected, actual)
case <-time.After(testTimeout):
t.Fatalf("invoice: %v not canceled", h.hash)
}
}