Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invoice Acceptor: ensure asset ID match between RFQ and HTLC #1299

Merged
merged 4 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
GeorgeTsagk marked this conversation as resolved.
Show resolved Hide resolved
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",
guggero marked this conversation as resolved.
Show resolved Hide resolved
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
Loading