From 4156101de5781119b2e004f4990f623c3c1ed877 Mon Sep 17 00:00:00 2001 From: Jesse de Wit Date: Thu, 31 Aug 2023 13:04:15 +0200 Subject: [PATCH] lsps2: forwarding logic tests --- lsps2/intercept_test.go | 727 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 727 insertions(+) create mode 100644 lsps2/intercept_test.go diff --git a/lsps2/intercept_test.go b/lsps2/intercept_test.go new file mode 100644 index 00000000..6a3b1b7b --- /dev/null +++ b/lsps2/intercept_test.go @@ -0,0 +1,727 @@ +package lsps2 + +import ( + "context" + "crypto/sha256" + "encoding/binary" + "fmt" + "strconv" + "sync" + "testing" + "time" + + "github.com/breez/lspd/basetypes" + "github.com/breez/lspd/chain" + "github.com/breez/lspd/lightning" + "github.com/breez/lspd/shared" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/stretchr/testify/assert" +) + +var defaultScid uint64 = 123 +var defaultPaymentSizeMsat uint64 = 1_000_000 +var defaultMinViableAmount uint64 = defaultOpeningFeeParams().MinFeeMsat + defaultConfig().HtlcMinimumMsat +var defaultFee, _ = computeOpeningFee( + defaultPaymentSizeMsat, + defaultOpeningFeeParams().Proportional, + defaultOpeningFeeParams().MinFeeMsat, +) +var defaultChainHash = chainhash.Hash([32]byte{}) +var defaultOutPoint = wire.NewOutPoint(&defaultChainHash, 0) +var defaultChannelScid uint64 = 456 +var defaultChanResult = &lightning.GetChannelResult{ + HtlcMinimumMsat: defaultConfig().HtlcMinimumMsat, + InitialChannelID: basetypes.ShortChannelID(defaultChannelScid), + ConfirmedChannelID: basetypes.ShortChannelID(defaultChannelScid), +} + +func defaultOpeningFeeParams() shared.OpeningFeeParams { + return shared.OpeningFeeParams{ + MinFeeMsat: 1000, + Proportional: 1000, + ValidUntil: time.Now().UTC().Add(5 * time.Hour).Format(basetypes.TIME_FORMAT), + MinLifetime: 1000, + MaxClientToSelfDelay: 2016, + Promise: "fake", + } +} +func defaultStore() *mockLsps2Store { + return &mockLsps2Store{ + registrations: map[uint64]*BuyRegistration{ + defaultScid: { + PeerId: "peer", + Scid: basetypes.ShortChannelID(defaultScid), + Mode: OpeningMode_NoMppVarInvoice, + OpeningFeeParams: defaultOpeningFeeParams(), + }, + }, + } +} + +func mppStore() *mockLsps2Store { + s := defaultStore() + for _, r := range s.registrations { + r.Mode = OpeningMode_MppFixedInvoice + r.PaymentSizeMsat = &defaultPaymentSizeMsat + } + return s +} + +func defaultClient() *mockLightningClient { + return &mockLightningClient{ + openResponses: []*wire.OutPoint{ + defaultOutPoint, + }, + getChanResponses: []*lightning.GetChannelResult{ + defaultChanResult, + }, + } +} + +func defaultFeeEstimator() *mockFeeEstimator { + return nil +} + +func defaultConfig() *InterceptorConfig { + var minConfs uint32 = 1 + return &InterceptorConfig{ + AdditionalChannelCapacitySat: 100_000, + MinConfs: &minConfs, + TargetConf: 6, + FeeStrategy: chain.FeeStrategyEconomy, + MinPaymentSizeMsat: 1_000, + MaxPaymentSizeMsat: 4_000_000_000, + TimeLockDelta: 144, + HtlcMinimumMsat: 100, + } +} + +type interceptP struct { + store *mockLsps2Store + client *mockLightningClient + feeEstimator *mockFeeEstimator + config *InterceptorConfig +} + +func setupInterceptor( + ctx context.Context, + p *interceptP, +) *Interceptor { + var store *mockLsps2Store + if p != nil && p.store != nil { + store = p.store + } else { + store = defaultStore() + } + + var client *mockLightningClient + if p != nil && p.client != nil { + client = p.client + } else { + client = defaultClient() + } + + var f *mockFeeEstimator + if p != nil && p.feeEstimator != nil { + f = p.feeEstimator + } else { + f = defaultFeeEstimator() + } + + var config *InterceptorConfig + if p != nil && p.config != nil { + config = p.config + } else { + config = defaultConfig() + } + + i := NewInterceptor(store, client, f, config) + go i.Start(ctx) + return i +} + +type part struct { + id string + scid uint64 + ph []byte + amt uint64 + cltvDelta uint32 +} + +func createPart(p *part) *InterceptRequest { + id := "first" + if p != nil && p.id != "" { + id = p.id + } + + scid := basetypes.ShortChannelID(defaultScid) + if p != nil && p.scid != 0 { + scid = basetypes.ShortChannelID(p.scid) + } + + ph := []byte("fake payment hash") + if p != nil && p.ph != nil && len(p.ph) > 0 { + ph = p.ph + } + + var amt uint64 = 1_000_000 + if p != nil && p.amt != 0 { + amt = p.amt + } + + var cltv uint32 = 146 + if p != nil && p.cltvDelta != 0 { + cltv = p.cltvDelta + } + + return &InterceptRequest{ + Identifier: id, + Scid: scid, + PaymentHash: ph, + IncomingAmountMsat: amt, + OutgoingAmountMsat: amt, + IncomingExpiry: 100 + cltv, + OutgoingExpiry: 100, + } +} + +func runIntercept(i *Interceptor, req *InterceptRequest, res *InterceptResult, wg *sync.WaitGroup) { + go func() { + *res = *i.Intercept(req) + wg.Done() + }() +} + +func assertEmpty(t *testing.T, i *Interceptor) { + assert.Empty(t, i.inflightPayments) + assert.Empty(t, i.newPart) + assert.Empty(t, i.paymentChanOpened) + assert.Empty(t, i.paymentFailure) + assert.Empty(t, i.paymentReady) + assert.Empty(t, i.paymentTimeout) +} + +// Asserts that a part that is not associated with a bought channel is not +// handled by the interceptor. This allows the legacy interceptor to pick up +// from there. +func Test_NotBought_SinglePart(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + i := setupInterceptor(ctx, nil) + res := i.Intercept(createPart(&part{scid: 999})) + assert.Equal(t, INTERCEPT_CANNOT_HANDLE, res.Action) + assertEmpty(t, i) +} + +func Test_NotBought_TwoParts(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + i := setupInterceptor(ctx, nil) + + var wg sync.WaitGroup + wg.Add(2) + var res1 InterceptResult + runIntercept(i, createPart(&part{id: "first", scid: 999}), &res1, &wg) + + var res2 InterceptResult + runIntercept(i, createPart(&part{id: "second", scid: 999}), &res2, &wg) + wg.Wait() + assert.Equal(t, INTERCEPT_CANNOT_HANDLE, res1.Action) + assert.Equal(t, INTERCEPT_CANNOT_HANDLE, res2.Action) + assertEmpty(t, i) +} + +// Asserts that a no-MPP+var-invoice mode payment works in the happy flow. +func Test_NoMpp_Happyflow(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + i := setupInterceptor(ctx, nil) + + res := i.Intercept(createPart(nil)) + assert.Equal(t, INTERCEPT_RESUME_WITH_ONION, res.Action) + assert.Equal(t, defaultPaymentSizeMsat-defaultFee, res.AmountMsat) + assert.Equal(t, defaultFee, *res.FeeMsat) + assert.Equal(t, defaultChannelScid, uint64(res.Scid)) + assertEmpty(t, i) +} + +// Asserts that a no-MPP+var-invoice mode payment works with the exact minimum +// amount. +func Test_NoMpp_AmountMinFeePlusHtlcMinPlusOne(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + i := setupInterceptor(ctx, nil) + + res := i.Intercept(createPart(&part{amt: defaultMinViableAmount})) + assert.Equal(t, INTERCEPT_RESUME_WITH_ONION, res.Action) + assert.Equal(t, defaultConfig().HtlcMinimumMsat, res.AmountMsat) + assert.Equal(t, defaultOpeningFeeParams().MinFeeMsat, *res.FeeMsat) + assert.Equal(t, defaultChannelScid, uint64(res.Scid)) + assertEmpty(t, i) +} + +// Asserts that a no-MPP+var-invoice mode payment fails with the exact minimum +// amount minus one. +func Test_NoMpp_AmtBelowMinimum(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + i := setupInterceptor(ctx, nil) + + res := i.Intercept(createPart(&part{amt: defaultMinViableAmount - 1})) + assert.Equal(t, INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action) + assert.Equal(t, FAILURE_UNKNOWN_NEXT_PEER, res.FailureCode) + assertEmpty(t, i) +} + +// Asserts that a no-MPP+var-invoice mode payment succeeds with the exact +// maximum amount. +func Test_NoMpp_AmtAtMaximum(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + i := setupInterceptor(ctx, nil) + + res := i.Intercept(createPart(&part{amt: defaultConfig().MaxPaymentSizeMsat})) + assert.Equal(t, INTERCEPT_RESUME_WITH_ONION, res.Action) + assertEmpty(t, i) +} + +// Asserts that a no-MPP+var-invoice mode payment fails with the exact +// maximum amount plus one. +func Test_NoMpp_AmtAboveMaximum(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + i := setupInterceptor(ctx, nil) + + res := i.Intercept(createPart(&part{amt: defaultConfig().MaxPaymentSizeMsat + 1})) + assert.Equal(t, INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action) + assert.Equal(t, FAILURE_UNKNOWN_NEXT_PEER, res.FailureCode) + assertEmpty(t, i) +} + +// Asserts that a no-MPP+var-invoice mode payment fails when the cltv delta is +// less than cltv delta + 2. +func Test_NoMpp_CltvDeltaBelowMinimum(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + i := setupInterceptor(ctx, nil) + + res := i.Intercept(createPart(&part{cltvDelta: 145})) + assert.Equal(t, INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action) + assert.Equal(t, FAILURE_INCORRECT_CLTV_EXPIRY, res.FailureCode) + assertEmpty(t, i) +} + +// Asserts that a no-MPP+var-invoice mode payment succeeds when the cltv delta +// is higher than expected. +func Test_NoMpp_HigherCltvDelta(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + i := setupInterceptor(ctx, nil) + + res := i.Intercept(createPart(&part{cltvDelta: 1000})) + assert.Equal(t, INTERCEPT_RESUME_WITH_ONION, res.Action) + assertEmpty(t, i) +} + +// Asserts that a no-MPP+var-invoice mode payment fails if the opening params +// have expired. +func Test_NoMpp_ParamsExpired(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + store := defaultStore() + store.registrations[defaultScid].OpeningFeeParams.ValidUntil = time.Now(). + UTC().Add(-time.Nanosecond).Format(basetypes.TIME_FORMAT) + i := setupInterceptor(ctx, &interceptP{store: store}) + + res := i.Intercept(createPart(nil)) + assert.Equal(t, INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action) + assert.Equal(t, FAILURE_UNKNOWN_NEXT_PEER, res.FailureCode) + assertEmpty(t, i) +} + +func Test_NoMpp_ChannelAlreadyOpened_NotComplete_Forwards(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + store := defaultStore() + store.registrations[defaultScid].ChannelPoint = defaultOutPoint + i := setupInterceptor(ctx, &interceptP{store: store}) + + res := i.Intercept(createPart(nil)) + assert.Equal(t, INTERCEPT_RESUME_WITH_ONION, res.Action) + assertEmpty(t, i) +} + +func Test_NoMpp_ChannelAlreadyOpened_Complete_Fails(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + store := defaultStore() + store.registrations[defaultScid].ChannelPoint = defaultOutPoint + store.registrations[defaultScid].IsComplete = true + i := setupInterceptor(ctx, &interceptP{store: store}) + + res := i.Intercept(createPart(nil)) + assert.Equal(t, INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action) + assert.Equal(t, FAILURE_UNKNOWN_NEXT_PEER, res.FailureCode) + assertEmpty(t, i) +} + +// Asserts that a MPP+fixed-invoice mode payment succeeds in the happy flow +// case. +func Test_Mpp_SinglePart_Happyflow(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + i := setupInterceptor(ctx, &interceptP{store: mppStore()}) + + res := i.Intercept(createPart(&part{amt: defaultPaymentSizeMsat})) + assert.Equal(t, INTERCEPT_RESUME_WITH_ONION, res.Action) + assert.Equal(t, defaultPaymentSizeMsat-defaultFee, res.AmountMsat) + assert.Equal(t, defaultFee, *res.FeeMsat) + assert.Equal(t, defaultChannelScid, uint64(res.Scid)) + assertEmpty(t, i) +} + +// Asserts that a MPP+fixed-invoice mode payment times out when it receives only +// a single part below payment_size_msat. +func Test_Mpp_SinglePart_AmtTooSmall(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + config := defaultConfig() + config.MppTimeout = time.Millisecond * 500 + i := setupInterceptor(ctx, &interceptP{store: mppStore(), config: config}) + + start := time.Now() + res := i.Intercept(createPart(&part{amt: defaultPaymentSizeMsat - 1})) + end := time.Now() + assert.Equal(t, INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action) + assert.Equal(t, FAILURE_TEMPORARY_CHANNEL_FAILURE, res.FailureCode) + assert.GreaterOrEqual(t, end.Sub(start).Milliseconds(), config.MppTimeout.Milliseconds()) + assertEmpty(t, i) +} + +// Asserts that a MPP+fixed-invoice mode payment finalizes after it receives the +// second part that finalizes the payment. +func Test_Mpp_TwoParts_FinalizedOnSecond(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + config := defaultConfig() + config.MppTimeout = time.Millisecond * 500 + i := setupInterceptor(ctx, &interceptP{store: mppStore(), config: config}) + + var wg sync.WaitGroup + wg.Add(2) + var res1 *InterceptResult + var res2 *InterceptResult + var t1 time.Time + var t2 time.Time + start := time.Now() + go func() { + res1 = i.Intercept(createPart(&part{ + id: "first", + amt: defaultPaymentSizeMsat - defaultConfig().HtlcMinimumMsat, + })) + t1 = time.Now() + wg.Done() + }() + + <-time.After(time.Millisecond * 250) + + go func() { + res2 = i.Intercept(createPart(&part{ + id: "second", + amt: defaultConfig().HtlcMinimumMsat, + })) + t2 = time.Now() + wg.Done() + }() + + wg.Wait() + assert.Equal(t, INTERCEPT_RESUME_WITH_ONION, res1.Action) + assert.Equal(t, defaultPaymentSizeMsat-defaultConfig().HtlcMinimumMsat-defaultFee, res1.AmountMsat) + assert.Equal(t, defaultFee, *res1.FeeMsat) + + assert.Equal(t, INTERCEPT_RESUME_WITH_ONION, res2.Action) + assert.Equal(t, defaultConfig().HtlcMinimumMsat, res2.AmountMsat) + assert.Nil(t, res2.FeeMsat) + + assert.LessOrEqual(t, int64(250), t1.Sub(start).Milliseconds()) + assert.LessOrEqual(t, int64(250), t2.Sub(start).Milliseconds()) + assertEmpty(t, i) +} + +// Asserts that a MPP+fixed-invoice mode payment with the following parts +// 1) payment size - htlc minimum +// 2) htlc minimum - 1 +// 3) htlc minimum +// still succeeds. The second part is dropped, but the third part completes the +// payment. +func Test_Mpp_BadSecondPart_ThirdPartCompletes(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + config := defaultConfig() + config.MppTimeout = time.Millisecond * 500 + i := setupInterceptor(ctx, &interceptP{store: mppStore(), config: config}) + + var wg sync.WaitGroup + wg.Add(2) + var res1 *InterceptResult + var res2 *InterceptResult + var res3 *InterceptResult + var t1 time.Time + var t2 time.Time + var t3 time.Time + start := time.Now() + go func() { + res1 = i.Intercept(createPart(&part{ + id: "first", + amt: defaultPaymentSizeMsat - defaultConfig().HtlcMinimumMsat, + })) + t1 = time.Now() + wg.Done() + }() + + <-time.After(time.Millisecond * 100) + res2 = i.Intercept(createPart(&part{ + id: "second", + amt: defaultConfig().HtlcMinimumMsat - 1, + })) + t2 = time.Now() + + <-time.After(time.Millisecond * 100) + go func() { + res3 = i.Intercept(createPart(&part{ + id: "third", + amt: defaultConfig().HtlcMinimumMsat, + })) + t3 = time.Now() + wg.Done() + }() + + wg.Wait() + assert.Equal(t, INTERCEPT_RESUME_WITH_ONION, res1.Action) + assert.Equal(t, defaultPaymentSizeMsat-defaultConfig().HtlcMinimumMsat-defaultFee, res1.AmountMsat) + assert.Equal(t, defaultFee, *res1.FeeMsat) + + assert.Equal(t, INTERCEPT_FAIL_HTLC_WITH_CODE, res2.Action) + assert.Equal(t, FAILURE_AMOUNT_BELOW_MINIMUM, res2.FailureCode) + + assert.Equal(t, INTERCEPT_RESUME_WITH_ONION, res3.Action) + assert.Equal(t, defaultConfig().HtlcMinimumMsat, res3.AmountMsat) + assert.Nil(t, res3.FeeMsat) + + assert.LessOrEqual(t, int64(200), t1.Sub(start).Milliseconds()) + assert.Greater(t, int64(200), t2.Sub(start).Milliseconds()) + assert.LessOrEqual(t, int64(200), t3.Sub(start).Milliseconds()) + + assertEmpty(t, i) +} + +// Asserts that a MPP+fixed-invoice mode payment fails when the cltv delta is +// less than cltv delta + 2. +func Test_Mpp_CltvDeltaBelowMinimum(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + i := setupInterceptor(ctx, &interceptP{store: mppStore()}) + + res := i.Intercept(createPart(&part{cltvDelta: 145})) + assert.Equal(t, INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action) + assert.Equal(t, FAILURE_INCORRECT_CLTV_EXPIRY, res.FailureCode) + assertEmpty(t, i) +} + +// Asserts that a MPP+fixed-invoice mode payment succeeds when the cltv delta +// is higher than expected. +func Test_Mpp_HigherCltvDelta(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + i := setupInterceptor(ctx, &interceptP{store: mppStore()}) + + res := i.Intercept(createPart(&part{cltvDelta: 1000})) + assert.Equal(t, INTERCEPT_RESUME_WITH_ONION, res.Action) + assertEmpty(t, i) +} + +// Asserts that a MPP+fixed-invoice mode payment fails if the opening params +// have expired. +func Test_Mpp_ParamsExpired(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + store := mppStore() + store.registrations[defaultScid].OpeningFeeParams.ValidUntil = time.Now(). + UTC().Add(-time.Nanosecond).Format(basetypes.TIME_FORMAT) + i := setupInterceptor(ctx, &interceptP{store: store}) + + res := i.Intercept(createPart(nil)) + assert.Equal(t, INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action) + assert.Equal(t, FAILURE_UNKNOWN_NEXT_PEER, res.FailureCode) + assertEmpty(t, i) +} + +// Asserts that a MPP+fixed-invoice mode payment fails if the opening params +// expire while the part is in-flight. +func Test_Mpp_ParamsExpireInFlight(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + store := mppStore() + i := setupInterceptor(ctx, &interceptP{store: store}) + + start := time.Now() + store.registrations[defaultScid].OpeningFeeParams.ValidUntil = start. + UTC().Add(time.Millisecond * 250).Format(basetypes.TIME_FORMAT) + res := i.Intercept(createPart(&part{amt: defaultPaymentSizeMsat - 1})) + end := time.Now() + assert.Equal(t, INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action) + assert.Equal(t, FAILURE_UNKNOWN_NEXT_PEER, res.FailureCode) + + // Using 249 here instead of 250 to avoid flakiness in the test. It + // sometimes ccompletes in 249 milliseconds. + assert.LessOrEqual(t, int64(249), end.Sub(start).Milliseconds()) + assertEmpty(t, i) +} + +// Asserts that a MPP+fixed-invoice mode replacement of a part ignores that +// part, and the replacement is used for completing the payment +func Test_Mpp_PartReplacement(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + i := setupInterceptor(ctx, &interceptP{store: mppStore()}) + + var wg sync.WaitGroup + wg.Add(3) + var res1 *InterceptResult + var res2 *InterceptResult + var res3 *InterceptResult + var t1 time.Time + var t2 time.Time + var t3 time.Time + start := time.Now() + go func() { + res1 = i.Intercept(createPart(&part{ + id: "first", + amt: defaultPaymentSizeMsat - defaultConfig().HtlcMinimumMsat, + })) + t1 = time.Now() + wg.Done() + }() + + <-time.After(time.Millisecond * 100) + go func() { + res2 = i.Intercept(createPart(&part{ + id: "first", + amt: defaultPaymentSizeMsat - defaultConfig().HtlcMinimumMsat, + })) + t2 = time.Now() + wg.Done() + }() + + <-time.After(time.Millisecond * 100) + go func() { + res3 = i.Intercept(createPart(&part{ + id: "second", + amt: defaultConfig().HtlcMinimumMsat, + })) + t3 = time.Now() + wg.Done() + }() + + wg.Wait() + assert.Equal(t, INTERCEPT_IGNORE, res1.Action) + + assert.Equal(t, INTERCEPT_RESUME_WITH_ONION, res2.Action) + assert.Equal(t, defaultPaymentSizeMsat-defaultConfig().HtlcMinimumMsat-defaultFee, res2.AmountMsat) + assert.Equal(t, defaultFee, *res2.FeeMsat) + + assert.Equal(t, INTERCEPT_RESUME_WITH_ONION, res3.Action) + assert.Equal(t, defaultConfig().HtlcMinimumMsat, res3.AmountMsat) + assert.Nil(t, res3.FeeMsat) + + assert.LessOrEqual(t, int64(100), t1.Sub(start).Milliseconds()) + assert.LessOrEqual(t, int64(200), t2.Sub(start).Milliseconds()) + assert.LessOrEqual(t, int64(200), t3.Sub(start).Milliseconds()) + + assertEmpty(t, i) +} + +func Test_Mpp_ChannelAlreadyOpened_NotComplete_Forwards(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + store := mppStore() + store.registrations[defaultScid].ChannelPoint = defaultOutPoint + i := setupInterceptor(ctx, &interceptP{store: store}) + + res := i.Intercept(createPart(nil)) + assert.Equal(t, INTERCEPT_RESUME_WITH_ONION, res.Action) + assertEmpty(t, i) +} + +func Test_Mpp_ChannelAlreadyOpened_Complete_Fails(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + store := mppStore() + store.registrations[defaultScid].ChannelPoint = defaultOutPoint + store.registrations[defaultScid].IsComplete = true + i := setupInterceptor(ctx, &interceptP{store: store}) + + res := i.Intercept(createPart(nil)) + assert.Equal(t, INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action) + assert.Equal(t, FAILURE_UNKNOWN_NEXT_PEER, res.FailureCode) + assertEmpty(t, i) +} + +func Test_Mpp_Performance(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + paymentCount := 100 + partCount := 10 + store := &mockLsps2Store{ + delay: time.Millisecond * 500, + registrations: make(map[uint64]*BuyRegistration), + } + + client := &mockLightningClient{} + for paymentNo := 0; paymentNo < paymentCount; paymentNo++ { + scid := uint64(paymentNo + 1_000_000) + client.getChanResponses = append(client.getChanResponses, defaultChanResult) + client.openResponses = append(client.openResponses, defaultOutPoint) + store.registrations[scid] = &BuyRegistration{ + PeerId: strconv.FormatUint(scid, 10), + Scid: basetypes.ShortChannelID(scid), + Mode: OpeningMode_MppFixedInvoice, + OpeningFeeParams: defaultOpeningFeeParams(), + PaymentSizeMsat: &defaultPaymentSizeMsat, + } + } + i := setupInterceptor(ctx, &interceptP{store: store, client: client}) + var wg sync.WaitGroup + wg.Add(partCount * paymentCount) + start := time.Now() + for paymentNo := 0; paymentNo < paymentCount; paymentNo++ { + for partNo := 0; partNo < partCount; partNo++ { + scid := paymentNo + 1_000_000 + id := fmt.Sprintf("%d|%d", paymentNo, partNo) + var a [8]byte + binary.BigEndian.PutUint64(a[:], uint64(scid)) + ph := sha256.Sum256(a[:]) + + go func() { + res := i.Intercept(createPart(&part{ + scid: uint64(scid), + id: id, + ph: ph[:], + amt: defaultPaymentSizeMsat / uint64(partCount), + })) + + assert.Equal(t, INTERCEPT_RESUME_WITH_ONION, res.Action) + wg.Done() + }() + } + } + wg.Wait() + end := time.Now() + + assert.LessOrEqual(t, end.Sub(start).Milliseconds(), int64(1000)) + assertEmpty(t, i) +}