Skip to content

Commit a304937

Browse files
author
ffranr
authored
Merge pull request #1197 from lightninglabs/rfq-improvments-20241115
More RFQ improvments
2 parents 47a265d + a1d989d commit a304937

File tree

15 files changed

+307
-207
lines changed

15 files changed

+307
-207
lines changed

rfq/manager.go

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,7 @@ func (m *Manager) UpsertAssetBuyOffer(offer BuyOffer) error {
689689
}
690690

691691
// BuyOrder instructs the RFQ (Request For Quote) system to request a quote from
692-
// a peer for the acquisition of an asset.
692+
// one or more peers for the acquisition of an asset.
693693
//
694694
// The normal use of a buy order is as follows:
695695
// 1. Alice, operating a wallet node, wants to receive a Tap asset as payment
@@ -715,8 +715,8 @@ type BuyOrder struct {
715715
// be willing to offer.
716716
AssetMaxAmt uint64
717717

718-
// Expiry is the unix timestamp at which the buy order expires.
719-
Expiry uint64
718+
// Expiry is the time at which the order expires.
719+
Expiry time.Time
720720

721721
// Peer is the peer that the buy order is intended for. This field is
722722
// optional.
@@ -745,28 +745,37 @@ func (m *Manager) UpsertAssetBuyOrder(order BuyOrder) error {
745745
return nil
746746
}
747747

748-
// SellOrder is a struct that represents an asset sell order.
748+
// SellOrder instructs the RFQ (Request For Quote) system to request a quote
749+
// from one or more peers for the disposition of an asset.
750+
//
751+
// Normal usage of a sell order:
752+
// 1. Alice creates a Lightning invoice for Bob to pay.
753+
// 2. Bob wants to pay the invoice using a Tap asset. To do so, Bob pays an
754+
// edge node with a Tap asset, and the edge node forwards the payment to the
755+
// network to settle Alice's invoice. Bob submits a SellOrder to his local
756+
// RFQ service.
757+
// 3. The RFQ service converts the SellOrder into one or more SellRequests.
758+
// These requests are sent to Charlie (the edge node), who shares a relevant
759+
// Tap asset channel with Bob and can forward payments to settle Alice's
760+
// invoice.
761+
// 4. Charlie responds with a quote that satisfies Bob.
762+
// 5. Bob transfers the appropriate Tap asset amount to Charlie via their
763+
// shared Tap asset channel, and Charlie forwards the corresponding amount
764+
// to Alice to settle the Lightning invoice.
749765
type SellOrder struct {
750-
// AssetID is the ID of the asset to sell.
751-
AssetID *asset.ID
752-
753-
// AssetGroupKey is the public key of the asset group to sell.
754-
AssetGroupKey *btcec.PublicKey
766+
// AssetSpecifier is the asset that the seller is interested in.
767+
AssetSpecifier asset.Specifier
755768

756769
// PaymentMaxAmt is the maximum msat amount that the responding peer
757770
// must agree to pay.
758771
PaymentMaxAmt lnwire.MilliSatoshi
759772

760-
// Expiry is the unix timestamp at which the order expires.
761-
//
762-
// TODO(ffranr): This is the invoice expiry unix timestamp in seconds.
763-
// We should make use of this field to ensure quotes are valid for the
764-
// duration of the invoice.
765-
Expiry uint64
773+
// Expiry is the time at which the order expires.
774+
Expiry time.Time
766775

767776
// Peer is the peer that the buy order is intended for. This field is
768777
// optional.
769-
Peer *route.Vertex
778+
Peer fn.Option[route.Vertex]
770779
}
771780

772781
// UpsertAssetSellOrder upserts an asset sell order for management.
@@ -775,7 +784,7 @@ func (m *Manager) UpsertAssetSellOrder(order SellOrder) error {
775784
//
776785
// TODO(ffranr): Add support for peerless sell orders. The negotiator
777786
// should be able to determine the optimal peer.
778-
if order.Peer == nil {
787+
if order.Peer.IsNone() {
779788
return fmt.Errorf("sell order peer must be specified")
780789
}
781790

@@ -795,7 +804,7 @@ func (m *Manager) PeerAcceptedBuyQuotes() BuyAcceptMap {
795804
buyQuotesCopy := make(map[SerialisedScid]rfqmsg.BuyAccept)
796805
m.peerAcceptedBuyQuotes.ForEach(
797806
func(scid SerialisedScid, accept rfqmsg.BuyAccept) error {
798-
if time.Now().Unix() > int64(accept.Expiry) {
807+
if time.Now().After(accept.AssetRate.Expiry) {
799808
m.peerAcceptedBuyQuotes.Delete(scid)
800809
return nil
801810
}
@@ -817,7 +826,7 @@ func (m *Manager) PeerAcceptedSellQuotes() SellAcceptMap {
817826
sellQuotesCopy := make(map[SerialisedScid]rfqmsg.SellAccept)
818827
m.peerAcceptedSellQuotes.ForEach(
819828
func(scid SerialisedScid, accept rfqmsg.SellAccept) error {
820-
if time.Now().Unix() > int64(accept.Expiry) {
829+
if time.Now().After(accept.AssetRate.Expiry) {
821830
m.peerAcceptedSellQuotes.Delete(scid)
822831
return nil
823832
}
@@ -839,7 +848,7 @@ func (m *Manager) LocalAcceptedBuyQuotes() BuyAcceptMap {
839848
buyQuotesCopy := make(map[SerialisedScid]rfqmsg.BuyAccept)
840849
m.localAcceptedBuyQuotes.ForEach(
841850
func(scid SerialisedScid, accept rfqmsg.BuyAccept) error {
842-
if time.Now().Unix() > int64(accept.Expiry) {
851+
if time.Now().After(accept.AssetRate.Expiry) {
843852
m.localAcceptedBuyQuotes.Delete(scid)
844853
return nil
845854
}
@@ -861,7 +870,7 @@ func (m *Manager) LocalAcceptedSellQuotes() SellAcceptMap {
861870
sellQuotesCopy := make(map[SerialisedScid]rfqmsg.SellAccept)
862871
m.localAcceptedSellQuotes.ForEach(
863872
func(scid SerialisedScid, accept rfqmsg.SellAccept) error {
864-
if time.Now().Unix() > int64(accept.Expiry) {
873+
if time.Now().After(accept.AssetRate.Expiry) {
865874
m.localAcceptedSellQuotes.Delete(scid)
866875
return nil
867876
}

rfq/negotiator.go

Lines changed: 53 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"github.com/lightninglabs/taproot-assets/rfqmsg"
1313
"github.com/lightningnetwork/lnd/lnutils"
1414
"github.com/lightningnetwork/lnd/lnwire"
15-
"github.com/lightningnetwork/lnd/routing/route"
1615
)
1716

1817
const (
@@ -105,8 +104,8 @@ func NewNegotiator(cfg NegotiatorCfg) (*Negotiator, error) {
105104

106105
// queryBidFromPriceOracle queries the price oracle for a bid price. It returns
107106
// an appropriate outgoing response message which should be sent to the peer.
108-
func (n *Negotiator) queryBidFromPriceOracle(peer route.Vertex,
109-
assetSpecifier asset.Specifier, assetMaxAmt fn.Option[uint64],
107+
func (n *Negotiator) queryBidFromPriceOracle(assetSpecifier asset.Specifier,
108+
assetMaxAmt fn.Option[uint64],
110109
paymentMaxAmt fn.Option[lnwire.MilliSatoshi],
111110
assetRateHint fn.Option[rfqmsg.AssetRate]) (*rfqmsg.AssetRate, error) {
112111

@@ -177,8 +176,11 @@ func (n *Negotiator) HandleOutgoingBuyOrder(buyOrder BuyOrder) error {
177176
buyOrder.AssetSpecifier.IsSome() {
178177

179178
// Query the price oracle for a bid price.
179+
//
180+
// TODO(ffranr): Pass the BuyOrder expiry to the price
181+
// oracle at this point.
180182
assetRate, err := n.queryBidFromPriceOracle(
181-
peer, buyOrder.AssetSpecifier,
183+
buyOrder.AssetSpecifier,
182184
fn.Some(buyOrder.AssetMaxAmt),
183185
fn.None[lnwire.MilliSatoshi](),
184186
fn.None[rfqmsg.AssetRate](),
@@ -227,8 +229,8 @@ func (n *Negotiator) HandleOutgoingBuyOrder(buyOrder BuyOrder) error {
227229
// queryAskFromPriceOracle queries the price oracle for an asking price. It
228230
// returns an appropriate outgoing response message which should be sent to the
229231
// peer.
230-
func (n *Negotiator) queryAskFromPriceOracle(peer *route.Vertex,
231-
assetSpecifier asset.Specifier, assetMaxAmt fn.Option[uint64],
232+
func (n *Negotiator) queryAskFromPriceOracle(assetSpecifier asset.Specifier,
233+
assetMaxAmt fn.Option[uint64],
232234
paymentMaxAmt fn.Option[lnwire.MilliSatoshi],
233235
assetRateHint fn.Option[rfqmsg.AssetRate]) (*rfqmsg.AssetRate, error) {
234236

@@ -326,7 +328,7 @@ func (n *Negotiator) HandleIncomingBuyRequest(
326328

327329
// Query the price oracle for an asking price.
328330
assetRate, err := n.queryAskFromPriceOracle(
329-
nil, request.AssetSpecifier,
331+
request.AssetSpecifier,
330332
fn.Some(request.AssetMaxAmt),
331333
fn.None[lnwire.MilliSatoshi](),
332334
request.AssetRateHint,
@@ -347,10 +349,7 @@ func (n *Negotiator) HandleIncomingBuyRequest(
347349
}
348350

349351
// Construct and send a buy accept message.
350-
expiry := uint64(assetRate.Expiry.Unix())
351-
msg := rfqmsg.NewBuyAcceptFromRequest(
352-
request, assetRate.Rate, expiry,
353-
)
352+
msg := rfqmsg.NewBuyAcceptFromRequest(request, *assetRate)
354353
sendOutgoingMsg(msg)
355354
}()
356355

@@ -426,9 +425,8 @@ func (n *Negotiator) HandleIncomingSellRequest(
426425
// are willing to pay for the asset that our peer is trying to
427426
// sell to us.
428427
assetRate, err := n.queryBidFromPriceOracle(
429-
request.Peer, request.AssetSpecifier,
430-
fn.None[uint64](), fn.Some(request.PaymentMaxAmt),
431-
request.AssetRateHint,
428+
request.AssetSpecifier, fn.None[uint64](),
429+
fn.Some(request.PaymentMaxAmt), request.AssetRateHint,
432430
)
433431
if err != nil {
434432
// Send a reject message to the peer.
@@ -446,10 +444,7 @@ func (n *Negotiator) HandleIncomingSellRequest(
446444
}
447445

448446
// Construct and send a sell accept message.
449-
expiry := uint64(assetRate.Expiry.Unix())
450-
msg := rfqmsg.NewSellAcceptFromRequest(
451-
request, assetRate.Rate, expiry,
452-
)
447+
msg := rfqmsg.NewSellAcceptFromRequest(request, *assetRate)
453448
sendOutgoingMsg(msg)
454449
}()
455450

@@ -467,27 +462,27 @@ func (n *Negotiator) HandleOutgoingSellOrder(order SellOrder) {
467462
go func() {
468463
defer n.Wg.Done()
469464

465+
// Unwrap the peer from the order. For now, we can assume that
466+
// the peer is always specified.
467+
peer, err := order.Peer.UnwrapOrErr(
468+
fmt.Errorf("buy order peer must be specified"),
469+
)
470+
if err != nil {
471+
n.cfg.ErrChan <- err
472+
}
473+
470474
// We calculate a proposed ask price for our peer's
471475
// consideration. If a price oracle is not specified we will
472476
// skip this step.
473477
var assetRateHint fn.Option[rfqmsg.AssetRate]
474478

475-
// Construct an asset specifier from the order.
476-
// TODO(ffranr): The order should have an asset specifier.
477-
assetSpecifier, err := asset.NewSpecifier(
478-
order.AssetID, order.AssetGroupKey, nil,
479-
true,
480-
)
481-
if err != nil {
482-
log.Warnf("failed to construct asset "+
483-
"specifier from buy order: %v", err)
484-
}
485-
486-
if n.cfg.PriceOracle != nil && assetSpecifier.IsSome() {
479+
if n.cfg.PriceOracle != nil && order.AssetSpecifier.IsSome() {
487480
// Query the price oracle for an asking price.
481+
//
482+
// TODO(ffranr): Pass the SellOrder expiry to the
483+
// price oracle at this point.
488484
assetRate, err := n.queryAskFromPriceOracle(
489-
order.Peer, assetSpecifier,
490-
fn.None[uint64](),
485+
order.AssetSpecifier, fn.None[uint64](),
491486
fn.Some(order.PaymentMaxAmt),
492487
fn.None[rfqmsg.AssetRate](),
493488
)
@@ -498,12 +493,12 @@ func (n *Negotiator) HandleOutgoingSellOrder(order SellOrder) {
498493
return
499494
}
500495

501-
assetRateHint = fn.Some[rfqmsg.AssetRate](*assetRate)
496+
assetRateHint = fn.MaybeSome(assetRate)
502497
}
503498

504499
request, err := rfqmsg.NewSellRequest(
505-
*order.Peer, order.AssetID, order.AssetGroupKey,
506-
order.PaymentMaxAmt, assetRateHint,
500+
peer, order.AssetSpecifier, order.PaymentMaxAmt,
501+
assetRateHint,
507502
)
508503
if err != nil {
509504
err := fmt.Errorf("unable to create sell request "+
@@ -530,12 +525,8 @@ func (n *Negotiator) HandleOutgoingSellOrder(order SellOrder) {
530525
// expiryWithinBounds checks if a quote expiry unix timestamp (in seconds) is
531526
// within acceptable bounds. This check ensures that the expiry timestamp is far
532527
// enough in the future for the quote to be useful.
533-
func expiryWithinBounds(expiryUnixTimestamp uint64,
534-
minExpiryLifetime uint64) bool {
535-
536-
// Convert the expiry timestamp into a time.Time.
537-
actualExpiry := time.Unix(int64(expiryUnixTimestamp), 0)
538-
diff := actualExpiry.Unix() - time.Now().Unix()
528+
func expiryWithinBounds(expiry time.Time, minExpiryLifetime uint64) bool {
529+
diff := expiry.Unix() - time.Now().Unix()
539530
return diff >= int64(minExpiryLifetime)
540531
}
541532

@@ -549,12 +540,16 @@ func (n *Negotiator) HandleIncomingBuyAccept(msg rfqmsg.BuyAccept,
549540
// Ensure that the quote expiry time is within acceptable bounds.
550541
//
551542
// TODO(ffranr): Sanity check the buy accept quote expiry
552-
// timestamp given the expiry timestamp provided by the price
553-
// oracle.
554-
if !expiryWithinBounds(msg.Expiry, minAssetRatesExpiryLifetime) {
543+
// timestamp given the expiry timestamp in our outgoing buy request.
544+
// The expiry timestamp in the outgoing request relates to the lifetime
545+
// of the lightning invoice.
546+
if !expiryWithinBounds(
547+
msg.AssetRate.Expiry, minAssetRatesExpiryLifetime,
548+
) {
555549
// The expiry time is not within the acceptable bounds.
556550
log.Debugf("Buy accept quote expiry time is not within "+
557-
"acceptable bounds (expiry=%d)", msg.Expiry)
551+
"acceptable bounds (asset_rate=%s)",
552+
msg.AssetRate.String())
558553

559554
// Construct an invalid quote response event so that we can
560555
// inform the peer that the quote response has not validated
@@ -601,10 +596,9 @@ func (n *Negotiator) HandleIncomingBuyAccept(msg rfqmsg.BuyAccept,
601596
// for an ask price. We will then compare the ask price returned
602597
// by the price oracle with the ask price provided by the peer.
603598
assetRate, err := n.queryAskFromPriceOracle(
604-
&msg.Peer, msg.Request.AssetSpecifier,
599+
msg.Request.AssetSpecifier,
605600
fn.Some(msg.Request.AssetMaxAmt),
606-
fn.None[lnwire.MilliSatoshi](),
607-
fn.None[rfqmsg.AssetRate](),
601+
fn.None[lnwire.MilliSatoshi](), fn.Some(msg.AssetRate),
608602
)
609603
if err != nil {
610604
// The price oracle returned an error. We will return
@@ -635,17 +629,17 @@ func (n *Negotiator) HandleIncomingBuyAccept(msg rfqmsg.BuyAccept,
635629
tolerance := rfqmath.NewBigIntFromUint64(
636630
n.cfg.AcceptPriceDeviationPpm,
637631
)
638-
acceptablePrice := msg.AssetRate.WithinTolerance(
632+
acceptablePrice := msg.AssetRate.Rate.WithinTolerance(
639633
assetRate.Rate, tolerance,
640634
)
641635
if !acceptablePrice {
642636
// The price is not within the acceptable tolerance.
643637
// We will return without calling the quote accept
644638
// callback.
645639
log.Debugf("Buy accept price is not within "+
646-
"acceptable bounds (ask_asset_rate=%v, "+
647-
"oracle_asset_rate=%v)", msg.AssetRate,
648-
assetRate)
640+
"acceptable bounds (peer_asset_rate=%s, "+
641+
"oracle_asset_rate=%s)", msg.AssetRate.String(),
642+
assetRate.String())
649643

650644
// Construct an invalid quote response event so that we
651645
// can inform the peer that the quote response has not
@@ -677,10 +671,13 @@ func (n *Negotiator) HandleIncomingSellAccept(msg rfqmsg.SellAccept,
677671
//
678672
// TODO(ffranr): Sanity check the quote expiry timestamp given
679673
// the expiry timestamp provided by the price oracle.
680-
if !expiryWithinBounds(msg.Expiry, minAssetRatesExpiryLifetime) {
674+
if !expiryWithinBounds(
675+
msg.AssetRate.Expiry, minAssetRatesExpiryLifetime,
676+
) {
681677
// The expiry time is not within the acceptable bounds.
682678
log.Debugf("Sell accept quote expiry time is not within "+
683-
"acceptable bounds (expiry=%d)", msg.Expiry)
679+
"acceptable bounds (asset_rate=%s)",
680+
msg.AssetRate.String())
684681

685682
// Construct an invalid quote response event so that we can
686683
// inform the peer that the quote response has not validated
@@ -727,8 +724,7 @@ func (n *Negotiator) HandleIncomingSellAccept(msg rfqmsg.SellAccept,
727724
// for a bid price. We will then compare the bid price returned
728725
// by the price oracle with the bid price provided by the peer.
729726
assetRate, err := n.queryBidFromPriceOracle(
730-
msg.Peer, msg.Request.AssetSpecifier,
731-
fn.None[uint64](),
727+
msg.Request.AssetSpecifier, fn.None[uint64](),
732728
fn.Some(msg.Request.PaymentMaxAmt),
733729
msg.Request.AssetRateHint,
734730
)
@@ -761,7 +757,7 @@ func (n *Negotiator) HandleIncomingSellAccept(msg rfqmsg.SellAccept,
761757
tolerance := rfqmath.NewBigIntFromUint64(
762758
n.cfg.AcceptPriceDeviationPpm,
763759
)
764-
acceptablePrice := msg.AssetRate.WithinTolerance(
760+
acceptablePrice := msg.AssetRate.Rate.WithinTolerance(
765761
assetRate.Rate, tolerance,
766762
)
767763
if !acceptablePrice {

rfq/order.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ func NewAssetSalePolicy(quote rfqmsg.BuyAccept) *AssetSalePolicy {
108108
AssetSpecifier: quote.Request.AssetSpecifier,
109109
AcceptedQuoteId: quote.ID,
110110
MaxOutboundAssetAmount: quote.Request.AssetMaxAmt,
111-
AskAssetRate: quote.AssetRate,
112-
expiry: quote.Expiry,
111+
AskAssetRate: quote.AssetRate.Rate,
112+
expiry: uint64(quote.AssetRate.Expiry.Unix()),
113113
}
114114
}
115115

@@ -262,9 +262,9 @@ func NewAssetPurchasePolicy(quote rfqmsg.SellAccept) *AssetPurchasePolicy {
262262
scid: quote.ShortChannelId(),
263263
AssetSpecifier: quote.Request.AssetSpecifier,
264264
AcceptedQuoteId: quote.ID,
265-
BidAssetRate: quote.AssetRate,
265+
BidAssetRate: quote.AssetRate.Rate,
266266
PaymentMaxAmt: quote.Request.PaymentMaxAmt,
267-
expiry: quote.Expiry,
267+
expiry: uint64(quote.AssetRate.Expiry.Unix()),
268268
}
269269
}
270270

0 commit comments

Comments
 (0)