@@ -2,6 +2,7 @@ package tapchannel
22
33import (
44 "context"
5+ "encoding/json"
56 "fmt"
67 "sync"
78
@@ -13,7 +14,9 @@ import (
1314 "github.com/lightninglabs/taproot-assets/rfqmath"
1415 "github.com/lightninglabs/taproot-assets/rfqmsg"
1516 "github.com/lightninglabs/taproot-assets/taprpc"
17+ "github.com/lightningnetwork/lnd/invoices"
1618 "github.com/lightningnetwork/lnd/lnrpc"
19+ "github.com/lightningnetwork/lnd/lnutils"
1720 "github.com/lightningnetwork/lnd/lnwire"
1821 "github.com/lightningnetwork/lnd/routing/route"
1922)
@@ -77,6 +80,10 @@ type InvoiceManagerConfig struct {
7780 // accepted quotes for determining the incoming value of invoice related
7881 // HTLCs.
7982 RfqManager RfqManager
83+
84+ // LndClient is the lnd client that will be used to interact with the
85+ // lnd node.
86+ LightningClient lndclient.LightningClient
8087}
8188
8289// AuxInvoiceManager is a Taproot Asset auxiliary invoice manager that can be
@@ -87,6 +94,12 @@ type AuxInvoiceManager struct {
8794
8895 cfg * InvoiceManagerConfig
8996
97+ // channelFundingCache is a cache used to store the channel funding
98+ // information for the channels that are used to receive assets. The map
99+ // is keyed by the main channel ID, and the value is the asset channel
100+ // funding information.
101+ channelFundingCache lnutils.SyncMap [uint64 , rfqmsg.JsonAssetChannel ]
102+
90103 // ContextGuard provides a wait group and main quit channel that can be
91104 // used to create guarded contexts.
92105 * fn.ContextGuard
@@ -206,7 +219,7 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(ctx context.Context,
206219 }
207220
208221 // We now run some validation checks on the asset HTLC.
209- err = s .validateAssetHTLC (ctx , htlc )
222+ err = s .validateAssetHTLC (ctx , htlc , resp . CircuitKey )
210223 if err != nil {
211224 log .Errorf ("Failed to validate asset HTLC: %v" , err )
212225
@@ -268,35 +281,40 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(ctx context.Context,
268281 return resp , nil
269282}
270283
271- // identifierFromQuote retrieves the quote by looking up the rfq manager's maps
272- // of accepted quotes based on the passed rfq ID. If there's a match, the asset
273- // specifier is returned.
274- func (s * AuxInvoiceManager ) identifierFromQuote (
275- rfqID rfqmsg.ID ) (asset.Specifier , error ) {
284+ // identifierAndPeerFromQuote retrieves the quote by looking up the rfq
285+ // manager's maps of accepted quotes based on the passed rfq ID. If there's a
286+ // match, the asset specifier and peer are returned.
287+ func (s * AuxInvoiceManager ) identifierAndPeerFromQuote (
288+ rfqID rfqmsg.ID ) (asset.Specifier , route. Vertex , error ) {
276289
277290 acceptedBuyQuotes := s .cfg .RfqManager .PeerAcceptedBuyQuotes ()
278291 acceptedSellQuotes := s .cfg .RfqManager .LocalAcceptedSellQuotes ()
279292
280293 buyQuote , isBuy := acceptedBuyQuotes [rfqID .Scid ()]
281294 sellQuote , isSell := acceptedSellQuotes [rfqID .Scid ()]
282295
283- var specifier asset.Specifier
296+ var (
297+ specifier asset.Specifier
298+ peer route.Vertex
299+ )
284300
285301 switch {
286302 case isBuy :
287303 specifier = buyQuote .Request .AssetSpecifier
304+ peer = buyQuote .Peer
288305
289306 case isSell :
290307 specifier = sellQuote .Request .AssetSpecifier
308+ peer = sellQuote .Peer
291309 }
292310
293311 err := specifier .AssertNotEmpty ()
294312 if err != nil {
295- return specifier , fmt .Errorf ("rfqID does not match any " +
313+ return specifier , peer , fmt .Errorf ("rfqID does not match any " +
296314 "accepted buy or sell quote: %v" , err )
297315 }
298316
299- return specifier , nil
317+ return specifier , peer , nil
300318}
301319
302320// priceFromQuote retrieves the price from the accepted quote for the given RFQ
@@ -390,19 +408,20 @@ func isAssetInvoice(invoice *lnrpc.Invoice, rfqLookup RfqLookup) bool {
390408
391409// validateAssetHTLC runs a couple of checks on the provided asset HTLC.
392410func (s * AuxInvoiceManager ) validateAssetHTLC (ctx context.Context ,
393- htlc * rfqmsg.Htlc ) error {
411+ htlc * rfqmsg.Htlc , circuitKey invoices. CircuitKey ) error {
394412
395413 rfqID := htlc .RfqID .ValOpt ().UnsafeFromSome ()
396414
397415 // Retrieve the asset identifier from the RFQ quote.
398- identifier , err := s .identifierFromQuote (rfqID )
416+ identifier , peer , err := s .identifierAndPeerFromQuote (rfqID )
399417 if err != nil {
400418 return fmt .Errorf ("could not extract assetID from " +
401419 "quote: %v" , err )
402420 }
403421
404422 // Check for each of the asset balances of the HTLC that the identifier
405423 // matches that of the RFQ quote.
424+ assetIDs := fn .NewSet [asset.ID ]()
406425 for _ , v := range htlc .Balances () {
407426 match , err := s .cfg .RfqManager .AssetMatchesSpecifier (
408427 ctx , identifier , v .AssetID .Val ,
@@ -415,11 +434,81 @@ func (s *AuxInvoiceManager) validateAssetHTLC(ctx context.Context,
415434 return fmt .Errorf ("asset ID %s does not match %s" ,
416435 v .AssetID .Val .String (), identifier .String ())
417436 }
437+
438+ assetIDs .Add (v .AssetID .Val )
439+ }
440+
441+ assetData , err := s .fetchChannelAssetData (ctx , circuitKey .ChanID , peer )
442+ if err != nil {
443+ return fmt .Errorf ("unable to fetch channel asset data: %w" , err )
444+ }
445+
446+ if ! assetData .HasAllAssetIDs (assetIDs ) {
447+ return fmt .Errorf ("channel %d does not have all asset IDs " +
448+ "required for HTLC settlement" ,
449+ circuitKey .ChanID )
418450 }
419451
420452 return nil
421453}
422454
455+ // fetchChannelAssetData retrieves the asset channel data for the provided
456+ // channel ID. If the cache doesn't contain the data, it is queried from the
457+ // backing lnd node.
458+ func (s * AuxInvoiceManager ) fetchChannelAssetData (ctx context.Context ,
459+ chanID lnwire.ShortChannelID ,
460+ peer route.Vertex ) (* rfqmsg.JsonAssetChannel , error ) {
461+
462+ // Do we have the information cached? Great, no lookup necessary. We
463+ // don't need to worry about cache invalidation because the funding
464+ // information remains constant for the lifetime of the channel.
465+ cachedAssetData , ok := s .channelFundingCache .Load (chanID .ToUint64 ())
466+ if ok {
467+ return & cachedAssetData , nil
468+ }
469+
470+ // We also need to validate that the HTLC is actually the correct asset
471+ // and arrived through the correct asset channel.
472+ channels , err := s .cfg .LightningClient .ListChannels (
473+ ctx , true , false , lndclient .WithPeer (peer [:]),
474+ )
475+ if err != nil {
476+ return nil , fmt .Errorf ("unable to list channels: %w" , err )
477+ }
478+
479+ var inboundChannel * lndclient.ChannelInfo
480+ for _ , channel := range channels {
481+ if channel .ChannelID == chanID .ToUint64 () {
482+ inboundChannel = & channel
483+ break
484+ }
485+ }
486+
487+ if inboundChannel == nil {
488+ return nil , fmt .Errorf ("unable to find channel with short " +
489+ "channel ID %d" , chanID .ToUint64 ())
490+ }
491+
492+ if len (inboundChannel .CustomChannelData ) == 0 {
493+ return nil , fmt .Errorf ("channel %d does not have custom " +
494+ "channel data, can't accept asset HTLC over non-asset " +
495+ "channel" , inboundChannel .ChannelID )
496+ }
497+
498+ var assetData rfqmsg.JsonAssetChannel
499+ err = json .Unmarshal (inboundChannel .CustomChannelData , & assetData )
500+ if err != nil {
501+ return nil , fmt .Errorf ("unable to unmarshal channel asset " +
502+ "data: %w" , err )
503+ }
504+
505+ // We cache the asset data for the channel so we don't have to look it
506+ // up again.
507+ s .channelFundingCache .Store (chanID .ToUint64 (), assetData )
508+
509+ return & assetData , nil
510+ }
511+
423512// Stop signals for an aux invoice manager to gracefully exit.
424513func (s * AuxInvoiceManager ) Stop () error {
425514 var stopErr error
0 commit comments