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

More RFQ improvments #1197

Merged
merged 15 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading