From 83a1848d56f967907aef190356de1990b566f56d Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Thu, 20 Oct 2022 13:21:37 +0200 Subject: [PATCH] utils: fix SelectHopHints for lnd0.15.3 This commit adds the old SelectHopHints logic to utils in order to allow loop to compile with lnd0.15.3 --- go.mod | 4 +- go.sum | 16 ++- utils.go | 358 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 357 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 336cc98..e52756e 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/lightninglabs/loop require ( - github.com/btcsuite/btcd v0.23.1 + github.com/btcsuite/btcd v0.23.2 github.com/btcsuite/btcd/btcec/v2 v2.2.1 github.com/btcsuite/btcd/btcutil v1.1.2 github.com/btcsuite/btcd/btcutil/psbt v1.1.5 @@ -18,7 +18,7 @@ require ( github.com/lightninglabs/lndclient v0.15.1-5 github.com/lightninglabs/loop/swapserverrpc v1.0.1 github.com/lightninglabs/protobuf-hex-display v1.4.3-hex-display - github.com/lightningnetwork/lnd v0.15.1-beta + github.com/lightningnetwork/lnd v0.15.3-beta github.com/lightningnetwork/lnd/cert v1.1.1 github.com/lightningnetwork/lnd/clock v1.1.0 github.com/lightningnetwork/lnd/queue v1.1.0 diff --git a/go.sum b/go.sum index 390aff8..e4492b8 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,9 @@ github.com/btcsuite/btcd v0.22.0-beta.0.20220204213055-eaf0459ff879/go.mod h1:os github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4/go.mod h1:7alexyj/lHlOtr2PJK7L/+HDJZpcGDn/pAU98r7DY08= github.com/btcsuite/btcd v0.22.0-beta.0.20220316175102-8d5c75c28923/go.mod h1:taIcYprAW2g6Z9S0gGUxyR+zDwimyDMK5ePOX+iJ2ds= github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= -github.com/btcsuite/btcd v0.23.1 h1:IB8cVQcC2X5mHbnfirLG5IZnkWYNTPlLZVrxUYSotbE= github.com/btcsuite/btcd v0.23.1/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd v0.23.2 h1:/YOgUp25sdCnP5ho6Hl3s0E438zlX+Kak7E6TgBgoT0= +github.com/btcsuite/btcd v0.23.2/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.1/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= @@ -101,15 +102,19 @@ github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcwallet v0.14.0/go.mod h1:KFR1x3ZH7c31i4qA34XIvcsnhrEBLK1SHli52lN8E54= -github.com/btcsuite/btcwallet v0.15.1 h1:SKfh/l2Bgz9sJwHZvfiVbZ8Pl3N/8fFcWWXzsAPz9GU= github.com/btcsuite/btcwallet v0.15.1/go.mod h1:7OFsQ8ypiRwmr67hE0z98uXgJgXGAihE79jCib9x6ag= +github.com/btcsuite/btcwallet v0.16.1 h1:nD8qXJeAU8c7a0Jlx5jwI2ufbf/9ouy29XGapRLTmos= +github.com/btcsuite/btcwallet v0.16.1/go.mod h1:NCO8+5rIcbUm5CtVNSQM0xrtK4iYprlyuvpGzhkejaM= github.com/btcsuite/btcwallet/wallet/txauthor v1.2.1/go.mod h1:/74bubxX5Js48d76nf/TsNabpYp/gndUuJw4chzCmhU= -github.com/btcsuite/btcwallet/wallet/txauthor v1.2.3 h1:M2yr5UlULvpqtxUqpMxTME/pA92Z9cpqeyvAFk9lAg0= github.com/btcsuite/btcwallet/wallet/txauthor v1.2.3/go.mod h1:T2xSiKGpUkSLCh68aF+FMXmKK9mFqNdHl9VaqOr+JjU= +github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2 h1:etuLgGEojecsDOYTII8rYiGHjGyV5xTqsXi+ZQ715UU= +github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2/go.mod h1:Zpk/LOb2sKqwP2lmHjaZT9AdaKsHPSbNLm2Uql5IQ/0= github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 h1:BtEN5Empw62/RVnZ0VcJaVtVlBijnLlJY+dwjAye2Bg= github.com/btcsuite/btcwallet/wallet/txrules v1.2.0/go.mod h1:AtkqiL7ccKWxuLYtZm8Bu8G6q82w4yIZdgq6riy60z0= -github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 h1:wZnOolEAeNOHzHTnznw/wQv+j35ftCIokNrnOTOU5o8= github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs= +github.com/btcsuite/btcwallet/wallet/txsizes v1.2.2/go.mod h1:q08Rms52VyWyXcp5zDc4tdFRKkFgNsMQrv3/LvE1448= +github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 h1:PszOub7iXVYbtGybym5TGCp9Dv1h1iX4rIC3HICZGLg= +github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3/go.mod h1:q08Rms52VyWyXcp5zDc4tdFRKkFgNsMQrv3/LvE1448= github.com/btcsuite/btcwallet/walletdb v1.3.5/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= github.com/btcsuite/btcwallet/walletdb v1.3.6-0.20210803004036-eebed51155ec/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU= github.com/btcsuite/btcwallet/walletdb v1.4.0 h1:/C5JRF+dTuE2CNMCO/or5N8epsrhmSM4710uBQoYPTQ= @@ -523,8 +528,9 @@ github.com/lightninglabs/protobuf-hex-display v1.4.3-hex-display/go.mod h1:2oKOB github.com/lightningnetwork/lightning-onion v1.0.2-0.20220211021909-bb84a1ccb0c5 h1:TkKwqFcQTGYoI+VEqyxA8rxpCin8qDaYX0AfVRinT3k= github.com/lightningnetwork/lightning-onion v1.0.2-0.20220211021909-bb84a1ccb0c5/go.mod h1:7dDx73ApjEZA0kcknI799m2O5kkpfg4/gr7N092ojNo= github.com/lightningnetwork/lnd v0.14.1-beta.0.20220324135938-0dcaa511a249/go.mod h1:Tp3ZxsfioUl6kQ30RrbMqWoZyZ4K+fv/o1lMEU8U7rA= -github.com/lightningnetwork/lnd v0.15.1-beta h1:XR6L/TM3IzWqziS8DgsXLlDsu2/4EMK4WzvZpyYhrdM= github.com/lightningnetwork/lnd v0.15.1-beta/go.mod h1:21UpSyTj8n94nsaJ0OFRXk4yOEkE7xA9JEX5QS4oMy4= +github.com/lightningnetwork/lnd v0.15.3-beta h1:mm7fyXMgZPRyh0fFLO3tnvfvXszp/kVnGGciS76Wefs= +github.com/lightningnetwork/lnd v0.15.3-beta/go.mod h1:UaCwJBMCJbwPMsUjfTIaKPKF8K79btRPnhqfiNPyKtA= github.com/lightningnetwork/lnd/cert v1.1.1 h1:Nsav0RlIDRbOnzz2Yu69SQlK939IKya3Q2S0mDviIN8= github.com/lightningnetwork/lnd/cert v1.1.1/go.mod h1:1P46svkkd73oSoeI4zjkVKgZNwGq8bkGuPR8z+5vQUs= github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg= diff --git a/utils.go b/utils.go index a3f78d7..4391145 100644 --- a/utils.go +++ b/utils.go @@ -1,6 +1,7 @@ package loop import ( + "bytes" "context" "fmt" "strconv" @@ -12,7 +13,6 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/lndclient" "github.com/lightningnetwork/lnd/channeldb" - "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/zpay32" @@ -21,6 +21,11 @@ import ( var ( // DefaultMaxHopHints is set to 20 as that is the default set in LND. DefaultMaxHopHints = 20 + + // hopHintFactor is factor by which we scale the total amount of + // inbound capacity we want our hop hints to represent, allowing us to + // have some leeway if peers go offline. + hopHintFactor = 2 ) // isPublicNode checks if a node is public, by simply checking if there's any @@ -106,30 +111,33 @@ func parseOutPoint(s string) (*wire.OutPoint, error) { }, nil } +// getAlias tries to get the ShortChannelId from the passed ChannelId and +// aliasCache. +func getAlias(aliasCache map[lnwire.ChannelID]lnwire.ShortChannelID, + channelID lnwire.ChannelID) (lnwire.ShortChannelID, error) { + + if channelID, ok := aliasCache[channelID]; ok { + return channelID, nil + } + + return lnwire.ShortChannelID{}, fmt.Errorf("can't find channelId") +} + // SelectHopHints calls into LND's exposed SelectHopHints prefiltered to the // includeNodes map (unless it's empty). func SelectHopHints(ctx context.Context, lnd *lndclient.LndServices, amt btcutil.Amount, numMaxHophints int, includeNodes map[route.Vertex]struct{}) ([][]zpay32.HopHint, error) { - cfg := &invoicesrpc.SelectHopHintsCfg{ - IsPublicNode: func(pubKey [33]byte) (bool, error) { - return isPublicNode(ctx, lnd, pubKey) - }, - FetchChannelEdgesByID: func(chanID uint64) ( - *channeldb.ChannelEdgeInfo, *channeldb.ChannelEdgePolicy, - *channeldb.ChannelEdgePolicy, error) { + aliasCache := make(map[lnwire.ChannelID]lnwire.ShortChannelID) - return fetchChannelEdgesByID(ctx, lnd, chanID) - }, - } // Fetch all active and public channels. channels, err := lnd.Client.ListChannels(ctx, false, false) if err != nil { return nil, err } - openChannels := []*invoicesrpc.HopHintInfo{} + openChannels := []*HopHintInfo{} for _, channel := range channels { if len(includeNodes) > 0 { if _, ok := includeNodes[channel.PubKeyBytes]; !ok { @@ -148,7 +156,7 @@ func SelectHopHints(ctx context.Context, lnd *lndclient.LndServices, } openChannels = append( - openChannels, &invoicesrpc.HopHintInfo{ + openChannels, &HopHintInfo{ IsPublic: !channel.Private, IsActive: channel.Active, FundingOutpoint: *outPoint, @@ -159,11 +167,333 @@ func SelectHopHints(ctx context.Context, lnd *lndclient.LndServices, ShortChannelID: channel.ChannelID, }, ) + + channelID := lnwire.NewChanIDFromOutPoint(outPoint) + scID := lnwire.NewShortChanIDFromInt(channel.ChannelID) + aliasCache[channelID] = scID + } + + cfg := &SelectHopHintsCfg{ + IsPublicNode: func(pubKey [33]byte) (bool, error) { + return isPublicNode(ctx, lnd, pubKey) + }, + FetchChannelEdgesByID: func(chanID uint64) ( + *channeldb.ChannelEdgeInfo, *channeldb.ChannelEdgePolicy, + *channeldb.ChannelEdgePolicy, error) { + + return fetchChannelEdgesByID(ctx, lnd, chanID) + }, + GetAlias: func(id lnwire.ChannelID) ( + lnwire.ShortChannelID, error) { + + return getAlias(aliasCache, id) + }, } - routeHints := invoicesrpc.SelectHopHints( + routeHints := invoicesrpcSelectHopHints( lnwire.MilliSatoshi(amt*1000), cfg, openChannels, numMaxHophints, ) return routeHints, nil } + +// chanCanBeHopHint returns true if the target channel is eligible to be a hop +// hint. +func chanCanBeHopHint(channel *HopHintInfo, cfg *SelectHopHintsCfg) ( + *channeldb.ChannelEdgePolicy, bool) { + + // Since we're only interested in our private channels, we'll skip + // public ones. + if channel.IsPublic { + return nil, false + } + + // Make sure the channel is active. + if !channel.IsActive { + log.Debugf("Skipping channel %v due to not "+ + "being eligible to forward payments", + channel.ShortChannelID) + return nil, false + } + + // To ensure we don't leak unadvertised nodes, we'll make sure our + // counterparty is publicly advertised within the network. Otherwise, + // we'll end up leaking information about nodes that intend to stay + // unadvertised, like in the case of a node only having private + // channels. + var remotePub [33]byte + copy(remotePub[:], channel.RemotePubkey.SerializeCompressed()) + isRemoteNodePublic, err := cfg.IsPublicNode(remotePub) + if err != nil { + log.Errorf("Unable to determine if node %x "+ + "is advertised: %v", remotePub, err) + return nil, false + } + + if !isRemoteNodePublic { + log.Debugf("Skipping channel %v due to "+ + "counterparty %x being unadvertised", + channel.ShortChannelID, remotePub) + return nil, false + } + + // Fetch the policies for each end of the channel. + info, p1, p2, err := cfg.FetchChannelEdgesByID(channel.ShortChannelID) + if err != nil { + // In the case of zero-conf channels, it may be the case that + // the alias SCID was deleted from the graph, and replaced by + // the confirmed SCID. Check the Graph for the confirmed SCID. + confirmedScid := channel.ConfirmedScidZC + info, p1, p2, err = cfg.FetchChannelEdgesByID(confirmedScid) + if err != nil { + log.Errorf("Unable to fetch the routing policies for "+ + "the edges of the channel %v: %v", + channel.ShortChannelID, err) + return nil, false + } + } + + // Now, we'll need to determine which is the correct policy for HTLCs + // being sent from the remote node. + var remotePolicy *channeldb.ChannelEdgePolicy + if bytes.Equal(remotePub[:], info.NodeKey1Bytes[:]) { + remotePolicy = p1 + } else { + remotePolicy = p2 + } + + return remotePolicy, true +} + +// addHopHint creates a hop hint out of the passed channel and channel policy. +// The new hop hint is appended to the passed slice. +func addHopHint(hopHints *[][]zpay32.HopHint, + channel *HopHintInfo, chanPolicy *channeldb.ChannelEdgePolicy, + aliasScid lnwire.ShortChannelID) { + + hopHint := zpay32.HopHint{ + NodeID: channel.RemotePubkey, + ChannelID: channel.ShortChannelID, + FeeBaseMSat: uint32(chanPolicy.FeeBaseMSat), + FeeProportionalMillionths: uint32( + chanPolicy.FeeProportionalMillionths, + ), + CLTVExpiryDelta: chanPolicy.TimeLockDelta, + } + + var defaultScid lnwire.ShortChannelID + if aliasScid != defaultScid { + hopHint.ChannelID = aliasScid.ToUint64() + } + + *hopHints = append(*hopHints, []zpay32.HopHint{hopHint}) +} + +// HopHintInfo contains the channel information required to create a hop hint. +type HopHintInfo struct { + // IsPublic indicates whether a channel is advertised to the network. + IsPublic bool + + // IsActive indicates whether the channel is online and available for + // use. + IsActive bool + + // FundingOutpoint is the funding txid:index for the channel. + FundingOutpoint wire.OutPoint + + // RemotePubkey is the public key of the remote party that this channel + // is in. + RemotePubkey *btcec.PublicKey + + // RemoteBalance is the remote party's balance (our current incoming + // capacity). + RemoteBalance lnwire.MilliSatoshi + + // ShortChannelID is the short channel ID of the channel. + ShortChannelID uint64 + + // ConfirmedScidZC is the confirmed SCID of a zero-conf channel. This + // may be used for looking up a channel in the graph. + ConfirmedScidZC uint64 + + // ScidAliasFeature denotes whether the channel has negotiated the + // option-scid-alias feature bit. + ScidAliasFeature bool +} + +// SelectHopHintsCfg contains the dependencies required to obtain hop hints +// for an invoice. +type SelectHopHintsCfg struct { + // IsPublicNode is returns a bool indicating whether the node with the + // given public key is seen as a public node in the graph from the + // graph's source node's point of view. + IsPublicNode func(pubKey [33]byte) (bool, error) + + // FetchChannelEdgesByID attempts to lookup the two directed edges for + // the channel identified by the channel ID. + FetchChannelEdgesByID func(chanID uint64) (*channeldb.ChannelEdgeInfo, + *channeldb.ChannelEdgePolicy, *channeldb.ChannelEdgePolicy, + error) + + // GetAlias allows the peer's alias SCID to be retrieved for private + // option_scid_alias channels. + GetAlias func(lnwire.ChannelID) (lnwire.ShortChannelID, error) +} + +// sufficientHints checks whether we have sufficient hop hints, based on the +// following criteria: +// - Hop hint count: limit to a set number of hop hints, regardless of whether +// we've reached our invoice amount or not. +// - Total incoming capacity: limit to our invoice amount * scaling factor to +// allow for some of our links going offline. +// +// We limit our number of hop hints like this to keep our invoice size down, +// and to avoid leaking all our private channels when we don't need to. +func sufficientHints(numHints, maxHints, scalingFactor int, amount, + totalHintAmount lnwire.MilliSatoshi) bool { + + if numHints >= maxHints { + log.Debug("Reached maximum number of hop hints") + return true + } + + requiredAmount := amount * lnwire.MilliSatoshi(scalingFactor) + if totalHintAmount >= requiredAmount { + log.Debugf("Total hint amount: %v has reached target hint "+ + "bandwidth: %v (invoice amount: %v * factor: %v)", + totalHintAmount, requiredAmount, amount, + scalingFactor) + + return true + } + + return false +} + +// SelectHopHints will select up to numMaxHophints from the set of passed open +// channels. The set of hop hints will be returned as a slice of functional +// options that'll append the route hint to the set of all route hints. +// +// TODO(sputn1ck): remove when https://github.com/lightningnetwork/lnd/pull/7065 +// is merged to a new lnd release. +func invoicesrpcSelectHopHints(amtMSat lnwire.MilliSatoshi, cfg *SelectHopHintsCfg, + openChannels []*HopHintInfo, + numMaxHophints int) [][]zpay32.HopHint { + + // We'll add our hop hints in two passes, first we'll add all channels + // that are eligible to be hop hints, and also have a local balance + // above the payment amount. + var totalHintBandwidth lnwire.MilliSatoshi + hopHintChans := make(map[wire.OutPoint]struct{}) + hopHints := make([][]zpay32.HopHint, 0, numMaxHophints) + for _, channel := range openChannels { + enoughHopHints := sufficientHints( + len(hopHints), numMaxHophints, hopHintFactor, amtMSat, + totalHintBandwidth, + ) + if enoughHopHints { + log.Debugf("First pass of hop selection has " + + "sufficient hints") + + return hopHints + } + + // If this channel can't be a hop hint, then skip it. + edgePolicy, canBeHopHint := chanCanBeHopHint(channel, cfg) + if edgePolicy == nil || !canBeHopHint { + continue + } + + // Similarly, in this first pass, we'll ignore all channels in + // isolation can't satisfy this payment. + if channel.RemoteBalance < amtMSat { + continue + } + + // Lookup and see if there is an alias SCID that exists. + chanID := lnwire.NewChanIDFromOutPoint( + &channel.FundingOutpoint, + ) + alias, _ := cfg.GetAlias(chanID) + + // If this is a channel where the option-scid-alias feature bit + // was negotiated and the alias is not yet assigned, we cannot + // issue an invoice. Doing so might expose the confirmed SCID + // of a private channel. + if channel.ScidAliasFeature { + var defaultScid lnwire.ShortChannelID + if alias == defaultScid { + continue + } + } + + // Now that we now this channel use usable, add it as a hop + // hint and the indexes we'll use later. + addHopHint(&hopHints, channel, edgePolicy, alias) + + hopHintChans[channel.FundingOutpoint] = struct{}{} + totalHintBandwidth += channel.RemoteBalance + } + + // In this second pass we'll add channels, and we'll either stop when + // we have 20 hop hints, we've run through all the available channels, + // or if the sum of available bandwidth in the routing hints exceeds 2x + // the payment amount. We do 2x here to account for a margin of error + // if some of the selected channels no longer become operable. + for i := 0; i < len(openChannels); i++ { + enoughHopHints := sufficientHints( + len(hopHints), numMaxHophints, hopHintFactor, amtMSat, + totalHintBandwidth, + ) + if enoughHopHints { + log.Debugf("Second pass of hop selection has " + + "sufficient hints") + + return hopHints + } + + channel := openChannels[i] + + // Skip the channel if we already selected it. + if _, ok := hopHintChans[channel.FundingOutpoint]; ok { + continue + } + + // If the channel can't be a hop hint, then we'll skip it. + // Otherwise, we'll use the policy information to populate the + // hop hint. + remotePolicy, canBeHopHint := chanCanBeHopHint(channel, cfg) + if !canBeHopHint || remotePolicy == nil { + continue + } + + // Lookup and see if there's an alias SCID that exists. + chanID := lnwire.NewChanIDFromOutPoint( + &channel.FundingOutpoint, + ) + alias, _ := cfg.GetAlias(chanID) + + // If this is a channel where the option-scid-alias feature bit + // was negotiated and the alias is not yet assigned, we cannot + // issue an invoice. Doing so might expose the confirmed SCID + // of a private channel. + if channel.ScidAliasFeature { + var defaultScid lnwire.ShortChannelID + if alias == defaultScid { + continue + } + } + + // Include the route hint in our set of options that will be + // used when creating the invoice. + addHopHint(&hopHints, channel, remotePolicy, alias) + + // As we've just added a new hop hint, we'll accumulate it's + // available balance now to update our tally. + // + // TODO(roasbeef): have a cut off based on min bandwidth? + totalHintBandwidth += channel.RemoteBalance + } + + return hopHints +}