Skip to content

Commit 921adfb

Browse files
committed
tapcfg+tapchannel: validate incoming channel in invoice mgr
We need to make sure an asset HTLC actually comes through the correct channel that commits to that asset in the first place.
1 parent 8f8cf2b commit 921adfb

File tree

3 files changed

+217
-26
lines changed

3 files changed

+217
-26
lines changed

tapcfg/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger,
487487
ChainParams: &tapChainParams,
488488
InvoiceHtlcModifier: lndInvoicesClient,
489489
RfqManager: rfqManager,
490+
LightningClient: lndServices.Client,
490491
},
491492
)
492493
auxChanCloser := tapchannel.NewAuxChanCloser(

tapchannel/aux_invoice_manager.go

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package tapchannel
22

33
import (
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.
392410
func (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.
424513
func (s *AuxInvoiceManager) Stop() error {
425514
var stopErr error

0 commit comments

Comments
 (0)