mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-23 14:40:30 +01:00
To be able to do MPP payment to multiple blinded routes we need to add a constant dummy hop as a final hop to every blined path. This is used when sending or querying a blinded path, to let the pathfinder be able to send MPP payments over different blinded routes. For this dummy final hop we use a NUMS key so that we are sure no other node can use this blinded pubkey either in a normal or blinded route. Moreover this helps us handling the mission control data for blinded paths correctly because we always consider the blinded pubkey pairs which are registered with mission control when a payment to a blinded path fails.
280 lines
6.7 KiB
Go
280 lines
6.7 KiB
Go
package routing
|
|
|
|
import (
|
|
"bytes"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
|
"github.com/lightningnetwork/lnd/graph/db/models"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestBlindedPathValidation tests validation of blinded paths.
|
|
func TestBlindedPathValidation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
payment *BlindedPayment
|
|
err error
|
|
}{
|
|
{
|
|
name: "no path",
|
|
payment: &BlindedPayment{},
|
|
err: ErrNoBlindedPath,
|
|
},
|
|
{
|
|
name: "insufficient hops",
|
|
payment: &BlindedPayment{
|
|
BlindedPath: &sphinx.BlindedPath{
|
|
BlindedHops: []*sphinx.BlindedHopInfo{},
|
|
},
|
|
},
|
|
err: ErrInsufficientBlindedHops,
|
|
},
|
|
{
|
|
name: "maximum < minimum",
|
|
payment: &BlindedPayment{
|
|
BlindedPath: &sphinx.BlindedPath{
|
|
BlindedHops: []*sphinx.BlindedHopInfo{
|
|
{},
|
|
},
|
|
},
|
|
HtlcMaximum: 10,
|
|
HtlcMinimum: 20,
|
|
},
|
|
err: ErrHTLCRestrictions,
|
|
},
|
|
{
|
|
name: "valid",
|
|
payment: &BlindedPayment{
|
|
BlindedPath: &sphinx.BlindedPath{
|
|
BlindedHops: []*sphinx.BlindedHopInfo{
|
|
{},
|
|
},
|
|
},
|
|
HtlcMaximum: 15,
|
|
HtlcMinimum: 5,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, testCase := range tests {
|
|
testCase := testCase
|
|
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
err := testCase.payment.Validate()
|
|
require.ErrorIs(t, err, testCase.err)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBlindedPaymentToHints tests conversion of a blinded path to a chain of
|
|
// route hints. As our function assumes that the blinded payment has already
|
|
// been validated.
|
|
func TestBlindedPaymentToHints(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
_, pk1 = btcec.PrivKeyFromBytes([]byte{1})
|
|
_, pkb1 = btcec.PrivKeyFromBytes([]byte{2})
|
|
_, pkb2 = btcec.PrivKeyFromBytes([]byte{3})
|
|
_, pkb3 = btcec.PrivKeyFromBytes([]byte{4})
|
|
|
|
v1 = route.NewVertex(pk1)
|
|
vb2 = route.NewVertex(pkb2)
|
|
vb3 = route.NewVertex(pkb3)
|
|
|
|
baseFee uint32 = 1000
|
|
ppmFee uint32 = 500
|
|
cltvDelta uint16 = 140
|
|
htlcMin uint64 = 100
|
|
htlcMax uint64 = 100_000_000
|
|
|
|
sizeEncryptedData = 100
|
|
cipherText = bytes.Repeat(
|
|
[]byte{1}, sizeEncryptedData,
|
|
)
|
|
_, blindedPoint = btcec.PrivKeyFromBytes([]byte{5})
|
|
|
|
rawFeatures = lnwire.NewRawFeatureVector(
|
|
lnwire.AMPOptional,
|
|
)
|
|
|
|
features = lnwire.NewFeatureVector(
|
|
rawFeatures, lnwire.Features,
|
|
)
|
|
)
|
|
|
|
// Create a blinded payment that's just to the introduction node and
|
|
// assert that we get nil hints.
|
|
blindedPayment := &BlindedPayment{
|
|
BlindedPath: &sphinx.BlindedPath{
|
|
IntroductionPoint: pk1,
|
|
BlindingPoint: blindedPoint,
|
|
BlindedHops: []*sphinx.BlindedHopInfo{
|
|
{},
|
|
},
|
|
},
|
|
BaseFee: baseFee,
|
|
ProportionalFeeRate: ppmFee,
|
|
CltvExpiryDelta: cltvDelta,
|
|
HtlcMinimum: htlcMin,
|
|
HtlcMaximum: htlcMax,
|
|
Features: features,
|
|
}
|
|
hints, err := blindedPayment.toRouteHints()
|
|
require.NoError(t, err)
|
|
require.Nil(t, hints)
|
|
|
|
// Populate the blinded payment with hops.
|
|
blindedPayment.BlindedPath.BlindedHops = []*sphinx.BlindedHopInfo{
|
|
{
|
|
BlindedNodePub: pkb1,
|
|
CipherText: cipherText,
|
|
},
|
|
{
|
|
BlindedNodePub: pkb2,
|
|
CipherText: cipherText,
|
|
},
|
|
{
|
|
BlindedNodePub: pkb3,
|
|
CipherText: cipherText,
|
|
},
|
|
}
|
|
|
|
policy1 := &models.CachedEdgePolicy{
|
|
TimeLockDelta: cltvDelta,
|
|
MinHTLC: lnwire.MilliSatoshi(htlcMin),
|
|
MaxHTLC: lnwire.MilliSatoshi(htlcMax),
|
|
FeeBaseMSat: lnwire.MilliSatoshi(baseFee),
|
|
FeeProportionalMillionths: lnwire.MilliSatoshi(
|
|
ppmFee,
|
|
),
|
|
ToNodePubKey: func() route.Vertex {
|
|
return vb2
|
|
},
|
|
ToNodeFeatures: features,
|
|
}
|
|
policy2 := &models.CachedEdgePolicy{
|
|
ToNodePubKey: func() route.Vertex {
|
|
return vb3
|
|
},
|
|
ToNodeFeatures: features,
|
|
}
|
|
|
|
blindedEdge1, err := NewBlindedEdge(policy1, blindedPayment, 0)
|
|
require.NoError(t, err)
|
|
|
|
blindedEdge2, err := NewBlindedEdge(policy2, blindedPayment, 1)
|
|
require.NoError(t, err)
|
|
|
|
expected := RouteHints{
|
|
v1: {
|
|
blindedEdge1,
|
|
},
|
|
vb2: {
|
|
blindedEdge2,
|
|
},
|
|
}
|
|
|
|
actual, err := blindedPayment.toRouteHints()
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, len(expected), len(actual))
|
|
for vertex, expectedHint := range expected {
|
|
actualHint, ok := actual[vertex]
|
|
require.True(t, ok, "node not found: %v", vertex)
|
|
|
|
require.Len(t, expectedHint, 1)
|
|
require.Len(t, actualHint, 1)
|
|
|
|
// We can't assert that our functions are equal, so we check
|
|
// their output and then mark them as nil so that we can use
|
|
// require.Equal for all our other fields.
|
|
require.Equal(t, expectedHint[0].EdgePolicy().ToNodePubKey(),
|
|
actualHint[0].EdgePolicy().ToNodePubKey())
|
|
|
|
actualHint[0].EdgePolicy().ToNodePubKey = nil
|
|
expectedHint[0].EdgePolicy().ToNodePubKey = nil
|
|
|
|
// The arguments we use for the payload do not matter as long as
|
|
// both functions return the same payload.
|
|
expectedPayloadSize := expectedHint[0].IntermediatePayloadSize(
|
|
0, 0, 0,
|
|
)
|
|
actualPayloadSize := actualHint[0].IntermediatePayloadSize(
|
|
0, 0, 0,
|
|
)
|
|
|
|
require.Equal(t, expectedPayloadSize, actualPayloadSize)
|
|
|
|
require.Equal(t, expectedHint[0], actualHint[0])
|
|
}
|
|
}
|
|
|
|
// TestBlindedPaymentDeepCopy tests the deep copy method of the BLindedPayment
|
|
// struct.
|
|
//
|
|
// TODO(ziggie): Make this a property test instead.
|
|
func TestBlindedPaymentDeepCopy(t *testing.T) {
|
|
_, pkBlind1 := btcec.PrivKeyFromBytes([]byte{1})
|
|
_, blindingPoint := btcec.PrivKeyFromBytes([]byte{2})
|
|
_, pkBlind2 := btcec.PrivKeyFromBytes([]byte{3})
|
|
|
|
// Create a test BlindedPayment with non-nil fields
|
|
original := &BlindedPayment{
|
|
BaseFee: 1000,
|
|
ProportionalFeeRate: 2000,
|
|
CltvExpiryDelta: 144,
|
|
HtlcMinimum: 1000,
|
|
HtlcMaximum: 1000000,
|
|
Features: lnwire.NewFeatureVector(nil, nil),
|
|
BlindedPath: &sphinx.BlindedPath{
|
|
IntroductionPoint: pkBlind1,
|
|
BlindingPoint: blindingPoint,
|
|
BlindedHops: []*sphinx.BlindedHopInfo{
|
|
{
|
|
BlindedNodePub: pkBlind2,
|
|
CipherText: []byte("test cipher"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Make a deep copy
|
|
cpyPayment := original.deepCopy()
|
|
|
|
// Test 1: Verify the copy is not the same pointer
|
|
if cpyPayment == original {
|
|
t.Fatal("deepCopy returned same pointer")
|
|
}
|
|
|
|
// Verify all fields are equal
|
|
if !reflect.DeepEqual(original, cpyPayment) {
|
|
t.Fatal("copy is not equal to original")
|
|
}
|
|
|
|
// Modify the copy and verify it doesn't affect the original
|
|
cpyPayment.BaseFee = 2000
|
|
cpyPayment.BlindedPath.BlindedHops[0].CipherText = []byte("modified")
|
|
|
|
require.NotEqual(t, original.BaseFee, cpyPayment.BaseFee)
|
|
|
|
require.NotEqual(
|
|
t,
|
|
original.BlindedPath.BlindedHops[0].CipherText,
|
|
cpyPayment.BlindedPath.BlindedHops[0].CipherText,
|
|
)
|
|
|
|
// Verify nil handling.
|
|
var nilPayment *BlindedPayment
|
|
nilCopy := nilPayment.deepCopy()
|
|
require.Nil(t, nilCopy)
|
|
}
|