github.com/decred/dcrlnd@v0.7.6/lnrpc/invoicesrpc/addinvoice_test.go (about)

     1  package invoicesrpc
     2  
     3  import (
     4  	"errors"
     5  	"testing"
     6  
     7  	"github.com/decred/dcrd/dcrec/secp256k1/v4"
     8  	"github.com/decred/dcrd/wire"
     9  	"github.com/decred/dcrlnd/channeldb"
    10  	"github.com/decred/dcrlnd/lnwire"
    11  	"github.com/decred/dcrlnd/zpay32"
    12  	"github.com/stretchr/testify/mock"
    13  	"github.com/stretchr/testify/require"
    14  )
    15  
    16  type hopHintsConfigMock struct {
    17  	mock.Mock
    18  }
    19  
    20  // IsPublicNode mocks node public state lookup.
    21  func (h *hopHintsConfigMock) IsPublicNode(pubKey [33]byte) (bool, error) {
    22  	args := h.Mock.Called(pubKey)
    23  	return args.Bool(0), args.Error(1)
    24  }
    25  
    26  // FetchChannelEdgesByID mocks channel edge lookup.
    27  func (h *hopHintsConfigMock) FetchChannelEdgesByID(chanID uint64) (
    28  	*channeldb.ChannelEdgeInfo, *channeldb.ChannelEdgePolicy,
    29  	*channeldb.ChannelEdgePolicy, error) {
    30  
    31  	args := h.Mock.Called(chanID)
    32  
    33  	// If our error is non-nil, we expect nil responses otherwise. Our
    34  	// casts below will fail with nil values, so we check our error and
    35  	// return early on failure first.
    36  	err := args.Error(3)
    37  	if err != nil {
    38  		return nil, nil, nil, err
    39  	}
    40  
    41  	edgeInfo := args.Get(0).(*channeldb.ChannelEdgeInfo)
    42  	policy1 := args.Get(1).(*channeldb.ChannelEdgePolicy)
    43  	policy2 := args.Get(2).(*channeldb.ChannelEdgePolicy)
    44  
    45  	return edgeInfo, policy1, policy2, err
    46  }
    47  
    48  // TestSelectHopHints tests selection of hop hints for a node with private
    49  // channels.
    50  func TestSelectHopHints(t *testing.T) {
    51  	var (
    52  		// We need to serialize our pubkey in SelectHopHints so it
    53  		// needs to be valid.
    54  		privKey, _ = secp256k1.GeneratePrivateKey()
    55  		pubkey     = privKey.PubKey()
    56  		compressed = pubkey.SerializeCompressed()
    57  
    58  		publicChannel = &HopHintInfo{
    59  			IsPublic: true,
    60  			IsActive: true,
    61  			FundingOutpoint: wire.OutPoint{
    62  				Index: 0,
    63  			},
    64  			RemoteBalance:  10,
    65  			ShortChannelID: 0,
    66  		}
    67  
    68  		inactiveChannel = &HopHintInfo{
    69  			IsPublic: false,
    70  			IsActive: false,
    71  		}
    72  
    73  		// Create a private channel that we'll generate hints from.
    74  		private1ShortID uint64 = 1
    75  		privateChannel1        = &HopHintInfo{
    76  			IsPublic: false,
    77  			IsActive: true,
    78  			FundingOutpoint: wire.OutPoint{
    79  				Index: 1,
    80  			},
    81  			RemotePubkey:   pubkey,
    82  			RemoteBalance:  100,
    83  			ShortChannelID: private1ShortID,
    84  		}
    85  
    86  		// Create a edge policy for private channel 1.
    87  		privateChan1Policy = &channeldb.ChannelEdgePolicy{
    88  			FeeBaseMAtoms:             10,
    89  			FeeProportionalMillionths: 100,
    90  			TimeLockDelta:             1000,
    91  		}
    92  
    93  		// Create an edge policy different to ours which we'll use for
    94  		// the other direction
    95  		otherChanPolicy = &channeldb.ChannelEdgePolicy{
    96  			FeeBaseMAtoms:             90,
    97  			FeeProportionalMillionths: 900,
    98  			TimeLockDelta:             9000,
    99  		}
   100  
   101  		// Create a hop hint based on privateChan1Policy.
   102  		privateChannel1Hint = zpay32.HopHint{
   103  			NodeID:        privateChannel1.RemotePubkey,
   104  			ChannelID:     private1ShortID,
   105  			FeeBaseMAtoms: uint32(privateChan1Policy.FeeBaseMAtoms),
   106  			FeeProportionalMillionths: uint32(
   107  				privateChan1Policy.FeeProportionalMillionths,
   108  			),
   109  			CLTVExpiryDelta: privateChan1Policy.TimeLockDelta,
   110  		}
   111  
   112  		// Create a second private channel that we'll use for hints.
   113  		private2ShortID uint64 = 2
   114  		privateChannel2        = &HopHintInfo{
   115  			IsPublic: false,
   116  			IsActive: true,
   117  			FundingOutpoint: wire.OutPoint{
   118  				Index: 2,
   119  			},
   120  			RemotePubkey:   pubkey,
   121  			RemoteBalance:  100,
   122  			ShortChannelID: private2ShortID,
   123  		}
   124  
   125  		// Create a edge policy for private channel 1.
   126  		privateChan2Policy = &channeldb.ChannelEdgePolicy{
   127  			FeeBaseMAtoms:             20,
   128  			FeeProportionalMillionths: 200,
   129  			TimeLockDelta:             2000,
   130  		}
   131  
   132  		// Create a hop hint based on privateChan2Policy.
   133  		privateChannel2Hint = zpay32.HopHint{
   134  			NodeID:        privateChannel2.RemotePubkey,
   135  			ChannelID:     private2ShortID,
   136  			FeeBaseMAtoms: uint32(privateChan2Policy.FeeBaseMAtoms),
   137  			FeeProportionalMillionths: uint32(
   138  				privateChan2Policy.FeeProportionalMillionths,
   139  			),
   140  			CLTVExpiryDelta: privateChan2Policy.TimeLockDelta,
   141  		}
   142  
   143  		// Create a third private channel that we'll use for hints.
   144  		private3ShortID uint64 = 3
   145  		privateChannel3        = &HopHintInfo{
   146  			IsPublic: false,
   147  			IsActive: true,
   148  			FundingOutpoint: wire.OutPoint{
   149  				Index: 3,
   150  			},
   151  			RemotePubkey:   pubkey,
   152  			RemoteBalance:  100,
   153  			ShortChannelID: private3ShortID,
   154  		}
   155  
   156  		// Create a edge policy for private channel 1.
   157  		privateChan3Policy = &channeldb.ChannelEdgePolicy{
   158  			FeeBaseMAtoms:             30,
   159  			FeeProportionalMillionths: 300,
   160  			TimeLockDelta:             3000,
   161  		}
   162  
   163  		// Create a hop hint based on privateChan2Policy.
   164  		privateChannel3Hint = zpay32.HopHint{
   165  			NodeID:        privateChannel3.RemotePubkey,
   166  			ChannelID:     private3ShortID,
   167  			FeeBaseMAtoms: uint32(privateChan3Policy.FeeBaseMAtoms),
   168  			FeeProportionalMillionths: uint32(
   169  				privateChan3Policy.FeeProportionalMillionths,
   170  			),
   171  			CLTVExpiryDelta: privateChan3Policy.TimeLockDelta,
   172  		}
   173  	)
   174  
   175  	// We can't copy in the above var decls, so we copy in our pubkey here.
   176  	var peer [33]byte
   177  	copy(peer[:], compressed)
   178  
   179  	var (
   180  		// We pick our policy based on which node (1 or 2) the remote
   181  		// peer is. Here we create two different sets of edge
   182  		// information. One where our peer is node 1, the other where
   183  		// our peer is edge 2. This ensures that we always pick the
   184  		// right edge policy for our hint.
   185  		infoNode1 = &channeldb.ChannelEdgeInfo{
   186  			NodeKey1Bytes: peer,
   187  		}
   188  
   189  		infoNode2 = &channeldb.ChannelEdgeInfo{
   190  			NodeKey1Bytes: [33]byte{9, 9, 9},
   191  			NodeKey2Bytes: peer,
   192  		}
   193  
   194  		// setMockChannelUsed preps our mock for the case where we
   195  		// want our private channel to be used for a hop hint.
   196  		setMockChannelUsed = func(h *hopHintsConfigMock,
   197  			shortID uint64,
   198  			policy *channeldb.ChannelEdgePolicy) {
   199  
   200  			// Return public node = true so that we'll consider
   201  			// this node for our hop hints.
   202  			h.Mock.On(
   203  				"IsPublicNode", peer,
   204  			).Once().Return(true, nil)
   205  
   206  			// When it gets time to find an edge policy for this
   207  			// node, fail it. We won't use it as a hop hint.
   208  			h.Mock.On(
   209  				"FetchChannelEdgesByID",
   210  				shortID,
   211  			).Once().Return(
   212  				infoNode1, policy, otherChanPolicy, nil,
   213  			)
   214  		}
   215  	)
   216  
   217  	tests := []struct {
   218  		name      string
   219  		setupMock func(*hopHintsConfigMock)
   220  		amount    lnwire.MilliAtom
   221  		channels  []*HopHintInfo
   222  		numHints  int
   223  
   224  		// expectedHints is the set of hop hints that we expect. We
   225  		// initialize this slice with our max hop hints length, so this
   226  		// value won't be nil even if its empty.
   227  		expectedHints [][]zpay32.HopHint
   228  	}{
   229  		{
   230  			// We don't need hop hints for public channels.
   231  			name: "channel is public",
   232  			// When a channel is public, we exit before we make any
   233  			// calls.
   234  			setupMock: func(h *hopHintsConfigMock) {
   235  			},
   236  			amount: 100,
   237  			channels: []*HopHintInfo{
   238  				publicChannel,
   239  			},
   240  			numHints:      2,
   241  			expectedHints: nil,
   242  		},
   243  		{
   244  			name:      "channel is inactive",
   245  			setupMock: func(h *hopHintsConfigMock) {},
   246  			amount:    100,
   247  			channels: []*HopHintInfo{
   248  				inactiveChannel,
   249  			},
   250  			numHints:      2,
   251  			expectedHints: nil,
   252  		},
   253  		{
   254  			// If we can't lookup an edge policy, we skip channels.
   255  			name: "no edge policy",
   256  			setupMock: func(h *hopHintsConfigMock) {
   257  				// Return public node = true so that we'll
   258  				// consider this node for our hop hints.
   259  				h.Mock.On(
   260  					"IsPublicNode", peer,
   261  				).Return(true, nil)
   262  
   263  				// When it gets time to find an edge policy for
   264  				// this node, fail it. We won't use it as a
   265  				// hop hint.
   266  				h.Mock.On(
   267  					"FetchChannelEdgesByID",
   268  					private1ShortID,
   269  				).Return(
   270  					nil, nil, nil,
   271  					errors.New("no edge"),
   272  				)
   273  			},
   274  			amount: 100,
   275  			channels: []*HopHintInfo{
   276  				privateChannel1,
   277  			},
   278  			numHints:      3,
   279  			expectedHints: nil,
   280  		},
   281  		{
   282  			// If one of our private channels belongs to a node
   283  			// that is otherwise not announced to the network, we're
   284  			// polite and don't include them (they can't be routed
   285  			// through anyway).
   286  			name: "node is private",
   287  			setupMock: func(h *hopHintsConfigMock) {
   288  				// Return public node = false so that we'll
   289  				// give up on this node.
   290  				h.Mock.On(
   291  					"IsPublicNode", peer,
   292  				).Return(false, nil)
   293  			},
   294  			amount: 100,
   295  			channels: []*HopHintInfo{
   296  				privateChannel1,
   297  			},
   298  			numHints:      1,
   299  			expectedHints: nil,
   300  		},
   301  		{
   302  			// If a channel has more balance than the amount we're
   303  			// looking for, it'll be added in our first pass. We
   304  			// can be sure we're adding it in our first pass because
   305  			// we assert that there are no additional calls to our
   306  			// mock (which would happen if we ran a second pass).
   307  			//
   308  			// We set our peer to be node 1 in our policy ordering.
   309  			name: "balance > total amount, node 1",
   310  			setupMock: func(h *hopHintsConfigMock) {
   311  				setMockChannelUsed(
   312  					h, private1ShortID, privateChan1Policy,
   313  				)
   314  			},
   315  			// Our channel has balance of 100 (> 50).
   316  			amount: 50,
   317  			channels: []*HopHintInfo{
   318  				privateChannel1,
   319  			},
   320  			numHints: 2,
   321  			expectedHints: [][]zpay32.HopHint{
   322  				{
   323  					privateChannel1Hint,
   324  				},
   325  			},
   326  		},
   327  		{
   328  			// As above, but we set our peer to be node 2 in our
   329  			// policy ordering.
   330  			name: "balance > total amount, node 2",
   331  			setupMock: func(h *hopHintsConfigMock) {
   332  				// Return public node = true so that we'll
   333  				// consider this node for our hop hints.
   334  				h.Mock.On(
   335  					"IsPublicNode", peer,
   336  				).Return(true, nil)
   337  
   338  				// When it gets time to find an edge policy for
   339  				// this node, fail it. We won't use it as a
   340  				// hop hint.
   341  				h.Mock.On(
   342  					"FetchChannelEdgesByID",
   343  					private1ShortID,
   344  				).Return(
   345  					infoNode2, otherChanPolicy,
   346  					privateChan1Policy, nil,
   347  				)
   348  			},
   349  			// Our channel has balance of 100 (> 50).
   350  			amount: 50,
   351  			channels: []*HopHintInfo{
   352  				privateChannel1,
   353  			},
   354  			numHints: 2,
   355  			expectedHints: [][]zpay32.HopHint{
   356  				{
   357  					privateChannel1Hint,
   358  				},
   359  			},
   360  		},
   361  		{
   362  			// Since our balance is less than the amount we're
   363  			// looking to route, we expect this hint to be picked
   364  			// up in our second pass on the channel set.
   365  			name: "balance < total amount",
   366  			setupMock: func(h *hopHintsConfigMock) {
   367  				// We expect to call all our checks twice
   368  				// because we pick up this channel in the
   369  				// second round.
   370  				setMockChannelUsed(
   371  					h, private1ShortID, privateChan1Policy,
   372  				)
   373  				setMockChannelUsed(
   374  					h, private1ShortID, privateChan1Policy,
   375  				)
   376  			},
   377  			// Our channel has balance of 100 (< 150).
   378  			amount: 150,
   379  			channels: []*HopHintInfo{
   380  				privateChannel1,
   381  			},
   382  			numHints: 2,
   383  			expectedHints: [][]zpay32.HopHint{
   384  				{
   385  					privateChannel1Hint,
   386  				},
   387  			},
   388  		},
   389  		{
   390  			// Test the case where we hit our total amount of
   391  			// required liquidity in our first pass.
   392  			name: "first pass sufficient balance",
   393  			setupMock: func(h *hopHintsConfigMock) {
   394  				setMockChannelUsed(
   395  					h, private1ShortID, privateChan1Policy,
   396  				)
   397  			},
   398  			// Divide our balance by hop hint factor so that the
   399  			// channel balance will always reach our factored up
   400  			// amount, even if we change this value.
   401  			amount: privateChannel1.RemoteBalance / hopHintFactor,
   402  			channels: []*HopHintInfo{
   403  				privateChannel1,
   404  			},
   405  			numHints: 2,
   406  			expectedHints: [][]zpay32.HopHint{
   407  				{
   408  					privateChannel1Hint,
   409  				},
   410  			},
   411  		},
   412  		{
   413  			// Setup our amount so that we don't have enough
   414  			// inbound total for our amount, but we hit our
   415  			// desired hint limit.
   416  			name: "second pass sufficient hint count",
   417  			setupMock: func(h *hopHintsConfigMock) {
   418  				// We expect all of our channels to be passed
   419  				// on in the first pass.
   420  				setMockChannelUsed(
   421  					h, private1ShortID, privateChan1Policy,
   422  				)
   423  
   424  				setMockChannelUsed(
   425  					h, private2ShortID, privateChan2Policy,
   426  				)
   427  
   428  				// In the second pass, our first two channels
   429  				// should be added before we hit our hint count.
   430  				setMockChannelUsed(
   431  					h, private1ShortID, privateChan1Policy,
   432  				)
   433  
   434  			},
   435  			// Add two channels that we'd want to use, but the
   436  			// second one will be cut off due to our hop hint count
   437  			// limit.
   438  			channels: []*HopHintInfo{
   439  				privateChannel1, privateChannel2,
   440  			},
   441  			// Set the amount we need to more than our two channels
   442  			// can provide us.
   443  			amount: privateChannel1.RemoteBalance +
   444  				privateChannel2.RemoteBalance,
   445  			numHints: 1,
   446  			expectedHints: [][]zpay32.HopHint{
   447  				{
   448  					privateChannel1Hint,
   449  				},
   450  			},
   451  		},
   452  		{
   453  			// Add three channels that are all less than the amount
   454  			// we wish to receive, but collectively will reach the
   455  			// total amount that we need.
   456  			name: "second pass reaches bandwidth requirement",
   457  			setupMock: func(h *hopHintsConfigMock) {
   458  				// In the first round, all channels should be
   459  				// passed on.
   460  				setMockChannelUsed(
   461  					h, private1ShortID, privateChan1Policy,
   462  				)
   463  
   464  				setMockChannelUsed(
   465  					h, private2ShortID, privateChan2Policy,
   466  				)
   467  
   468  				setMockChannelUsed(
   469  					h, private3ShortID, privateChan3Policy,
   470  				)
   471  
   472  				// In the second round, we'll pick up all of
   473  				// our hop hints.
   474  				setMockChannelUsed(
   475  					h, private1ShortID, privateChan1Policy,
   476  				)
   477  
   478  				setMockChannelUsed(
   479  					h, private2ShortID, privateChan2Policy,
   480  				)
   481  
   482  				setMockChannelUsed(
   483  					h, private3ShortID, privateChan3Policy,
   484  				)
   485  			},
   486  			channels: []*HopHintInfo{
   487  				privateChannel1, privateChannel2,
   488  				privateChannel3,
   489  			},
   490  
   491  			// All of our channels have 100 inbound, so none will
   492  			// be picked up in the first round.
   493  			amount:   110,
   494  			numHints: 5,
   495  			expectedHints: [][]zpay32.HopHint{
   496  				{
   497  					privateChannel1Hint,
   498  				},
   499  				{
   500  					privateChannel2Hint,
   501  				},
   502  				{
   503  					privateChannel3Hint,
   504  				},
   505  			},
   506  		},
   507  	}
   508  
   509  	for _, test := range tests {
   510  		test := test
   511  
   512  		t.Run(test.name, func(t *testing.T) {
   513  			// Create mock and prime it for the test case.
   514  			mock := &hopHintsConfigMock{}
   515  			test.setupMock(mock)
   516  			defer mock.AssertExpectations(t)
   517  
   518  			cfg := &SelectHopHintsCfg{
   519  				IsPublicNode:          mock.IsPublicNode,
   520  				FetchChannelEdgesByID: mock.FetchChannelEdgesByID,
   521  			}
   522  
   523  			hints := SelectHopHints(
   524  				test.amount, cfg, test.channels, test.numHints,
   525  			)
   526  
   527  			// SelectHopHints preallocates its hop hint slice, so
   528  			// we check that it is empty if we don't expect any
   529  			// hints, and otherwise assert that the two slices are
   530  			// equal. This allows tests to set their expected value
   531  			// to nil, rather than providing a preallocated empty
   532  			// slice.
   533  			if len(test.expectedHints) == 0 {
   534  				require.Zero(t, len(hints))
   535  			} else {
   536  				require.Equal(t, test.expectedHints, hints)
   537  			}
   538  		})
   539  	}
   540  }