Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi rfq receive (AddInvoice multiple hop hints) #1457

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6665348
rfqmsg: Htlc.SumAssetBalance requires specifier checker
GeorgeTsagk Feb 27, 2025
4ddf460
rfq: policies use specifier checker
GeorgeTsagk Mar 26, 2025
08752b8
rfq: populate assetID field in interceptor response
GeorgeTsagk Mar 26, 2025
8a22cc0
multi: move rfq marshal code into rfq package
GeorgeTsagk Mar 26, 2025
3ebcce5
tapchannel: aux invoice manager uses specifier checker
GeorgeTsagk Feb 27, 2025
b61c791
tapchannel: add group key tests to AuxInvoiceManager
GeorgeTsagk Feb 27, 2025
3d704c2
rfq: don't return error on empty groupkey lookup
GeorgeTsagk Mar 5, 2025
33f55b1
rfq: asset specifier matcher checks against groupkey hash
GeorgeTsagk Mar 5, 2025
32f4184
tapchannel: make ProduceHtlcExtraData groupkey aware
GeorgeTsagk Mar 5, 2025
49a0de8
taprpc: add groupkey to SendPaymentRequest
GeorgeTsagk Mar 5, 2025
5a1ab5f
rpcserver: add asset specifier marshaller
GeorgeTsagk Mar 26, 2025
a8ebcaf
rpcserver: SendPayment uses groupkey
GeorgeTsagk Mar 5, 2025
a486a7b
taprpc: add groupkey to AddInvoice
GeorgeTsagk Mar 5, 2025
99e85a7
rpcserver: AddInvoice uses groupkey
GeorgeTsagk Mar 5, 2025
46a2f28
multi: move rfq channel helpers to rfq manager
GeorgeTsagk Mar 27, 2025
f5f7ae4
rfq+rpcserver: RfqChannel returns map of peers to channels
GeorgeTsagk Mar 27, 2025
b4eeae1
rfq: add RfqToHopHint helper in manager
GeorgeTsagk Mar 27, 2025
ea6446a
rpcserver: add AcquireBuyOrder helper
GeorgeTsagk Mar 27, 2025
f97fef1
rpcserver: AddInvoices supports multi-rfq
GeorgeTsagk Mar 27, 2025
c5d02b1
taprpc: update AddInvoice documentation
GeorgeTsagk Mar 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 61 additions & 74 deletions rfq/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"sort"
"sync"
"time"

Expand Down Expand Up @@ -1066,9 +1067,11 @@ type ChannelWithSpecifier struct {
// each asset channel that matches the provided asset specifier.
func (m *Manager) ComputeChannelAssetBalance(ctx context.Context,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this code move is also a good opportunity to add some test coverage to these functions. Once we add varying RFQ selection strategies, the scope of the functions will expand, so good to nail down some test coverage now while we're at it.

a lil aider can prob go a long way here

activeChannels []lndclient.ChannelInfo,
specifier asset.Specifier) ([]ChannelWithSpecifier, error) {
specifier asset.Specifier) (map[route.Vertex][]ChannelWithSpecifier,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style nit: can use a type def here to give the data structure a more descriptive name/type, and also cut down on the amt of chars needed to ref it a bit.

error) {

peerChanMap := make(map[route.Vertex][]ChannelWithSpecifier)

channels := make([]ChannelWithSpecifier, 0)
for chanIdx := range activeChannels {
openChan := activeChannels[chanIdx]
if len(openChan.CustomChannelData) == 0 {
Expand Down Expand Up @@ -1104,25 +1107,39 @@ func (m *Manager) ComputeChannelAssetBalance(ctx context.Context,
aggrInfo.RemoteBalance += info.RemoteBalance
}

channels = append(channels, ChannelWithSpecifier{
_, ok := peerChanMap[openChan.PubKeyBytes]
if !ok {
peerChanMap[openChan.PubKeyBytes] =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think since this isn't a nested map, you can actually just ignore ok here and append to the return value, which will automatically allocate space as needed.

make([]ChannelWithSpecifier, 0)
}

chanMap := peerChanMap[openChan.PubKeyBytes]

chanMap = append(chanMap, ChannelWithSpecifier{
Specifier: specifier,
ChannelInfo: openChan,
AssetInfo: aggrInfo,
})

peerChanMap[openChan.PubKeyBytes] = chanMap
}
}

return channels, nil
return peerChanMap, nil
}

// ChanLister is a helper that is able to list the channels of the node.
type ChanLister func(ctx context.Context, activeOnly,
publicOnly bool) ([]lndclient.ChannelInfo, error)

// chanIntention defines the intention of calling rfqChannel. This helps with
// returning the channel that is most suitable for what we want to do.
type chanIntention uint8
type ChanIntention uint8

const (
// NoIntention defines the absence of any intention, signalling that we
// don't really care which channel is returned.
NoIntention chanIntention = iota
NoIntention ChanIntention = iota

// SendIntention defines the intention to send over an asset channel.
SendIntention
Expand All @@ -1132,97 +1149,67 @@ const (
ReceiveIntention
)

// ChanLister is a helper that is able to list the channels of the node.
type ChanLister func(ctx context.Context, activeOnly,
publicOnly bool) ([]lndclient.ChannelInfo, error)

// RfqChannel returns the channel to use for RFQ operations. If a peer public
// key is specified, the channels are filtered by that peer. If there are
// multiple channels for the same specifier, the user must specify the peer
// public key.
// RfqChannel returns the channel to use for RFQ operations. It returns a map of
// peers and their eligible channels. If a peerPubKey is specified then the map
// will only contain one entry for that peer.
func (m *Manager) RfqChannel(ctx context.Context,
chanLister ChanLister,
specifier asset.Specifier, peerPubKey *route.Vertex,
intention chanIntention) (*ChannelWithSpecifier, error) {
chanLister ChanLister, specifier asset.Specifier,
peerPubKey *route.Vertex, intention ChanIntention) (map[route.Vertex][]ChannelWithSpecifier,
error) {

activeChannels, err := chanLister(ctx, true, false)
if err != nil {
return nil, err
}

balances, err := m.ComputeChannelAssetBalance(
balancesMap, err := m.ComputeChannelAssetBalance(
ctx, activeChannels, specifier,
)
if err != nil {
return nil, fmt.Errorf("error computing available asset "+
"channel balance: %w", err)
}

if len(balances) == 0 {
if len(balancesMap) == 0 {
return nil, fmt.Errorf("no asset channel balance found for %s",
&specifier)
}

// If a peer public key was specified, we always want to use that to
// filter the asset channels.
if peerPubKey != nil {
balances = fn.Filter(
balances, func(c ChannelWithSpecifier) bool {
return c.ChannelInfo.PubKeyBytes == *peerPubKey
},
)
}

switch {
// If there are multiple asset channels for the same specifier, we need
// to ask the user to specify the peer public key. Otherwise, we don't
// know who to ask for a quote.
case len(balances) > 1 && peerPubKey == nil:
return nil, fmt.Errorf("multiple asset channels found for "+
"%s, please specify the peer pubkey", &specifier)

// We don't have any channels with that asset ID and peer.
case len(balances) == 0:
return nil, fmt.Errorf("no asset channel found for %s",
&specifier)
}

// If the user specified a peer public key, and we still have multiple
// channels, it means we have multiple channels with the same asset and
// the same peer, as we ruled out the rest of the cases above.

// Initialize best balance to first channel of the list.
bestBalance := balances[0]

switch intention {
case ReceiveIntention:
// If the intention is to receive, return the channel
// with the best remote balance.
fn.ForEach(balances, func(b ChannelWithSpecifier) {
if b.AssetInfo.RemoteBalance >
bestBalance.AssetInfo.RemoteBalance {

bestBalance = b
}
})

case SendIntention:
// If the intention is to send, return the channel with
// the best local balance.
fn.ForEach(balances, func(b ChannelWithSpecifier) {
if b.AssetInfo.LocalBalance >
bestBalance.AssetInfo.LocalBalance {

bestBalance = b
}
})

// When sending we care about the volume of our local balances,
// so we sort by local balances in descending order.
for _, v := range balancesMap {
sort.Slice(v, func(i, j int) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, will this actually mutate the underlying value here (the slice) captured by the loop?

Would favor making the re-assignment explicit here, which may guard against future foot guns.

return v[i].AssetInfo.LocalBalance >
v[j].AssetInfo.LocalBalance
})
}
case ReceiveIntention:
// When sending we care about the volume of the remote balances,
// so we sort by remote balances in descending order.
for _, v := range balancesMap {
sort.Slice(v, func(i, j int) bool {
return v[i].AssetInfo.RemoteBalance >
v[j].AssetInfo.RemoteBalance
})
}
case NoIntention:
// Do nothing. Just return the first element that was
// assigned above.
// We don't care about sending or receiving, this means that
// the method was called as a dry check. Do nothing.
}

// If a peer public key was specified, we always want to use that to
// filter the asset channels.
if peerPubKey != nil {
_, ok := balancesMap[*peerPubKey]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if !ok {
return nil, fmt.Errorf("no asset channels found for "+
"%s and peer=%s", &specifier, peerPubKey)
}
}

return &bestBalance, nil
return balancesMap, nil
}

// publishSubscriberEvent publishes an event to all subscribers.
Expand Down
28 changes: 23 additions & 5 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -7401,7 +7401,7 @@ func (r *rpcServer) SendPayment(req *tchrpc.SendPaymentRequest,
rpcSpecifier := marshalAssetSpecifier(specifier)

// We can now query the asset channels we have.
assetChan, err := r.cfg.RfqManager.RfqChannel(
chanMap, err := r.cfg.RfqManager.RfqChannel(
ctx, r.cfg.Lnd.Client.ListChannels, specifier,
peerPubKey, rfq.SendIntention,
)
Expand All @@ -7410,10 +7410,22 @@ func (r *rpcServer) SendPayment(req *tchrpc.SendPaymentRequest,
"use: %w", err)
}

// TODO(george): temporary as multi-rfq send is not supported
// yet
if peerPubKey == nil && len(chanMap) > 1 {
return fmt.Errorf("multiple valid peers found, need " +
"specify peer pub key")
}
// Even if the user didn't specify the peer public key before,
// we definitely know it now. So let's make sure it's always
// set.
peerPubKey = &assetChan.ChannelInfo.PubKeyBytes
//
// TODO(george): we just grab the first value, this is temporary
// until multi-rfq send is implemented.
for _, v := range chanMap {
peerPubKey = &v[0].ChannelInfo.PubKeyBytes
break
}

// paymentMaxAmt is the maximum amount that the counterparty is
// expected to pay. This is the amount that the invoice is
Expand Down Expand Up @@ -7727,7 +7739,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
}

// We can now query the asset channels we have.
assetChan, err := r.cfg.RfqManager.RfqChannel(
chanMap, err := r.cfg.RfqManager.RfqChannel(
ctx, r.cfg.Lnd.Client.ListChannels, specifier, peerPubKey,
rfq.ReceiveIntention,
)
Expand All @@ -7736,9 +7748,15 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
err)
}

// TODO(george): this is temporary just for the commit to compile.
var firstChan rfq.ChannelWithSpecifier
for _, v := range chanMap {
firstChan = v[0]
}

// Even if the user didn't specify the peer public key before, we
// definitely know it now. So let's make sure it's always set.
peerPubKey = &assetChan.ChannelInfo.PubKeyBytes
peerPubKey = &firstChan.ChannelInfo.PubKeyBytes

expirySeconds := iReq.Expiry
if expirySeconds == 0 {
Expand Down Expand Up @@ -7816,7 +7834,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
// the quote, alongside the channel's routing policy. We need to choose
// the policy that points towards us, as the payment will be flowing in.
// So we get the policy that's being set by the remote peer.
channelID := assetChan.ChannelInfo.ChannelID
channelID := firstChan.ChannelInfo.ChannelID
inboundPolicy, err := r.getInboundPolicy(
ctx, channelID, peerPubKey.String(),
)
Expand Down