Skip to content

Commit

Permalink
Merge pull request #1299 from GeorgeTsagk/inv-accept-asset-id-match
Browse files Browse the repository at this point in the history
Invoice Acceptor: ensure asset ID match between RFQ and HTLC
  • Loading branch information
GeorgeTsagk authored Jan 23, 2025
2 parents ac2961a + 070dff0 commit 71fee14
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 9 deletions.
5 changes: 5 additions & 0 deletions asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,11 @@ func (s *Specifier) WhenId(f func(ID)) {
s.id.WhenSome(f)
}

// ID returns the underlying asset ID option of the specifier.
func (s *Specifier) ID() fn.Option[ID] {
return s.id
}

// WhenGroupPubKey executes the given function if asset group public key field
// is specified.
func (s *Specifier) WhenGroupPubKey(f func(btcec.PublicKey)) {
Expand Down
78 changes: 78 additions & 0 deletions tapchannel/aux_invoice_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/taproot-assets/address"
"github.com/lightninglabs/taproot-assets/asset"
"github.com/lightninglabs/taproot-assets/fn"
"github.com/lightninglabs/taproot-assets/rfq"
"github.com/lightninglabs/taproot-assets/rfqmath"
Expand Down Expand Up @@ -194,6 +195,16 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(_ context.Context,
return resp, nil
}

// We now run some validation checks on the asset HTLC.
err = s.validateAssetHTLC(htlc)
if err != nil {
log.Errorf("Failed to validate asset HTLC: %v", err)

resp.CancelSet = true

return resp, nil
}

// Convert the total asset amount to milli-satoshis using the price from
// the accepted quote.
rfqID := htlc.RfqID.ValOpt().UnsafeFromSome()
Expand Down Expand Up @@ -247,6 +258,36 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(_ context.Context,
return resp, nil
}

// identifierFromQuote retrieves the quote by looking up the rfq manager's maps
// of accepted quotes based on the passed rfq ID. If there's a match, the asset
// specifier is returned.
func (s *AuxInvoiceManager) identifierFromQuote(
rfqID rfqmsg.ID) (asset.Specifier, error) {

acceptedBuyQuotes := s.cfg.RfqManager.PeerAcceptedBuyQuotes()
acceptedSellQuotes := s.cfg.RfqManager.LocalAcceptedSellQuotes()

buyQuote, isBuy := acceptedBuyQuotes[rfqID.Scid()]
sellQuote, isSell := acceptedSellQuotes[rfqID.Scid()]

switch {
case isBuy:
if buyQuote.Request.AssetSpecifier.HasId() {
req := buyQuote.Request
return req.AssetSpecifier, nil
}

case isSell:
if sellQuote.Request.AssetSpecifier.HasId() {
req := sellQuote.Request
return req.AssetSpecifier, nil
}
}

return asset.Specifier{}, fmt.Errorf("rfqID does not match any " +
"accepted buy or sell quote")
}

// 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
Expand Down Expand Up @@ -336,6 +377,43 @@ func isAssetInvoice(invoice *lnrpc.Invoice, rfqLookup RfqLookup) bool {
return false
}

// validateAssetHTLC runs a couple of checks on the provided asset HTLC.
func (s *AuxInvoiceManager) validateAssetHTLC(htlc *rfqmsg.Htlc) error {
rfqID := htlc.RfqID.ValOpt().UnsafeFromSome()

// Retrieve the asset identifier from the RFQ quote.
identifier, err := s.identifierFromQuote(rfqID)
if err != nil {
return fmt.Errorf("could not extract assetID from "+
"quote: %v", err)
}

if !identifier.HasId() {
return fmt.Errorf("asset specifier has empty assetID")
}

// Check for each of the asset balances of the HTLC that the identifier
// matches that of the RFQ quote.
for _, v := range htlc.Balances() {
err := fn.MapOptionZ(
identifier.ID(), func(id asset.ID) error {
if v.AssetID.Val != id {
return fmt.Errorf("mismatch between " +
"htlc asset ID and rfq asset " +
"ID")
}

return nil
},
)
if err != nil {
return err
}
}

return nil
}

// Stop signals for an aux invoice manager to gracefully exit.
func (s *AuxInvoiceManager) Stop() error {
var stopErr error
Expand Down
70 changes: 61 additions & 9 deletions tapchannel/aux_invoice_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,10 @@ func (m *mockHtlcModifierProperty) HtlcModifier(ctx context.Context,
}
} else {
if !assert.ErrorContains(
m.t, err, "price from quote",
m.t, err, "extract assetID from quote",
) {

m.t.Errorf("expected quote price err")
m.t.Errorf("expected assetID error")
}
}

Expand Down Expand Up @@ -216,7 +216,15 @@ func (m *mockHtlcModifierProperty) HtlcModifier(ctx context.Context,

quote, ok := m.rfqMap[rfqID.Scid()]
if !ok {
m.t.Errorf("no rfq quote found")
if res.CancelSet {
continue
}

if !assert.ErrorContains(m.t, err, "price from quote") {
m.t.Errorf("expected quote related error")
}

continue
}

assetRate := lnwire.MilliSatoshi(
Expand Down Expand Up @@ -264,6 +272,11 @@ func (m *mockHtlcModifierProperty) HtlcModifier(ctx context.Context,
// TestAuxInvoiceManager tests that the htlc modifications of the aux invoice
// manager align with our expectations.
func TestAuxInvoiceManager(t *testing.T) {
var (
assetID = dummyAssetID(1)
assetSpecifier = asset.NewSpecifierFromId(assetID)
)

testCases := []struct {
name string
buyQuotes rfq.BuyAcceptMap
Expand Down Expand Up @@ -342,8 +355,7 @@ func TestAuxInvoiceManager(t *testing.T) {
WireCustomRecords: newWireCustomRecords(
t, []*rfqmsg.AssetBalance{
rfqmsg.NewAssetBalance(
dummyAssetID(1),
3,
assetID, 3,
),
}, fn.Some(testRfqID),
),
Expand All @@ -360,6 +372,9 @@ func TestAuxInvoiceManager(t *testing.T) {
AssetRate: rfqmsg.NewAssetRate(
testAssetRate, time.Now(),
),
Request: rfqmsg.BuyRequest{
AssetSpecifier: assetSpecifier,
},
},
},
},
Expand All @@ -375,8 +390,7 @@ func TestAuxInvoiceManager(t *testing.T) {
WireCustomRecords: newWireCustomRecords(
t, []*rfqmsg.AssetBalance{
rfqmsg.NewAssetBalance(
dummyAssetID(1),
4,
assetID, 4,
),
}, fn.Some(testRfqID),
),
Expand All @@ -394,6 +408,9 @@ func TestAuxInvoiceManager(t *testing.T) {
AssetRate: rfqmsg.NewAssetRate(
testAssetRate, time.Now(),
),
Request: rfqmsg.BuyRequest{
AssetSpecifier: assetSpecifier,
},
},
},
},
Expand All @@ -408,8 +425,7 @@ func TestAuxInvoiceManager(t *testing.T) {
WireCustomRecords: newWireCustomRecords(
t, []*rfqmsg.AssetBalance{
rfqmsg.NewAssetBalance(
dummyAssetID(1),
4,
assetID, 4,
),
}, fn.Some(testRfqID),
),
Expand All @@ -422,6 +438,42 @@ func TestAuxInvoiceManager(t *testing.T) {
},
},
},
{
name: "asset invoice, wrong asset htlc",
requests: []lndclient.InvoiceHtlcModifyRequest{
{
Invoice: &lnrpc.Invoice{
RouteHints: testRouteHints(),
ValueMsat: 3_000_000,
PaymentAddr: []byte{1, 1, 1},
},
WireCustomRecords: newWireCustomRecords(
t, []*rfqmsg.AssetBalance{
rfqmsg.NewAssetBalance(
dummyAssetID(5),
3,
),
}, fn.Some(testRfqID),
),
},
},
responses: []lndclient.InvoiceHtlcModifyResponse{
{
CancelSet: true,
},
},
buyQuotes: rfq.BuyAcceptMap{
testScid: {
Peer: testNodeID,
AssetRate: rfqmsg.NewAssetRate(
testAssetRate, time.Now(),
),
Request: rfqmsg.BuyRequest{
AssetSpecifier: assetSpecifier,
},
},
},
},
}

for _, testCase := range testCases {
Expand Down

0 comments on commit 71fee14

Please sign in to comment.