diff --git a/lnwire/msat.go b/lnwire/msat.go index 67ee4972..d3789dfa 100644 --- a/lnwire/msat.go +++ b/lnwire/msat.go @@ -6,9 +6,15 @@ import ( "github.com/btcsuite/btcutil" ) -// mSatScale is a value that's used to scale satoshis to milli-satoshis, and -// the other way around. -const mSatScale uint64 = 1000 +const ( + // mSatScale is a value that's used to scale satoshis to milli-satoshis, and + // the other way around. + mSatScale uint64 = 1000 + + // MaxMilliSatoshi is the maximum number of msats that can be expressed + // in this data type. + MaxMilliSatoshi = ^MilliSatoshi(0) +) // MilliSatoshi are the native unit of the Lightning Network. A milli-satoshi // is simply 1/1000th of a satoshi. There are 1000 milli-satoshis in a single diff --git a/routing/pathfind.go b/routing/pathfind.go index 743adad1..c3e335ce 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -392,32 +392,11 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, // processEdge is a helper closure that will be used to make sure edges // satisfy our specific requirements. - processEdge := func(fromVertex route.Vertex, bandwidth lnwire.MilliSatoshi, + processEdge := func(fromVertex route.Vertex, edge *channeldb.ChannelEdgePolicy, toNodeDist *nodeWithDist) { edgesExpanded++ - // If this is not a local channel and it is disabled, we will - // skip it. - // TODO(halseth): also ignore disable flags for non-local - // channels if bandwidth hint is set? - isSourceChan := fromVertex == source - - edgeFlags := edge.ChannelFlags - isDisabled := edgeFlags&lnwire.ChanUpdateDisabled != 0 - - if !isSourceChan && isDisabled { - return - } - - // If we have an outgoing channel restriction and this is not - // the specified channel, skip it. - if isSourceChan && r.OutgoingChannelID != nil && - *r.OutgoingChannelID != edge.ChannelID { - - return - } - // Calculate amount that the candidate node would have to sent // out. amountToSend := toNodeDist.amountToReceive @@ -438,25 +417,6 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, return } - // If the estimated bandwidth of the channel edge is not able - // to carry the amount that needs to be send, return. - if bandwidth < amountToSend { - return - } - - // If the amountToSend is less than the minimum required - // amount, return. - if amountToSend < edge.MinHTLC { - return - } - - // If this edge was constructed from a hop hint, we won't have access to - // its max HTLC. Therefore, only consider discarding this edge here if - // the field is set. - if edge.MaxHTLC != 0 && edge.MaxHTLC < amountToSend { - return - } - // Compute fee that fromVertex is charging. It is based on the // amount that needs to be sent to the next node in the route. // @@ -585,67 +545,34 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, break } - cb := func(_ *bbolt.Tx, edgeInfo *channeldb.ChannelEdgeInfo, _, - inEdge *channeldb.ChannelEdgePolicy) error { + // Create unified policies for all incoming connections. + u := newUnifiedPolicies(source, pivot, r.OutgoingChannelID) - // If there is no edge policy for this candidate - // node, skip. Note that we are searching backwards - // so this node would have come prior to the pivot - // node in the route. - if inEdge == nil { - return nil - } - - // We'll query the lower layer to see if we can obtain - // any more up to date information concerning the - // bandwidth of this edge. - edgeBandwidth, ok := g.bandwidthHints[edgeInfo.ChannelID] - if !ok { - // If we don't have a hint for this edge, then - // we'll just use the known Capacity/MaxHTLC as - // the available bandwidth. It's possible for - // the capacity to be unknown when operating - // under a light client. - edgeBandwidth = inEdge.MaxHTLC - if edgeBandwidth == 0 { - edgeBandwidth = lnwire.NewMSatFromSatoshis( - edgeInfo.Capacity, - ) - } - } - - // Before we can process the edge, we'll need to fetch - // the node on the _other_ end of this channel as we - // may later need to iterate over the incoming edges of - // this node if we explore it further. - chanSource, err := edgeInfo.OtherNodeKeyBytes(pivot[:]) - if err != nil { - return err - } - - // Check if this candidate node is better than what we - // already have. - processEdge(chanSource, edgeBandwidth, inEdge, partialPath) - return nil - } - - // Now that we've found the next potential step to take we'll - // examine all the incoming edges (channels) from this node to - // further our graph traversal. - err := g.graph.ForEachNodeChannel(tx, pivot[:], cb) + err := u.addGraphPolicies(g.graph, tx) if err != nil { return nil, err } - // Then, we'll examine all the additional edges from the node - // we're currently visiting. Since we don't know the capacity - // of the private channel, we'll assume it was selected as a - // routing hint due to having enough capacity for the payment - // and use the payment amount as its capacity. - bandWidth := partialPath.amountToReceive for _, reverseEdge := range additionalEdgesWithSrc[pivot] { - processEdge(reverseEdge.sourceNode, bandWidth, - reverseEdge.edge, partialPath) + u.addPolicy(reverseEdge.sourceNode, reverseEdge.edge, 0) + } + + amtToSend := partialPath.amountToReceive + + // Expand all connections using the optimal policy for each + // connection. + for fromNode, unifiedPolicy := range u.policies { + policy := unifiedPolicy.getPolicy( + amtToSend, g.bandwidthHints, + ) + + if policy == nil { + continue + } + + // Check if this candidate node is better than what we + // already have. + processEdge(fromNode, policy, partialPath) } } diff --git a/routing/unified_policies.go b/routing/unified_policies.go new file mode 100644 index 00000000..ead387a2 --- /dev/null +++ b/routing/unified_policies.go @@ -0,0 +1,269 @@ +package routing + +import ( + "github.com/btcsuite/btcutil" + "github.com/coreos/bbolt" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" +) + +// unifiedPolicies holds all unified policies for connections towards a node. +type unifiedPolicies struct { + // policies contains a unified policy for every from node. + policies map[route.Vertex]*unifiedPolicy + + // sourceNode is the sender of a payment. The rules to pick the final + // policy are different for local channels. + sourceNode route.Vertex + + // toNode is the node for which the unified policies are instantiated. + toNode route.Vertex + + // outChanRestr is an optional outgoing channel restriction for the + // local channel to use. + outChanRestr *uint64 +} + +// newUnifiedPolicies instantiates a new unifiedPolicies object. Channel +// policies can be added to this object. +func newUnifiedPolicies(sourceNode, toNode route.Vertex, + outChanRestr *uint64) *unifiedPolicies { + + return &unifiedPolicies{ + policies: make(map[route.Vertex]*unifiedPolicy), + toNode: toNode, + sourceNode: sourceNode, + outChanRestr: outChanRestr, + } +} + +// addPolicy adds a single channel policy. Capacity may be zero if unknown +// (light clients). +func (u *unifiedPolicies) addPolicy(fromNode route.Vertex, + edge *channeldb.ChannelEdgePolicy, capacity btcutil.Amount) { + + localChan := fromNode == u.sourceNode + + // Skip channels if there is an outgoing channel restriction. + if localChan && u.outChanRestr != nil && + *u.outChanRestr != edge.ChannelID { + + return + } + + // Update the policies map. + policy, ok := u.policies[fromNode] + if !ok { + policy = &unifiedPolicy{ + localChan: localChan, + } + u.policies[fromNode] = policy + } + + policy.edges = append(policy.edges, &unifiedPolicyEdge{ + policy: edge, + capacity: capacity, + }) +} + +// addGraphPolicies adds all policies that are known for the toNode in the +// graph. +func (u *unifiedPolicies) addGraphPolicies(g *channeldb.ChannelGraph, + tx *bbolt.Tx) error { + + cb := func(_ *bbolt.Tx, edgeInfo *channeldb.ChannelEdgeInfo, _, + inEdge *channeldb.ChannelEdgePolicy) error { + + // If there is no edge policy for this candidate node, skip. + // Note that we are searching backwards so this node would have + // come prior to the pivot node in the route. + if inEdge == nil { + return nil + } + + // The node on the other end of this channel is the from node. + fromNode, err := edgeInfo.OtherNodeKeyBytes(u.toNode[:]) + if err != nil { + return err + } + + // Add this policy to the unified policies map. + u.addPolicy(fromNode, inEdge, edgeInfo.Capacity) + + return nil + } + + // Iterate over all channels of the to node. + return g.ForEachNodeChannel(tx, u.toNode[:], cb) +} + +// unifiedPolicyEdge is the individual channel data that is kept inside an +// unifiedPolicy object. +type unifiedPolicyEdge struct { + policy *channeldb.ChannelEdgePolicy + capacity btcutil.Amount +} + +// amtInRange checks whether an amount falls within the valid range for a +// channel. +func (u *unifiedPolicyEdge) amtInRange(amt lnwire.MilliSatoshi) bool { + // If the capacity is available (non-light clients), skip channels that + // are too small. + if u.capacity > 0 && + amt > lnwire.NewMSatFromSatoshis(u.capacity) { + + return false + } + + // Skip channels for which this htlc is too large. + if u.policy.MessageFlags.HasMaxHtlc() && + amt > u.policy.MaxHTLC { + + return false + } + + // Skip channels for which this htlc is too small. + if amt < u.policy.MinHTLC { + return false + } + + return true +} + +// unifiedPolicy is the unified policy that covers all channels between a pair +// of nodes. +type unifiedPolicy struct { + edges []*unifiedPolicyEdge + localChan bool +} + +// getPolicy returns the optimal policy to use for this connection given a +// specific amount to send. It differentiates between local and network +// channels. +func (u *unifiedPolicy) getPolicy(amt lnwire.MilliSatoshi, + bandwidthHints map[uint64]lnwire.MilliSatoshi) *channeldb.ChannelEdgePolicy { + + if u.localChan { + return u.getPolicyLocal(amt, bandwidthHints) + } + + return u.getPolicyNetwork(amt) +} + +// getPolicyLocal returns the optimal policy to use for this local connection +// given a specific amount to send. +func (u *unifiedPolicy) getPolicyLocal(amt lnwire.MilliSatoshi, + bandwidthHints map[uint64]lnwire.MilliSatoshi) *channeldb.ChannelEdgePolicy { + + var ( + bestPolicy *channeldb.ChannelEdgePolicy + maxBandwidth lnwire.MilliSatoshi + ) + + for _, edge := range u.edges { + // Check valid amount range for the channel. + if !edge.amtInRange(amt) { + continue + } + + // For local channels, there is no fee to pay or an extra time + // lock. We only consider the currently available bandwidth for + // channel selection. The disabled flag is ignored for local + // channels. + + // Retrieve bandwidth for this local channel. If not + // available, assume this channel has enough bandwidth. + // + // TODO(joostjager): Possibly change to skipping this + // channel. The bandwidth hint is expected to be + // available. + bandwidth, ok := bandwidthHints[edge.policy.ChannelID] + if !ok { + bandwidth = lnwire.MaxMilliSatoshi + } + + // Skip channels that can't carry the payment. + if amt > bandwidth { + continue + } + + // We pick the local channel with the highest available + // bandwidth, to maximize the success probability. It + // can be that the channel state changes between + // querying the bandwidth hints and sending out the + // htlc. + if bandwidth < maxBandwidth { + continue + } + maxBandwidth = bandwidth + + // Update best policy. + bestPolicy = edge.policy + } + + return bestPolicy +} + +// getPolicyNetwork returns the optimal policy to use for this connection given +// a specific amount to send. The goal is to return a policy that maximizes the +// probability of a successful forward in a non-strict forwarding context. +func (u *unifiedPolicy) getPolicyNetwork( + amt lnwire.MilliSatoshi) *channeldb.ChannelEdgePolicy { + + var ( + bestPolicy *channeldb.ChannelEdgePolicy + maxFee lnwire.MilliSatoshi + maxTimelock uint16 + ) + + for _, edge := range u.edges { + // Check valid amount range for the channel. + if !edge.amtInRange(amt) { + continue + } + + // For network channels, skip the disabled ones. + edgeFlags := edge.policy.ChannelFlags + isDisabled := edgeFlags&lnwire.ChanUpdateDisabled != 0 + if isDisabled { + continue + } + + // Track the maximum time lock of all channels that are + // candidate for non-strict forwarding at the routing node. + if edge.policy.TimeLockDelta > maxTimelock { + maxTimelock = edge.policy.TimeLockDelta + } + + // Use the policy that results in the highest fee for this + // specific amount. + fee := edge.policy.ComputeFee(amt) + if fee < maxFee { + continue + } + maxFee = fee + + bestPolicy = edge.policy + } + + // Return early if no channel matches. + if bestPolicy == nil { + return nil + } + + // We have already picked the highest fee that could be required for + // non-strict forwarding. To also cover the case where a lower fee + // channel requires a longer time lock, we modify the policy by setting + // the maximum encountered time lock. Note that this results in a + // synthetic policy that is not actually present on the routing node. + // + // The reason we do this, is that we try to maximize the chance that we + // get forwarded. Because we penalize pair-wise, there won't be a second + // chance for this node pair. But this is all only needed for nodes that + // have distinct policies for channels to the same peer. + modifiedPolicy := *bestPolicy + modifiedPolicy.TimeLockDelta = maxTimelock + + return &modifiedPolicy +} diff --git a/routing/unified_policies_test.go b/routing/unified_policies_test.go new file mode 100644 index 00000000..e89a3cb1 --- /dev/null +++ b/routing/unified_policies_test.go @@ -0,0 +1,91 @@ +package routing + +import ( + "testing" + + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" +) + +// TestUnifiedPolicies tests the composition of unified policies for nodes that +// have multiple channels between them. +func TestUnifiedPolicies(t *testing.T) { + source := route.Vertex{1} + toNode := route.Vertex{2} + fromNode := route.Vertex{3} + + bandwidthHints := map[uint64]lnwire.MilliSatoshi{} + + u := newUnifiedPolicies(source, toNode, nil) + + // Add two channels between the pair of nodes. + p1 := channeldb.ChannelEdgePolicy{ + FeeProportionalMillionths: 100000, + FeeBaseMSat: 30, + TimeLockDelta: 60, + MessageFlags: lnwire.ChanUpdateOptionMaxHtlc, + MaxHTLC: 500, + MinHTLC: 100, + } + p2 := channeldb.ChannelEdgePolicy{ + FeeProportionalMillionths: 190000, + FeeBaseMSat: 10, + TimeLockDelta: 40, + MessageFlags: lnwire.ChanUpdateOptionMaxHtlc, + MaxHTLC: 400, + MinHTLC: 100, + } + u.addPolicy(fromNode, &p1, 7) + u.addPolicy(fromNode, &p2, 7) + + checkPolicy := func(policy *channeldb.ChannelEdgePolicy, + feeBase lnwire.MilliSatoshi, feeRate lnwire.MilliSatoshi, + timeLockDelta uint16) { + + t.Helper() + + if policy.FeeBaseMSat != feeBase { + t.Fatalf("expected fee base %v, got %v", + feeBase, policy.FeeBaseMSat) + } + + if policy.TimeLockDelta != timeLockDelta { + t.Fatalf("expected fee base %v, got %v", + timeLockDelta, policy.TimeLockDelta) + } + + if policy.FeeProportionalMillionths != feeRate { + t.Fatalf("expected fee rate %v, got %v", + feeRate, policy.FeeProportionalMillionths) + } + } + + policy := u.policies[fromNode].getPolicy(50, bandwidthHints) + if policy != nil { + t.Fatal("expected no policy for amt below min htlc") + } + + policy = u.policies[fromNode].getPolicy(550, bandwidthHints) + if policy != nil { + t.Fatal("expected no policy for amt above max htlc") + } + + // For 200 sat, p1 yields the highest fee. Use that policy to forward, + // because it will also match p2 in case p1 does not have enough + // balance. + policy = u.policies[fromNode].getPolicy(200, bandwidthHints) + checkPolicy( + policy, p1.FeeBaseMSat, p1.FeeProportionalMillionths, + p1.TimeLockDelta, + ) + + // For 400 sat, p2 yields the highest fee. Use that policy to forward, + // because it will also match p1 in case p2 does not have enough + // balance. In order to match p1, it needs to have p1's time lock delta. + policy = u.policies[fromNode].getPolicy(400, bandwidthHints) + checkPolicy( + policy, p2.FeeBaseMSat, p2.FeeProportionalMillionths, + p1.TimeLockDelta, + ) +}