Skip to content

Commit

Permalink
Merge pull request #1197 from lightninglabs/rfq-improvments-20241115
Browse files Browse the repository at this point in the history
More RFQ improvments
  • Loading branch information
ffranr authored Nov 19, 2024
2 parents 47a265d + a1d989d commit a304937
Show file tree
Hide file tree
Showing 15 changed files with 307 additions and 207 deletions.
51 changes: 30 additions & 21 deletions rfq/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,7 @@ func (m *Manager) UpsertAssetBuyOffer(offer BuyOffer) error {
}

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

// Expiry is the unix timestamp at which the buy order expires.
Expiry uint64
// Expiry is the time at which the order expires.
Expiry time.Time

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

// SellOrder is a struct that represents an asset sell order.
// SellOrder instructs the RFQ (Request For Quote) system to request a quote
// from one or more peers for the disposition of an asset.
//
// Normal usage of a sell order:
// 1. Alice creates a Lightning invoice for Bob to pay.
// 2. Bob wants to pay the invoice using a Tap asset. To do so, Bob pays an
// edge node with a Tap asset, and the edge node forwards the payment to the
// network to settle Alice's invoice. Bob submits a SellOrder to his local
// RFQ service.
// 3. The RFQ service converts the SellOrder into one or more SellRequests.
// These requests are sent to Charlie (the edge node), who shares a relevant
// Tap asset channel with Bob and can forward payments to settle Alice's
// invoice.
// 4. Charlie responds with a quote that satisfies Bob.
// 5. Bob transfers the appropriate Tap asset amount to Charlie via their
// shared Tap asset channel, and Charlie forwards the corresponding amount
// to Alice to settle the Lightning invoice.
type SellOrder struct {
// AssetID is the ID of the asset to sell.
AssetID *asset.ID

// AssetGroupKey is the public key of the asset group to sell.
AssetGroupKey *btcec.PublicKey
// AssetSpecifier is the asset that the seller is interested in.
AssetSpecifier asset.Specifier

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

// Expiry is the unix timestamp at which the order expires.
//
// TODO(ffranr): This is the invoice expiry unix timestamp in seconds.
// We should make use of this field to ensure quotes are valid for the
// duration of the invoice.
Expiry uint64
// Expiry is the time at which the order expires.
Expiry time.Time

// Peer is the peer that the buy order is intended for. This field is
// optional.
Peer *route.Vertex
Peer fn.Option[route.Vertex]
}

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

Expand All @@ -795,7 +804,7 @@ func (m *Manager) PeerAcceptedBuyQuotes() BuyAcceptMap {
buyQuotesCopy := make(map[SerialisedScid]rfqmsg.BuyAccept)
m.peerAcceptedBuyQuotes.ForEach(
func(scid SerialisedScid, accept rfqmsg.BuyAccept) error {
if time.Now().Unix() > int64(accept.Expiry) {
if time.Now().After(accept.AssetRate.Expiry) {
m.peerAcceptedBuyQuotes.Delete(scid)
return nil
}
Expand All @@ -817,7 +826,7 @@ func (m *Manager) PeerAcceptedSellQuotes() SellAcceptMap {
sellQuotesCopy := make(map[SerialisedScid]rfqmsg.SellAccept)
m.peerAcceptedSellQuotes.ForEach(
func(scid SerialisedScid, accept rfqmsg.SellAccept) error {
if time.Now().Unix() > int64(accept.Expiry) {
if time.Now().After(accept.AssetRate.Expiry) {
m.peerAcceptedSellQuotes.Delete(scid)
return nil
}
Expand All @@ -839,7 +848,7 @@ func (m *Manager) LocalAcceptedBuyQuotes() BuyAcceptMap {
buyQuotesCopy := make(map[SerialisedScid]rfqmsg.BuyAccept)
m.localAcceptedBuyQuotes.ForEach(
func(scid SerialisedScid, accept rfqmsg.BuyAccept) error {
if time.Now().Unix() > int64(accept.Expiry) {
if time.Now().After(accept.AssetRate.Expiry) {
m.localAcceptedBuyQuotes.Delete(scid)
return nil
}
Expand All @@ -861,7 +870,7 @@ func (m *Manager) LocalAcceptedSellQuotes() SellAcceptMap {
sellQuotesCopy := make(map[SerialisedScid]rfqmsg.SellAccept)
m.localAcceptedSellQuotes.ForEach(
func(scid SerialisedScid, accept rfqmsg.SellAccept) error {
if time.Now().Unix() > int64(accept.Expiry) {
if time.Now().After(accept.AssetRate.Expiry) {
m.localAcceptedSellQuotes.Delete(scid)
return nil
}
Expand Down
110 changes: 53 additions & 57 deletions rfq/negotiator.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/lightninglabs/taproot-assets/rfqmsg"
"github.com/lightningnetwork/lnd/lnutils"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
)

const (
Expand Down Expand Up @@ -105,8 +104,8 @@ func NewNegotiator(cfg NegotiatorCfg) (*Negotiator, error) {

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

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

// Query the price oracle for a bid price.
//
// TODO(ffranr): Pass the BuyOrder expiry to the price
// oracle at this point.
assetRate, err := n.queryBidFromPriceOracle(
peer, buyOrder.AssetSpecifier,
buyOrder.AssetSpecifier,
fn.Some(buyOrder.AssetMaxAmt),
fn.None[lnwire.MilliSatoshi](),
fn.None[rfqmsg.AssetRate](),
Expand Down Expand Up @@ -227,8 +229,8 @@ func (n *Negotiator) HandleOutgoingBuyOrder(buyOrder BuyOrder) error {
// queryAskFromPriceOracle queries the price oracle for an asking price. It
// returns an appropriate outgoing response message which should be sent to the
// peer.
func (n *Negotiator) queryAskFromPriceOracle(peer *route.Vertex,
assetSpecifier asset.Specifier, assetMaxAmt fn.Option[uint64],
func (n *Negotiator) queryAskFromPriceOracle(assetSpecifier asset.Specifier,
assetMaxAmt fn.Option[uint64],
paymentMaxAmt fn.Option[lnwire.MilliSatoshi],
assetRateHint fn.Option[rfqmsg.AssetRate]) (*rfqmsg.AssetRate, error) {

Expand Down Expand Up @@ -326,7 +328,7 @@ func (n *Negotiator) HandleIncomingBuyRequest(

// Query the price oracle for an asking price.
assetRate, err := n.queryAskFromPriceOracle(
nil, request.AssetSpecifier,
request.AssetSpecifier,
fn.Some(request.AssetMaxAmt),
fn.None[lnwire.MilliSatoshi](),
request.AssetRateHint,
Expand All @@ -347,10 +349,7 @@ func (n *Negotiator) HandleIncomingBuyRequest(
}

// Construct and send a buy accept message.
expiry := uint64(assetRate.Expiry.Unix())
msg := rfqmsg.NewBuyAcceptFromRequest(
request, assetRate.Rate, expiry,
)
msg := rfqmsg.NewBuyAcceptFromRequest(request, *assetRate)
sendOutgoingMsg(msg)
}()

Expand Down Expand Up @@ -426,9 +425,8 @@ func (n *Negotiator) HandleIncomingSellRequest(
// are willing to pay for the asset that our peer is trying to
// sell to us.
assetRate, err := n.queryBidFromPriceOracle(
request.Peer, request.AssetSpecifier,
fn.None[uint64](), fn.Some(request.PaymentMaxAmt),
request.AssetRateHint,
request.AssetSpecifier, fn.None[uint64](),
fn.Some(request.PaymentMaxAmt), request.AssetRateHint,
)
if err != nil {
// Send a reject message to the peer.
Expand All @@ -446,10 +444,7 @@ func (n *Negotiator) HandleIncomingSellRequest(
}

// Construct and send a sell accept message.
expiry := uint64(assetRate.Expiry.Unix())
msg := rfqmsg.NewSellAcceptFromRequest(
request, assetRate.Rate, expiry,
)
msg := rfqmsg.NewSellAcceptFromRequest(request, *assetRate)
sendOutgoingMsg(msg)
}()

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

// Unwrap the peer from the order. For now, we can assume that
// the peer is always specified.
peer, err := order.Peer.UnwrapOrErr(
fmt.Errorf("buy order peer must be specified"),
)
if err != nil {
n.cfg.ErrChan <- err
}

// We calculate a proposed ask price for our peer's
// consideration. If a price oracle is not specified we will
// skip this step.
var assetRateHint fn.Option[rfqmsg.AssetRate]

// Construct an asset specifier from the order.
// TODO(ffranr): The order should have an asset specifier.
assetSpecifier, err := asset.NewSpecifier(
order.AssetID, order.AssetGroupKey, nil,
true,
)
if err != nil {
log.Warnf("failed to construct asset "+
"specifier from buy order: %v", err)
}

if n.cfg.PriceOracle != nil && assetSpecifier.IsSome() {
if n.cfg.PriceOracle != nil && order.AssetSpecifier.IsSome() {
// Query the price oracle for an asking price.
//
// TODO(ffranr): Pass the SellOrder expiry to the
// price oracle at this point.
assetRate, err := n.queryAskFromPriceOracle(
order.Peer, assetSpecifier,
fn.None[uint64](),
order.AssetSpecifier, fn.None[uint64](),
fn.Some(order.PaymentMaxAmt),
fn.None[rfqmsg.AssetRate](),
)
Expand All @@ -498,12 +493,12 @@ func (n *Negotiator) HandleOutgoingSellOrder(order SellOrder) {
return
}

assetRateHint = fn.Some[rfqmsg.AssetRate](*assetRate)
assetRateHint = fn.MaybeSome(assetRate)
}

request, err := rfqmsg.NewSellRequest(
*order.Peer, order.AssetID, order.AssetGroupKey,
order.PaymentMaxAmt, assetRateHint,
peer, order.AssetSpecifier, order.PaymentMaxAmt,
assetRateHint,
)
if err != nil {
err := fmt.Errorf("unable to create sell request "+
Expand All @@ -530,12 +525,8 @@ func (n *Negotiator) HandleOutgoingSellOrder(order SellOrder) {
// expiryWithinBounds checks if a quote expiry unix timestamp (in seconds) is
// within acceptable bounds. This check ensures that the expiry timestamp is far
// enough in the future for the quote to be useful.
func expiryWithinBounds(expiryUnixTimestamp uint64,
minExpiryLifetime uint64) bool {

// Convert the expiry timestamp into a time.Time.
actualExpiry := time.Unix(int64(expiryUnixTimestamp), 0)
diff := actualExpiry.Unix() - time.Now().Unix()
func expiryWithinBounds(expiry time.Time, minExpiryLifetime uint64) bool {
diff := expiry.Unix() - time.Now().Unix()
return diff >= int64(minExpiryLifetime)
}

Expand All @@ -549,12 +540,16 @@ func (n *Negotiator) HandleIncomingBuyAccept(msg rfqmsg.BuyAccept,
// Ensure that the quote expiry time is within acceptable bounds.
//
// TODO(ffranr): Sanity check the buy accept quote expiry
// timestamp given the expiry timestamp provided by the price
// oracle.
if !expiryWithinBounds(msg.Expiry, minAssetRatesExpiryLifetime) {
// timestamp given the expiry timestamp in our outgoing buy request.
// The expiry timestamp in the outgoing request relates to the lifetime
// of the lightning invoice.
if !expiryWithinBounds(
msg.AssetRate.Expiry, minAssetRatesExpiryLifetime,
) {
// The expiry time is not within the acceptable bounds.
log.Debugf("Buy accept quote expiry time is not within "+
"acceptable bounds (expiry=%d)", msg.Expiry)
"acceptable bounds (asset_rate=%s)",
msg.AssetRate.String())

// Construct an invalid quote response event so that we can
// inform the peer that the quote response has not validated
Expand Down Expand Up @@ -601,10 +596,9 @@ func (n *Negotiator) HandleIncomingBuyAccept(msg rfqmsg.BuyAccept,
// for an ask price. We will then compare the ask price returned
// by the price oracle with the ask price provided by the peer.
assetRate, err := n.queryAskFromPriceOracle(
&msg.Peer, msg.Request.AssetSpecifier,
msg.Request.AssetSpecifier,
fn.Some(msg.Request.AssetMaxAmt),
fn.None[lnwire.MilliSatoshi](),
fn.None[rfqmsg.AssetRate](),
fn.None[lnwire.MilliSatoshi](), fn.Some(msg.AssetRate),
)
if err != nil {
// The price oracle returned an error. We will return
Expand Down Expand Up @@ -635,17 +629,17 @@ func (n *Negotiator) HandleIncomingBuyAccept(msg rfqmsg.BuyAccept,
tolerance := rfqmath.NewBigIntFromUint64(
n.cfg.AcceptPriceDeviationPpm,
)
acceptablePrice := msg.AssetRate.WithinTolerance(
acceptablePrice := msg.AssetRate.Rate.WithinTolerance(
assetRate.Rate, tolerance,
)
if !acceptablePrice {
// The price is not within the acceptable tolerance.
// We will return without calling the quote accept
// callback.
log.Debugf("Buy accept price is not within "+
"acceptable bounds (ask_asset_rate=%v, "+
"oracle_asset_rate=%v)", msg.AssetRate,
assetRate)
"acceptable bounds (peer_asset_rate=%s, "+
"oracle_asset_rate=%s)", msg.AssetRate.String(),
assetRate.String())

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

// Construct an invalid quote response event so that we can
// inform the peer that the quote response has not validated
Expand Down Expand Up @@ -727,8 +724,7 @@ func (n *Negotiator) HandleIncomingSellAccept(msg rfqmsg.SellAccept,
// for a bid price. We will then compare the bid price returned
// by the price oracle with the bid price provided by the peer.
assetRate, err := n.queryBidFromPriceOracle(
msg.Peer, msg.Request.AssetSpecifier,
fn.None[uint64](),
msg.Request.AssetSpecifier, fn.None[uint64](),
fn.Some(msg.Request.PaymentMaxAmt),
msg.Request.AssetRateHint,
)
Expand Down Expand Up @@ -761,7 +757,7 @@ func (n *Negotiator) HandleIncomingSellAccept(msg rfqmsg.SellAccept,
tolerance := rfqmath.NewBigIntFromUint64(
n.cfg.AcceptPriceDeviationPpm,
)
acceptablePrice := msg.AssetRate.WithinTolerance(
acceptablePrice := msg.AssetRate.Rate.WithinTolerance(
assetRate.Rate, tolerance,
)
if !acceptablePrice {
Expand Down
8 changes: 4 additions & 4 deletions rfq/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ func NewAssetSalePolicy(quote rfqmsg.BuyAccept) *AssetSalePolicy {
AssetSpecifier: quote.Request.AssetSpecifier,
AcceptedQuoteId: quote.ID,
MaxOutboundAssetAmount: quote.Request.AssetMaxAmt,
AskAssetRate: quote.AssetRate,
expiry: quote.Expiry,
AskAssetRate: quote.AssetRate.Rate,
expiry: uint64(quote.AssetRate.Expiry.Unix()),
}
}

Expand Down Expand Up @@ -262,9 +262,9 @@ func NewAssetPurchasePolicy(quote rfqmsg.SellAccept) *AssetPurchasePolicy {
scid: quote.ShortChannelId(),
AssetSpecifier: quote.Request.AssetSpecifier,
AcceptedQuoteId: quote.ID,
BidAssetRate: quote.AssetRate,
BidAssetRate: quote.AssetRate.Rate,
PaymentMaxAmt: quote.Request.PaymentMaxAmt,
expiry: quote.Expiry,
expiry: uint64(quote.AssetRate.Expiry.Unix()),
}
}

Expand Down
Loading

0 comments on commit a304937

Please sign in to comment.