invoicesrpc: refactor addinvocie hop hint selection

In order to reduce the number of calls to the db we try to process as
few channels as we can + try to not do extra work for each of them.

- First fetch all the channels. Then, filter all the public ones and
  sort the potential candidates by remote balance.

- Filter out each potential candidate as soon as possible.

- Only check the alias if the channel supports scid aliases.

- Because we sort the channels by remote balance, we will hit the
  target amount, if possible, as soon as we can.

We do not want to leak information about our remote balances, so we
shuffle the hop hints (the forced ones go always first) so the invoice
receiver does not know which channels have more balance than others.
This commit is contained in:
positiveblue committed 2022-09-30 06:55:10 -07:00
1 parent 0e803172d6
commit fbe79811cf
3 files changed
+228 -863

No files matched your search

-629
View File
@@ -1,629 +0,0 @@
package invoicesrpc
import (
"encoding/hex"
"errors"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/zpay32"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type hopHintsConfigMock struct {
mock.Mock
}
// IsPublicNode mocks node public state lookup.
func (h *hopHintsConfigMock) IsPublicNode(pubKey [33]byte) (bool, error) {
args := h.Mock.Called(pubKey)
return args.Bool(0), args.Error(1)
}
// FetchChannelEdgesByID mocks channel edge lookup.
func (h *hopHintsConfigMock) FetchChannelEdgesByID(chanID uint64) (
*channeldb.ChannelEdgeInfo, *channeldb.ChannelEdgePolicy,
*channeldb.ChannelEdgePolicy, error) {
args := h.Mock.Called(chanID)
// If our error is non-nil, we expect nil responses otherwise. Our
// casts below will fail with nil values, so we check our error and
// return early on failure first.
err := args.Error(3)
if err != nil {
return nil, nil, nil, err
}
edgeInfo := args.Get(0).(*channeldb.ChannelEdgeInfo)
policy1 := args.Get(1).(*channeldb.ChannelEdgePolicy)
policy2 := args.Get(2).(*channeldb.ChannelEdgePolicy)
return edgeInfo, policy1, policy2, err
}
// TestSelectHopHints tests selection of hop hints for a node with private
// channels.
func TestSelectHopHints(t *testing.T) {
var (
// We need to serialize our pubkey in SelectHopHints so it
// needs to be valid.
pubkeyBytes, _ = hex.DecodeString(
"598ec453728e0ffe0ae2f5e174243cf58f2" +
"a3f2c83d2457b43036db568b11093",
)
pubKeyY = new(btcec.FieldVal)
_ = pubKeyY.SetByteSlice(pubkeyBytes)
pubkey = btcec.NewPublicKey(
new(btcec.FieldVal).SetInt(4),
pubKeyY,
)
compressed = pubkey.SerializeCompressed()
publicChannel = &HopHintInfo{
IsPublic: true,
IsActive: true,
FundingOutpoint: wire.OutPoint{
Index: 0,
},
RemoteBalance: 10,
ShortChannelID: 0,
}
inactiveChannel = &HopHintInfo{
IsPublic: false,
IsActive: false,
}
// Create a private channel that we'll generate hints from.
private1ShortID uint64 = 1
privateChannel1 = &HopHintInfo{
IsPublic: false,
IsActive: true,
FundingOutpoint: wire.OutPoint{
Index: 1,
},
RemotePubkey: pubkey,
RemoteBalance: 100,
ShortChannelID: private1ShortID,
}
// Create a edge policy for private channel 1.
privateChan1Policy = &channeldb.ChannelEdgePolicy{
FeeBaseMSat: 10,
FeeProportionalMillionths: 100,
TimeLockDelta: 1000,
}
// Create an edge policy different to ours which we'll use for
// the other direction
otherChanPolicy = &channeldb.ChannelEdgePolicy{
FeeBaseMSat: 90,
FeeProportionalMillionths: 900,
TimeLockDelta: 9000,
}
// Create a hop hint based on privateChan1Policy.
privateChannel1Hint = zpay32.HopHint{
NodeID: privateChannel1.RemotePubkey,
ChannelID: private1ShortID,
FeeBaseMSat: uint32(privateChan1Policy.FeeBaseMSat),
FeeProportionalMillionths: uint32(
privateChan1Policy.FeeProportionalMillionths,
),
CLTVExpiryDelta: privateChan1Policy.TimeLockDelta,
}
// Create a second private channel that we'll use for hints.
private2ShortID uint64 = 2
privateChannel2 = &HopHintInfo{
IsPublic: false,
IsActive: true,
FundingOutpoint: wire.OutPoint{
Index: 2,
},
RemotePubkey: pubkey,
RemoteBalance: 100,
ShortChannelID: private2ShortID,
}
// Create a edge policy for private channel 1.
privateChan2Policy = &channeldb.ChannelEdgePolicy{
FeeBaseMSat: 20,
FeeProportionalMillionths: 200,
TimeLockDelta: 2000,
}
// Create a hop hint based on privateChan2Policy.
privateChannel2Hint = zpay32.HopHint{
NodeID: privateChannel2.RemotePubkey,
ChannelID: private2ShortID,
FeeBaseMSat: uint32(privateChan2Policy.FeeBaseMSat),
FeeProportionalMillionths: uint32(
privateChan2Policy.FeeProportionalMillionths,
),
CLTVExpiryDelta: privateChan2Policy.TimeLockDelta,
}
// Create a third private channel that we'll use for hints.
private3ShortID uint64 = 3
privateChannel3 = &HopHintInfo{
IsPublic: false,
IsActive: true,
FundingOutpoint: wire.OutPoint{
Index: 3,
},
RemotePubkey: pubkey,
RemoteBalance: 100,
ShortChannelID: private3ShortID,
}
// Create a edge policy for private channel 1.
privateChan3Policy = &channeldb.ChannelEdgePolicy{
FeeBaseMSat: 30,
FeeProportionalMillionths: 300,
TimeLockDelta: 3000,
}
// Create a hop hint based on privateChan2Policy.
privateChannel3Hint = zpay32.HopHint{
NodeID: privateChannel3.RemotePubkey,
ChannelID: private3ShortID,
FeeBaseMSat: uint32(privateChan3Policy.FeeBaseMSat),
FeeProportionalMillionths: uint32(
privateChan3Policy.FeeProportionalMillionths,
),
CLTVExpiryDelta: privateChan3Policy.TimeLockDelta,
}
)
// We can't copy in the above var decls, so we copy in our pubkey here.
var peer [33]byte
copy(peer[:], compressed)
var (
// We pick our policy based on which node (1 or 2) the remote
// peer is. Here we create two different sets of edge
// information. One where our peer is node 1, the other where
// our peer is edge 2. This ensures that we always pick the
// right edge policy for our hint.
infoNode1 = &channeldb.ChannelEdgeInfo{
NodeKey1Bytes: peer,
}
infoNode2 = &channeldb.ChannelEdgeInfo{
NodeKey1Bytes: [33]byte{9, 9, 9},
NodeKey2Bytes: peer,
}
// setMockChannelUsed preps our mock for the case where we
// want our private channel to be used for a hop hint.
setMockChannelUsed = func(h *hopHintsConfigMock,
shortID uint64,
policy *channeldb.ChannelEdgePolicy) {
// Return public node = true so that we'll consider
// this node for our hop hints.
h.Mock.On(
"IsPublicNode", peer,
).Once().Return(true, nil)
// When it gets time to find an edge policy for this
// node, fail it. We won't use it as a hop hint.
h.Mock.On(
"FetchChannelEdgesByID",
shortID,
).Once().Return(
infoNode1, policy, otherChanPolicy, nil,
)
}
)
tests := []struct {
name string
setupMock func(*hopHintsConfigMock)
amount lnwire.MilliSatoshi
channels []*HopHintInfo
numHints int
// expectedHints is the set of hop hints that we expect. We
// initialize this slice with our max hop hints length, so this
// value won't be nil even if its empty.
expectedHints [][]zpay32.HopHint
}{
{
// We don't need hop hints for public channels.
name: "channel is public",
// When a channel is public, we exit before we make any
// calls.
setupMock: func(h *hopHintsConfigMock) {
},
amount: 100,
channels: []*HopHintInfo{
publicChannel,
},
numHints: 2,
expectedHints: nil,
},
{
name: "channel is inactive",
setupMock: func(h *hopHintsConfigMock) {},
amount: 100,
channels: []*HopHintInfo{
inactiveChannel,
},
numHints: 2,
expectedHints: nil,
},
{
// If we can't lookup an edge policy, we skip channels.
name: "no edge policy",
setupMock: func(h *hopHintsConfigMock) {
// Return public node = true so that we'll
// consider this node for our hop hints.
h.Mock.On(
"IsPublicNode", peer,
).Return(true, nil)
// When it gets time to find an edge policy for
// this node, fail it. We won't use it as a
// hop hint.
h.Mock.On(
"FetchChannelEdgesByID",
mock.Anything,
).Return(
nil, nil, nil,
errors.New("no edge"),
).Times(4)
},
amount: 100,
channels: []*HopHintInfo{
privateChannel1,
},
numHints: 3,
expectedHints: nil,
},
{
// If one of our private channels belongs to a node
// that is otherwise not announced to the network, we're
// polite and don't include them (they can't be routed
// through anyway).
name: "node is private",
setupMock: func(h *hopHintsConfigMock) {
// Return public node = false so that we'll
// give up on this node.
h.Mock.On(
"IsPublicNode", peer,
).Return(false, nil)
},
amount: 100,
channels: []*HopHintInfo{
privateChannel1,
},
numHints: 1,
expectedHints: nil,
},
{
// This test case asserts that we limit our hop hints
// when we've reached our maximum number of hints.
name: "too many hints",
setupMock: func(h *hopHintsConfigMock) {
setMockChannelUsed(
h, private1ShortID, privateChan1Policy,
)
},
// Set our amount to less than our channel balance of
// 100.
amount: 30,
channels: []*HopHintInfo{
privateChannel1, privateChannel2,
},
numHints: 1,
expectedHints: [][]zpay32.HopHint{
{
privateChannel1Hint,
},
},
},
{
// If a channel has more balance than the amount we're
// looking for, it'll be added in our first pass. We
// can be sure we're adding it in our first pass because
// we assert that there are no additional calls to our
// mock (which would happen if we ran a second pass).
//
// We set our peer to be node 1 in our policy ordering.
name: "balance > total amount, node 1",
setupMock: func(h *hopHintsConfigMock) {
setMockChannelUsed(
h, private1ShortID, privateChan1Policy,
)
},
// Our channel has balance of 100 (> 50).
amount: 50,
channels: []*HopHintInfo{
privateChannel1,
},
numHints: 2,
expectedHints: [][]zpay32.HopHint{
{
privateChannel1Hint,
},
},
},
{
// As above, but we set our peer to be node 2 in our
// policy ordering.
name: "balance > total amount, node 2",
setupMock: func(h *hopHintsConfigMock) {
// Return public node = true so that we'll
// consider this node for our hop hints.
h.Mock.On(
"IsPublicNode", peer,
).Return(true, nil)
// When it gets time to find an edge policy for
// this node, fail it. We won't use it as a
// hop hint.
h.Mock.On(
"FetchChannelEdgesByID",
private1ShortID,
).Return(
infoNode2, otherChanPolicy,
privateChan1Policy, nil,
)
},
// Our channel has balance of 100 (> 50).
amount: 50,
channels: []*HopHintInfo{
privateChannel1,
},
numHints: 2,
expectedHints: [][]zpay32.HopHint{
{
privateChannel1Hint,
},
},
},
{
// Since our balance is less than the amount we're
// looking to route, we expect this hint to be picked
// up in our second pass on the channel set.
name: "balance < total amount",
setupMock: func(h *hopHintsConfigMock) {
// We expect to call all our checks twice
// because we pick up this channel in the
// second round.
setMockChannelUsed(
h, private1ShortID, privateChan1Policy,
)
setMockChannelUsed(
h, private1ShortID, privateChan1Policy,
)
},
// Our channel has balance of 100 (< 150).
amount: 150,
channels: []*HopHintInfo{
privateChannel1,
},
numHints: 2,
expectedHints: [][]zpay32.HopHint{
{
privateChannel1Hint,
},
},
},
{
// Test the case where we hit our total amount of
// required liquidity in our first pass.
name: "first pass sufficient balance",
setupMock: func(h *hopHintsConfigMock) {
setMockChannelUsed(
h, private1ShortID, privateChan1Policy,
)
},
// Divide our balance by hop hint factor so that the
// channel balance will always reach our factored up
// amount, even if we change this value.
amount: privateChannel1.RemoteBalance / hopHintFactor,
channels: []*HopHintInfo{
privateChannel1,
},
numHints: 2,
expectedHints: [][]zpay32.HopHint{
{
privateChannel1Hint,
},
},
},
{
// Setup our amount so that we don't have enough
// inbound total for our amount, but we hit our
// desired hint limit.
name: "second pass sufficient hint count",
setupMock: func(h *hopHintsConfigMock) {
// We expect all of our channels to be passed
// on in the first pass.
setMockChannelUsed(
h, private1ShortID, privateChan1Policy,
)
setMockChannelUsed(
h, private2ShortID, privateChan2Policy,
)
// In the second pass, our first two channels
// should be added before we hit our hint count.
setMockChannelUsed(
h, private1ShortID, privateChan1Policy,
)
},
// Add two channels that we'd want to use, but the
// second one will be cut off due to our hop hint count
// limit.
channels: []*HopHintInfo{
privateChannel1, privateChannel2,
},
// Set the amount we need to more than our two channels
// can provide us.
amount: privateChannel1.RemoteBalance +
privateChannel2.RemoteBalance,
numHints: 1,
expectedHints: [][]zpay32.HopHint{
{
privateChannel1Hint,
},
},
},
{
// Add three channels that are all less than the amount
// we wish to receive, but collectively will reach the
// total amount that we need.
name: "second pass reaches bandwidth requirement",
setupMock: func(h *hopHintsConfigMock) {
// In the first round, all channels should be
// passed on.
setMockChannelUsed(
h, private1ShortID, privateChan1Policy,
)
setMockChannelUsed(
h, private2ShortID, privateChan2Policy,
)
setMockChannelUsed(
h, private3ShortID, privateChan3Policy,
)
// In the second round, we'll pick up all of
// our hop hints.
setMockChannelUsed(
h, private1ShortID, privateChan1Policy,
)
setMockChannelUsed(
h, private2ShortID, privateChan2Policy,
)
setMockChannelUsed(
h, private3ShortID, privateChan3Policy,
)
},
channels: []*HopHintInfo{
privateChannel1, privateChannel2,
privateChannel3,
},
// All of our channels have 100 inbound, so none will
// be picked up in the first round.
amount: 110,
numHints: 5,
expectedHints: [][]zpay32.HopHint{
{
privateChannel1Hint,
},
{
privateChannel2Hint,
},
{
privateChannel3Hint,
},
},
},
}
getAlias := func(lnwire.ChannelID) (lnwire.ShortChannelID, error) {
return lnwire.ShortChannelID{}, nil
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
// Create mock and prime it for the test case.
mock := &hopHintsConfigMock{}
test.setupMock(mock)
defer mock.AssertExpectations(t)
cfg := &SelectHopHintsCfg{
IsPublicNode: mock.IsPublicNode,
FetchChannelEdgesByID: mock.FetchChannelEdgesByID,
GetAlias: getAlias,
}
hints := SelectHopHints(
test.amount, cfg, test.channels, test.numHints,
)
// SelectHopHints preallocates its hop hint slice, so
// we check that it is empty if we don't expect any
// hints, and otherwise assert that the two slices are
// equal. This allows tests to set their expected value
// to nil, rather than providing a preallocated empty
// slice.
if len(test.expectedHints) == 0 {
require.Zero(t, len(hints))
} else {
require.Equal(t, test.expectedHints, hints)
}
})
}
}
// TestSufficientHopHints tests limiting our hops to a set number of hints or
// scaled amount of capacity.
func TestSufficientHopHints(t *testing.T) {
t.Parallel()
tests := []struct {
name string
numHints int
maxHints int
scalingFactor int
amount lnwire.MilliSatoshi
totalHintAmount lnwire.MilliSatoshi
sufficient bool
}{
{
name: "not enough hints or amount",
numHints: 3,
maxHints: 10,
// We want to have at least 200, and we currently have
// 10.
scalingFactor: 2,
amount: 100,
totalHintAmount: 10,
sufficient: false,
},
{
name: "enough hints",
numHints: 3,
maxHints: 3,
sufficient: true,
},
{
name: "not enough hints, insufficient bandwidth",
numHints: 1,
maxHints: 3,
// We want at least 200, and we have enough.
scalingFactor: 2,
amount: 100,
totalHintAmount: 700,
sufficient: true,
},
}
for _, testCase := range tests {
sufficient := sufficientHints(
testCase.numHints, testCase.maxHints,
testCase.scalingFactor, testCase.amount,
testCase.totalHintAmount,
)
require.Equal(t, testCase.sufficient, sufficient)
}
}