loop/routing_plugin_test.go
Boris Nagaev d554b42ef6
multi: migrate to btcd v2 modules
Update LND, Aperture, and Taproot Assets to revisions using the
btcd v2 modules, and update lndclient to v0.21.0-3. Migrate Loop
chain, transaction, and address types to their corresponding v2
packages.

The lndclient release includes the migration from:
https://github.com/lightninglabs/lndclient/pull/280

Taproot Assets is temporarily replaced with its btcd v2 revision
because the v0.8 release branch has not adopted the new modules.

This raises the minimum Go version to 1.26 and changes exported
address types.
2026-08-12 23:39:26 +00:00

742 lines
18 KiB
Go

package loop
import (
"context"
"testing"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil/v2"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/zpay32"
"github.com/stretchr/testify/require"
)
var (
alice = route.Vertex{1}
bob = route.Vertex{2}
charlie = route.Vertex{3}
dave = route.Vertex{4}
eugene = route.Vertex{5}
loopNode = route.Vertex{99}
privFrank, _ = btcec.NewPrivateKey()
frankPubKey = privFrank.PubKey()
frank = route.NewVertex(frankPubKey)
privGeorge, _ = btcec.NewPrivateKey()
georgePubKey = privGeorge.PubKey()
george = route.NewVertex(georgePubKey)
)
// testChan holds simplified test data for channels.
type testChan struct {
nodeID1 route.Vertex
nodeID2 route.Vertex
chanID uint64
capacity int64
feeBase1 int64
feeRate1 int64
feeBase2 int64
feeRate2 int64
}
// makeTestNetwork is a helper creating mocked network data from test inputs.
func makeTestNetwork(channels []testChan) ([]lndclient.ChannelInfo,
map[uint64]*lndclient.ChannelEdge) {
chanInfos := make([]lndclient.ChannelInfo, len(channels))
edges := make(map[uint64]*lndclient.ChannelEdge, len(channels))
for i, ch := range channels {
chanInfos[i] = lndclient.ChannelInfo{
ChannelID: ch.chanID,
}
edges[ch.chanID] = &lndclient.ChannelEdge{
ChannelID: ch.chanID,
Capacity: btcutil.Amount(ch.capacity),
Node1: ch.nodeID1,
Node2: ch.nodeID2,
Node1Policy: &lndclient.RoutingPolicy{
FeeBaseMsat: ch.feeBase1,
FeeRateMilliMsat: ch.feeRate1,
},
Node2Policy: &lndclient.RoutingPolicy{
FeeBaseMsat: ch.feeBase2,
FeeRateMilliMsat: ch.feeRate2,
},
}
}
return chanInfos, edges
}
// TestLowHighRoutingPlugin tests that the low-high routing plugin does indeed
// gradually change MC settings in favour of more expensive inbound channels
// towards the Loop server.
func TestLowHighRoutingPlugin(t *testing.T) {
target := loopNode
amt := btcutil.Amount(50)
testTime := time.Now().UTC()
tests := []struct {
name string
channels []testChan
routeHints [][]zpay32.HopHint
initError error
missionControlState [][]lndclient.MissionControlEntry
restoredMissionControlState []lndclient.MissionControlEntry
}{
{
name: "degenerate network 1",
//
// Alice --- Loop
//
channels: []testChan{
{alice, loopNode, 1, 1000, 1000, 1, 1000, 1},
},
initError: ErrRoutingPluginNotApplicable,
missionControlState: [][]lndclient.MissionControlEntry{
// The original MC state we start with.
{
{
NodeFrom: alice,
NodeTo: loopNode,
SuccessTime: testTime,
SuccessAmt: 10000,
},
},
},
},
{
name: "degenerate network 2",
//
// Alice --- Bob --- Loop
//
channels: []testChan{
// Alice - Bob
{alice, bob, 1, 1000, 1000, 1, 1000, 1},
// Bob - Loop
{bob, loopNode, 2, 1000, 1000, 1, 1000, 1},
},
initError: ErrRoutingPluginNotApplicable,
missionControlState: [][]lndclient.MissionControlEntry{
// The original MC state we start with.
{
{},
},
},
},
{
name: "degenerate network 3",
//
// _____Bob_____
// / \
// Alice Dave---Loop
// \___
// Charlie
//
channels: []testChan{
{alice, bob, 1, 1000, 1000, 1, 1000, 1},
{alice, charlie, 2, 1000, 1000, 1, 1000, 1},
// Bob - Dave (cheap)
{bob, dave, 3, 1000, 1000, 1, 1000, 1},
{dave, loopNode, 5, 1000, 1000, 1, 1000, 1},
},
initError: ErrRoutingPluginNotApplicable,
missionControlState: [][]lndclient.MissionControlEntry{
// The original MC state we start with.
{
{
NodeFrom: bob,
NodeTo: dave,
FailTime: time.Time{},
FailAmt: 0,
SuccessTime: testTime,
SuccessAmt: 10000,
},
},
},
restoredMissionControlState: []lndclient.MissionControlEntry{
{
NodeFrom: bob,
NodeTo: dave,
FailTime: time.Time{},
FailAmt: 0,
SuccessTime: testTime,
SuccessAmt: 10000,
},
},
},
{ // nolint: dupl
name: "fork before loop node 1",
//
// _____Bob_____
// / \
// Alice Dave---Loop
// \___ ___/
// Charlie
//
channels: []testChan{
{alice, bob, 1, 1000, 1000, 1, 1000, 1},
{alice, charlie, 2, 1000, 1000, 1, 1000, 1},
// Bob - Dave (cheap)
{bob, dave, 3, 1000, 1000, 1, 1000, 1},
// Charlie - Dave (expensive)
{charlie, dave, 4, 1000, 1000, 100, 1000, 1},
{dave, loopNode, 5, 1000, 1000, 1, 1000, 1},
},
initError: nil,
missionControlState: [][]lndclient.MissionControlEntry{
// The original MC state we start with.
{
{
NodeFrom: bob,
NodeTo: dave,
FailTime: time.Time{},
FailAmt: 0,
SuccessTime: testTime,
SuccessAmt: 10000,
},
},
// MC state set on the second attempt.
{
// Discourage Bob - Dave
{
NodeFrom: bob,
NodeTo: dave,
FailTime: testTime,
FailAmt: 1,
},
// Encourage Charlie - Dave
{
NodeFrom: charlie,
NodeTo: dave,
SuccessTime: testTime,
SuccessAmt: 1000000,
},
},
},
restoredMissionControlState: []lndclient.MissionControlEntry{
{
NodeFrom: bob,
NodeTo: dave,
FailTime: time.Time{},
FailAmt: 0,
SuccessTime: testTime,
SuccessAmt: 10000,
},
{
NodeFrom: charlie,
NodeTo: dave,
FailTime: testTime,
FailAmt: 1000001,
SuccessTime: testTime,
SuccessAmt: 1000000,
},
},
},
{ // nolint: dupl
name: "fork before loop node 1 with equal inbound fees",
//
// _____Bob_____
// / \
// Alice Dave---Loop
// \___ ___/
// Charlie
//
channels: []testChan{
{alice, bob, 1, 999, 1000, 1, 1000, 1},
{alice, charlie, 2, 9999, 1000, 1, 1000, 1},
// Bob - Dave (expensive)
{bob, dave, 3, 999, 1000, 100, 1000, 1},
// Charlie - Dave (expensive)
{charlie, dave, 4, 999, 1000, 100, 1000, 1},
{dave, loopNode, 5, 999, 1000, 1, 1000, 1},
},
initError: nil,
missionControlState: [][]lndclient.MissionControlEntry{
// The original MC state we start with.
{
{
NodeFrom: dave,
NodeTo: loopNode,
SuccessTime: testTime,
SuccessAmt: 10000,
},
},
// MC state on the second attempt encourages
// both inbound peers to make sure we do try
// to route through both.
{
{
NodeFrom: dave,
NodeTo: loopNode,
SuccessTime: testTime,
SuccessAmt: 10000,
},
{
NodeFrom: bob,
NodeTo: dave,
SuccessTime: testTime,
SuccessAmt: 999000,
},
{
NodeFrom: charlie,
NodeTo: dave,
SuccessTime: testTime,
SuccessAmt: 999000,
},
},
},
restoredMissionControlState: []lndclient.MissionControlEntry{
{
NodeFrom: dave,
NodeTo: loopNode,
SuccessTime: testTime,
SuccessAmt: 10000,
},
{
NodeFrom: bob,
NodeTo: dave,
SuccessTime: testTime,
SuccessAmt: 999000,
FailTime: testTime,
FailAmt: 999001,
},
{
NodeFrom: charlie,
NodeTo: dave,
SuccessTime: testTime,
SuccessAmt: 999000,
FailTime: testTime,
FailAmt: 999001,
},
},
},
{
name: "fork before loop node 2",
//
// _____Bob_____
// / \
// Alice Eugene---Frank---George---Loop
// |\___ ___//
// | Charlie /
// \ /
// \___ ___/
// Dave
//
channels: []testChan{
{alice, bob, 1, 1000, 1000, 1, 1000, 1},
{alice, charlie, 2, 1000, 1000, 1, 1000, 1},
{alice, dave, 3, 1000, 1000, 1, 1000, 1},
// Bob - Eugene (cheap)
{bob, eugene, 4, 1000, 1000, 1, 1000, 1},
// Charlie - Eugene (more expensive)
{charlie, eugene, 5, 1000, 1000, 2, 1000, 1},
// Dave - Eugene (most expensive)
{dave, eugene, 6, 1000, 1001, 2, 1000, 1},
{eugene, frank, 7, 1000, 1000, 1, 1000, 1},
},
// Private channels: Frank - George - Loop
routeHints: [][]zpay32.HopHint{{
{
NodeID: frankPubKey,
ChannelID: 8,
FeeBaseMSat: 1000,
FeeProportionalMillionths: 1,
},
{
NodeID: georgePubKey,
ChannelID: 9,
FeeBaseMSat: 1000,
FeeProportionalMillionths: 1,
},
}},
initError: nil,
missionControlState: [][]lndclient.MissionControlEntry{
// The original MC state we start with.
{
{
NodeFrom: charlie,
NodeTo: eugene,
SuccessTime: testTime,
SuccessAmt: 10000,
},
},
// MC state set on the second attempt.
{
// Discourage Bob - Eugene
{
NodeFrom: bob,
NodeTo: eugene,
FailTime: testTime,
FailAmt: 1,
},
// Encourage Charlie - Eugene
{
NodeFrom: charlie,
NodeTo: eugene,
SuccessTime: testTime,
SuccessAmt: 1000000,
},
// Encourage Dave - Eugene
{
NodeFrom: dave,
NodeTo: eugene,
SuccessTime: testTime,
SuccessAmt: 1000000,
},
},
// MC state set on the third attempt.
{
// Discourage Bob - Eugene
{
NodeFrom: bob,
NodeTo: eugene,
FailTime: testTime,
FailAmt: 1,
},
// Discourage Charlie - Eugene
{
NodeFrom: charlie,
NodeTo: eugene,
FailTime: testTime,
FailAmt: 1,
},
// Encourage Dave - Eugene
{
NodeFrom: dave,
NodeTo: eugene,
SuccessTime: testTime,
SuccessAmt: 1000000,
},
},
},
restoredMissionControlState: []lndclient.MissionControlEntry{
{
NodeFrom: bob,
NodeTo: eugene,
FailTime: testTime,
FailAmt: 1000001,
SuccessTime: testTime,
SuccessAmt: 1000000,
},
{
NodeFrom: charlie,
NodeTo: eugene,
FailTime: time.Time{},
FailAmt: 0,
SuccessTime: testTime,
SuccessAmt: 10000,
},
{
NodeFrom: dave,
NodeTo: eugene,
FailTime: testTime,
FailAmt: 1000001,
SuccessTime: testTime,
SuccessAmt: 1000000,
},
},
},
{
name: "fork before loop node 3",
//
// _____Bob_____
// / \
// Alice Eugene---Frank---George---Loop
// |\___ ___/ /
// | Charlie /
// \ /
// \___ ___________________/
// Dave
//
channels: []testChan{
// Alice - Bob
{alice, bob, 1, 1000, 1000, 1, 1000, 1},
// Alice - Charlie
{alice, charlie, 2, 1000, 1000, 1, 1000, 1},
// Alice - Dave
{alice, dave, 3, 1000, 1000, 1, 1000, 1},
// Bob - Eugene
{bob, eugene, 4, 1000, 1000, 1, 1000, 1},
// Charlie - Eugene
{charlie, eugene, 5, 1000, 1000, 2, 1000, 1},
// Dave - George (expensive)
{dave, george, 6, 1000, 1001, 2, 1000, 1},
// Eugene - Frank
{eugene, frank, 7, 1000, 1000, 1, 1000, 1},
// Frank - George (cheap)
{frank, george, 8, 1000, 1000, 1, 1000, 1},
// George - Loop
{george, loopNode, 9, 1000, 1000, 1, 1000, 1},
},
initError: nil,
missionControlState: [][]lndclient.MissionControlEntry{
// The original MC state we start with.
{
{
NodeFrom: charlie,
NodeTo: eugene,
SuccessTime: testTime,
SuccessAmt: 10000,
},
},
// MC state set on the second attempt.
{
{
NodeFrom: charlie,
NodeTo: eugene,
SuccessTime: testTime,
SuccessAmt: 10000,
},
// Discourage Frank - George
{
NodeFrom: frank,
NodeTo: george,
FailTime: testTime,
FailAmt: 1,
},
// Encourage Dave - George
{
NodeFrom: dave,
NodeTo: george,
SuccessTime: testTime,
SuccessAmt: 1000000,
},
},
},
restoredMissionControlState: []lndclient.MissionControlEntry{
{
NodeFrom: charlie,
NodeTo: eugene,
SuccessTime: testTime,
SuccessAmt: 10000,
},
{
NodeFrom: frank,
NodeTo: george,
FailTime: testTime,
FailAmt: 1000001,
SuccessTime: testTime,
SuccessAmt: 1000000,
},
{
NodeFrom: dave,
NodeTo: george,
FailTime: testTime,
FailAmt: 1000001,
SuccessTime: testTime,
SuccessAmt: 1000000,
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
mockLnd := test.NewMockLnd()
mockLnd.Channels, mockLnd.ChannelEdges =
makeTestNetwork(tc.channels)
lnd := lndclient.LndServices{
Client: mockLnd.Client,
Router: mockLnd.Router,
}
testClock := clock.NewTestClock(testTime)
plugin := makeRoutingPlugin(
RoutingPluginLowHigh, lnd, testClock,
)
require.NotNil(t, plugin)
// Set start state for MC.
mockLnd.MissionControlState = tc.missionControlState[0]
// Initialize the routing plugin.
require.Equal(
t, tc.initError,
plugin.Init(
context.TODO(), target, tc.routeHints,
amt,
),
)
if tc.initError != nil {
// Make sure that MC state is untouched.
require.Equal(
t, tc.missionControlState[0],
mockLnd.MissionControlState,
)
return
}
maxAttempts := len(tc.missionControlState)
for i, expectedState := range tc.missionControlState {
// Check that after each step, MC state is what
// we expect it to be.
require.NoError(
t, plugin.BeforePayment(
context.TODO(),
i+1, maxAttempts,
),
)
require.ElementsMatch(
t, expectedState,
mockLnd.MissionControlState,
)
}
// Make sure we covered all inbound channels.
require.Error(
t, ErrRoutingPluginNoMoreRetries,
plugin.BeforePayment(
context.TODO(), maxAttempts, maxAttempts,
),
)
// Deinitialize the routing plugin.
require.NoError(t, plugin.Done(context.TODO()))
// Make sure that MC state is reset after Done() is
// called.
require.ElementsMatch(
t, tc.restoredMissionControlState,
mockLnd.MissionControlState,
)
})
}
}
func TestRoutingPluginAcquireRelease(t *testing.T) {
mockLnd := test.NewMockLnd()
// _____Bob_____
// / \
// Alice Dave---Loop
// \___ ___/
// Charlie
//
channels := []testChan{
{alice, bob, 1, 1000, 1000, 1, 1000, 1},
{alice, charlie, 2, 1000, 1000, 1, 1000, 1},
{bob, dave, 3, 1000, 1000, 1, 1000, 1},
{charlie, dave, 4, 1000, 1000, 100, 1000, 1},
{dave, loopNode, 5, 1000, 1000, 1, 1000, 1},
}
mockLnd.Channels, mockLnd.ChannelEdges = makeTestNetwork(channels)
lnd := lndclient.LndServices{
Client: mockLnd.Client,
Router: mockLnd.Router,
}
target := loopNode
amt := btcutil.Amount(50)
ctx := context.TODO()
// RoutingPluginNone returns nil.
plugin, err := AcquireRoutingPlugin(
ctx, RoutingPluginNone, lnd, target, nil, amt,
)
require.Nil(t, plugin)
require.NoError(t, err)
// Attempting to acquire RoutingPluginNone again still returns nil.
plugin, err = AcquireRoutingPlugin(
ctx, RoutingPluginNone, lnd, target, nil, amt,
)
require.Nil(t, plugin)
require.NoError(t, err)
// Call ReleaseRoutingPlugin twice to ensure we can call it even when no
// plugin is acquired.
ReleaseRoutingPlugin(ctx)
ReleaseRoutingPlugin(ctx)
// RoutingPluginNone returns nil.
plugin2, err := AcquireRoutingPlugin(
ctx, RoutingPluginNone, lnd, target, nil, amt,
)
require.Nil(t, plugin2)
require.NoError(t, err)
// Acquire is successful.
plugin, err = AcquireRoutingPlugin(
ctx, RoutingPluginLowHigh, lnd, target, nil, amt,
)
require.NotNil(t, plugin)
require.NoError(t, err)
// Plugin already acquired, above.
plugin2, err = AcquireRoutingPlugin(
ctx, RoutingPluginLowHigh, lnd, target, nil, amt,
)
require.Nil(t, plugin2)
require.NoError(t, err)
// Release acruired plugin.
ReleaseRoutingPlugin(ctx)
// Acquire is successful.
plugin2, err = AcquireRoutingPlugin(
ctx, RoutingPluginLowHigh, lnd, target, nil, amt,
)
require.NotNil(t, plugin2)
require.NoError(t, err)
}
// mockRoutingPlugin is a minimal RoutingPlugin used to capture the context
// passed to Done.
type mockRoutingPlugin struct {
doneCtxErr error
}
// Init is a no-op initializer for the mock plugin.
func (m *mockRoutingPlugin) Init(_ context.Context, _ route.Vertex,
_ [][]zpay32.HopHint, _ btcutil.Amount) error {
return nil
}
// Done records ctx.Err() so tests can assert whether teardown ran with a live
// context.
func (m *mockRoutingPlugin) Done(ctx context.Context) error {
m.doneCtxErr = ctx.Err()
return nil
}
// BeforePayment is a no-op hook for the mock plugin.
func (m *mockRoutingPlugin) BeforePayment(_ context.Context, _, _ int) error {
return nil
}
// TestReleaseRoutingPluginUsesLiveContext checks that ReleaseRoutingPlugin does
// not propagate caller cancellation to plugin teardown. The test cancels the
// caller context before release and verifies mock Done still sees nil ctx.Err.
func TestReleaseRoutingPluginUsesLiveContext(t *testing.T) {
ReleaseRoutingPlugin(context.Background())
t.Cleanup(func() {
ReleaseRoutingPlugin(context.Background())
})
mockPlugin := &mockRoutingPlugin{}
routingPluginMx.Lock()
routingPluginInstance = mockPlugin
routingPluginMx.Unlock()
ctx, cancel := context.WithCancel(context.Background())
cancel()
ReleaseRoutingPlugin(ctx)
require.NoError(t, mockPlugin.doneCtxErr)
}