github.com/decred/dcrlnd@v0.7.6/routing/missioncontrol_test.go (about)

     1  package routing
     2  
     3  import (
     4  	"io/ioutil"
     5  	"os"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/decred/dcrlnd/kvdb"
    10  	"github.com/decred/dcrlnd/lnwire"
    11  	"github.com/decred/dcrlnd/routing/route"
    12  	"github.com/stretchr/testify/require"
    13  )
    14  
    15  var (
    16  	mcTestRoute = &route.Route{
    17  		SourcePubKey: mcTestSelf,
    18  		Hops: []*route.Hop{
    19  			{
    20  				ChannelID:     1,
    21  				PubKeyBytes:   route.Vertex{11},
    22  				AmtToForward:  1000,
    23  				LegacyPayload: true,
    24  			},
    25  			{
    26  				ChannelID:     2,
    27  				PubKeyBytes:   route.Vertex{12},
    28  				LegacyPayload: true,
    29  			},
    30  		},
    31  	}
    32  
    33  	mcTestTime  = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC)
    34  	mcTestSelf  = route.Vertex{10}
    35  	mcTestNode1 = mcTestRoute.Hops[0].PubKeyBytes
    36  	mcTestNode2 = mcTestRoute.Hops[1].PubKeyBytes
    37  
    38  	testPenaltyHalfLife       = 30 * time.Minute
    39  	testAprioriHopProbability = 0.9
    40  	testAprioriWeight         = 0.5
    41  )
    42  
    43  type mcTestContext struct {
    44  	t   *testing.T
    45  	mc  *MissionControl
    46  	now time.Time
    47  
    48  	db     kvdb.Backend
    49  	dbPath string
    50  
    51  	pid uint64
    52  }
    53  
    54  func createMcTestContext(t *testing.T) *mcTestContext {
    55  	ctx := &mcTestContext{
    56  		t:   t,
    57  		now: mcTestTime,
    58  	}
    59  
    60  	file, err := ioutil.TempFile("", "*.db")
    61  	if err != nil {
    62  		t.Fatal(err)
    63  	}
    64  
    65  	ctx.dbPath = file.Name()
    66  
    67  	ctx.db, err = kvdb.Open(
    68  		kvdb.BoltBackendName, ctx.dbPath, true, kvdb.DefaultDBTimeout,
    69  	)
    70  	if err != nil {
    71  		t.Fatal(err)
    72  	}
    73  
    74  	ctx.restartMc()
    75  
    76  	return ctx
    77  }
    78  
    79  // restartMc creates a new instances of mission control on the same database.
    80  func (ctx *mcTestContext) restartMc() {
    81  	// Since we don't run a timer to store results in unit tests, we store
    82  	// them here before fetching back everything in NewMissionControl.
    83  	if ctx.mc != nil {
    84  		require.NoError(ctx.t, ctx.mc.store.storeResults())
    85  	}
    86  
    87  	mc, err := NewMissionControl(
    88  		ctx.db, mcTestSelf,
    89  		&MissionControlConfig{
    90  			ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{
    91  				PenaltyHalfLife:       testPenaltyHalfLife,
    92  				AprioriHopProbability: testAprioriHopProbability,
    93  				AprioriWeight:         testAprioriWeight,
    94  			},
    95  		},
    96  	)
    97  	if err != nil {
    98  		ctx.t.Fatal(err)
    99  	}
   100  
   101  	mc.now = func() time.Time { return ctx.now }
   102  	ctx.mc = mc
   103  }
   104  
   105  // cleanup closes the database and removes the temp file.
   106  func (ctx *mcTestContext) cleanup() {
   107  	ctx.db.Close()
   108  	os.Remove(ctx.dbPath)
   109  }
   110  
   111  // Assert that mission control returns a probability for an edge.
   112  func (ctx *mcTestContext) expectP(amt lnwire.MilliAtom, expected float64) {
   113  	ctx.t.Helper()
   114  
   115  	p := ctx.mc.GetProbability(mcTestNode1, mcTestNode2, amt)
   116  	if p != expected {
   117  		ctx.t.Fatalf("expected probability %v but got %v", expected, p)
   118  	}
   119  }
   120  
   121  // reportFailure reports a failure by using a test route.
   122  func (ctx *mcTestContext) reportFailure(amt lnwire.MilliAtom,
   123  	failure lnwire.FailureMessage) {
   124  
   125  	mcTestRoute.Hops[0].AmtToForward = amt
   126  
   127  	errorSourceIdx := 1
   128  	ctx.mc.ReportPaymentFail(
   129  		ctx.pid, mcTestRoute, &errorSourceIdx, failure,
   130  	)
   131  }
   132  
   133  // reportSuccess reports a success by using a test route.
   134  func (ctx *mcTestContext) reportSuccess() {
   135  	err := ctx.mc.ReportPaymentSuccess(ctx.pid, mcTestRoute)
   136  	if err != nil {
   137  		ctx.t.Fatal(err)
   138  	}
   139  
   140  	ctx.pid++
   141  }
   142  
   143  // TestMissionControl tests mission control probability estimation.
   144  func TestMissionControl(t *testing.T) {
   145  	ctx := createMcTestContext(t)
   146  	defer ctx.cleanup()
   147  
   148  	ctx.now = testTime
   149  
   150  	testTime := time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC)
   151  
   152  	// For local channels, we expect a higher probability than our a prior
   153  	// test probability.
   154  	selfP := ctx.mc.GetProbability(mcTestSelf, mcTestNode1, 100)
   155  	if selfP != prevSuccessProbability {
   156  		t.Fatalf("expected prev success prob for untried local chans")
   157  	}
   158  
   159  	// Initial probability is expected to be the a priori.
   160  	ctx.expectP(1000, testAprioriHopProbability)
   161  
   162  	// Expect probability to be zero after reporting the edge as failed.
   163  	ctx.reportFailure(1000, lnwire.NewTemporaryChannelFailure(nil))
   164  	ctx.expectP(1000, 0)
   165  
   166  	// As we reported with a min penalization amt, a lower amt than reported
   167  	// should return the node probability, which is the a priori
   168  	// probability.
   169  	ctx.expectP(500, testAprioriHopProbability)
   170  
   171  	// Edge decay started. The node probability weighted average should now
   172  	// have shifted from 1:1 to 1:0.5 -> 60%. The connection probability is
   173  	// half way through the recovery, so we expect 30% here.
   174  	ctx.now = testTime.Add(30 * time.Minute)
   175  	ctx.expectP(1000, 0.3)
   176  
   177  	// Edge fails again, this time without a min penalization amt. The edge
   178  	// should be penalized regardless of amount.
   179  	ctx.reportFailure(0, lnwire.NewTemporaryChannelFailure(nil))
   180  	ctx.expectP(1000, 0)
   181  	ctx.expectP(500, 0)
   182  
   183  	// Edge decay started.
   184  	ctx.now = testTime.Add(60 * time.Minute)
   185  	ctx.expectP(1000, 0.3)
   186  
   187  	// Restart mission control to test persistence.
   188  	ctx.restartMc()
   189  	ctx.expectP(1000, 0.3)
   190  
   191  	// A node level failure should bring probability of all known channels
   192  	// back to zero.
   193  	ctx.reportFailure(0, lnwire.NewExpiryTooSoon(lnwire.ChannelUpdate{}))
   194  	ctx.expectP(1000, 0)
   195  
   196  	// Check whether history snapshot looks sane.
   197  	history := ctx.mc.GetHistorySnapshot()
   198  
   199  	if len(history.Pairs) != 4 {
   200  		t.Fatalf("expected 4 pairs, but got %v", len(history.Pairs))
   201  	}
   202  
   203  	// Test reporting a success.
   204  	ctx.reportSuccess()
   205  }
   206  
   207  // TestMissionControlChannelUpdate tests that the first channel update is not
   208  // penalizing the channel yet.
   209  func TestMissionControlChannelUpdate(t *testing.T) {
   210  	ctx := createMcTestContext(t)
   211  
   212  	// Report a policy related failure. Because it is the first, we don't
   213  	// expect a penalty.
   214  	ctx.reportFailure(
   215  		0, lnwire.NewFeeInsufficient(0, lnwire.ChannelUpdate{}),
   216  	)
   217  	ctx.expectP(100, testAprioriHopProbability)
   218  
   219  	// Report another failure for the same channel. We expect it to be
   220  	// pruned.
   221  	ctx.reportFailure(
   222  		0, lnwire.NewFeeInsufficient(0, lnwire.ChannelUpdate{}),
   223  	)
   224  	ctx.expectP(100, 0)
   225  }