tests: add unit tests for the low/high routing plugin

pull/439/head
Andras Banki-Horvath 2 years ago
parent ffd52aba2f
commit 01bf94ad11
No known key found for this signature in database
GPG Key ID: 80E5375C094198D8

@ -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)
}

@ -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

@ -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()

@ -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
}

Loading…
Cancel
Save