Skip to content

Add Multi-RFQ Send #1613

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open

Add Multi-RFQ Send #1613

wants to merge 4 commits into from

Conversation

GeorgeTsagk
Copy link
Member

Description

Allows payments to use multiple quotes across multiple peers in order to pay an invoice. Previously we had to define a specific peer to negotiate a quote with, with this PR this is no longer required (but still supported) as Tapd will automatically scan our peers and establish quotes with all valid ones for the asset/amount of this payment.

The signature of ProduceHtlcExtraData had to be changed, as it's not possible to distinguish which of the quotes in the rfqmsg.Htlc should be used. We now provide the pubkey of the peer this HTLC is being sent to, in order to help Tapd extract the corresponding quote and calculate the correct amount of asset units.

Closes #1358

Depends on: lightningnetwork/lnd#9980

Copy link
Member

@guggero guggero left a comment

Choose a reason for hiding this comment

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

Looks pretty good! Have a couple of questions and suggestions, nothing major though.

if err != nil {
return err
}
list := make([]ID, num)
Copy link
Member

Choose a reason for hiding this comment

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

We allocate memory from a number we received over the wire. Need to check and error out the length before to make sure we limit the number of bytes we allocate.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, so far the total length of available RFQ IDs is open ended.

We should introduce a static limit (which would also limit the max number of quotes we acquire) and enforce it everywhere.

@@ -456,16 +457,43 @@ func (s *AuxTrafficShaper) ProduceHtlcExtraData(totalAmount lnwire.MilliSatoshi,
return totalAmount, htlcCustomRecords, nil
}

if htlc.RfqID.ValOpt().IsNone() {
return 0, nil, fmt.Errorf("no RFQ ID present in HTLC blob")
if htlc.AvailableRfqIDs.IsNone() {
Copy link
Member

Choose a reason for hiding this comment

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

Same here re backward compatibility. And also just to make sure we don't miss any edge case. After all, we explicitly set the singular htlc.RfqID in this function, so are we a 100% sure there is no code path where either the ProduceHtlcExtraData or PaymentBandwidth would be called with htlc.RfqID set?
IMO we should check both and only error out if none of them (or both) are set at the same time, with a comment that we'd only really expect the list to be set in the future.

Copy link
Member Author

Choose a reason for hiding this comment

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

For the PaymentBandwidth and ProduceHtlcExtraData the htlc.RfqID is replaced by htlc.AvailableRfqIDs

The only use of htlc.RfqID is when ProduceHtlcExtraData locks into a specific RFQ, which will be used to produce the actual HTLC that will be sent out to our peer

From that point forward everything is the same (we only use the htlc.RfqID to route HTLCs / accept HTLCs to invoices)

"%v, error message: %v", r.RejectedQuote.Peer,
r.RejectedQuote.ErrorCode,
r.RejectedQuote.ErrorMessage)
err = checkOverpayment(
Copy link
Member

Choose a reason for hiding this comment

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

So if a single peer gives us such a bad quote that we error out on this, the whole payment attempt is aborted. Not sure if we should instead log a warning here as well and continue? And perhaps store the latest error we logged and return that on len(acquiredQuotes) == 0 so the user knows why no quotes could be found without needing to look at the log? IMO that would be useful for both the r.acquireSellOrder and checkOverpayment errors.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed to instead continue in the loop and store the error in a var

The last errors (if any) of r.acquireSellOrder and checkOverpayment are joined and returned if no quotes were acquired

We add a new field to rfqmsg.Htlc which expresses the available quotes
that may be used to send out this HTLC. This is done to allow LND to
store an array of RFQ IDs to use later via the AuxTrafficShaper
interface in order to query asset bandwidth and produce the correct
asset related records for outgoing HTLCs.
This commit performs a small refactor to the paymentBandwidth helper.
Since we now have multiple candidate RFQ IDs, we extract the main logic
of calculating the bandwidth into a helper, and call it once for each of
the available RFQ IDs.
When LND queries the PaymentBandwidth there's no way to signal back
which RFQ ID ended up being used for that bandwidth calculation. We rely
on the assumption that one quote is established per peer within the
scope of a payment. This way, the AuxTrafficShaper methods spot the
quote that it needs to use by matching the peer of the quote with the
peer that LND is going to send this HTLC to.
The RPC method now uses all of the introduced features. Instead of
acquiring just one quote we now extract that logic into a helper and
call it once for each valid peer. We then encode the array of available
RFQ IDs into the first hop records and hand it over to LND.
@coveralls
Copy link

Pull Request Test Coverage Report for Build 15900277054

Details

  • 66 of 260 (25.38%) changed or added relevant lines in 5 files are covered.
  • 51 unchanged lines in 10 files lost coverage.
  • Overall coverage increased (+0.04%) to 38.781%

Changes Missing Coverage Covered Lines Changed/Added Lines %
rfq/order.go 0 3 0.0%
server.go 0 4 0.0%
rfqmsg/records.go 66 81 81.48%
tapchannel/aux_traffic_shaper.go 0 77 0.0%
rpcserver.go 0 95 0.0%
Files with Coverage Reduction New Missed Lines %
rpcserver.go 1 0.0%
address/address.go 2 70.93%
internal/test/helpers.go 2 86.95%
rfqmsg/records.go 2 67.57%
tappsbt/create.go 2 26.74%
tapchannel/aux_leaf_signer.go 3 43.08%
commitment/tap.go 4 71.82%
tapchannel/aux_traffic_shaper.go 5 0.0%
tapdb/multiverse.go 6 53.28%
internal/test/copy.go 24 75.0%
Totals Coverage Status
Change from base Build 15899212451: 0.04%
Covered Lines: 29915
Relevant Lines: 77139

💛 - Coveralls

@GeorgeTsagk GeorgeTsagk requested a review from guggero June 26, 2025 14:30
// to encode the list of available RFQ IDs that can be used for an HTLC.
// This list is only meant to be handled by the complementary LND
// instance via the AuxTrafficShaper hooks.
AvailableRfqIDsType = tlv.TlvType65540
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should drop the term "available" from these changes. From my point of view it's implied.

We could name this TLV type: HtlcRfqIDsType. As I understand it, it's basically HtlcRfqIDType but it can hold multiple IDs.

Comment on lines +65 to +68
// RfqIDs is a helper wrapper around the array of IDs that can be encoded as
// records.
type RfqIDs struct {
// IDs is a list of RFQ IDs that are associated with the HTLC.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should call this struct HtlcRfqIDs because the TLV type used will be specific to HTLC.

Comment on lines +171 to +173
func NewHtlc(amounts []*AssetBalance, rfqID fn.Option[ID],
availableRfqIDs fn.Option[[]ID]) *Htlc {

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should change this function signature to

func NewHtlc(amounts []*AssetBalance, rfqIDs []ID) *Htlc {

And if one ID is given set htlc.RfqID otherwise, if multiple, set the multi ID field on HTLC.

If rfqIDs is empty we should return an error here? Or to put that Q in the context of the current PR state, if rfqID and availableRfqIDs are both None, is that an error? I would expect RFQ ID is mandatory for NewHtlc in rfqmsg package.

@ZZiigguurraatt
Copy link

Please update

// The node identity public key of the peer to ask for a quote for sending
// out the assets and converting them to satoshis. This must be specified if
// there are multiple channels with the given asset ID.
bytes peer_pubkey = 3;
to reflect this new capability. Also, I think we need to be more specific to include group_key in addition to asset_id in there.

@ZZiigguurraatt
Copy link

What is this expected to do with an invoice with no amount specified?

@ZZiigguurraatt
Copy link

rfq_id is of type bytes

// The rfq id to use for this payment. If the user sets this value then the
// payment will immediately be dispatched, skipping the rfq negotiation
// phase, and using the following rfq id instead.
bytes rfq_id = 5;

but with multi-rfq send, I would expect the ability to provide an array. Not sure that you want to fix that in this PR, but maybe it should be a separate issue? I did not make this comment for multi-rfq receive because of #1442 .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: 🏗 In progress
Development

Successfully merging this pull request may close these issues.

[feature]: Multi-RFQ send
6 participants