diff --git a/routing_plugin_test.go b/routing_plugin_test.go index 31f9010..4c2b69c 100644 --- a/routing_plugin_test.go +++ b/routing_plugin_test.go @@ -3,12 +3,15 @@ package loop import ( "context" "testing" + "time" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/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" ) @@ -71,6 +74,551 @@ func makeTestNetwork(channels []testChan) ([]lndclient.ChannelInfo, 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() + // We expect Mission Control entries to be set to now + 1 sec. + testTimeMc := testTime.Add(time.Second) + + 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: "degenrate 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: testTimeMc, + 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: testTimeMc, + FailAmt: 1, + }, + // Encourage Charlie - Dave + { + NodeFrom: charlie, + NodeTo: dave, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + }, + }, + restoredMissionControlState: []lndclient.MissionControlEntry{ + { + NodeFrom: bob, + NodeTo: dave, + FailTime: time.Time{}, + FailAmt: 0, + SuccessTime: testTimeMc, + SuccessAmt: 10000, + }, + { + NodeFrom: charlie, + NodeTo: dave, + FailTime: testTimeMc, + FailAmt: 1000001, + SuccessTime: testTimeMc, + 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: testTimeMc, + SuccessAmt: 999000, + }, + { + NodeFrom: charlie, + NodeTo: dave, + SuccessTime: testTimeMc, + SuccessAmt: 999000, + }, + }, + }, + restoredMissionControlState: []lndclient.MissionControlEntry{ + { + NodeFrom: dave, + NodeTo: loopNode, + SuccessTime: testTime, + SuccessAmt: 10000, + }, + { + NodeFrom: bob, + NodeTo: dave, + SuccessTime: testTimeMc, + SuccessAmt: 999000, + FailTime: testTimeMc, + FailAmt: 999001, + }, + { + NodeFrom: charlie, + NodeTo: dave, + SuccessTime: testTimeMc, + SuccessAmt: 999000, + FailTime: testTimeMc, + 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: testTimeMc, + FailAmt: 1, + }, + // Encourage Charlie - Eugene + { + NodeFrom: charlie, + NodeTo: eugene, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + // Encourage Dave - Eugene + { + NodeFrom: dave, + NodeTo: eugene, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + }, + // MC state set on the third attempt. + { + // Discourage Bob - Eugene + { + NodeFrom: bob, + NodeTo: eugene, + FailTime: testTimeMc, + FailAmt: 1, + }, + // Discourage Charlie - Eugene + { + NodeFrom: charlie, + NodeTo: eugene, + FailTime: testTimeMc, + FailAmt: 1, + }, + // Encourage Dave - Eugene + { + NodeFrom: dave, + NodeTo: eugene, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + }, + }, + restoredMissionControlState: []lndclient.MissionControlEntry{ + { + NodeFrom: bob, + NodeTo: eugene, + FailTime: testTimeMc, + FailAmt: 1000001, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + { + NodeFrom: charlie, + NodeTo: eugene, + FailTime: time.Time{}, + FailAmt: 0, + SuccessTime: testTimeMc, + SuccessAmt: 10000, + }, + { + NodeFrom: dave, + NodeTo: eugene, + FailTime: testTimeMc, + FailAmt: 1000001, + SuccessTime: testTimeMc, + 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: testTimeMc, + FailAmt: 1, + }, + // Encourage Dave - George + { + NodeFrom: dave, + NodeTo: george, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + }, + }, + restoredMissionControlState: []lndclient.MissionControlEntry{ + { + NodeFrom: charlie, + NodeTo: eugene, + SuccessTime: testTime, + SuccessAmt: 10000, + }, + { + NodeFrom: frank, + NodeTo: george, + FailTime: testTimeMc, + FailAmt: 1000001, + SuccessTime: testTimeMc, + SuccessAmt: 1000000, + }, + { + NodeFrom: dave, + NodeTo: george, + FailTime: testTimeMc, + FailAmt: 1000001, + SuccessTime: testTimeMc, + 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() @@ -116,4 +664,35 @@ func TestRoutingPluginAcquireRelease(t *testing.T) { // 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) } diff --git a/test/lightning_client_mock.go b/test/lightning_client_mock.go index 2202223..92d803a 100644 --- a/test/lightning_client_mock.go +++ b/test/lightning_client_mock.go @@ -186,10 +186,14 @@ func (h *mockLightningClient) ListTransactions( func (h *mockLightningClient) GetNodeInfo(ctx context.Context, pubKeyBytes route.Vertex, includeChannels bool) (*lndclient.NodeInfo, error) { - nodeInfo := lndclient.NodeInfo{} + nodeInfo := &lndclient.NodeInfo{ + Node: &lndclient.Node{ + PubKey: pubKeyBytes, + }, + } if !includeChannels { - return nil, nil + return nodeInfo, nil } nodePubKey, err := route.NewVertexFromStr(h.lnd.NodePubkey) @@ -214,7 +218,7 @@ func (h *mockLightningClient) GetNodeInfo(ctx context.Context, nodeInfo.ChannelCount = len(nodeInfo.Channels) - return &nodeInfo, nil + return nodeInfo, nil } // GetChanInfo retrieves all the info the node has on the given channel diff --git a/test/lnd_services_mock.go b/test/lnd_services_mock.go index ccb14d8..ad227ef 100644 --- a/test/lnd_services_mock.go +++ b/test/lnd_services_mock.go @@ -163,11 +163,12 @@ type LndMockServices struct { // keyed by hash string. Invoices map[lntypes.Hash]*lndclient.Invoice - Channels []lndclient.ChannelInfo - ChannelEdges map[uint64]*lndclient.ChannelEdge - ClosedChannels []lndclient.ClosedChannel - ForwardingEvents []lndclient.ForwardingEvent - Payments []lndclient.Payment + Channels []lndclient.ChannelInfo + ChannelEdges map[uint64]*lndclient.ChannelEdge + ClosedChannels []lndclient.ClosedChannel + ForwardingEvents []lndclient.ForwardingEvent + Payments []lndclient.Payment + MissionControlState []lndclient.MissionControlEntry WaitForFinished func() diff --git a/test/router_mock.go b/test/router_mock.go index d094b8f..e14411a 100644 --- a/test/router_mock.go +++ b/test/router_mock.go @@ -42,3 +42,66 @@ func (r *mockRouter) TrackPayment(ctx context.Context, return statusChan, errorChan, nil } + +func (r *mockRouter) QueryMissionControl(ctx context.Context) ( + []lndclient.MissionControlEntry, error) { + + return r.lnd.MissionControlState, nil +} + +// ImpotMissionControl is a mocked reimplementation of the pair import. +// Reference: lnd/router/missioncontrol_state.go:importSnapshot(). +func (r *mockRouter) ImportMissionControl(ctx context.Context, + entries []lndclient.MissionControlEntry) error { + + for _, entry := range entries { + found := false + for i := range r.lnd.MissionControlState { + current := &r.lnd.MissionControlState[i] + if entry.NodeFrom == current.NodeFrom && + entry.NodeTo == current.NodeTo { + + // Mark that the entry has been found and updated. + found = true + + // Import failure result first. We ignore failure + // relax interval here for convenience. + current.FailTime = entry.FailTime + current.FailAmt = entry.FailAmt + + switch { + case entry.FailAmt == 0: + current.SuccessAmt = 0 + + case entry.FailAmt <= current.SuccessAmt: + current.SuccessAmt = entry.FailAmt - 1 + } + + // Import success result second. + current.SuccessTime = entry.SuccessTime + if entry.SuccessAmt > current.SuccessAmt { + current.SuccessAmt = entry.SuccessAmt + } + + if !current.FailTime.IsZero() && + entry.SuccessAmt >= current.FailAmt { + + current.FailAmt = entry.SuccessAmt + 1 + } + } + } + + if !found { + r.lnd.MissionControlState = append( + r.lnd.MissionControlState, entry, + ) + } + } + + return nil +} + +func (r *mockRouter) ResetMissionControl(ctx context.Context) error { + r.lnd.MissionControlState = []lndclient.MissionControlEntry{} + return nil +}