Skip to content

Commit 26afb9e

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 97974e9 commit 26afb9e

File tree

3 files changed

+114
-15
lines changed

3 files changed

+114
-15
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: 50 additions & 2 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,6 +14,7 @@ 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"
1719
"github.com/lightningnetwork/lnd/lnwire"
1820
"github.com/lightningnetwork/lnd/routing/route"
@@ -77,6 +79,10 @@ type InvoiceManagerConfig struct {
7779
// accepted quotes for determining the incoming value of invoice related
7880
// HTLCs.
7981
RfqManager RfqManager
82+
83+
// LndClient is the lnd client that will be used to interact with the
84+
// lnd node.
85+
LightningClient lndclient.LightningClient
8086
}
8187

8288
// AuxInvoiceManager is a Taproot Asset auxiliary invoice manager that can be
@@ -206,7 +212,7 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(ctx context.Context,
206212
}
207213

208214
// We now run some validation checks on the asset HTLC.
209-
err = s.validateAssetHTLC(ctx, htlc)
215+
err = s.validateAssetHTLC(ctx, htlc, resp.CircuitKey)
210216
if err != nil {
211217
log.Errorf("Failed to validate asset HTLC: %v", err)
212218

@@ -390,7 +396,7 @@ func isAssetInvoice(invoice *lnrpc.Invoice, rfqLookup RfqLookup) bool {
390396

391397
// validateAssetHTLC runs a couple of checks on the provided asset HTLC.
392398
func (s *AuxInvoiceManager) validateAssetHTLC(ctx context.Context,
393-
htlc *rfqmsg.Htlc) error {
399+
htlc *rfqmsg.Htlc, circuitKey invoices.CircuitKey) error {
394400

395401
rfqID := htlc.RfqID.ValOpt().UnsafeFromSome()
396402

@@ -403,6 +409,7 @@ func (s *AuxInvoiceManager) validateAssetHTLC(ctx context.Context,
403409

404410
// Check for each of the asset balances of the HTLC that the identifier
405411
// matches that of the RFQ quote.
412+
assetIDs := fn.NewSet[asset.ID]()
406413
for _, v := range htlc.Balances() {
407414
match, err := s.cfg.RfqManager.AssetMatchesSpecifier(
408415
ctx, identifier, v.AssetID.Val,
@@ -415,6 +422,47 @@ func (s *AuxInvoiceManager) validateAssetHTLC(ctx context.Context,
415422
return fmt.Errorf("asset ID %s does not match %s",
416423
v.AssetID.Val.String(), identifier.String())
417424
}
425+
426+
assetIDs.Add(v.AssetID.Val)
427+
}
428+
429+
// We also need to validate that the HTLC is actually the correct asset
430+
// and arrived through the correct asset channel.
431+
channels, err := s.cfg.LightningClient.ListChannels(ctx, true, false)
432+
if err != nil {
433+
return fmt.Errorf("unable to list channels: %w", err)
434+
}
435+
436+
var inboundChannel *lndclient.ChannelInfo
437+
for _, channel := range channels {
438+
if channel.ChannelID == circuitKey.ChanID.ToUint64() {
439+
inboundChannel = &channel
440+
break
441+
}
442+
}
443+
444+
if inboundChannel == nil {
445+
return fmt.Errorf("unable to find channel with short channel "+
446+
"ID %d", circuitKey.ChanID.ToUint64())
447+
}
448+
449+
if len(inboundChannel.CustomChannelData) == 0 {
450+
return fmt.Errorf("channel %d does not have custom channel "+
451+
"data, can't accept asset HTLC over non-asset channel",
452+
inboundChannel.ChannelID)
453+
}
454+
455+
var assetData rfqmsg.JsonAssetChannel
456+
err = json.Unmarshal(inboundChannel.CustomChannelData, &assetData)
457+
if err != nil {
458+
return fmt.Errorf("unable to unmarshal channel asset data: %w",
459+
err)
460+
}
461+
462+
if !assetData.HasAllAssetIDs(assetIDs) {
463+
return fmt.Errorf("channel %d does not have all asset IDs "+
464+
"required for HTLC settlement",
465+
inboundChannel.ChannelID)
418466
}
419467

420468
return nil

tapchannel/aux_invoice_manager_test.go

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package tapchannel
22

33
import (
4+
"bytes"
45
"context"
56
"crypto/sha256"
7+
"encoding/json"
68
"fmt"
79
"math/big"
810
"testing"
@@ -298,7 +300,9 @@ func (m *mockHtlcModifierProperty) HtlcModifier(ctx context.Context,
298300
}
299301
} else {
300302
if assetValueMsat != res.AmtPaid {
301-
m.t.Errorf("unexpected final asset value")
303+
m.t.Errorf("unexpected final asset value, "+
304+
"wanted %d, got %d", assetValueMsat,
305+
res.AmtPaid)
302306
}
303307
}
304308
}
@@ -309,6 +313,19 @@ func (m *mockHtlcModifierProperty) HtlcModifier(ctx context.Context,
309313
return nil
310314
}
311315

316+
type mockLndClient struct {
317+
lndclient.LightningClient
318+
319+
channels []lndclient.ChannelInfo
320+
}
321+
322+
// ListChannels retrieves all channels of the backing lnd node.
323+
func (m *mockLndClient) ListChannels(_ context.Context, _,
324+
_ bool) ([]lndclient.ChannelInfo, error) {
325+
326+
return m.channels, nil
327+
}
328+
312329
// TestAuxInvoiceManager tests that the htlc modifications of the aux invoice
313330
// manager align with our expectations.
314331
func TestAuxInvoiceManager(t *testing.T) {
@@ -609,6 +626,7 @@ func TestAuxInvoiceManager(t *testing.T) {
609626
peerBuyQuotes: testCase.buyQuotes,
610627
localSellQuotes: testCase.sellQuotes,
611628
}
629+
mockLnd := &mockLndClient{}
612630

613631
done := make(chan bool)
614632

@@ -626,6 +644,7 @@ func TestAuxInvoiceManager(t *testing.T) {
626644
ChainParams: testChainParams,
627645
InvoiceHtlcModifier: mockModifier,
628646
RfqManager: mockRfq,
647+
LightningClient: mockLnd,
629648
},
630649
)
631650

@@ -800,13 +819,12 @@ func genRequest(t *rapid.T) (lndclient.InvoiceHtlcModifyRequest, uint64,
800819
// genRequests generates a random array of requests to be processed by the
801820
// AuxInvoiceManager. It also returns the rfq map with the related rfq quotes.
802821
func genRequests(t *rapid.T) ([]lndclient.InvoiceHtlcModifyRequest,
803-
rfq.BuyAcceptMap) {
822+
rfq.BuyAcceptMap, []lndclient.ChannelInfo) {
804823

805824
rfqMap := rfq.BuyAcceptMap{}
806825

807826
numRequests := rapid.IntRange(1, 5).Draw(t, "requestsLen")
808-
requests := make([]lndclient.InvoiceHtlcModifyRequest, 0)
809-
827+
var requests []lndclient.InvoiceHtlcModifyRequest
810828
for range numRequests {
811829
req, numAssets, assetID, scid := genRequest(t)
812830
requests = append(requests, req)
@@ -819,7 +837,37 @@ func genRequests(t *rapid.T) ([]lndclient.InvoiceHtlcModifyRequest,
819837
genBuyQuotes(t, rfqMap, numAssets, quoteAmt, assetID, scid)
820838
}
821839

822-
return requests, rfqMap
840+
channels := make([]lndclient.ChannelInfo, len(requests))
841+
for i, req := range requests {
842+
var (
843+
buf bytes.Buffer
844+
htlc rfqmsg.Htlc
845+
jsonAssetChan rfqmsg.JsonAssetChannel
846+
)
847+
err := req.WireCustomRecords.SerializeTo(&buf)
848+
require.NoError(t, err)
849+
850+
err = htlc.Decode(&buf)
851+
require.NoError(t, err)
852+
853+
jsonAssetChan.FundingAssets = make(
854+
[]rfqmsg.JsonAssetUtxo, len(htlc.Balances()),
855+
)
856+
for idx, balance := range htlc.Balances() {
857+
fundingAsset := &jsonAssetChan.FundingAssets[idx]
858+
fundingAssetGen := &fundingAsset.AssetGenesis
859+
fundingAssetGen.AssetID = balance.AssetID.Val.String()
860+
}
861+
862+
jsonChan, err := json.Marshal(jsonAssetChan)
863+
require.NoError(t, err)
864+
channels[i] = lndclient.ChannelInfo{
865+
ChannelID: req.CircuitKey.ChanID.ToUint64(),
866+
CustomChannelData: jsonChan,
867+
}
868+
}
869+
870+
return requests, rfqMap, channels
823871
}
824872

825873
// genRandomVertex generates a route.Vertex instance filled with random bytes.
@@ -898,11 +946,14 @@ func genBuyQuotes(t *rapid.T, rfqMap rfq.BuyAcceptMap, units, amtMsat uint64,
898946
// testInvoiceManager creates an array of requests to be processed by the
899947
// AuxInvoiceManager. Uses the enhanced HtlcModifierMockProperty instance.
900948
func testInvoiceManager(t *rapid.T) {
901-
requests, rfqMap := genRequests(t)
949+
requests, rfqMap, channels := genRequests(t)
902950

903951
mockRfq := &mockRfqManager{
904952
peerBuyQuotes: rfqMap,
905953
}
954+
mockLnd := &mockLndClient{
955+
channels: channels,
956+
}
906957

907958
done := make(chan bool)
908959

@@ -913,13 +964,12 @@ func testInvoiceManager(t *rapid.T) {
913964
t: t,
914965
}
915966

916-
manager := NewAuxInvoiceManager(
917-
&InvoiceManagerConfig{
918-
ChainParams: testChainParams,
919-
InvoiceHtlcModifier: mockModifier,
920-
RfqManager: mockRfq,
921-
},
922-
)
967+
manager := NewAuxInvoiceManager(&InvoiceManagerConfig{
968+
ChainParams: testChainParams,
969+
InvoiceHtlcModifier: mockModifier,
970+
RfqManager: mockRfq,
971+
LightningClient: mockLnd,
972+
})
923973

924974
err := manager.Start()
925975
require.NoError(t, err)

0 commit comments

Comments
 (0)