mirror of
https://github.com/aljazceru/lspd.git
synced 2025-12-19 06:44:23 +01:00
lsps2: main forwarding logic
This commit is contained in:
758
lsps2/intercept_test.go
Normal file
758
lsps2/intercept_test.go
Normal file
@@ -0,0 +1,758 @@
|
||||
package lsps2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/breez/lspd/chain"
|
||||
"github.com/breez/lspd/lightning"
|
||||
"github.com/breez/lspd/lsps0"
|
||||
"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: lightning.ShortChannelID(defaultChannelScid),
|
||||
ConfirmedChannelID: lightning.ShortChannelID(defaultChannelScid),
|
||||
}
|
||||
|
||||
func defaultOpeningFeeParams() shared.OpeningFeeParams {
|
||||
return shared.OpeningFeeParams{
|
||||
MinFeeMsat: 1000,
|
||||
Proportional: 1000,
|
||||
ValidUntil: time.Now().UTC().Add(5 * time.Hour).Format(lsps0.TIME_FORMAT),
|
||||
MinLifetime: 1000,
|
||||
MaxClientToSelfDelay: 2016,
|
||||
Promise: "fake",
|
||||
}
|
||||
}
|
||||
func defaultStore() *mockLsps2Store {
|
||||
return &mockLsps2Store{
|
||||
registrations: map[uint64]*BuyRegistration{
|
||||
defaultScid: {
|
||||
PeerId: "peer",
|
||||
Scid: lightning.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 defaultopeningService() *mockOpeningService {
|
||||
return &mockOpeningService{
|
||||
isCurrentChainFeeCheaper: false,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
openingService *mockOpeningService
|
||||
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()
|
||||
}
|
||||
|
||||
var openingService *mockOpeningService
|
||||
if p != nil && p.openingService != nil {
|
||||
openingService = p.openingService
|
||||
} else {
|
||||
openingService = defaultopeningService()
|
||||
}
|
||||
|
||||
i := NewInterceptHandler(store, openingService, 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) shared.InterceptRequest {
|
||||
id := "first"
|
||||
if p != nil && p.id != "" {
|
||||
id = p.id
|
||||
}
|
||||
|
||||
scid := lightning.ShortChannelID(defaultScid)
|
||||
if p != nil && p.scid != 0 {
|
||||
scid = lightning.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 shared.InterceptRequest{
|
||||
Identifier: id,
|
||||
Scid: scid,
|
||||
PaymentHash: ph,
|
||||
IncomingAmountMsat: amt,
|
||||
OutgoingAmountMsat: amt,
|
||||
IncomingExpiry: 100 + cltv,
|
||||
OutgoingExpiry: 100,
|
||||
}
|
||||
}
|
||||
|
||||
func runIntercept(i *Interceptor, req shared.InterceptRequest, res *shared.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.registrationFetched)
|
||||
assert.Empty(t, i.paymentChanOpened)
|
||||
assert.Empty(t, i.paymentFailure)
|
||||
assert.Empty(t, i.paymentReady)
|
||||
}
|
||||
|
||||
// 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, shared.INTERCEPT_RESUME, 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 shared.InterceptResult
|
||||
runIntercept(i, createPart(&part{id: "first", scid: 999}), &res1, &wg)
|
||||
|
||||
var res2 shared.InterceptResult
|
||||
runIntercept(i, createPart(&part{id: "second", scid: 999}), &res2, &wg)
|
||||
wg.Wait()
|
||||
assert.Equal(t, shared.INTERCEPT_RESUME, res1.Action)
|
||||
assert.Equal(t, shared.INTERCEPT_RESUME, 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, shared.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, shared.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, shared.INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action)
|
||||
assert.Equal(t, shared.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, shared.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, shared.INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action)
|
||||
assert.Equal(t, shared.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, shared.INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action)
|
||||
assert.Equal(t, shared.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, shared.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(lsps0.TIME_FORMAT)
|
||||
i := setupInterceptor(ctx, &interceptP{store: store})
|
||||
|
||||
res := i.Intercept(createPart(nil))
|
||||
assert.Equal(t, shared.INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action)
|
||||
assert.Equal(t, shared.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, shared.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, shared.INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action)
|
||||
assert.Equal(t, shared.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, shared.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, shared.INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action)
|
||||
assert.Equal(t, shared.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 shared.InterceptResult
|
||||
var res2 shared.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, shared.INTERCEPT_RESUME_WITH_ONION, res1.Action)
|
||||
assert.Equal(t, defaultPaymentSizeMsat-defaultConfig().HtlcMinimumMsat-defaultFee, res1.AmountMsat)
|
||||
assert.Equal(t, defaultFee, *res1.FeeMsat)
|
||||
|
||||
assert.Equal(t, shared.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 shared.InterceptResult
|
||||
var res2 shared.InterceptResult
|
||||
var res3 shared.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, shared.INTERCEPT_RESUME_WITH_ONION, res1.Action)
|
||||
assert.Equal(t, defaultPaymentSizeMsat-defaultConfig().HtlcMinimumMsat-defaultFee, res1.AmountMsat)
|
||||
assert.Equal(t, defaultFee, *res1.FeeMsat)
|
||||
|
||||
assert.Equal(t, shared.INTERCEPT_FAIL_HTLC_WITH_CODE, res2.Action)
|
||||
assert.Equal(t, shared.FAILURE_AMOUNT_BELOW_MINIMUM, res2.FailureCode)
|
||||
|
||||
assert.Equal(t, shared.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, shared.INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action)
|
||||
assert.Equal(t, shared.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, shared.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(lsps0.TIME_FORMAT)
|
||||
i := setupInterceptor(ctx, &interceptP{store: store})
|
||||
|
||||
res := i.Intercept(createPart(nil))
|
||||
assert.Equal(t, shared.INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action)
|
||||
assert.Equal(t, shared.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(lsps0.TIME_FORMAT)
|
||||
|
||||
var res1 shared.InterceptResult
|
||||
var res2 shared.InterceptResult
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
res1 = i.Intercept(createPart(&part{
|
||||
id: "first",
|
||||
amt: defaultPaymentSizeMsat - defaultConfig().HtlcMinimumMsat,
|
||||
}))
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
<-time.After(time.Millisecond * 300)
|
||||
res2 = i.Intercept(createPart(&part{
|
||||
id: "second",
|
||||
amt: defaultConfig().HtlcMinimumMsat,
|
||||
}))
|
||||
|
||||
wg.Wait()
|
||||
assert.Equal(t, shared.INTERCEPT_FAIL_HTLC_WITH_CODE, res1.Action)
|
||||
assert.Equal(t, shared.FAILURE_UNKNOWN_NEXT_PEER, res1.FailureCode)
|
||||
assert.Equal(t, shared.INTERCEPT_FAIL_HTLC_WITH_CODE, res2.Action)
|
||||
assert.Equal(t, shared.FAILURE_UNKNOWN_NEXT_PEER, res2.FailureCode)
|
||||
|
||||
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 shared.InterceptResult
|
||||
var res2 shared.InterceptResult
|
||||
var res3 shared.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, shared.INTERCEPT_IGNORE, res1.Action)
|
||||
|
||||
assert.Equal(t, shared.INTERCEPT_RESUME_WITH_ONION, res2.Action)
|
||||
assert.Equal(t, defaultPaymentSizeMsat-defaultConfig().HtlcMinimumMsat-defaultFee, res2.AmountMsat)
|
||||
assert.Equal(t, defaultFee, *res2.FeeMsat)
|
||||
|
||||
assert.Equal(t, shared.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, shared.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, shared.INTERCEPT_FAIL_HTLC_WITH_CODE, res.Action)
|
||||
assert.Equal(t, shared.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: lightning.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, shared.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)
|
||||
}
|
||||
Reference in New Issue
Block a user