From 1de0592c989c7834aabaac0804833c5f313f7b24 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 6 Jun 2024 09:42:31 +0200 Subject: [PATCH] rfq+tapchannel: track locally accepted quotes in manager To allow direct peer (zero-hop) payments, we also need to be able to look up quotes we've accepted that our peer requested. The example flow here is: Dave creates an invoice (either specifying a BTC amount or asset amount, in any case the invoice is always denominated in milli-satoshi), Charlie has a direct channel with Dave and wants to pay the invoice with assets. Charlie only sees the milli-satoshi amount of the invoice and needs to find out how many asset units to send. For that, Charlie asks Dave for a quote over the given amount of milli-satoshi, gets an asset unit amount back. Charlie then creates an HTLC referencing that asset sell quote RFQ ID. But the invoice interceptor on Dave's side would previously not recognize that RFQ ID, because it's not an ID for a quote Dave requested but one Dave accepted. --- rfq/manager.go | 78 +++++++++++++++++++++++++++++-- tapchannel/aux_invoice_manager.go | 50 ++++++++++++++++---- 2 files changed, 116 insertions(+), 12 deletions(-) diff --git a/rfq/manager.go b/rfq/manager.go index c06671384..629c2c0fc 100644 --- a/rfq/manager.go +++ b/rfq/manager.go @@ -124,8 +124,23 @@ type Manager struct { // requested and that have been accepted by peer nodes. These quotes are // exclusively used by our node for the sale of assets, as they // represent agreed-upon terms for sale transactions with our peers. - peerAcceptedSellQuotes lnutils.SyncMap[SerialisedScid, - rfqmsg.SellAccept] + peerAcceptedSellQuotes lnutils.SyncMap[ + SerialisedScid, rfqmsg.SellAccept, + ] + + // localAcceptedBuyQuotes holds buy quotes for assets that our node has + // accepted and that have been requested by peer nodes. These quotes are + // exclusively used by our node for the acquisition of assets, as they + // represent agreed-upon terms for purchase transactions with our peers. + localAcceptedBuyQuotes lnutils.SyncMap[SerialisedScid, rfqmsg.BuyAccept] + + // localAcceptedSellQuotes holds sell quotes for assets that our node + // has accepted and that have been requested by peer nodes. These quotes + // are exclusively used by our node for the sale of assets, as they + // represent agreed-upon terms for sale transactions with our peers. + localAcceptedSellQuotes lnutils.SyncMap[ + SerialisedScid, rfqmsg.SellAccept, + ] // subscribers is a map of components that want to be notified on new // events, keyed by their subscription ID. @@ -427,6 +442,10 @@ func (m *Manager) handleOutgoingMessage(outgoingMsg rfqmsg.OutgoingMsg) error { // sale policy. m.orderHandler.RegisterAssetSalePolicy(*msg) + // We want to store that we accepted the buy quote, in case we + // need to look it up for a direct peer payment. + m.localAcceptedBuyQuotes.Store(msg.ShortChannelId(), *msg) + // Since our peer is going to buy assets from us, we need to // make sure we can identify the forwarded asset payment by the // outgoing SCID alias within the onion packet. @@ -444,6 +463,10 @@ func (m *Manager) handleOutgoingMessage(outgoingMsg rfqmsg.OutgoingMsg) error { // handler that we are willing to buy the asset subject to a // purchase policy. m.orderHandler.RegisterAssetPurchasePolicy(*msg) + + // We want to store that we accepted the sell quote, in case we + // need to look it up for a direct peer payment. + m.localAcceptedSellQuotes.Store(msg.ShortChannelId(), *msg) } // Send the outgoing message to the peer. @@ -713,7 +736,8 @@ func (m *Manager) PeerAcceptedBuyQuotes() map[SerialisedScid]rfqmsg.BuyAccept { // PeerAcceptedSellQuotes returns sell quotes that were requested by our node // and have been accepted by our peers. These quotes are exclusively available // to our node for the sale of assets. -// nolint: lll +// +//nolint:lll func (m *Manager) PeerAcceptedSellQuotes() map[SerialisedScid]rfqmsg.SellAccept { // Returning the map directly is not thread safe. We will therefore // create a copy. @@ -721,7 +745,53 @@ func (m *Manager) PeerAcceptedSellQuotes() map[SerialisedScid]rfqmsg.SellAccept m.peerAcceptedSellQuotes.ForEach( func(scid SerialisedScid, accept rfqmsg.SellAccept) error { if time.Now().Unix() > int64(accept.Expiry) { - m.peerAcceptedBuyQuotes.Delete(scid) + m.peerAcceptedSellQuotes.Delete(scid) + return nil + } + + sellQuotesCopy[scid] = accept + return nil + }, + ) + + return sellQuotesCopy +} + +// LocalAcceptedBuyQuotes returns buy quotes that were accepted by our node and +// have been requested by our peers. These quotes are exclusively available to +// our node for the acquisition of assets. +func (m *Manager) LocalAcceptedBuyQuotes() map[SerialisedScid]rfqmsg.BuyAccept { + // Returning the map directly is not thread safe. We will therefore + // create a copy. + buyQuotesCopy := make(map[SerialisedScid]rfqmsg.BuyAccept) + m.localAcceptedBuyQuotes.ForEach( + func(scid SerialisedScid, accept rfqmsg.BuyAccept) error { + if time.Now().Unix() > int64(accept.Expiry) { + m.localAcceptedBuyQuotes.Delete(scid) + return nil + } + + buyQuotesCopy[scid] = accept + return nil + }, + ) + + return buyQuotesCopy +} + +// LocalAcceptedSellQuotes returns sell quotes that were accepted by our node +// and have been requested by our peers. These quotes are exclusively available +// to our node for the sale of assets. +// +//nolint:lll +func (m *Manager) LocalAcceptedSellQuotes() map[SerialisedScid]rfqmsg.SellAccept { + // Returning the map directly is not thread safe. We will therefore + // create a copy. + sellQuotesCopy := make(map[SerialisedScid]rfqmsg.SellAccept) + m.localAcceptedSellQuotes.ForEach( + func(scid SerialisedScid, accept rfqmsg.SellAccept) error { + if time.Now().Unix() > int64(accept.Expiry) { + m.localAcceptedSellQuotes.Delete(scid) return nil } diff --git a/tapchannel/aux_invoice_manager.go b/tapchannel/aux_invoice_manager.go index 65ccd4d68..8e4d5475a 100644 --- a/tapchannel/aux_invoice_manager.go +++ b/tapchannel/aux_invoice_manager.go @@ -5,6 +5,7 @@ import ( "fmt" "sync" + "github.com/davecgh/go-spew/spew" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/fn" @@ -143,16 +144,12 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(_ context.Context, } rfqID := htlc.RfqID.ValOpt().UnsafeFromSome() - acceptedQuotes := s.cfg.RfqManager.PeerAcceptedBuyQuotes() - quote, ok := acceptedQuotes[rfqID.Scid()] - if !ok { - return nil, fmt.Errorf("no accepted quote found for RFQ SCID "+ - "%d", rfqID.Scid()) + mSatPerAssetUnit, err := s.priceFromQuote(rfqID) + if err != nil { + return nil, fmt.Errorf("unable to get price from quote with "+ + "ID %x / SCID %d: %w", rfqID[:], rfqID.Scid(), err) } - log.Debugf("Found quote for SCID %d: %#v", rfqID.Scid(), quote) - - mSatPerAssetUnit := quote.AskPrice htlcAssetAmount := lnwire.MilliSatoshi(htlc.Amounts.Val.Sum()) resp.AmtPaid = htlcAssetAmount * mSatPerAssetUnit @@ -193,6 +190,43 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(_ context.Context, return resp, nil } +// priceFromQuote retrieves the price from the accepted quote for the given RFQ +// ID. We allow the quote to either be a buy or a sell quote, since we don't +// know if this is a direct peer payment or a payment that is routed through the +// multiple hops. If it's a direct peer payment, then the quote will be a sell +// quote, since that's what the peer created to find out how many units to send +// for an invoice denominated in BTC. +func (s *AuxInvoiceManager) priceFromQuote(rfqID rfqmsg.ID) ( + lnwire.MilliSatoshi, error) { + + acceptedBuyQuotes := s.cfg.RfqManager.PeerAcceptedBuyQuotes() + acceptedSellQuotes := s.cfg.RfqManager.LocalAcceptedSellQuotes() + + log.Tracef("Currently available quotes: buy %v, sell %v", + spew.Sdump(acceptedBuyQuotes), spew.Sdump(acceptedSellQuotes)) + + buyQuote, isBuy := acceptedBuyQuotes[rfqID.Scid()] + sellQuote, isSell := acceptedSellQuotes[rfqID.Scid()] + + switch { + case isBuy: + log.Debugf("Found buy quote for ID %x / SCID %d: %#v", + rfqID[:], rfqID.Scid(), buyQuote) + + return buyQuote.AskPrice, nil + + case isSell: + log.Debugf("Found sell quote for ID %x / SCID %d: %#v", + rfqID[:], rfqID.Scid(), sellQuote) + + return sellQuote.BidPrice, nil + + default: + return 0, fmt.Errorf("no accepted quote found for RFQ SCID "+ + "%d", rfqID.Scid()) + } +} + // Stop signals for an aux invoice manager to gracefully exit. func (s *AuxInvoiceManager) Stop() error { var stopErr error