diff --git a/aliasmgr/aliasmgr.go b/aliasmgr/aliasmgr.go index 3048e0e26f..67d4840998 100644 --- a/aliasmgr/aliasmgr.go +++ b/aliasmgr/aliasmgr.go @@ -87,18 +87,22 @@ type Manager struct { // negotiated option-scid-alias feature bit. aliasToBase map[lnwire.ShortChannelID]lnwire.ShortChannelID + // peerAlias is a cache for the alias SCIDs that our peers send us in + // the funding_locked TLV. The keys are the ChannelID generated from + // the FundingOutpoint and the values are the remote peer's alias SCID. + // The values should match the ones stored in the "invoice-alias-bucket" + // bucket. + peerAlias map[lnwire.ChannelID]lnwire.ShortChannelID + sync.RWMutex } // NewManager initializes an alias Manager from the passed database backend. func NewManager(db kvdb.Backend) (*Manager, error) { m := &Manager{backend: db} - m.baseToSet = make( - map[lnwire.ShortChannelID][]lnwire.ShortChannelID, - ) - m.aliasToBase = make( - map[lnwire.ShortChannelID]lnwire.ShortChannelID, - ) + m.baseToSet = make(map[lnwire.ShortChannelID][]lnwire.ShortChannelID) + m.aliasToBase = make(map[lnwire.ShortChannelID]lnwire.ShortChannelID) + m.peerAlias = make(map[lnwire.ChannelID]lnwire.ShortChannelID) err := m.populateMaps() return m, err @@ -115,6 +119,10 @@ func (m *Manager) populateMaps() error { // populate the Manager's actual maps. aliasMap := make(map[lnwire.ShortChannelID]lnwire.ShortChannelID) + // This map caches the ChannelID/alias SCIDs stored in the database and + // is used to populate the Manager's cache. + peerAliasMap := make(map[lnwire.ChannelID]lnwire.ShortChannelID) + err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error { baseConfBucket, err := tx.CreateTopLevelBucket(confirmedBucket) if err != nil { @@ -152,12 +160,34 @@ func (m *Manager) populateMaps() error { aliasMap[aliasScid] = baseScid return nil }) + if err != nil { + return err + } + + invAliasBucket, err := tx.CreateTopLevelBucket( + invoiceAliasBucket, + ) + if err != nil { + return err + } + + err = invAliasBucket.ForEach(func(k, v []byte) error { + var chanID lnwire.ChannelID + copy(chanID[:], k) + alias := lnwire.NewShortChanIDFromInt( + byteOrder.Uint64(v), + ) + + peerAliasMap[chanID] = alias + + return nil + }) + return err }, func() { baseConfMap = make(map[lnwire.ShortChannelID]struct{}) - aliasMap = make( - map[lnwire.ShortChannelID]lnwire.ShortChannelID, - ) + aliasMap = make(map[lnwire.ShortChannelID]lnwire.ShortChannelID) + peerAliasMap = make(map[lnwire.ChannelID]lnwire.ShortChannelID) }) if err != nil { return err @@ -176,6 +206,9 @@ func (m *Manager) populateMaps() error { m.aliasToBase[aliasSCID] = baseSCID } + // Populate the peer alias cache. + m.peerAlias = peerAliasMap + return nil } @@ -242,7 +275,9 @@ func (m *Manager) AddLocalAlias(alias, baseScid lnwire.ShortChannelID, // GetAliases fetches the set of aliases stored under a given base SCID from // write-through caches. -func (m *Manager) GetAliases(base lnwire.ShortChannelID) []lnwire.ShortChannelID { +func (m *Manager) GetAliases( + base lnwire.ShortChannelID) []lnwire.ShortChannelID { + m.RLock() defer m.RUnlock() @@ -310,7 +345,10 @@ func (m *Manager) DeleteSixConfs(baseScid lnwire.ShortChannelID) error { func (m *Manager) PutPeerAlias(chanID lnwire.ChannelID, alias lnwire.ShortChannelID) error { - return kvdb.Update(m.backend, func(tx kvdb.RwTx) error { + m.Lock() + defer m.Unlock() + + err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error { bucket, err := tx.CreateTopLevelBucket(invoiceAliasBucket) if err != nil { return err @@ -320,36 +358,30 @@ func (m *Manager) PutPeerAlias(chanID lnwire.ChannelID, byteOrder.PutUint64(scratch[:], alias.ToUint64()) return bucket.Put(chanID[:], scratch[:]) }, func() {}) -} - -// GetPeerAlias retrieves a peer's alias SCID by the channel's ChanID. -func (m *Manager) GetPeerAlias(chanID lnwire.ChannelID) ( - lnwire.ShortChannelID, error) { + if err != nil { + return err + } - var alias lnwire.ShortChannelID + // Now that the database state has been updated, we can update it in + // our cache. + m.peerAlias[chanID] = alias - err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error { - bucket, err := tx.CreateTopLevelBucket(invoiceAliasBucket) - if err != nil { - return err - } + return nil +} - aliasBytes := bucket.Get(chanID[:]) - if aliasBytes == nil { - return nil - } +// GetPeerAlias retrieves a peer's alias SCID by the channel's ChanID. +func (m *Manager) GetPeerAlias(chanID lnwire.ChannelID) (lnwire.ShortChannelID, + error) { - alias = lnwire.NewShortChanIDFromInt( - byteOrder.Uint64(aliasBytes), - ) - return nil - }, func() {}) + m.RLock() + defer m.RUnlock() - if alias == hop.Source { - return alias, errNoPeerAlias + alias, ok := m.peerAlias[chanID] + if !ok || alias == hop.Source { + return lnwire.ShortChannelID{}, errNoPeerAlias } - return alias, err + return alias, nil } // RequestAlias returns a new ALIAS ShortChannelID to the caller by allocating diff --git a/build/version.go b/build/version.go index 6dbb169645..8898f9a612 100644 --- a/build/version.go +++ b/build/version.go @@ -44,11 +44,11 @@ const ( AppMinor uint = 15 // AppPatch defines the application patch for this binary. - AppPatch uint = 2 + AppPatch uint = 3 // AppPreRelease MUST only contain characters from semanticAlphabet // per the semantic versioning spec. - AppPreRelease = "beta" + AppPreRelease = "beta.rc1" ) func init() { diff --git a/cmd/lncli/walletrpc_active.go b/cmd/lncli/walletrpc_active.go index fe0bd2351a..e1255310cd 100644 --- a/cmd/lncli/walletrpc_active.go +++ b/cmd/lncli/walletrpc_active.go @@ -82,9 +82,11 @@ func parseAddrType(addrTypeStr string) (walletrpc.AddressType, error) { return walletrpc.AddressType_NESTED_WITNESS_PUBKEY_HASH, nil case "np2wkh-p2wkh": return walletrpc.AddressType_HYBRID_NESTED_WITNESS_PUBKEY_HASH, nil + case "p2tr": + return walletrpc.AddressType_TAPROOT_PUBKEY, nil default: return 0, errors.New("invalid address type, supported address " + - "types are: p2wkh, np2wkh, and np2wkh-p2wkh") + "types are: p2wkh, p2tr, np2wkh, and np2wkh-p2wkh") } } diff --git a/docs/release-notes/release-notes-0.15.0.md b/docs/release-notes/release-notes-0.15.0.md index 0aec864735..89cf40ac9a 100644 --- a/docs/release-notes/release-notes-0.15.0.md +++ b/docs/release-notes/release-notes-0.15.0.md @@ -34,6 +34,12 @@ were created after introducing the Taproot key derivation](https://github.com/lightningnetwork/lnd/pull/6524) to simplify detecting Taproot compatibility of a seed. +**NOTE** for users running a remote signing setup: A manual account import is +necessary when upgrading from `lnd v0.14.x-beta` to `lnd v0.15.x-beta`, see [the +remote signing documentation for more +details](../remote-signing.md#migrating-a-remote-signing-setup-from-014x-to-015x). +Please upgrade to `lnd v0.15.3-beta` or later directly! + ## MuSig2 The [`signrpc.Signer` RPC service now supports EXPERIMENTAL MuSig2 diff --git a/docs/release-notes/release-notes-0.15.1.md b/docs/release-notes/release-notes-0.15.1.md index 30b1126c3f..723593906f 100644 --- a/docs/release-notes/release-notes-0.15.1.md +++ b/docs/release-notes/release-notes-0.15.1.md @@ -44,6 +44,12 @@ supports the feature.](https://github.com/lightningnetwork/lnd/pull/6633) The [wallet also creates P2TR change addresses by default](https://github.com/lightningnetwork/lnd/pull/6810) in most cases. +**NOTE** for users running a remote signing setup: A manual account import is +necessary when upgrading from `lnd v0.14.x-beta` to `lnd v0.15.x-beta`, see [the +remote signing documentation for more +details](../remote-signing.md#migrating-a-remote-signing-setup-from-014x-to-015x). +Please upgrade to `lnd v0.15.3-beta` or later directly! + ## `lncli` * [Add `payment_addr` flag to diff --git a/docs/release-notes/release-notes-0.15.3.md b/docs/release-notes/release-notes-0.15.3.md new file mode 100644 index 0000000000..295ad3e2d8 --- /dev/null +++ b/docs/release-notes/release-notes-0.15.3.md @@ -0,0 +1,39 @@ +# Release Notes + +## RPC/REST Server + +- A `POST` URL mapping [was added to the REST version of the `QueryRoutes` call + (`POST /v1/graph/routes`)](https://github.com/lightningnetwork/lnd/pull/6926) + to make it possible to specify `route_hints` over REST. + +## Bug Fixes + +* [A bug has been fixed where the responder of a zero-conf channel could forget + about the channel after a hard-coded 2016 blocks.](https://github.com/lightningnetwork/lnd/pull/6998) + +* [A bug where LND wouldn't send a ChannelUpdate during a channel open has + been fixed.](https://github.com/lightningnetwork/lnd/pull/6892) + +* [A bug has been fixed that caused fee estimation to be incorrect for taproot + inputs when using the `SendOutputs` call.](https://github.com/lightningnetwork/lnd/pull/6941) + +* [A bug has been fixed that could cause lnd to underpay for co-op close + transaction when or both of the outputs used a P2TR + addresss.](https://github.com/lightningnetwork/lnd/pull/6957) + +## Taproot + +* [Add `p2tr` address type to account + import](https://github.com/lightningnetwork/lnd/pull/6966). + +**NOTE** for users running a remote signing setup: A manual account import is +necessary when upgrading from `lnd v0.14.x-beta` to `lnd v0.15.x-beta`, see [the +remote signing documentation for more +details](../remote-signing.md#migrating-a-remote-signing-setup-from-014x-to-015x). + +# Contributors (Alphabetical Order) + +* Eugene Siegel +* Jordi Montes +* Olaoluwa Osuntokun +* Oliver Gugger diff --git a/docs/remote-signing.md b/docs/remote-signing.md index 6bcef1b572..ce254adcc9 100644 --- a/docs/remote-signing.md +++ b/docs/remote-signing.md @@ -153,6 +153,28 @@ To migrate an existing node, follow these steps: a watch-only one (by purging all private key material from it) by adding the `remotesigner.migrate-wallet-to-watch-only=true` configuration entry. +## Migrating a remote signing setup from 0.14.x to 0.15.x + +If you were running a remote signing setup with `lnd v0.14.x-beta` and want to +upgrade to `lnd v0.15.x-beta`, you need to manually import the newly added +Taproot account to the watch-only node, otherwise you will encounter errors such +as `account 0 not found` when doing on-chain operations that require creating +(change) P2TR addresses. + +**NOTE**: For this to work, you need to upgrade to at least `lnd v0.15.3-beta` +or later! + +The upgrade process should look like this: +1. Upgrade the "signer" node to `lnd v0.15.x-beta` and unlock it. +2. Run `lncli wallet accounts list | grep -A5 TAPROOT` on the **"signer"** node + and copy the `xpub...` value from `extended_public_key`. +3. Upgrade the "watch-only" node to `lnd v0.15.x-beta` and unlock it. +4. Run `lncli wallet accounts import --address_type p2tr default` on + the **"watch-only"** node (notice the `default` account name at the end, + that's important). +5. Run `lncli newaddress p2tr` on the "watch-only" node to test that everything + works as expected. + ## Example initialization script This section shows an example script that initializes the watch-only wallet of diff --git a/funding/manager.go b/funding/manager.go index d10872bf62..a952923446 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -2457,7 +2457,7 @@ func (f *Manager) waitForFundingWithTimeout( // If we are not the initiator, we have no money at stake and will // timeout waiting for the funding transaction to confirm after a // while. - if !ch.IsInitiator { + if !ch.IsInitiator && !ch.IsZeroConf() { f.wg.Add(1) go f.waitForTimeout(ch, cancelChan, timeoutChan) } @@ -3200,9 +3200,7 @@ func (f *Manager) waitForZeroConfChannel(c *channeldb.OpenChannel, // is already confirmed, the chainntnfs subsystem will return with the // confirmed tx. Otherwise, we'll wait here until confirmation occurs. confChan, err := f.waitForFundingWithTimeout(c) - if err == ErrConfirmationTimeout { - return f.fundingTimeout(c, pendingID) - } else if err != nil { + if err != nil { return fmt.Errorf("error waiting for zero-conf funding "+ "confirmation for ChannelPoint(%v): %v", c.FundingOutpoint, err) diff --git a/go.mod b/go.mod index dcf8eba0cf..3db10f7d69 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,8 @@ require ( github.com/btcsuite/btcd/btcutil/psbt v1.1.5 github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f - github.com/btcsuite/btcwallet v0.15.1 - github.com/btcsuite/btcwallet/wallet/txauthor v1.2.3 + github.com/btcsuite/btcwallet v0.16.1 + github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2 github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 github.com/btcsuite/btcwallet/walletdb v1.4.0 github.com/btcsuite/btcwallet/wtxmgr v1.5.0 @@ -66,7 +66,7 @@ require ( github.com/aead/siphash v1.0.1 // indirect github.com/andybalholm/brotli v1.0.3 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 // indirect + github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect github.com/btcsuite/winsvc v1.0.0 // indirect diff --git a/go.sum b/go.sum index 12b157b190..f290d26faf 100644 --- a/go.sum +++ b/go.sum @@ -98,14 +98,17 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtyd github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= 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.15.1 h1:SKfh/l2Bgz9sJwHZvfiVbZ8Pl3N/8fFcWWXzsAPz9GU= -github.com/btcsuite/btcwallet v0.15.1/go.mod h1:7OFsQ8ypiRwmr67hE0z98uXgJgXGAihE79jCib9x6ag= -github.com/btcsuite/btcwallet/wallet/txauthor v1.2.3 h1:M2yr5UlULvpqtxUqpMxTME/pA92Z9cpqeyvAFk9lAg0= +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.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= diff --git a/lnrpc/autopilotrpc/autopilot.pb.json.go b/lnrpc/autopilotrpc/autopilot.pb.json.go index 1edb72ff44..53b286315c 100644 --- a/lnrpc/autopilotrpc/autopilot.pb.json.go +++ b/lnrpc/autopilotrpc/autopilot.pb.json.go @@ -1,8 +1,6 @@ // Code generated by falafel 0.9.1. DO NOT EDIT. // source: autopilot.proto -// +build js - package autopilotrpc import ( diff --git a/lnrpc/chainrpc/chainnotifier.pb.json.go b/lnrpc/chainrpc/chainnotifier.pb.json.go index 68004b968a..c1a3294871 100644 --- a/lnrpc/chainrpc/chainnotifier.pb.json.go +++ b/lnrpc/chainrpc/chainnotifier.pb.json.go @@ -1,8 +1,6 @@ // Code generated by falafel 0.9.1. DO NOT EDIT. // source: chainnotifier.proto -// +build js - package chainrpc import ( diff --git a/lnrpc/devrpc/dev.pb.json.go b/lnrpc/devrpc/dev.pb.json.go index 13927671f6..41f40609c7 100644 --- a/lnrpc/devrpc/dev.pb.json.go +++ b/lnrpc/devrpc/dev.pb.json.go @@ -1,8 +1,6 @@ // Code generated by falafel 0.9.1. DO NOT EDIT. // source: dev.proto -// +build js - package devrpc import ( diff --git a/lnrpc/gen_protos.sh b/lnrpc/gen_protos.sh index c523a203ea..83f9871cc1 100755 --- a/lnrpc/gen_protos.sh +++ b/lnrpc/gen_protos.sh @@ -41,7 +41,7 @@ function generate() { # Generate the JSON/WASM client stubs. falafel=$(which falafel) pkg="lnrpc" - opts="package_name=$pkg,js_stubs=1,build_tags=// +build js" + opts="package_name=$pkg,js_stubs=1" protoc -I/usr/local/include -I. -I.. \ --plugin=protoc-gen-custom=$falafel\ --custom_out=. \ @@ -61,7 +61,7 @@ function generate() { manual_import="github.com/lightningnetwork/lnd/lnrpc" fi - opts="package_name=$package,manual_import=$manual_import,js_stubs=1,build_tags=// +build js" + opts="package_name=$package,manual_import=$manual_import,js_stubs=1" pushd $package protoc -I/usr/local/include -I. -I.. \ --plugin=protoc-gen-custom=$falafel\ diff --git a/lnrpc/invoicesrpc/addinvoice.go b/lnrpc/invoicesrpc/addinvoice.go index 6b749918db..c37ed001d8 100644 --- a/lnrpc/invoicesrpc/addinvoice.go +++ b/lnrpc/invoicesrpc/addinvoice.go @@ -7,6 +7,8 @@ import ( "errors" "fmt" "math" + mathRand "math/rand" + "sort" "time" "github.com/btcsuite/btcd/btcec/v2" @@ -35,6 +37,10 @@ const ( // inbound capacity we want our hop hints to represent, allowing us to // have some leeway if peers go offline. hopHintFactor = 2 + + // maxHopHints is the maximum number of hint paths that will be included + // in an invoice. + maxHopHints = 20 ) // AddInvoiceConfig contains dependencies for invoice creation. @@ -126,8 +132,8 @@ type AddInvoiceData struct { // NOTE: Preimage should always be set to nil when this value is true. Amp bool - // RouteHints are optional route hints that can each be individually used - // to assist in reaching the invoice's destination. + // RouteHints are optional route hints that can each be individually + // used to assist in reaching the invoice's destination. RouteHints [][]zpay32.HopHint } @@ -159,7 +165,9 @@ func (d *AddInvoiceData) paymentHashAndPreimage() ( // ampPaymentHashAndPreimage returns the payment hash to use for an AMP invoice. // The preimage will always be nil. -func (d *AddInvoiceData) ampPaymentHashAndPreimage() (*lntypes.Preimage, lntypes.Hash, error) { +func (d *AddInvoiceData) ampPaymentHashAndPreimage() (*lntypes.Preimage, + lntypes.Hash, error) { + switch { // Preimages cannot be set on AMP invoice. case d.Preimage != nil: @@ -184,7 +192,9 @@ func (d *AddInvoiceData) ampPaymentHashAndPreimage() (*lntypes.Preimage, lntypes // mppPaymentHashAndPreimage returns the payment hash and preimage to use for an // MPP invoice. -func (d *AddInvoiceData) mppPaymentHashAndPreimage() (*lntypes.Preimage, lntypes.Hash, error) { +func (d *AddInvoiceData) mppPaymentHashAndPreimage() (*lntypes.Preimage, + lntypes.Hash, error) { + var ( paymentPreimage *lntypes.Preimage paymentHash lntypes.Hash @@ -235,11 +245,14 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig, // exceed the maximum values for either of the fields. if len(invoice.Memo) > channeldb.MaxMemoSize { return nil, nil, fmt.Errorf("memo too large: %v bytes "+ - "(maxsize=%v)", len(invoice.Memo), channeldb.MaxMemoSize) + "(maxsize=%v)", len(invoice.Memo), + channeldb.MaxMemoSize) } - if len(invoice.DescriptionHash) > 0 && len(invoice.DescriptionHash) != 32 { - return nil, nil, fmt.Errorf("description hash is %v bytes, must be 32", - len(invoice.DescriptionHash)) + if len(invoice.DescriptionHash) > 0 && + len(invoice.DescriptionHash) != 32 { + + return nil, nil, fmt.Errorf("description hash is %v bytes, "+ + "must be 32", len(invoice.DescriptionHash)) } // We set the max invoice amount to 100k BTC, which itself is several @@ -281,8 +294,8 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig, addr, err := btcutil.DecodeAddress(invoice.FallbackAddr, cfg.ChainParams) if err != nil { - return nil, nil, fmt.Errorf("invalid fallback address: %v", - err) + return nil, nil, fmt.Errorf("invalid fallback "+ + "address: %v", err) } options = append(options, zpay32.FallbackAddr(addr)) } @@ -314,11 +327,13 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig, // Otherwise, use the default AMP expiry. default: - options = append(options, zpay32.Expiry(DefaultAMPInvoiceExpiry)) + defaultExpiry := zpay32.Expiry(DefaultAMPInvoiceExpiry) + options = append(options, defaultExpiry) } - // If the description hash is set, then we add it do the list of options. - // If not, use the memo field as the payment request description. + // If the description hash is set, then we add it do the list of + // options. If not, use the memo field as the payment request + // description. if len(invoice.DescriptionHash) > 0 { var descHash [32]byte copy(descHash[:], invoice.DescriptionHash[:]) @@ -333,8 +348,10 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig, // an option on the command line when creating an invoice. switch { case invoice.CltvExpiry > math.MaxUint16: - return nil, nil, fmt.Errorf("CLTV delta of %v is too large, max "+ - "accepted is: %v", invoice.CltvExpiry, math.MaxUint16) + return nil, nil, fmt.Errorf("CLTV delta of %v is too large, "+ + "max accepted is: %v", invoice.CltvExpiry, + math.MaxUint16) + case invoice.CltvExpiry != 0: // Disallow user-chosen final CLTV deltas below the required // minimum. @@ -346,99 +363,52 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig, options = append(options, zpay32.CLTVExpiry(invoice.CltvExpiry)) + default: // TODO(roasbeef): assumes set delta between versions - defaultDelta := cfg.DefaultCLTVExpiry - options = append(options, zpay32.CLTVExpiry(uint64(defaultDelta))) + defaultCLTVExpiry := uint64(cfg.DefaultCLTVExpiry) + options = append(options, zpay32.CLTVExpiry(defaultCLTVExpiry)) } - // We make sure that the given invoice routing hints number is within the - // valid range - if len(invoice.RouteHints) > 20 { - return nil, nil, fmt.Errorf("number of routing hints must not exceed " + - "maximum of 20") + // We make sure that the given invoice routing hints number is within + // the valid range + if len(invoice.RouteHints) > maxHopHints { + return nil, nil, fmt.Errorf("number of routing hints must "+ + "not exceed maximum of %v", maxHopHints) } - // We continue by populating the requested routing hints indexing their - // corresponding channels so we won't duplicate them. - forcedHints := make(map[uint64]struct{}) - for _, h := range invoice.RouteHints { - if len(h) == 0 { - return nil, nil, fmt.Errorf("number of hop hint within a route must " + - "be positive") + // Include route hints if needed. + if len(invoice.RouteHints) > 0 || invoice.Private { + // Validate provided hop hints. + for _, hint := range invoice.RouteHints { + if len(hint) == 0 { + return nil, nil, fmt.Errorf("number of hop " + + "hint within a route must be positive") + } } - options = append(options, zpay32.RouteHint(h)) - // Only this first hop is our direct channel. - forcedHints[h[0].ChannelID] = struct{}{} - } + totalHopHints := len(invoice.RouteHints) + if invoice.Private { + totalHopHints = maxHopHints + } - // If we were requested to include routing hints in the invoice, then - // we'll fetch all of our available private channels and create routing - // hints for them. - if invoice.Private { - openChannels, err := cfg.ChanDB.FetchAllChannels() + hopHintsCfg := newSelectHopHintsCfg(cfg, totalHopHints) + hopHints, err := PopulateHopHints( + hopHintsCfg, amtMSat, invoice.RouteHints, + ) if err != nil { - return nil, nil, fmt.Errorf("could not fetch all channels") + return nil, nil, fmt.Errorf("unable to populate hop "+ + "hints: %v", err) } - if len(openChannels) > 0 { - // We filter the channels by excluding the ones that were specified by - // the caller and were already added. - var filteredChannels []*HopHintInfo - for _, c := range openChannels { - if _, ok := forcedHints[c.ShortChanID().ToUint64()]; ok { - continue - } - - // If this is a zero-conf channel, check if the - // confirmed SCID was used in forcedHints. - realScid := c.ZeroConfRealScid().ToUint64() - if c.IsZeroConf() { - if _, ok := forcedHints[realScid]; ok { - continue - } - } - - chanID := lnwire.NewChanIDFromOutPoint( - &c.FundingOutpoint, - ) - - // Check whether the the peer's alias was - // provided in forcedHints. - peerAlias, _ := cfg.GetAlias(chanID) - peerScid := peerAlias.ToUint64() - if _, ok := forcedHints[peerScid]; ok { - continue - } - - isActive := cfg.IsChannelActive(chanID) - - hopHintInfo := newHopHintInfo(c, isActive) - filteredChannels = append( - filteredChannels, hopHintInfo, - ) - } - - // We'll restrict the number of individual route hints - // to 20 to avoid creating overly large invoices. - numMaxHophints := 20 - len(forcedHints) + // Convert our set of selected hop hints into route + // hints and add to our invoice options. + for _, hopHint := range hopHints { + routeHint := zpay32.RouteHint(hopHint) - hopHintsCfg := newSelectHopHintsCfg(cfg) - hopHints := SelectHopHints( - amtMSat, hopHintsCfg, filteredChannels, - numMaxHophints, + options = append( + options, routeHint, ) - - // Convert our set of selected hop hints into route - // hints and add to our invoice options. - for _, hopHint := range hopHints { - routeHint := zpay32.RouteHint(hopHint) - - options = append( - options, routeHint, - ) - } } } @@ -576,30 +546,6 @@ func chanCanBeHopHint(channel *HopHintInfo, cfg *SelectHopHintsCfg) ( 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. @@ -647,6 +593,22 @@ func newHopHintInfo(c *channeldb.OpenChannel, isActive bool) *HopHintInfo { } } +// newHopHint returns a new hop hint using the relevant data from a hopHintInfo +// and a ChannelEdgePolicy. +func newHopHint(hopHintInfo *HopHintInfo, + chanPolicy *channeldb.ChannelEdgePolicy) zpay32.HopHint { + + return zpay32.HopHint{ + NodeID: hopHintInfo.RemotePubkey, + ChannelID: hopHintInfo.ShortChannelID, + FeeBaseMSat: uint32(chanPolicy.FeeBaseMSat), + FeeProportionalMillionths: uint32( + chanPolicy.FeeProportionalMillionths, + ), + CLTVExpiryDelta: chanPolicy.TimeLockDelta, + } +} + // SelectHopHintsCfg contains the dependencies required to obtain hop hints // for an invoice. type SelectHopHintsCfg struct { @@ -664,169 +626,208 @@ type SelectHopHintsCfg struct { // GetAlias allows the peer's alias SCID to be retrieved for private // option_scid_alias channels. GetAlias func(lnwire.ChannelID) (lnwire.ShortChannelID, error) + + // FetchAllChannels retrieves all open channels currently stored + // within the database. + FetchAllChannels func() ([]*channeldb.OpenChannel, error) + + // IsChannelActive checks whether the channel identified by the provided + // ChannelID is considered active. + IsChannelActive func(chanID lnwire.ChannelID) bool + + // MaxHopHints is the maximum number of hop hints we are interested in. + MaxHopHints int } -func newSelectHopHintsCfg(invoicesCfg *AddInvoiceConfig) *SelectHopHintsCfg { +func newSelectHopHintsCfg(invoicesCfg *AddInvoiceConfig, + maxHopHints int) *SelectHopHintsCfg { + return &SelectHopHintsCfg{ + FetchAllChannels: invoicesCfg.ChanDB.FetchAllChannels, + IsChannelActive: invoicesCfg.IsChannelActive, IsPublicNode: invoicesCfg.Graph.IsPublicNode, FetchChannelEdgesByID: invoicesCfg.Graph.FetchChannelEdgesByID, GetAlias: invoicesCfg.GetAlias, + MaxHopHints: maxHopHints, } } // 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. +// any of the following criteria: +// - Hop hint count: the number of hints have reach our max target. +// - Total incoming capacity: the sum of the remote balance amount in the +// hints is bigger of equal than our target (currently twice the invoice +// amount) // // 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 { +func sufficientHints(nHintsLeft int, currentAmount, + targetAmount lnwire.MilliSatoshi) bool { - if numHints >= maxHints { - log.Debug("Reached maximum number of hop hints") + if nHintsLeft <= 0 { + log.Debugf("Reached targeted number of hop hints") return true } - requiredAmount := amount * lnwire.MilliSatoshi(scalingFactor) - if totalHintAmount >= requiredAmount { + if currentAmount >= targetAmount { log.Debugf("Total hint amount: %v has reached target hint "+ - "bandwidth: %v (invoice amount: %v * factor: %v)", - totalHintAmount, requiredAmount, amount, - scalingFactor) - + "bandwidth: %v", currentAmount, targetAmount) 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(roasbeef): do proper sub-set sum max hints usually << numChans. -func SelectHopHints(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") +// getPotentialHints returns a slice of open channels that should be considered +// for the hopHint list in an invoice. The slice is sorted in descending order +// based on the remote balance. +func getPotentialHints(cfg *SelectHopHintsCfg) ([]*channeldb.OpenChannel, + error) { - return hopHints - } + // TODO(positiveblue): get the channels slice already filtered by + // private == true and sorted by RemoteBalance? + openChannels, err := cfg.FetchAllChannels() + if err != nil { + return nil, err + } - // If this channel can't be a hop hint, then skip it. - edgePolicy, canBeHopHint := chanCanBeHopHint(channel, cfg) - if edgePolicy == nil || !canBeHopHint { - continue + privateChannels := make([]*channeldb.OpenChannel, 0, len(openChannels)) + for _, oc := range openChannels { + isPublic := oc.ChannelFlags&lnwire.FFAnnounceChannel != 0 + if !isPublic { + privateChannels = append(privateChannels, oc) } + } - // Similarly, in this first pass, we'll ignore all channels in - // isolation can't satisfy this payment. - if channel.RemoteBalance < amtMSat { - continue - } + // Sort the channels in descending remote balance. + compareRemoteBalance := func(i, j int) bool { + iBalance := privateChannels[i].LocalCommitment.RemoteBalance + jBalance := privateChannels[j].LocalCommitment.RemoteBalance + return iBalance > jBalance + } + sort.Slice(privateChannels, compareRemoteBalance) - // 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 - } + return privateChannels, nil +} + +// shouldIncludeChannel returns true if the channel passes all the checks to +// be a hopHint in a given invoice. +func shouldIncludeChannel(cfg *SelectHopHintsCfg, + channel *channeldb.OpenChannel, + alreadyIncluded map[uint64]bool) (zpay32.HopHint, lnwire.MilliSatoshi, + bool) { + + if _, ok := alreadyIncluded[channel.ShortChannelID.ToUint64()]; ok { + return zpay32.HopHint{}, 0, false + } + + chanID := lnwire.NewChanIDFromOutPoint( + &channel.FundingOutpoint, + ) + + hopHintInfo := newHopHintInfo(channel, cfg.IsChannelActive(chanID)) + + // If this channel can't be a hop hint, then skip it. + edgePolicy, canBeHopHint := chanCanBeHopHint(hopHintInfo, cfg) + if edgePolicy == nil || !canBeHopHint { + return zpay32.HopHint{}, 0, false + } + + if hopHintInfo.ScidAliasFeature { + alias, err := cfg.GetAlias(chanID) + if err != nil { + return zpay32.HopHint{}, 0, false } - // 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) + if alias.IsDefault() || alreadyIncluded[alias.ToUint64()] { + return zpay32.HopHint{}, 0, false + } - hopHintChans[channel.FundingOutpoint] = struct{}{} - totalHintBandwidth += channel.RemoteBalance + hopHintInfo.ShortChannelID = alias.ToUint64() } - // 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++ { + // Now that we know this channel use usable, add it as a hop hint and + // the indexes we'll use later. + hopHint := newHopHint(hopHintInfo, edgePolicy) + return hopHint, hopHintInfo.RemoteBalance, true +} + +// selectHopHints iterates a list of potential hints selecting the valid hop +// hints until we have enough hints or run out of channels. +// +// NOTE: selectHopHints expects potentialHints to be already sorted in +// descending priority. +func selectHopHints(cfg *SelectHopHintsCfg, nHintsLeft int, + targetBandwidth lnwire.MilliSatoshi, + potentialHints []*channeldb.OpenChannel, + alreadyIncluded map[uint64]bool) [][]zpay32.HopHint { + + currentBandwidth := lnwire.MilliSatoshi(0) + hopHints := make([][]zpay32.HopHint, 0, nHintsLeft) + for _, channel := range potentialHints { enoughHopHints := sufficientHints( - len(hopHints), numMaxHophints, hopHintFactor, amtMSat, - totalHintBandwidth, + nHintsLeft, currentBandwidth, targetBandwidth, ) if enoughHopHints { - log.Debugf("Second pass of hop selection has " + - "sufficient hints") - return hopHints } - channel := openChannels[i] + hopHint, remoteBalance, include := shouldIncludeChannel( + cfg, channel, alreadyIncluded, + ) - // Skip the channel if we already selected it. - if _, ok := hopHintChans[channel.FundingOutpoint]; ok { - continue + if include { + // Now that we now this channel use usable, add it as a hop + // hint and the indexes we'll use later. + hopHints = append(hopHints, []zpay32.HopHint{hopHint}) + currentBandwidth += remoteBalance + nHintsLeft-- } + } - // 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 - } + // We do not want to leak information about how our remote balance is + // distributed in our private channels. We shuffle the selected ones + // here so they do not appear in order in the invoice. + mathRand.Shuffle( + len(hopHints), func(i, j int) { + hopHints[i], hopHints[j] = hopHints[j], hopHints[i] + }, + ) + return hopHints +} - // 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 - } - } +// PopulateHopHints will select up to cfg.MaxHophints from the current 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(roasbeef): do proper sub-set sum max hints usually << numChans. +func PopulateHopHints(cfg *SelectHopHintsCfg, amtMSat lnwire.MilliSatoshi, + forcedHints [][]zpay32.HopHint) ([][]zpay32.HopHint, error) { - // Include the route hint in our set of options that will be - // used when creating the invoice. - addHopHint(&hopHints, channel, remotePolicy, alias) + hopHints := forcedHints - // 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 + // If we already have enough hints we don't need to add any more. + nHintsLeft := cfg.MaxHopHints - len(hopHints) + if nHintsLeft <= 0 { + return hopHints, nil } - return hopHints + alreadyIncluded := make(map[uint64]bool) + for _, hopHint := range hopHints { + alreadyIncluded[hopHint[0].ChannelID] = true + } + + potentialHints, err := getPotentialHints(cfg) + if err != nil { + return nil, err + } + + targetBandwidth := amtMSat * hopHintFactor + selectedHints := selectHopHints( + cfg, nHintsLeft, targetBandwidth, potentialHints, + alreadyIncluded, + ) + + hopHints = append(hopHints, selectedHints...) + return hopHints, nil } diff --git a/lnrpc/invoicesrpc/addinvoice_test.go b/lnrpc/invoicesrpc/addinvoice_test.go index 4720f7ad0d..d7ee620eb3 100644 --- a/lnrpc/invoicesrpc/addinvoice_test.go +++ b/lnrpc/invoicesrpc/addinvoice_test.go @@ -2,7 +2,7 @@ package invoicesrpc import ( "encoding/hex" - "errors" + "fmt" "testing" "github.com/btcsuite/btcd/btcec/v2" @@ -24,7 +24,32 @@ func (h *hopHintsConfigMock) IsPublicNode(pubKey [33]byte) (bool, error) { return args.Bool(0), args.Error(1) } -// FetchChannelEdgesByID mocks channel edge lookup. +// IsChannelActive is used to generate valid hop hints. +func (h *hopHintsConfigMock) IsChannelActive(chanID lnwire.ChannelID) bool { + args := h.Mock.Called(chanID) + return args.Bool(0) +} + +// GetAlias allows the peer's alias SCID to be retrieved for private +// option_scid_alias channels. +func (h *hopHintsConfigMock) GetAlias( + chanID lnwire.ChannelID) (lnwire.ShortChannelID, error) { + + args := h.Mock.Called(chanID) + return args.Get(0).(lnwire.ShortChannelID), args.Error(1) +} + +// FetchAllChannels retrieves all open channels currently stored +// within the database. +func (h *hopHintsConfigMock) FetchAllChannels() ([]*channeldb.OpenChannel, + error) { + + args := h.Mock.Called() + return args.Get(0).([]*channeldb.OpenChannel), args.Error(1) +} + +// FetchChannelEdgesByID attempts to lookup the two directed edges for +// the channel identified by the channel ID. func (h *hopHintsConfigMock) FetchChannelEdgesByID(chanID uint64) ( *channeldb.ChannelEdgeInfo, *channeldb.ChannelEdgePolicy, *channeldb.ChannelEdgePolicy, error) { @@ -46,584 +71,716 @@ func (h *hopHintsConfigMock) FetchChannelEdgesByID(chanID uint64) ( 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() +// getTestPubKey returns a valid parsed pub key to be used in our tests. +func getTestPubKey() *btcec.PublicKey { + pubkeyBytes, _ := hex.DecodeString( + "598ec453728e0ffe0ae2f5e174243cf58f2" + + "a3f2c83d2457b43036db568b11093", + ) + pubKeyY := new(btcec.FieldVal) + _ = pubKeyY.SetByteSlice(pubkeyBytes) + pubkey := btcec.NewPublicKey( + new(btcec.FieldVal).SetInt(4), + pubKeyY, + ) + return pubkey +} - publicChannel = &HopHintInfo{ - IsPublic: true, - IsActive: true, - FundingOutpoint: wire.OutPoint{ - Index: 0, - }, - RemoteBalance: 10, - ShortChannelID: 0, +var shouldIncludeChannelTestCases = []struct { + name string + setupMock func(*hopHintsConfigMock) + channel *channeldb.OpenChannel + alreadyIncluded map[uint64]bool + cfg *SelectHopHintsCfg + hopHint zpay32.HopHint + remoteBalance lnwire.MilliSatoshi + include bool +}{{ + name: "already included channels should not be included " + + "again", + alreadyIncluded: map[uint64]bool{1: true}, + channel: &channeldb.OpenChannel{ + ShortChannelID: lnwire.NewShortChanIDFromInt(1), + }, + include: false, +}, { + name: "public channels should not be included", + setupMock: func(h *hopHintsConfigMock) { + fundingOutpoint := wire.OutPoint{ + Index: 0, } - - inactiveChannel = &HopHintInfo{ - IsPublic: false, - IsActive: false, + chanID := lnwire.NewChanIDFromOutPoint(&fundingOutpoint) + h.Mock.On( + "IsChannelActive", chanID, + ).Once().Return(true) + }, + channel: &channeldb.OpenChannel{ + FundingOutpoint: wire.OutPoint{ + Index: 0, + }, + ChannelFlags: lnwire.FFAnnounceChannel, + }, +}, { + name: "not active channels should not be included", + setupMock: func(h *hopHintsConfigMock) { + fundingOutpoint := wire.OutPoint{ + Index: 0, } - - // 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, + chanID := lnwire.NewChanIDFromOutPoint(&fundingOutpoint) + h.Mock.On( + "IsChannelActive", chanID, + ).Once().Return(false) + }, + channel: &channeldb.OpenChannel{ + FundingOutpoint: wire.OutPoint{ + Index: 0, + }, + }, + include: false, +}, { + name: "a channel with a not public peer should not be included", + setupMock: func(h *hopHintsConfigMock) { + fundingOutpoint := wire.OutPoint{ + Index: 0, } - - // Create a edge policy for private channel 1. - privateChan1Policy = &channeldb.ChannelEdgePolicy{ - FeeBaseMSat: 10, - FeeProportionalMillionths: 100, - TimeLockDelta: 1000, + chanID := lnwire.NewChanIDFromOutPoint(&fundingOutpoint) + + h.Mock.On( + "IsChannelActive", chanID, + ).Once().Return(true) + + h.Mock.On( + "IsPublicNode", mock.Anything, + ).Once().Return(false, nil) + }, + channel: &channeldb.OpenChannel{ + FundingOutpoint: wire.OutPoint{ + Index: 0, + }, + IdentityPub: getTestPubKey(), + }, + include: false, +}, { + name: "if we are unable to fetch the edge policy for the channel it " + + "should not be included", + setupMock: func(h *hopHintsConfigMock) { + fundingOutpoint := wire.OutPoint{ + Index: 0, } - - // Create an edge policy different to ours which we'll use for - // the other direction - otherChanPolicy = &channeldb.ChannelEdgePolicy{ - FeeBaseMSat: 90, - FeeProportionalMillionths: 900, - TimeLockDelta: 9000, + chanID := lnwire.NewChanIDFromOutPoint(&fundingOutpoint) + + h.Mock.On( + "IsChannelActive", chanID, + ).Once().Return(true) + + h.Mock.On( + "IsPublicNode", mock.Anything, + ).Once().Return(true, nil) + + h.Mock.On( + "FetchChannelEdgesByID", mock.Anything, + ).Once().Return(nil, nil, nil, fmt.Errorf("no edge")) + + // TODO(positiveblue): check that the func is called with the + // right scid when we have access to the `confirmedscid` form + // here. + h.Mock.On( + "FetchChannelEdgesByID", mock.Anything, + ).Once().Return(nil, nil, nil, fmt.Errorf("no edge")) + }, + channel: &channeldb.OpenChannel{ + FundingOutpoint: wire.OutPoint{ + Index: 0, + }, + IdentityPub: getTestPubKey(), + }, + include: false, +}, { + name: "channels with the option-scid-alias but not assigned alias " + + "yet should not be included", + setupMock: func(h *hopHintsConfigMock) { + fundingOutpoint := wire.OutPoint{ + Index: 0, } + chanID := lnwire.NewChanIDFromOutPoint(&fundingOutpoint) + + h.Mock.On( + "IsChannelActive", chanID, + ).Once().Return(true) + + h.Mock.On( + "IsPublicNode", mock.Anything, + ).Once().Return(true, nil) + + h.Mock.On( + "FetchChannelEdgesByID", mock.Anything, + ).Once().Return( + &channeldb.ChannelEdgeInfo{}, + &channeldb.ChannelEdgePolicy{}, + &channeldb.ChannelEdgePolicy{}, nil, + ) - // 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, + h.Mock.On( + "GetAlias", mock.Anything, + ).Once().Return(lnwire.ShortChannelID{}, nil) + }, + channel: &channeldb.OpenChannel{ + FundingOutpoint: wire.OutPoint{ + Index: 0, + }, + IdentityPub: getTestPubKey(), + ChanType: channeldb.ScidAliasFeatureBit, + }, + include: false, +}, { + name: "channels with the option-scid-alias and an alias that has " + + "already been included should not be included again", + alreadyIncluded: map[uint64]bool{5: true}, + setupMock: func(h *hopHintsConfigMock) { + fundingOutpoint := wire.OutPoint{ + Index: 0, } - - // 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, + chanID := lnwire.NewChanIDFromOutPoint(&fundingOutpoint) + + h.Mock.On( + "IsChannelActive", chanID, + ).Once().Return(true) + + h.Mock.On( + "IsPublicNode", mock.Anything, + ).Once().Return(true, nil) + + h.Mock.On( + "FetchChannelEdgesByID", mock.Anything, + ).Once().Return( + &channeldb.ChannelEdgeInfo{}, + &channeldb.ChannelEdgePolicy{}, + &channeldb.ChannelEdgePolicy{}, nil, + ) + alias := lnwire.ShortChannelID{TxPosition: 5} + h.Mock.On( + "GetAlias", mock.Anything, + ).Once().Return(alias, nil) + }, + channel: &channeldb.OpenChannel{ + FundingOutpoint: wire.OutPoint{ + Index: 0, + }, + IdentityPub: getTestPubKey(), + ChanType: channeldb.ScidAliasFeatureBit, + }, + include: false, +}, { + name: "channels that pass all the checks should be " + + "included, using policy 1", + alreadyIncluded: map[uint64]bool{5: true}, + setupMock: func(h *hopHintsConfigMock) { + fundingOutpoint := wire.OutPoint{ + Index: 1, } + chanID := lnwire.NewChanIDFromOutPoint(&fundingOutpoint) - // Create a edge policy for private channel 1. - privateChan2Policy = &channeldb.ChannelEdgePolicy{ - FeeBaseMSat: 20, - FeeProportionalMillionths: 200, - TimeLockDelta: 2000, - } + h.Mock.On( + "IsChannelActive", chanID, + ).Once().Return(true) - // 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, - } + h.Mock.On( + "IsPublicNode", mock.Anything, + ).Once().Return(true, nil) + + var selectedPolicy [33]byte + copy(selectedPolicy[:], getTestPubKey().SerializeCompressed()) - // 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, + h.Mock.On( + "FetchChannelEdgesByID", mock.Anything, + ).Once().Return( + &channeldb.ChannelEdgeInfo{ + NodeKey1Bytes: selectedPolicy, + }, + &channeldb.ChannelEdgePolicy{ + FeeBaseMSat: 1000, + FeeProportionalMillionths: 20, + TimeLockDelta: 13, }, - RemotePubkey: pubkey, - RemoteBalance: 100, - ShortChannelID: private3ShortID, + &channeldb.ChannelEdgePolicy{}, + nil, + ) + }, + channel: &channeldb.OpenChannel{ + FundingOutpoint: wire.OutPoint{ + Index: 1, + }, + IdentityPub: getTestPubKey(), + ShortChannelID: lnwire.NewShortChanIDFromInt(12), + }, + hopHint: zpay32.HopHint{ + NodeID: getTestPubKey(), + FeeBaseMSat: 1000, + FeeProportionalMillionths: 20, + ChannelID: 12, + CLTVExpiryDelta: 13, + }, + include: true, +}, { + name: "channels that pass all the checks should be " + + "included, using policy 2", + alreadyIncluded: map[uint64]bool{5: true}, + setupMock: func(h *hopHintsConfigMock) { + fundingOutpoint := wire.OutPoint{ + Index: 1, } - - // Create a edge policy for private channel 1. - privateChan3Policy = &channeldb.ChannelEdgePolicy{ - FeeBaseMSat: 30, - FeeProportionalMillionths: 300, - TimeLockDelta: 3000, + chanID := lnwire.NewChanIDFromOutPoint(&fundingOutpoint) + + h.Mock.On( + "IsChannelActive", chanID, + ).Once().Return(true) + + h.Mock.On( + "IsPublicNode", mock.Anything, + ).Once().Return(true, nil) + + h.Mock.On( + "FetchChannelEdgesByID", mock.Anything, + ).Once().Return( + &channeldb.ChannelEdgeInfo{}, + &channeldb.ChannelEdgePolicy{}, + &channeldb.ChannelEdgePolicy{ + FeeBaseMSat: 1000, + FeeProportionalMillionths: 20, + TimeLockDelta: 13, + }, nil, + ) + }, + channel: &channeldb.OpenChannel{ + FundingOutpoint: wire.OutPoint{ + Index: 1, + }, + IdentityPub: getTestPubKey(), + ShortChannelID: lnwire.NewShortChanIDFromInt(12), + }, + hopHint: zpay32.HopHint{ + NodeID: getTestPubKey(), + FeeBaseMSat: 1000, + FeeProportionalMillionths: 20, + ChannelID: 12, + CLTVExpiryDelta: 13, + }, + include: true, +}, { + name: "channels that pass all the checks and have an alias " + + "should be included with the alias", + alreadyIncluded: map[uint64]bool{5: true}, + setupMock: func(h *hopHintsConfigMock) { + fundingOutpoint := wire.OutPoint{ + Index: 1, } + chanID := lnwire.NewChanIDFromOutPoint(&fundingOutpoint) + + h.Mock.On( + "IsChannelActive", chanID, + ).Once().Return(true) + + h.Mock.On( + "IsPublicNode", mock.Anything, + ).Once().Return(true, nil) + + h.Mock.On( + "FetchChannelEdgesByID", mock.Anything, + ).Once().Return( + &channeldb.ChannelEdgeInfo{}, + &channeldb.ChannelEdgePolicy{}, + &channeldb.ChannelEdgePolicy{ + FeeBaseMSat: 1000, + FeeProportionalMillionths: 20, + TimeLockDelta: 13, + }, nil, + ) - // 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, - } - ) + aliasSCID := lnwire.NewShortChanIDFromInt(15) - // 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, - } + h.Mock.On( + "GetAlias", mock.Anything, + ).Once().Return(aliasSCID, nil) + }, + channel: &channeldb.OpenChannel{ + FundingOutpoint: wire.OutPoint{ + Index: 1, + }, + IdentityPub: getTestPubKey(), + ShortChannelID: lnwire.NewShortChanIDFromInt(12), + ChanType: channeldb.ScidAliasFeatureBit, + }, + hopHint: zpay32.HopHint{ + NodeID: getTestPubKey(), + FeeBaseMSat: 1000, + FeeProportionalMillionths: 20, + ChannelID: 15, + CLTVExpiryDelta: 13, + }, + include: true, +}} + +func TestShouldIncludeChannel(t *testing.T) { + for _, tc := range shouldIncludeChannelTestCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - infoNode2 = &channeldb.ChannelEdgeInfo{ - NodeKey1Bytes: [33]byte{9, 9, 9}, - NodeKey2Bytes: peer, - } + // Create mock and prime it for the test case. + mock := &hopHintsConfigMock{} + if tc.setupMock != nil { + tc.setupMock(mock) + } + defer mock.AssertExpectations(t) - // 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, + cfg := &SelectHopHintsCfg{ + IsPublicNode: mock.IsPublicNode, + IsChannelActive: mock.IsChannelActive, + FetchChannelEdgesByID: mock.FetchChannelEdgesByID, + GetAlias: mock.GetAlias, + } + + hopHint, remoteBalance, include := shouldIncludeChannel( + cfg, tc.channel, tc.alreadyIncluded, ) - } - ) - 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, - }, + require.Equal(t, tc.include, include) + if include { + require.Equal(t, tc.hopHint, hopHint) + require.Equal( + t, tc.remoteBalance, remoteBalance, + ) + } + }) + } +} + +var sufficientHintsTestCases = []struct { + name string + nHintsLeft int + currentAmount lnwire.MilliSatoshi + targetAmount lnwire.MilliSatoshi + done bool +}{{ + name: "not enoguh hints neither bandwidth", + nHintsLeft: 3, + currentAmount: 100, + targetAmount: 200, + done: false, +}, { + name: "enough hints", + nHintsLeft: 0, + done: true, +}, { + name: "enoguh bandwidth", + nHintsLeft: 1, + currentAmount: 200, + targetAmount: 200, + done: true, +}} + +func TestSufficientHints(t *testing.T) { + for _, tc := range sufficientHintsTestCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + enoughHints := sufficientHints( + tc.nHintsLeft, tc.currentAmount, + tc.targetAmount, + ) + require.Equal(t, tc.done, enoughHints) + }) + } +} + +var populateHopHintsTestCases = []struct { + name string + setupMock func(*hopHintsConfigMock) + amount lnwire.MilliSatoshi + maxHopHints int + forcedHints [][]zpay32.HopHint + expectedHopHints [][]zpay32.HopHint +}{{ + name: "populate hop hints with forced hints", + maxHopHints: 1, + forcedHints: [][]zpay32.HopHint{ { - name: "channel is inactive", - setupMock: func(h *hopHintsConfigMock) {}, - amount: 100, - channels: []*HopHintInfo{ - inactiveChannel, - }, - numHints: 2, - expectedHints: nil, + {ChannelID: 12}, }, + }, + expectedHopHints: [][]zpay32.HopHint{ { - // 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, + {ChannelID: 12}, }, + }, +}, { + name: "populate hop hints stops when we reached the max number of " + + "hop hints allowed", + setupMock: func(h *hopHintsConfigMock) { + fundingOutpoint := wire.OutPoint{Index: 9} + chanID := lnwire.NewChanIDFromOutPoint(&fundingOutpoint) + allChannels := []*channeldb.OpenChannel{ + { + FundingOutpoint: fundingOutpoint, + ShortChannelID: lnwire.NewShortChanIDFromInt(9), + IdentityPub: getTestPubKey(), + }, + // Have one empty channel that we should not process + // because we have already finished. + {}, + } + + h.Mock.On( + "FetchAllChannels", + ).Once().Return(allChannels, nil) + + h.Mock.On( + "IsChannelActive", chanID, + ).Once().Return(true) + + h.Mock.On( + "IsPublicNode", mock.Anything, + ).Once().Return(true, nil) + + h.Mock.On( + "FetchChannelEdgesByID", mock.Anything, + ).Once().Return( + &channeldb.ChannelEdgeInfo{}, + &channeldb.ChannelEdgePolicy{}, + &channeldb.ChannelEdgePolicy{}, nil, + ) + }, + maxHopHints: 1, + amount: 1_000_000, + expectedHopHints: [][]zpay32.HopHint{ { - // 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) + { + NodeID: getTestPubKey(), + ChannelID: 9, }, - 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, + }, +}, { + name: "populate hop hints stops when we reached the targeted bandwidth", + setupMock: func(h *hopHintsConfigMock) { + fundingOutpoint := wire.OutPoint{Index: 9} + chanID := lnwire.NewChanIDFromOutPoint(&fundingOutpoint) + remoteBalance := lnwire.MilliSatoshi(10_000_000) + allChannels := []*channeldb.OpenChannel{ + { + LocalCommitment: channeldb.ChannelCommitment{ + RemoteBalance: remoteBalance, }, + FundingOutpoint: fundingOutpoint, + ShortChannelID: lnwire.NewShortChanIDFromInt(9), + IdentityPub: getTestPubKey(), }, - }, + // Have one empty channel that we should not process + // because we have already finished. + {}, + } + + h.Mock.On( + "FetchAllChannels", + ).Once().Return(allChannels, nil) + + h.Mock.On( + "IsChannelActive", chanID, + ).Once().Return(true) + + h.Mock.On( + "IsPublicNode", mock.Anything, + ).Once().Return(true, nil) + + h.Mock.On( + "FetchChannelEdgesByID", mock.Anything, + ).Once().Return( + &channeldb.ChannelEdgeInfo{}, + &channeldb.ChannelEdgePolicy{}, + &channeldb.ChannelEdgePolicy{}, nil, + ) + }, + maxHopHints: 10, + amount: 1_000_000, + expectedHopHints: [][]zpay32.HopHint{ { - // 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, - }, + { + NodeID: getTestPubKey(), + ChannelID: 9, }, }, - { - // 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, + }, +}, { + name: "populate hop hints tries to use the channels with higher " + + "remote balance frist", + setupMock: func(h *hopHintsConfigMock) { + fundingOutpoint := wire.OutPoint{Index: 9} + chanID := lnwire.NewChanIDFromOutPoint(&fundingOutpoint) + remoteBalance := lnwire.MilliSatoshi(10_000_000) + allChannels := []*channeldb.OpenChannel{ + // Because the channels with higher remote balance have + // enough bandwidth we should never use this one. + {}, + { + LocalCommitment: channeldb.ChannelCommitment{ + RemoteBalance: remoteBalance, }, + FundingOutpoint: fundingOutpoint, + ShortChannelID: lnwire.NewShortChanIDFromInt(9), + IdentityPub: getTestPubKey(), }, - }, + } + + h.Mock.On( + "FetchAllChannels", + ).Once().Return(allChannels, nil) + + h.Mock.On( + "IsChannelActive", chanID, + ).Once().Return(true) + + h.Mock.On( + "IsPublicNode", mock.Anything, + ).Once().Return(true, nil) + + h.Mock.On( + "FetchChannelEdgesByID", mock.Anything, + ).Once().Return( + &channeldb.ChannelEdgeInfo{}, + &channeldb.ChannelEdgePolicy{}, + &channeldb.ChannelEdgePolicy{}, nil, + ) + }, + maxHopHints: 1, + amount: 1_000_000, + expectedHopHints: [][]zpay32.HopHint{ { - // 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, - }, + { + NodeID: getTestPubKey(), + ChannelID: 9, }, }, - { - // 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, + }, +}, { + name: "populate hop hints stops after having considered all the open " + + "channels", + setupMock: func(h *hopHintsConfigMock) { + fundingOutpoint1 := wire.OutPoint{Index: 9} + chanID1 := lnwire.NewChanIDFromOutPoint(&fundingOutpoint1) + remoteBalance1 := lnwire.MilliSatoshi(10_000_000) + + fundingOutpoint2 := wire.OutPoint{Index: 2} + chanID2 := lnwire.NewChanIDFromOutPoint(&fundingOutpoint2) + remoteBalance2 := lnwire.MilliSatoshi(1_000_000) + + allChannels := []*channeldb.OpenChannel{ + // After sorting we will first process chanID1 and then + // chanID2. + { + LocalCommitment: channeldb.ChannelCommitment{ + RemoteBalance: remoteBalance2, }, + FundingOutpoint: fundingOutpoint2, + ShortChannelID: lnwire.NewShortChanIDFromInt(2), + IdentityPub: getTestPubKey(), }, - }, - { - // 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, + { + LocalCommitment: channeldb.ChannelCommitment{ + RemoteBalance: remoteBalance1, }, + FundingOutpoint: fundingOutpoint1, + ShortChannelID: lnwire.NewShortChanIDFromInt(9), + IdentityPub: getTestPubKey(), }, - }, - { - // 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, - ) + h.Mock.On( + "FetchAllChannels", + ).Once().Return(allChannels, nil) + + // Prepare the mock for the first channel. + h.Mock.On( + "IsChannelActive", chanID1, + ).Once().Return(true) + + h.Mock.On( + "IsPublicNode", mock.Anything, + ).Once().Return(true, nil) + + h.Mock.On( + "FetchChannelEdgesByID", mock.Anything, + ).Once().Return( + &channeldb.ChannelEdgeInfo{}, + &channeldb.ChannelEdgePolicy{}, + &channeldb.ChannelEdgePolicy{}, nil, + ) - setMockChannelUsed( - h, private3ShortID, privateChan3Policy, - ) + // Prepare the mock for the second channel. + h.Mock.On( + "IsChannelActive", chanID2, + ).Once().Return(true) + + h.Mock.On( + "IsPublicNode", mock.Anything, + ).Once().Return(true, nil) + + h.Mock.On( + "FetchChannelEdgesByID", mock.Anything, + ).Once().Return( + &channeldb.ChannelEdgeInfo{}, + &channeldb.ChannelEdgePolicy{}, + &channeldb.ChannelEdgePolicy{}, nil, + ) + }, + maxHopHints: 10, + amount: 100_000_000, + expectedHopHints: [][]zpay32.HopHint{ + { + { + NodeID: getTestPubKey(), + ChannelID: 9, }, - 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, - }, + }, { + { + NodeID: getTestPubKey(), + ChannelID: 2, }, }, - } + }, +}} - getAlias := func(lnwire.ChannelID) (lnwire.ShortChannelID, error) { - return lnwire.ShortChannelID{}, nil - } +func TestPopulateHopHints(t *testing.T) { + for _, tc := range populateHopHintsTestCases { + tc := tc - for _, test := range tests { - test := test + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - t.Run(test.name, func(t *testing.T) { // Create mock and prime it for the test case. mock := &hopHintsConfigMock{} - test.setupMock(mock) + if tc.setupMock != nil { + tc.setupMock(mock) + } defer mock.AssertExpectations(t) cfg := &SelectHopHintsCfg{ IsPublicNode: mock.IsPublicNode, + IsChannelActive: mock.IsChannelActive, FetchChannelEdgesByID: mock.FetchChannelEdgesByID, - GetAlias: getAlias, + GetAlias: mock.GetAlias, + FetchAllChannels: mock.FetchAllChannels, + MaxHopHints: tc.maxHopHints, } - - hints := SelectHopHints( - test.amount, cfg, test.channels, test.numHints, + hopHints, err := PopulateHopHints( + cfg, tc.amount, tc.forcedHints, ) - - // 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) - } + require.NoError(t, err) + // We shuffle the elements in the hop hint list so we + // need to compare the elements here. + require.ElementsMatch(t, tc.expectedHopHints, hopHints) }) } } - -// 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) - } -} diff --git a/lnrpc/invoicesrpc/invoices.pb.json.go b/lnrpc/invoicesrpc/invoices.pb.json.go index c8b2c00edf..6e2bb6f00a 100644 --- a/lnrpc/invoicesrpc/invoices.pb.json.go +++ b/lnrpc/invoicesrpc/invoices.pb.json.go @@ -1,8 +1,6 @@ // Code generated by falafel 0.9.1. DO NOT EDIT. // source: invoices.proto -// +build js - package invoicesrpc import ( diff --git a/lnrpc/lightning.pb.gw.go b/lnrpc/lightning.pb.gw.go index 13b7cd0a84..a9043ed737 100644 --- a/lnrpc/lightning.pb.gw.go +++ b/lnrpc/lightning.pb.gw.go @@ -1713,6 +1713,94 @@ func local_request_Lightning_QueryRoutes_0(ctx context.Context, marshaler runtim } +func request_Lightning_QueryRoutes_1(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryRoutesRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["pub_key"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "pub_key") + } + + protoReq.PubKey, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "pub_key", err) + } + + val, ok = pathParams["amt"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "amt") + } + + protoReq.Amt, err = runtime.Int64(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "amt", err) + } + + msg, err := client.QueryRoutes(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Lightning_QueryRoutes_1(ctx context.Context, marshaler runtime.Marshaler, server LightningServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryRoutesRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["pub_key"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "pub_key") + } + + protoReq.PubKey, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "pub_key", err) + } + + val, ok = pathParams["amt"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "amt") + } + + protoReq.Amt, err = runtime.Int64(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "amt", err) + } + + msg, err := server.QueryRoutes(ctx, &protoReq) + return msg, metadata, err + +} + func request_Lightning_GetNetworkInfo_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq NetworkInfoRequest var metadata runtime.ServerMetadata @@ -3262,6 +3350,29 @@ func RegisterLightningHandlerServer(ctx context.Context, mux *runtime.ServeMux, }) + mux.Handle("POST", pattern_Lightning_QueryRoutes_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/lnrpc.Lightning/QueryRoutes", runtime.WithHTTPPathPattern("/v1/graph/routes/{pub_key}/{amt}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Lightning_QueryRoutes_1(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Lightning_QueryRoutes_1(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_Lightning_GetNetworkInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -4602,6 +4713,26 @@ func RegisterLightningHandlerClient(ctx context.Context, mux *runtime.ServeMux, }) + mux.Handle("POST", pattern_Lightning_QueryRoutes_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/lnrpc.Lightning/QueryRoutes", runtime.WithHTTPPathPattern("/v1/graph/routes/{pub_key}/{amt}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Lightning_QueryRoutes_1(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Lightning_QueryRoutes_1(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_Lightning_GetNetworkInfo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -5114,6 +5245,8 @@ var ( pattern_Lightning_QueryRoutes_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 1, 0, 4, 1, 5, 4}, []string{"v1", "graph", "routes", "pub_key", "amt"}, "")) + pattern_Lightning_QueryRoutes_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 1, 0, 4, 1, 5, 4}, []string{"v1", "graph", "routes", "pub_key", "amt"}, "")) + pattern_Lightning_GetNetworkInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "graph", "info"}, "")) pattern_Lightning_StopDaemon_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "stop"}, "")) @@ -5246,6 +5379,8 @@ var ( forward_Lightning_QueryRoutes_0 = runtime.ForwardResponseMessage + forward_Lightning_QueryRoutes_1 = runtime.ForwardResponseMessage + forward_Lightning_GetNetworkInfo_0 = runtime.ForwardResponseMessage forward_Lightning_StopDaemon_0 = runtime.ForwardResponseMessage diff --git a/lnrpc/lightning.pb.json.go b/lnrpc/lightning.pb.json.go index bd7f295755..19f30cf85f 100644 --- a/lnrpc/lightning.pb.json.go +++ b/lnrpc/lightning.pb.json.go @@ -1,8 +1,6 @@ // Code generated by falafel 0.9.1. DO NOT EDIT. // source: lightning.proto -// +build js - package lnrpc import ( diff --git a/lnrpc/lightning.swagger.json b/lnrpc/lightning.swagger.json index 5007e143cf..23c7ec7dfb 100644 --- a/lnrpc/lightning.swagger.json +++ b/lnrpc/lightning.swagger.json @@ -1424,6 +1424,141 @@ "tags": [ "Lightning" ] + }, + "post": { + "summary": "lncli: `queryroutes`\nQueryRoutes attempts to query the daemon's Channel Router for a possible\nroute to a target destination capable of carrying a specific amount of\nsatoshis. The returned route contains the full details required to craft and\nsend an HTLC, also including the necessary information that should be\npresent within the Sphinx packet encapsulated within the HTLC.", + "description": "When using REST, the `dest_custom_records` map type can be set by appending\n`\u0026dest_custom_records[\u003crecord_number\u003e]=\u003crecord_data_base64_url_encoded\u003e`\nto the URL. Unfortunately this map type doesn't appear in the REST API\ndocumentation because of a bug in the grpc-gateway library.", + "operationId": "Lightning_QueryRoutes2", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/lnrpcQueryRoutesResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "pub_key", + "description": "The 33-byte hex-encoded public key for the payment destination", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "amt", + "description": "The amount to send expressed in satoshis.\n\nThe fields amt and amt_msat are mutually exclusive.", + "in": "path", + "required": true, + "type": "string", + "format": "int64" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "amt_msat": { + "type": "string", + "format": "int64", + "description": "The amount to send expressed in millisatoshis.\n\nThe fields amt and amt_msat are mutually exclusive." + }, + "final_cltv_delta": { + "type": "integer", + "format": "int32", + "description": "An optional CLTV delta from the current height that should be used for the\ntimelock of the final hop. Note that unlike SendPayment, QueryRoutes does\nnot add any additional block padding on top of final_ctlv_delta. This\npadding of a few blocks needs to be added manually or otherwise failures may\nhappen when a block comes in while the payment is in flight." + }, + "fee_limit": { + "$ref": "#/definitions/lnrpcFeeLimit", + "description": "The maximum number of satoshis that will be paid as a fee of the payment.\nThis value can be represented either as a percentage of the amount being\nsent, or as a fixed amount of the maximum fee the user is willing the pay to\nsend the payment. If not specified, lnd will use a default value of 100%\nfees for small amounts (\u003c=1k sat) or 5% fees for larger amounts." + }, + "ignored_nodes": { + "type": "array", + "items": { + "type": "string", + "format": "byte" + }, + "description": "A list of nodes to ignore during path finding. When using REST, these fields\nmust be encoded as base64." + }, + "ignored_edges": { + "type": "array", + "items": { + "$ref": "#/definitions/lnrpcEdgeLocator" + }, + "description": "Deprecated. A list of edges to ignore during path finding." + }, + "source_pub_key": { + "type": "string", + "description": "The source node where the request route should originated from. If empty,\nself is assumed." + }, + "use_mission_control": { + "type": "boolean", + "description": "If set to true, edge probabilities from mission control will be used to get\nthe optimal route." + }, + "ignored_pairs": { + "type": "array", + "items": { + "$ref": "#/definitions/lnrpcNodePair" + }, + "description": "A list of directed node pairs that will be ignored during path finding." + }, + "cltv_limit": { + "type": "integer", + "format": "int64", + "description": "An optional maximum total time lock for the route. If the source is empty or\nourselves, this should not exceed lnd's `--max-cltv-expiry` setting. If\nzero, then the value of `--max-cltv-expiry` is used as the limit." + }, + "dest_custom_records": { + "type": "object", + "additionalProperties": { + "type": "string", + "format": "byte" + }, + "description": "An optional field that can be used to pass an arbitrary set of TLV records\nto a peer which understands the new records. This can be used to pass\napplication specific data during the payment attempt. If the destination\ndoes not support the specified records, an error will be returned.\nRecord types are required to be in the custom range \u003e= 65536. When using\nREST, the values must be encoded as base64." + }, + "outgoing_chan_id": { + "type": "string", + "format": "uint64", + "description": "The channel id of the channel that must be taken to the first hop. If zero,\nany channel may be used." + }, + "last_hop_pubkey": { + "type": "string", + "format": "byte", + "description": "The pubkey of the last hop of the route. If empty, any hop may be used." + }, + "route_hints": { + "type": "array", + "items": { + "$ref": "#/definitions/lnrpcRouteHint" + }, + "description": "Optional route hints to reach the destination through private channels." + }, + "dest_features": { + "type": "array", + "items": { + "$ref": "#/definitions/lnrpcFeatureBit" + }, + "description": "Features assumed to be supported by the final node. All transitive feature\ndependencies must also be set properly. For a given feature bit pair, either\noptional or remote may be set, but not both. If this field is nil or empty,\nthe router will try to load destination features from the graph as a\nfallback." + }, + "time_pref": { + "type": "number", + "format": "double", + "description": "The time preference for this payment. Set to -1 to optimize for fees\nonly, to 1 to optimize for reliability only or a value inbetween for a mix." + } + } + } + } + ], + "tags": [ + "Lightning" + ] } }, "/v1/graph/subscribe": { diff --git a/lnrpc/lightning.yaml b/lnrpc/lightning.yaml index 0413291544..a78f60b6f7 100644 --- a/lnrpc/lightning.yaml +++ b/lnrpc/lightning.yaml @@ -111,6 +111,9 @@ http: get: "/v1/graph/node/{pub_key}" - selector: lnrpc.Lightning.QueryRoutes get: "/v1/graph/routes/{pub_key}/{amt}" + additional_bindings: + - post: "/v1/graph/routes/{pub_key}/{amt}" + body: "*" - selector: lnrpc.Lightning.GetNetworkInfo get: "/v1/graph/info" - selector: lnrpc.Lightning.StopDaemon diff --git a/lnrpc/neutrinorpc/neutrinokit.pb.json.go b/lnrpc/neutrinorpc/neutrinokit.pb.json.go index 5c5ee453b6..11cc19d306 100644 --- a/lnrpc/neutrinorpc/neutrinokit.pb.json.go +++ b/lnrpc/neutrinorpc/neutrinokit.pb.json.go @@ -1,8 +1,6 @@ // Code generated by falafel 0.9.1. DO NOT EDIT. // source: neutrino.proto -// +build js - package neutrinorpc import ( diff --git a/lnrpc/peersrpc/peers.pb.json.go b/lnrpc/peersrpc/peers.pb.json.go index 8efa8585ae..b47ba5a966 100644 --- a/lnrpc/peersrpc/peers.pb.json.go +++ b/lnrpc/peersrpc/peers.pb.json.go @@ -1,8 +1,6 @@ // Code generated by falafel 0.9.1. DO NOT EDIT. // source: peers.proto -// +build js - package peersrpc import ( diff --git a/lnrpc/routerrpc/router.pb.json.go b/lnrpc/routerrpc/router.pb.json.go index 15a1183cc0..a44b9a0e58 100644 --- a/lnrpc/routerrpc/router.pb.json.go +++ b/lnrpc/routerrpc/router.pb.json.go @@ -1,8 +1,6 @@ // Code generated by falafel 0.9.1. DO NOT EDIT. // source: router.proto -// +build js - package routerrpc import ( diff --git a/lnrpc/signrpc/signer.pb.json.go b/lnrpc/signrpc/signer.pb.json.go index 1fcafc1e40..1a18042dbf 100644 --- a/lnrpc/signrpc/signer.pb.json.go +++ b/lnrpc/signrpc/signer.pb.json.go @@ -1,8 +1,6 @@ // Code generated by falafel 0.9.1. DO NOT EDIT. // source: signer.proto -// +build js - package signrpc import ( diff --git a/lnrpc/state.pb.json.go b/lnrpc/state.pb.json.go index c9c6fd9d82..31130849ba 100644 --- a/lnrpc/state.pb.json.go +++ b/lnrpc/state.pb.json.go @@ -1,8 +1,6 @@ // Code generated by falafel 0.9.1. DO NOT EDIT. // source: stateservice.proto -// +build js - package lnrpc import ( diff --git a/lnrpc/verrpc/versioner.pb.json.go b/lnrpc/verrpc/versioner.pb.json.go index 211789c20d..fc7f0fcad1 100644 --- a/lnrpc/verrpc/versioner.pb.json.go +++ b/lnrpc/verrpc/versioner.pb.json.go @@ -1,8 +1,6 @@ // Code generated by falafel 0.9.1. DO NOT EDIT. // source: verrpc.proto -// +build js - package verrpc import ( diff --git a/lnrpc/walletrpc/walletkit.pb.json.go b/lnrpc/walletrpc/walletkit.pb.json.go index 0b174e56a9..1da7990a04 100644 --- a/lnrpc/walletrpc/walletkit.pb.json.go +++ b/lnrpc/walletrpc/walletkit.pb.json.go @@ -1,8 +1,6 @@ // Code generated by falafel 0.9.1. DO NOT EDIT. // source: walletkit.proto -// +build js - package walletrpc import ( diff --git a/lnrpc/walletunlocker.pb.json.go b/lnrpc/walletunlocker.pb.json.go index 38952a7006..98ae21c459 100644 --- a/lnrpc/walletunlocker.pb.json.go +++ b/lnrpc/walletunlocker.pb.json.go @@ -1,8 +1,6 @@ // Code generated by falafel 0.9.1. DO NOT EDIT. // source: walletunlocker.proto -// +build js - package lnrpc import ( diff --git a/lnrpc/watchtowerrpc/watchtower.pb.json.go b/lnrpc/watchtowerrpc/watchtower.pb.json.go index fe083c9294..92c3f73340 100644 --- a/lnrpc/watchtowerrpc/watchtower.pb.json.go +++ b/lnrpc/watchtowerrpc/watchtower.pb.json.go @@ -1,8 +1,6 @@ // Code generated by falafel 0.9.1. DO NOT EDIT. // source: watchtower.proto -// +build js - package watchtowerrpc import ( diff --git a/lnrpc/wtclientrpc/watchtowerclient.pb.json.go b/lnrpc/wtclientrpc/watchtowerclient.pb.json.go index 5a5d930268..aed077de5c 100644 --- a/lnrpc/wtclientrpc/watchtowerclient.pb.json.go +++ b/lnrpc/wtclientrpc/watchtowerclient.pb.json.go @@ -1,8 +1,6 @@ // Code generated by falafel 0.9.1. DO NOT EDIT. // source: wtclient.proto -// +build js - package wtclientrpc import ( diff --git a/lntest/itest/lnd_recovery_test.go b/lntest/itest/lnd_recovery_test.go index fe1f629ed7..37513be999 100644 --- a/lntest/itest/lnd_recovery_test.go +++ b/lntest/itest/lnd_recovery_test.go @@ -338,11 +338,11 @@ func testOnchainFundRecovery(net *lntest.NetworkHarness, t *harnessTest) { } restoreCheckBalance(finalBalance, 9, 20, promptChangeAddr) - // We should expect a static fee of 50100 satoshis for spending 9 inputs - // (3 P2WPKH, 3 NP2WPKH, 3 P2TR) to two P2WPKH outputs. Carol should - // therefore only have one UTXO present (the change output) of + // We should expect a static fee of 36400 satoshis for spending 9 + // inputs (3 P2WPKH, 3 NP2WPKH, 3 P2TR) to two P2WPKH outputs. Carol + // should therefore only have one UTXO present (the change output) of // 9 - 8 - fee BTC. - const fee = 50100 + const fee = 36400 restoreCheckBalance(finalBalance-minerAmt-fee, 1, 21, nil) // Last of all, make sure we can also restore a node from the extended diff --git a/lntest/itest/lnd_wallet_import_test.go b/lntest/itest/lnd_wallet_import_test.go index 616372e0fb..6489ea2f24 100644 --- a/lntest/itest/lnd_wallet_import_test.go +++ b/lntest/itest/lnd_wallet_import_test.go @@ -524,7 +524,7 @@ func fundChanAndCloseFromImportedAccount(t *harnessTest, srcNode, destNode, // they must've been redeemed after the close. Without a pre-negotiated // close address, the funds will go into the source node's wallet // instead of the imported account. - const chanCloseTxFee = 9050 + const chanCloseTxFee = 9650 balanceFromClosedChan := chanSize - invoiceAmt - chanCloseTxFee if account == defaultImportedAccount { diff --git a/lnwallet/chancloser/chancloser.go b/lnwallet/chancloser/chancloser.go index 1af162f7df..2ddf479500 100644 --- a/lnwallet/chancloser/chancloser.go +++ b/lnwallet/chancloser/chancloser.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/labels" @@ -97,9 +98,6 @@ const ( // interface that requires only the methods we need to carry out the channel // closing process. type Channel interface { - // CalcFee returns the absolute fee for the given fee rate. - CalcFee(chainfee.SatPerKWeight) btcutil.Amount - // ChannelPoint returns the channel point of the target channel. ChannelPoint() *wire.OutPoint @@ -118,6 +116,16 @@ type Channel interface { // an error should be returned. AbsoluteThawHeight() (uint32, error) + // LocalBalanceDust returns true if when creating a co-op close + // transaction, the balance of the local party will be dust after + // accounting for any anchor outputs. + LocalBalanceDust() bool + + // RemoteBalanceDust returns true if when creating a co-op close + // transaction, the balance of the remote party will be dust after + // accounting for any anchor outputs. + RemoteBalanceDust() bool + // RemoteUpfrontShutdownScript returns the upfront shutdown script of // the remote party. If the remote party didn't specify such a script, // an empty delivery address should be returned. @@ -137,6 +145,18 @@ type Channel interface { proposedFee btcutil.Amount) (*wire.MsgTx, btcutil.Amount, error) } +// CoopFeeEstimator is used to estimate the fee of a co-op close transaction. +type CoopFeeEstimator interface { + // EstimateFee estimates an _absolute_ fee for a co-op close transaction + // given the local+remote tx outs (for the co-op close transaction), + // channel type, and ideal fee rate. If a passed TxOut is nil, then + // that indicates that an output is dust on the co-op close transaction + // _before_ fees are accounted for. + EstimateFee(chanType channeldb.ChannelType, + localTxOut, remoteTxOut *wire.TxOut, + idealFeeRate chainfee.SatPerKWeight) btcutil.Amount +} + // ChanCloseCfg holds all the items that a ChanCloser requires to carry out its // duties. type ChanCloseCfg struct { @@ -163,6 +183,10 @@ type ChanCloseCfg struct { // Quit is a channel that should be sent upon in the occasion the state // machine should cease all progress and shutdown. Quit chan struct{} + + // FeeEstimator is used to estimate the absolute starting co-op close + // fee. + FeeEstimator CoopFeeEstimator } // ChanCloser is a state machine that handles the cooperative channel closure @@ -198,6 +222,9 @@ type ChanCloser struct { // multiplier based of the initial starting ideal fee. maxFee btcutil.Amount + // idealFeeRate is our ideal fee rate. + idealFeeRate chainfee.SatPerKWeight + // lastFeeProposal is the last fee that we proposed to the remote party. // We'll use this as a pivot point to ratchet our next offer up, down, or // simply accept the remote party's prior offer. @@ -231,6 +258,45 @@ type ChanCloser struct { locallyInitiated bool } +// calcCoopCloseFee computes an "ideal" absolute co-op close fee given the +// delivery scripts of both parties and our ideal fee rate. +func calcCoopCloseFee(localOutput, remoteOutput *wire.TxOut, + idealFeeRate chainfee.SatPerKWeight) btcutil.Amount { + + var weightEstimator input.TxWeightEstimator + + weightEstimator.AddWitnessInput(input.MultiSigWitnessSize) + + // One of these outputs might be dust, so we'll skip adding it to our + // mock transaction, so the fees are more accurate. + if localOutput != nil { + weightEstimator.AddTxOutput(localOutput) + } + if remoteOutput != nil { + weightEstimator.AddTxOutput(remoteOutput) + } + + totalWeight := int64(weightEstimator.Weight()) + + return idealFeeRate.FeeForWeight(totalWeight) +} + +// SimpleCoopFeeEstimator is the default co-op close fee estimator. It assumes +// a normal segwit v0 channel, and that no outputs on the closing transaction +// are dust. +type SimpleCoopFeeEstimator struct { +} + +// EstimateFee estimates an _absolute_ fee for a co-op close transaction given +// the local+remote tx outs (for the co-op close transaction), channel type, +// and ideal fee rate. +func (d *SimpleCoopFeeEstimator) EstimateFee(chanType channeldb.ChannelType, + localTxOut, remoteTxOut *wire.TxOut, + idealFeeRate chainfee.SatPerKWeight) btcutil.Amount { + + return calcCoopCloseFee(localTxOut, remoteTxOut, idealFeeRate) +} + // NewChanCloser creates a new instance of the channel closure given the passed // configuration, and delivery+fee preference. The final argument should only // be populated iff, we're the initiator of this closing request. @@ -238,21 +304,6 @@ func NewChanCloser(cfg ChanCloseCfg, deliveryScript []byte, idealFeePerKw chainfee.SatPerKWeight, negotiationHeight uint32, closeReq *htlcswitch.ChanClose, locallyInitiated bool) *ChanCloser { - // Given the target fee-per-kw, we'll compute what our ideal _total_ - // fee will be starting at for this fee negotiation. - idealFeeSat := cfg.Channel.CalcFee(idealFeePerKw) - - // When we're the initiator, we'll want to also factor in the highest - // fee we want to pay. This'll either be 3x the ideal fee, or the - // specified explicit max fee. - maxFee := idealFeeSat * defaultMaxFeeMultiplier - if cfg.MaxFee > 0 { - maxFee = cfg.Channel.CalcFee(cfg.MaxFee) - } - - chancloserLog.Infof("Ideal fee for closure of ChannelPoint(%v) is: %v sat", - cfg.Channel.ChannelPoint(), int64(idealFeeSat)) - cid := lnwire.NewChanIDFromOutPoint(cfg.Channel.ChannelPoint()) return &ChanCloser{ closeReq: closeReq, @@ -261,14 +312,53 @@ func NewChanCloser(cfg ChanCloseCfg, deliveryScript []byte, cid: cid, cfg: cfg, negotiationHeight: negotiationHeight, - idealFeeSat: idealFeeSat, - maxFee: maxFee, + idealFeeRate: idealFeePerKw, localDeliveryScript: deliveryScript, priorFeeOffers: make(map[btcutil.Amount]*lnwire.ClosingSigned), locallyInitiated: locallyInitiated, } } +// initFeeBaseline computes our ideal fee rate, and also the largest fee we'll +// accept given information about the delivery script of the remote party. +func (c *ChanCloser) initFeeBaseline() { + // Depending on if a balance ends up being dust or not, we'll pass a + // nil TxOut into the EstimateFee call which can handle it. + var localTxOut, remoteTxOut *wire.TxOut + if !c.cfg.Channel.LocalBalanceDust() { + localTxOut = &wire.TxOut{ + PkScript: c.localDeliveryScript, + Value: 0, + } + } + if !c.cfg.Channel.RemoteBalanceDust() { + remoteTxOut = &wire.TxOut{ + PkScript: c.remoteDeliveryScript, + Value: 0, + } + } + + // Given the target fee-per-kw, we'll compute what our ideal _total_ + // fee will be starting at for this fee negotiation. + c.idealFeeSat = c.cfg.FeeEstimator.EstimateFee( + 0, localTxOut, remoteTxOut, c.idealFeeRate, + ) + + // When we're the initiator, we'll want to also factor in the highest + // fee we want to pay. This'll either be 3x the ideal fee, or the + // specified explicit max fee. + c.maxFee = c.idealFeeSat * defaultMaxFeeMultiplier + if c.cfg.MaxFee > 0 { + c.maxFee = c.cfg.FeeEstimator.EstimateFee( + 0, localTxOut, remoteTxOut, c.cfg.MaxFee, + ) + } + + chancloserLog.Infof("Ideal fee for closure of ChannelPoint(%v) "+ + "is: %v sat (max_fee=%v sat)", c.cfg.Channel.ChannelPoint(), + int64(c.idealFeeSat), int64(c.maxFee)) +} + // initChanShutdown begins the shutdown process by un-registering the channel, // and creating a valid shutdown message to our target delivery address. func (c *ChanCloser) initChanShutdown() (*lnwire.Shutdown, error) { @@ -470,6 +560,10 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, // use this when we craft the closure transaction. c.remoteDeliveryScript = shutdownMsg.Address + // Now that we know their desried delivery script, we can + // compute what our max/ideal fee will be. + c.initFeeBaseline() + // We'll generate a shutdown message of our own to send across the // wire. localShutdown, err := c.initChanShutdown() @@ -534,6 +628,10 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, // closing transaction should look like. c.state = closeFeeNegotiation + // Now that we know their desried delivery script, we can + // compute what our max/ideal fee will be. + c.initFeeBaseline() + chancloserLog.Infof("ChannelPoint(%v): shutdown response received, "+ "entering fee negotiation", c.chanPoint) diff --git a/lnwallet/chancloser/chancloser_test.go b/lnwallet/chancloser/chancloser_test.go index 6d194398c8..046ccb5152 100644 --- a/lnwallet/chancloser/chancloser_test.go +++ b/lnwallet/chancloser/chancloser_test.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" @@ -131,14 +132,9 @@ func TestMaybeMatchScript(t *testing.T) { } type mockChannel struct { - absoluteFee btcutil.Amount - chanPoint wire.OutPoint - initiator bool - scid lnwire.ShortChannelID -} - -func (m *mockChannel) CalcFee(chainfee.SatPerKWeight) btcutil.Amount { - return m.absoluteFee + chanPoint wire.OutPoint + initiator bool + scid lnwire.ShortChannelID } func (m *mockChannel) ChannelPoint() *wire.OutPoint { @@ -179,12 +175,34 @@ func (m *mockChannel) CompleteCooperativeClose(localSig, return nil, 0, nil } +func (m *mockChannel) LocalBalanceDust() bool { + return false +} + +func (m *mockChannel) RemoteBalanceDust() bool { + return false +} + +type mockCoopFeeEstimator struct { + targetFee btcutil.Amount +} + +func (m *mockCoopFeeEstimator) EstimateFee(chanType channeldb.ChannelType, + localTxOut, remoteTxOut *wire.TxOut, + idealFeeRate chainfee.SatPerKWeight) btcutil.Amount { + + return m.targetFee +} + // TestMaxFeeClamp tests that if a max fee is specified, then it's used instead // of the default max fee multiplier. func TestMaxFeeClamp(t *testing.T) { t.Parallel() - const absoluteFee = btcutil.Amount(1000) + const ( + absoluteFeeOneSatByte = 126 + absoluteFeeTenSatByte = 1265 + ) tests := []struct { name string @@ -199,33 +217,40 @@ func TestMaxFeeClamp(t *testing.T) { name: "no max fee", idealFee: chainfee.SatPerKWeight(253), - maxFee: absoluteFee * defaultMaxFeeMultiplier, - }, { + maxFee: absoluteFeeOneSatByte * defaultMaxFeeMultiplier, + }, + { // Max fee specified, this should be used in place. name: "max fee clamp", idealFee: chainfee.SatPerKWeight(253), inputMaxFee: chainfee.SatPerKWeight(2530), - // Our mock just returns the canned absolute fee here. - maxFee: absoluteFee, + // We should get the resulting absolute fee based on a + // factor of 10 sat/byte (our new max fee). + maxFee: absoluteFeeTenSatByte, }, } for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { t.Parallel() - channel := mockChannel{ - absoluteFee: absoluteFee, - } + channel := mockChannel{} chanCloser := NewChanCloser( ChanCloseCfg{ - Channel: &channel, - MaxFee: test.inputMaxFee, + Channel: &channel, + MaxFee: test.inputMaxFee, + FeeEstimator: &SimpleCoopFeeEstimator{}, }, nil, test.idealFee, 0, nil, false, ) + // We'll call initFeeBaseline early here since we need + // the populate these internal variables. + chanCloser.initFeeBaseline() + require.Equal(t, test.maxFee, chanCloser.maxFee) }) } @@ -243,6 +268,8 @@ func TestMaxFeeBailOut(t *testing.T) { ) for _, isInitiator := range []bool{true, false} { + isInitiator := isInitiator + t.Run(fmt.Sprintf("initiator=%v", isInitiator), func(t *testing.T) { t.Parallel() @@ -250,8 +277,10 @@ func TestMaxFeeBailOut(t *testing.T) { // instantiate our channel closer. closeCfg := ChanCloseCfg{ Channel: &mockChannel{ - absoluteFee: absoluteFee, - initiator: isInitiator, + initiator: isInitiator, + }, + FeeEstimator: &mockCoopFeeEstimator{ + targetFee: absoluteFee, }, MaxFee: idealFee * 2, } diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 16c412f329..bbc893d3e3 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -7212,8 +7212,48 @@ func CreateCooperativeCloseTx(fundingTxIn wire.TxIn, return closeTx } -// CalcFee returns the commitment fee to use for the given -// fee rate (fee-per-kw). +// LocalBalanceDust returns true if when creating a co-op close transaction, +// the balance of the local party will be dust after accounting for any anchor +// outputs. +func (lc *LightningChannel) LocalBalanceDust() bool { + lc.RLock() + defer lc.RUnlock() + + chanState := lc.channelState + localBalance := chanState.LocalCommitment.LocalBalance.ToSatoshis() + + // If this is an anchor channel, and we're the initiator, then we'll + // regain the stats allocated to the anchor outputs with the co-op + // close transaction. + if chanState.ChanType.HasAnchors() && chanState.IsInitiator { + localBalance += 2 * anchorSize + } + + return localBalance <= chanState.LocalChanCfg.DustLimit +} + +// RemoteBalanceDust returns true if when creating a co-op close transaction, +// the balance of the remote party will be dust after accounting for any anchor +// outputs. +func (lc *LightningChannel) RemoteBalanceDust() bool { + lc.RLock() + defer lc.RUnlock() + + chanState := lc.channelState + remoteBalance := chanState.RemoteCommitment.RemoteBalance.ToSatoshis() + + // If this is an anchor channel, and they're the initiator, then we'll + // regain the stats allocated to the anchor outputs with the co-op + // close transaction. + if chanState.ChanType.HasAnchors() && !chanState.IsInitiator { + remoteBalance += 2 * anchorSize + } + + return remoteBalance <= chanState.RemoteChanCfg.DustLimit +} + +// CalcFee returns the commitment fee to use for the given fee rate +// (fee-per-kw). func (lc *LightningChannel) CalcFee(feeRate chainfee.SatPerKWeight) btcutil.Amount { return feeRate.FeeForWeight(CommitWeight(lc.channelState.ChanType)) } diff --git a/lnwire/short_channel_id.go b/lnwire/short_channel_id.go index f07de709f8..d4da518b76 100644 --- a/lnwire/short_channel_id.go +++ b/lnwire/short_channel_id.go @@ -64,6 +64,12 @@ func (c *ShortChannelID) Record() tlv.Record { ) } +// IsDefault returns true if the ShortChannelID represents the zero value for +// its type. +func (c ShortChannelID) IsDefault() bool { + return c == ShortChannelID{} +} + // EShortChannelID is an encoder for ShortChannelID. It is exported so other // packages can use the encoding scheme. func EShortChannelID(w io.Writer, val interface{}, buf *[8]byte) error { diff --git a/peer/brontide.go b/peer/brontide.go index cd77868191..1519627287 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -2712,8 +2712,9 @@ func (p *Brontide) createChanCloser(channel *lnwallet.LightningChannel, chanCloser := chancloser.NewChanCloser( chancloser.ChanCloseCfg{ - Channel: channel, - BroadcastTx: p.cfg.Wallet.PublishTransaction, + Channel: channel, + FeeEstimator: &chancloser.SimpleCoopFeeEstimator{}, + BroadcastTx: p.cfg.Wallet.PublishTransaction, DisableChannel: func(op wire.OutPoint) error { return p.cfg.ChanStatusMgr.RequestDisable( op, false, diff --git a/server.go b/server.go index cf8e8a52ba..219b3c8076 100644 --- a/server.go +++ b/server.go @@ -3195,7 +3195,13 @@ func (s *server) NotifyWhenOnline(peerKey [33]byte, select { case <-peer.ActiveSignal(): case <-peer.QuitSignal(): - // The peer quit so we'll just return. + // The peer quit, so we'll add the channel to the slice + // and return. + s.mu.Lock() + s.peerConnectedListeners[pubStr] = append( + s.peerConnectedListeners[pubStr], peerChan, + ) + s.mu.Unlock() return }