Skip to content

Commit 945fdd3

Browse files
committed
rpcserver: AddInvoice supports multi-rfq
We now change the quote related flow of AddInvoice to instead query all of our peers for valid quotes. We keep the best subset of accepted quotes up to the max allowed number of hop hints and place it into the bolt11 invoice.
1 parent b7ed69a commit 945fdd3

File tree

1 file changed

+125
-83
lines changed

1 file changed

+125
-83
lines changed

rpcserver.go

Lines changed: 125 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"io"
1212
"math"
1313
"net/http"
14+
"sort"
1415
"strings"
1516
"sync"
1617
"sync/atomic"
@@ -47,6 +48,7 @@ import (
4748
"github.com/lightninglabs/taproot-assets/taprpc"
4849
wrpc "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc"
4950
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
51+
"github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc"
5052
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
5153
tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc"
5254
"github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc"
@@ -102,6 +104,10 @@ const (
102104
// proofTypeReceive is an alias for the proof type used for receiving
103105
// assets.
104106
proofTypeReceive = tapdevrpc.ProofTransferType_PROOF_TRANSFER_TYPE_RECEIVE
107+
108+
// maxRfqHopHints is the maximum number of RFQ quotes that may be
109+
// encoded as hop hints in a bolt11 invoice.
110+
maxRfqHopHints = 20
105111
)
106112

107113
type (
@@ -7968,6 +7974,15 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
79687974
return nil, fmt.Errorf("invoice request must be specified")
79697975
}
79707976
iReq := req.InvoiceRequest
7977+
existingQuotes := iReq.RouteHints != nil
7978+
7979+
if existingQuotes && !tapchannel.IsAssetInvoice(
7980+
iReq, r.cfg.AuxInvoiceManager,
7981+
) {
7982+
7983+
return nil, fmt.Errorf("existing route hints should only " +
7984+
"contain valid accepted quotes")
7985+
}
79717986

79727987
assetID, groupKey, err := parseAssetSpecifier(
79737988
req.AssetId, "", req.GroupKey, "",
@@ -8003,16 +8018,6 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
80038018
err)
80048019
}
80058020

8006-
// TODO(george): this is temporary just for the commit to compile.
8007-
var firstChan rfq.ChannelWithSpecifier
8008-
for _, v := range chanMap {
8009-
firstChan = v[0]
8010-
}
8011-
8012-
// Even if the user didn't specify the peer public key before, we
8013-
// definitely know it now. So let's make sure it's always set.
8014-
peerPubKey = &firstChan.ChannelInfo.PubKeyBytes
8015-
80168021
expirySeconds := iReq.Expiry
80178022
if expirySeconds == 0 {
80188023
expirySeconds = int64(rfq.DefaultInvoiceExpiry.Seconds())
@@ -8036,63 +8041,93 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
80368041

80378042
rpcSpecifier := marshalAssetSpecifier(specifier)
80388043

8039-
resp, err := r.AddAssetBuyOrder(ctx, &rfqrpc.AddAssetBuyOrderRequest{
8040-
AssetSpecifier: &rpcSpecifier,
8041-
AssetMaxAmt: maxUnits,
8042-
Expiry: uint64(expiryTimestamp.Unix()),
8043-
PeerPubKey: peerPubKey[:],
8044-
TimeoutSeconds: uint32(
8045-
rfq.DefaultTimeout.Seconds(),
8046-
),
8047-
})
8048-
if err != nil {
8049-
return nil, fmt.Errorf("error adding buy order: %w", err)
8044+
type quoteWithInfo struct {
8045+
quote *rfqrpc.PeerAcceptedBuyQuote
8046+
rate *rfqmath.BigIntFixedPoint
8047+
channel rfq.ChannelWithSpecifier
80508048
}
80518049

8052-
var acceptedQuote *rfqrpc.PeerAcceptedBuyQuote
8053-
switch r := resp.Response.(type) {
8054-
case *rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote:
8055-
acceptedQuote = r.AcceptedQuote
8050+
var acquiredQuotes []quoteWithInfo
80568051

8057-
case *rfqrpc.AddAssetBuyOrderResponse_InvalidQuote:
8058-
return nil, fmt.Errorf("peer %v sent back an invalid quote, "+
8059-
"status: %v", r.InvalidQuote.Peer,
8060-
r.InvalidQuote.Status.String())
8052+
for peer, channels := range chanMap {
8053+
if existingQuotes {
8054+
break
8055+
}
80618056

8062-
case *rfqrpc.AddAssetBuyOrderResponse_RejectedQuote:
8063-
return nil, fmt.Errorf("peer %v rejected the quote, code: %v, "+
8064-
"error message: %v", r.RejectedQuote.Peer,
8065-
r.RejectedQuote.ErrorCode, r.RejectedQuote.ErrorMessage)
8057+
quote, err := r.acquireBuyOrder(
8058+
ctx, &rpcSpecifier, maxUnits, expiryTimestamp,
8059+
&peer,
8060+
)
8061+
if err != nil {
8062+
rpcsLog.Errorf("error while trying to acquire a buy "+
8063+
"order for invoice: %v", err)
8064+
continue
8065+
}
80668066

8067-
default:
8068-
return nil, fmt.Errorf("unexpected response type: %T", r)
8067+
rate, err := rpcutils.UnmarshalFixedPoint(
8068+
&priceoraclerpc.FixedPoint{
8069+
Coefficient: quote.AskAssetRate.Coefficient,
8070+
Scale: quote.AskAssetRate.Scale,
8071+
},
8072+
)
8073+
if err != nil {
8074+
return nil, err
8075+
}
8076+
8077+
acquiredQuotes = append(acquiredQuotes, quoteWithInfo{
8078+
quote: quote,
8079+
rate: rate,
8080+
// Since the channels are sorted, we know the value with
8081+
// the greatest remote balance is at index 0.
8082+
channel: channels[0],
8083+
})
8084+
}
8085+
8086+
// Let's sort the ask rate of the quotes in ascending order.
8087+
sort.Slice(acquiredQuotes, func(i, j int) bool {
8088+
return acquiredQuotes[i].rate.ToUint64() <
8089+
acquiredQuotes[j].rate.ToUint64()
8090+
})
8091+
8092+
// If we failed to get any quotes, we need to return an error. If the
8093+
// user has already defined quotes in the request we don't return an
8094+
// error.
8095+
if len(acquiredQuotes) == 0 && !existingQuotes {
8096+
return nil, fmt.Errorf("could not create any quotes for the " +
8097+
"invoice")
8098+
}
8099+
8100+
// We need to trim any extra quotes that cannot make it into the bolt11
8101+
// invoice due to size limitations.
8102+
if len(acquiredQuotes) > maxRfqHopHints {
8103+
acquiredQuotes = acquiredQuotes[:maxRfqHopHints]
8104+
}
8105+
8106+
// TODO(george): We want to cancel back quotes that didn't make it into
8107+
// the set. Need to add CancelOrder endpoints to RFQ manager.
8108+
8109+
// We grab the most expensive rate to use as reference for the total
8110+
// invoice amount. Since peers have varying prices for the assets, we
8111+
// pick the most expensive rate in order to allow for any combination of
8112+
// MPP shards through our set of chosen peers.
8113+
var expensiveQuote *rfqrpc.PeerAcceptedBuyQuote
8114+
if !existingQuotes {
8115+
expensiveQuote = acquiredQuotes[0].quote
80698116
}
80708117

8118+
// replace with above
80718119
// Now that we have the accepted quote, we know the amount in (milli)
80728120
// Satoshi that we need to pay. We can now update the invoice with this
80738121
// amount.
80748122
invoiceAmtMsat, err := validateInvoiceAmount(
8075-
acceptedQuote, req.AssetAmount, iReq,
8123+
expensiveQuote, req.AssetAmount, iReq,
80768124
)
80778125
if err != nil {
80788126
return nil, fmt.Errorf("error validating invoice amount: %w",
80798127
err)
80808128
}
80818129
iReq.ValueMsat = int64(invoiceAmtMsat)
80828130

8083-
// The last step is to create a hop hint that includes the fake SCID of
8084-
// the quote, alongside the channel's routing policy. We need to choose
8085-
// the policy that points towards us, as the payment will be flowing in.
8086-
// So we get the policy that's being set by the remote peer.
8087-
channelID := firstChan.ChannelInfo.ChannelID
8088-
inboundPolicy, err := r.getInboundPolicy(
8089-
ctx, channelID, peerPubKey.String(),
8090-
)
8091-
if err != nil {
8092-
return nil, fmt.Errorf("unable to get inbound channel policy "+
8093-
"for channel with ID %d: %w", channelID, err)
8094-
}
8095-
80968131
// If this is a hodl invoice, then we'll copy over the relevant fields,
80978132
// then route this through the invoicerpc instead.
80988133
if req.HodlInvoice != nil {
@@ -8102,24 +8137,21 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
81028137
"hash: %w", err)
81038138
}
81048139

8105-
peerPub, err := btcec.ParsePubKey(peerPubKey[:])
8106-
if err != nil {
8107-
return nil, fmt.Errorf("error parsing peer "+
8108-
"pubkey: %w", err)
8109-
}
8140+
routeHints := make([][]zpay32.HopHint, 0)
8141+
for _, v := range acquiredQuotes {
8142+
hopHint, err := r.cfg.RfqManager.RfqToHopHint(
8143+
ctx, r.getInboundPolicy,
8144+
v.channel.ChannelInfo.ChannelID,
8145+
v.channel.ChannelInfo.PubKeyBytes, v.quote,
8146+
true,
8147+
)
8148+
if err != nil {
8149+
return nil, err
8150+
}
81108151

8111-
hopHint := []zpay32.HopHint{
8112-
{
8113-
NodeID: peerPub,
8114-
ChannelID: acceptedQuote.Scid,
8115-
FeeBaseMSat: uint32(inboundPolicy.FeeBaseMsat),
8116-
FeeProportionalMillionths: uint32(
8117-
inboundPolicy.FeeRateMilliMsat,
8118-
),
8119-
CLTVExpiryDelta: uint16(
8120-
inboundPolicy.TimeLockDelta,
8121-
),
8122-
},
8152+
routeHints = append(
8153+
routeHints, []zpay32.HopHint{*hopHint},
8154+
)
81238155
}
81248156

81258157
payReq, err := r.cfg.Lnd.Invoices.AddHoldInvoice(
@@ -8135,7 +8167,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
81358167
// add any hop hints other than this one.
81368168
Private: false,
81378169
HodlInvoice: true,
8138-
RouteHints: [][]zpay32.HopHint{hopHint},
8170+
RouteHints: routeHints,
81398171
},
81408172
)
81418173
if err != nil {
@@ -8144,29 +8176,39 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
81448176
}
81458177

81468178
return &tchrpc.AddInvoiceResponse{
8147-
AcceptedBuyQuote: acceptedQuote,
8179+
// TODO(george): For now we just return the expensive
8180+
// quote
8181+
AcceptedBuyQuote: expensiveQuote,
81488182
InvoiceResult: &lnrpc.AddInvoiceResponse{
81498183
PaymentRequest: payReq,
81508184
},
81518185
}, nil
81528186
}
81538187

8154-
// Otherwise, we'll make this into a normal invoice.
8155-
hopHint := &lnrpc.HopHint{
8156-
NodeId: peerPubKey.String(),
8157-
ChanId: acceptedQuote.Scid,
8158-
FeeBaseMsat: uint32(inboundPolicy.FeeBaseMsat),
8159-
FeeProportionalMillionths: uint32(
8160-
inboundPolicy.FeeRateMilliMsat,
8161-
),
8162-
CltvExpiryDelta: inboundPolicy.TimeLockDelta,
8163-
}
8164-
iReq.RouteHints = []*lnrpc.RouteHint{
8165-
{
8188+
routeHints := make([]*lnrpc.RouteHint, 0)
8189+
for _, v := range acquiredQuotes {
8190+
hopHint, err := r.cfg.RfqManager.RfqToHopHint(
8191+
ctx, r.getInboundPolicy,
8192+
v.channel.ChannelInfo.ChannelID,
8193+
v.channel.ChannelInfo.PubKeyBytes, v.quote, false,
8194+
)
8195+
if err != nil {
8196+
return nil, err
8197+
}
8198+
8199+
lnrpcHopHint := rfq.Zpay32HopHintToLnrpc(hopHint)
8200+
8201+
routeHints = append(routeHints, &lnrpc.RouteHint{
81668202
HopHints: []*lnrpc.HopHint{
8167-
hopHint,
8203+
lnrpcHopHint,
81688204
},
8169-
},
8205+
})
8206+
}
8207+
8208+
// Only replace the route hints of the invoice request if the user has
8209+
// not already set them.
8210+
if !existingQuotes {
8211+
iReq.RouteHints = routeHints
81708212
}
81718213

81728214
rpcCtx, _, rawClient := r.cfg.Lnd.Client.RawClientWithMacAuth(ctx)
@@ -8176,7 +8218,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
81768218
}
81778219

81788220
return &tchrpc.AddInvoiceResponse{
8179-
AcceptedBuyQuote: acceptedQuote,
8221+
AcceptedBuyQuote: expensiveQuote,
81808222
InvoiceResult: invoiceResp,
81818223
}, nil
81828224
}

0 commit comments

Comments
 (0)