package loop import ( "context" "testing" "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "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 { tc := tc 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) }