Skip to content

Commit ad05718

Browse files
GeorgeTsagkguggero
authored andcommitted
tapchannel: enforce strict forwarding for asset invoices
1 parent 50d0e78 commit ad05718

File tree

2 files changed

+110
-11
lines changed

2 files changed

+110
-11
lines changed

rfq/manager.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ type ScidAliasManager interface {
5353
baseScid lnwire.ShortChannelID) error
5454
}
5555

56+
type (
57+
// BuyAcceptMap is a map of buy accepts, keyed by the serialised SCID.
58+
BuyAcceptMap map[SerialisedScid]rfqmsg.BuyAccept
59+
60+
// SellAcceptMap is a map of sell accepts, keyed by the serialised SCID.
61+
SellAcceptMap map[SerialisedScid]rfqmsg.SellAccept
62+
)
63+
5664
// ManagerCfg is a struct that holds the configuration parameters for the RFQ
5765
// manager.
5866
type ManagerCfg struct {
@@ -760,7 +768,7 @@ func (m *Manager) UpsertAssetSellOrder(order SellOrder) error {
760768
// PeerAcceptedBuyQuotes returns buy quotes that were requested by our node and
761769
// have been accepted by our peers. These quotes are exclusively available to
762770
// our node for the acquisition of assets.
763-
func (m *Manager) PeerAcceptedBuyQuotes() map[SerialisedScid]rfqmsg.BuyAccept {
771+
func (m *Manager) PeerAcceptedBuyQuotes() BuyAcceptMap {
764772
// Returning the map directly is not thread safe. We will therefore
765773
// create a copy.
766774
buyQuotesCopy := make(map[SerialisedScid]rfqmsg.BuyAccept)
@@ -782,9 +790,7 @@ func (m *Manager) PeerAcceptedBuyQuotes() map[SerialisedScid]rfqmsg.BuyAccept {
782790
// PeerAcceptedSellQuotes returns sell quotes that were requested by our node
783791
// and have been accepted by our peers. These quotes are exclusively available
784792
// to our node for the sale of assets.
785-
//
786-
//nolint:lll
787-
func (m *Manager) PeerAcceptedSellQuotes() map[SerialisedScid]rfqmsg.SellAccept {
793+
func (m *Manager) PeerAcceptedSellQuotes() SellAcceptMap {
788794
// Returning the map directly is not thread safe. We will therefore
789795
// create a copy.
790796
sellQuotesCopy := make(map[SerialisedScid]rfqmsg.SellAccept)
@@ -806,7 +812,7 @@ func (m *Manager) PeerAcceptedSellQuotes() map[SerialisedScid]rfqmsg.SellAccept
806812
// LocalAcceptedBuyQuotes returns buy quotes that were accepted by our node and
807813
// have been requested by our peers. These quotes are exclusively available to
808814
// our node for the acquisition of assets.
809-
func (m *Manager) LocalAcceptedBuyQuotes() map[SerialisedScid]rfqmsg.BuyAccept {
815+
func (m *Manager) LocalAcceptedBuyQuotes() BuyAcceptMap {
810816
// Returning the map directly is not thread safe. We will therefore
811817
// create a copy.
812818
buyQuotesCopy := make(map[SerialisedScid]rfqmsg.BuyAccept)
@@ -828,9 +834,7 @@ func (m *Manager) LocalAcceptedBuyQuotes() map[SerialisedScid]rfqmsg.BuyAccept {
828834
// LocalAcceptedSellQuotes returns sell quotes that were accepted by our node
829835
// and have been requested by our peers. These quotes are exclusively available
830836
// to our node for the sale of assets.
831-
//
832-
//nolint:lll
833-
func (m *Manager) LocalAcceptedSellQuotes() map[SerialisedScid]rfqmsg.SellAccept {
837+
func (m *Manager) LocalAcceptedSellQuotes() SellAcceptMap {
834838
// Returning the map directly is not thread safe. We will therefore
835839
// create a copy.
836840
sellQuotesCopy := make(map[SerialisedScid]rfqmsg.SellAccept)

tapchannel/aux_invoice_manager.go

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import (
1313
"github.com/lightninglabs/taproot-assets/rfqmath"
1414
"github.com/lightninglabs/taproot-assets/rfqmsg"
1515
"github.com/lightninglabs/taproot-assets/taprpc"
16+
"github.com/lightningnetwork/lnd/lnrpc"
1617
"github.com/lightningnetwork/lnd/lnwire"
18+
"github.com/lightningnetwork/lnd/routing/route"
1719
)
1820

1921
// InvoiceHtlcModifier is an interface that abstracts the invoice HTLC
@@ -28,6 +30,32 @@ type InvoiceHtlcModifier interface {
2830
handler lndclient.InvoiceHtlcModifyHandler) error
2931
}
3032

33+
// RfqManager is an interface that abstracts the functionalities of the rfq
34+
// manager that are needed by AuxInvoiceManager.
35+
type RfqManager interface {
36+
// PeerAcceptedBuyQuotes returns buy quotes that were requested by our
37+
// node and have been accepted by our peers. These quotes are
38+
// exclusively available to our node for the acquisition of assets.
39+
PeerAcceptedBuyQuotes() rfq.BuyAcceptMap
40+
41+
// LocalAcceptedSellQuotes returns sell quotes that were accepted by our
42+
// node and have been requested by our peers. These quotes are
43+
// exclusively available to our node for the sale of assets.
44+
LocalAcceptedSellQuotes() rfq.SellAcceptMap
45+
}
46+
47+
// A compile time assertion to ensure that the rfq.Manager meets the expected
48+
// tapchannel.RfqManager interface.
49+
var _ RfqManager = (*rfq.Manager)(nil)
50+
51+
// RfqLookup is an interface that abstracts away the process of performing
52+
// a lookup to the current set of existing RFQs.
53+
type RfqLookup interface {
54+
// RfqPeerFromScid retrieves the peer associated with the RFQ id that
55+
// is mapped to the provided scid, if it exists.
56+
RfqPeerFromScid(scid uint64) (route.Vertex, error)
57+
}
58+
3159
// InvoiceManagerConfig defines the configuration for the auxiliary invoice
3260
// manager.
3361
type InvoiceManagerConfig struct {
@@ -42,7 +70,7 @@ type InvoiceManagerConfig struct {
4270
// RfqManager is the RFQ manager that will be used to retrieve the
4371
// accepted quotes for determining the incoming value of invoice related
4472
// HTLCs.
45-
RfqManager *rfq.Manager
73+
RfqManager RfqManager
4674
}
4775

4876
// AuxInvoiceManager is a Taproot Asset auxiliary invoice manager that can be
@@ -110,6 +138,10 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(_ context.Context,
110138
AmtPaid: req.ExitHtlcAmt,
111139
}
112140

141+
if req.Invoice == nil {
142+
return nil, fmt.Errorf("cannot handle empty invoice")
143+
}
144+
113145
jsonBytes, err := taprpc.ProtoJSONMarshalOpts.Marshal(req.Invoice)
114146
if err != nil {
115147
return nil, fmt.Errorf("unable to decode response: %w", err)
@@ -121,6 +153,18 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(_ context.Context,
121153

122154
// No custom record on the HTLC, so we have nothing to do.
123155
if len(req.WireCustomRecords) == 0 {
156+
// If there's no wire custom records and the invoice is an asset
157+
// invoice do not settle the invoice. Since we are asking for
158+
// assets in the invoice, we may not let this HTLC go through
159+
// as it is not carrying assets. This could lead to undesired
160+
// behavior where the asset invoice may be settled by accepting
161+
// sats instead of assets.
162+
//
163+
// TODO(george): Strict-forwarding could be configurable?
164+
if isAssetInvoice(req.Invoice, s) {
165+
resp.CancelSet = true
166+
}
167+
124168
return resp, nil
125169
}
126170

@@ -216,12 +260,15 @@ func (s *AuxInvoiceManager) priceFromQuote(rfqID rfqmsg.ID) (
216260
sellQuote, isSell := acceptedSellQuotes[rfqID.Scid()]
217261

218262
switch {
263+
// This is a normal invoice payment with multiple hops, so we expect to
264+
// find a buy quote.
219265
case isBuy:
220-
log.Debugf("Found buy quote for ID %x / SCID %d: %#v",
221-
rfqID[:], rfqID.Scid(), buyQuote)
266+
log.Debugf("Found buy quote for ID %x / SCID %d: %#v", rfqID[:],
267+
rfqID.Scid(), buyQuote)
222268

223269
return &buyQuote.AssetRate, nil
224270

271+
// This is a direct peer payment, so we expect to find a sell quote.
225272
case isSell:
226273
log.Debugf("Found sell quote for ID %x / SCID %d: %#v",
227274
rfqID[:], rfqID.Scid(), sellQuote)
@@ -234,6 +281,54 @@ func (s *AuxInvoiceManager) priceFromQuote(rfqID rfqmsg.ID) (
234281
}
235282
}
236283

284+
// RfqPeerFromScid attempts to match the provided scid with a negotiated quote,
285+
// then it returns the RFQ peer's node id.
286+
func (s *AuxInvoiceManager) RfqPeerFromScid(scid uint64) (route.Vertex, error) {
287+
acceptedBuyQuotes := s.cfg.RfqManager.PeerAcceptedBuyQuotes()
288+
289+
buyQuote, isBuy := acceptedBuyQuotes[rfqmsg.SerialisedScid(scid)]
290+
291+
if !isBuy {
292+
return route.Vertex{}, fmt.Errorf("no peer found for RFQ "+
293+
"SCID %d", scid)
294+
}
295+
296+
return buyQuote.Peer, nil
297+
}
298+
299+
// isAssetInvoice checks whether the provided invoice is an asset invoice. This
300+
// method checks whether the routing hints of the invoice match those created
301+
// when generating an asset invoice, and if that's the case we then check that
302+
// the scid matches an existing quote.
303+
func isAssetInvoice(invoice *lnrpc.Invoice, rfqLookup RfqLookup) bool {
304+
hints := invoice.RouteHints
305+
306+
for _, hint := range hints {
307+
for _, h := range hint.HopHints {
308+
scid := h.ChanId
309+
nodeId := h.NodeId
310+
311+
// Check if for this hop hint we can retrieve a valid
312+
// rfq quote.
313+
peer, err := rfqLookup.RfqPeerFromScid(scid)
314+
if err != nil {
315+
log.Debugf("invoice hop hint scid %v does not "+
316+
"correspond to a valid RFQ quote", scid)
317+
318+
continue
319+
}
320+
321+
// If we also have a nodeId match, we're safe to assume
322+
// this is an asset invoice.
323+
if peer.String() == nodeId {
324+
return true
325+
}
326+
}
327+
}
328+
329+
return false
330+
}
331+
237332
// Stop signals for an aux invoice manager to gracefully exit.
238333
func (s *AuxInvoiceManager) Stop() error {
239334
var stopErr error

0 commit comments

Comments
 (0)