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

tapchannel: enforce strict forwarding for asset invoices #1144

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
13 changes: 9 additions & 4 deletions rfq/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ type ScidAliasManager interface {
baseScid lnwire.ShortChannelID) error
}

type (
BuyAcceptMap map[SerialisedScid]rfqmsg.BuyAccept
SellAcceptMap map[SerialisedScid]rfqmsg.SellAccept
)

// ManagerCfg is a struct that holds the configuration parameters for the RFQ
// manager.
type ManagerCfg struct {
Expand Down Expand Up @@ -759,7 +764,7 @@ func (m *Manager) UpsertAssetSellOrder(order SellOrder) error {
// PeerAcceptedBuyQuotes returns buy quotes that were requested by our node and
// have been accepted by our peers. These quotes are exclusively available to
// our node for the acquisition of assets.
func (m *Manager) PeerAcceptedBuyQuotes() map[SerialisedScid]rfqmsg.BuyAccept {
func (m *Manager) PeerAcceptedBuyQuotes() BuyAcceptMap {
// Returning the map directly is not thread safe. We will therefore
// create a copy.
buyQuotesCopy := make(map[SerialisedScid]rfqmsg.BuyAccept)
Expand All @@ -783,7 +788,7 @@ func (m *Manager) PeerAcceptedBuyQuotes() map[SerialisedScid]rfqmsg.BuyAccept {
// to our node for the sale of assets.
//
//nolint:lll
func (m *Manager) PeerAcceptedSellQuotes() map[SerialisedScid]rfqmsg.SellAccept {
func (m *Manager) PeerAcceptedSellQuotes() SellAcceptMap {
// Returning the map directly is not thread safe. We will therefore
// create a copy.
sellQuotesCopy := make(map[SerialisedScid]rfqmsg.SellAccept)
Expand All @@ -805,7 +810,7 @@ func (m *Manager) PeerAcceptedSellQuotes() map[SerialisedScid]rfqmsg.SellAccept
// LocalAcceptedBuyQuotes returns buy quotes that were accepted by our node and
// have been requested by our peers. These quotes are exclusively available to
// our node for the acquisition of assets.
func (m *Manager) LocalAcceptedBuyQuotes() map[SerialisedScid]rfqmsg.BuyAccept {
func (m *Manager) LocalAcceptedBuyQuotes() BuyAcceptMap {
// Returning the map directly is not thread safe. We will therefore
// create a copy.
buyQuotesCopy := make(map[SerialisedScid]rfqmsg.BuyAccept)
Expand All @@ -829,7 +834,7 @@ func (m *Manager) LocalAcceptedBuyQuotes() map[SerialisedScid]rfqmsg.BuyAccept {
// to our node for the sale of assets.
//
//nolint:lll
func (m *Manager) LocalAcceptedSellQuotes() map[SerialisedScid]rfqmsg.SellAccept {
func (m *Manager) LocalAcceptedSellQuotes() SellAcceptMap {
// Returning the map directly is not thread safe. We will therefore
// create a copy.
sellQuotesCopy := make(map[SerialisedScid]rfqmsg.SellAccept)
Expand Down
80 changes: 79 additions & 1 deletion tapchannel/aux_invoice_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tapchannel
import (
"context"
"fmt"
"strings"
"sync"

"github.com/davecgh/go-spew/spew"
Expand All @@ -12,7 +13,9 @@ import (
"github.com/lightninglabs/taproot-assets/rfq"
"github.com/lightninglabs/taproot-assets/rfqmsg"
"github.com/lightninglabs/taproot-assets/taprpc"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
)

// InvoiceHtlcModifier is an interface that abstracts the invoice HTLC
Expand All @@ -27,6 +30,20 @@ type InvoiceHtlcModifier interface {
handler lndclient.InvoiceHtlcModifyHandler) error
}

// RfqManager is an interface that abstracts the functionalities of the rfq
// manager that are needed by AuxInvoiceManager.
type RfqManager interface {
// PeerAcceptedBuyQuotes returns buy quotes that were requested by our
// node and have been accepted by our peers. These quotes are
// exclusively available to our node for the acquisition of assets.
PeerAcceptedBuyQuotes() rfq.BuyAcceptMap

// LocalAcceptedSellQuotes returns sell quotes that were accepted by our
// node and have been requested by our peers. These quotes are
// exclusively available to our node for the sale of assets.
LocalAcceptedSellQuotes() rfq.SellAcceptMap
}

// InvoiceManagerConfig defines the configuration for the auxiliary invoice
// manager.
type InvoiceManagerConfig struct {
Expand All @@ -41,7 +58,7 @@ type InvoiceManagerConfig struct {
// RfqManager is the RFQ manager that will be used to retrieve the
// accepted quotes for determining the incoming value of invoice related
// HTLCs.
RfqManager *rfq.Manager
RfqManager RfqManager
}

// AuxInvoiceManager is a Taproot Asset auxiliary invoice manager that can be
Expand Down Expand Up @@ -120,6 +137,14 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(_ context.Context,

// No custom record on the HTLC, so we have nothing to do.
if len(req.WireCustomRecords) == 0 {
// If there's no wire custom records and the invoice is an asset
// invoice do not settle the invoice.
//
// TODO(george): Strict-forwarding could be configurable?
if s.isAssetInvoice(req.Invoice) {
resp.AmtPaid = 1
}

return resp, nil
}

Expand Down Expand Up @@ -227,6 +252,59 @@ func (s *AuxInvoiceManager) priceFromQuote(rfqID rfqmsg.ID) (
}
}

// rfqPeerFromScid attempts to match the provided scid with a negotiated quote,
// then it returns the RFQ peer's node id.
func (s *AuxInvoiceManager) rfqPeerFromScid(scid uint64) (route.Vertex, error) {
acceptedBuyQuotes := s.cfg.RfqManager.PeerAcceptedBuyQuotes()
acceptedSellQuotes := s.cfg.RfqManager.LocalAcceptedSellQuotes()

buyQuote, isBuy := acceptedBuyQuotes[rfqmsg.SerialisedScid(scid)]
sellQuote, isSell := acceptedSellQuotes[rfqmsg.SerialisedScid(scid)]

switch {
case isBuy:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder: Shouldn't an invoice always relate to a buy order only? Even in a direct peer payment, when I create an invoice, there should be a buy quote for it.

return buyQuote.Peer, nil

case isSell:
return sellQuote.Peer, nil

default:
return route.Vertex{},
fmt.Errorf("no peer found for RFQ SCID %d", scid)
}
}

// isAssetInvoice checks whether the provided invoice is an asset invoice. This
// method checks whether the routing hints of the invoice match those created
// when generating an asset invoice, and if that's the case we then check that
// the scid matches an existing quote.
func (s *AuxInvoiceManager) isAssetInvoice(invoice *lnrpc.Invoice) bool {
hints := invoice.RouteHints

if len(hints) != 1 {
return false
}

hint := hints[0]
if len(hint.HopHints) != 1 {
return false
}

hop := hint.HopHints[0]
scid := hop.ChanId
peer, err := s.rfqPeerFromScid(scid)
GeorgeTsagk marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
log.Debugf("scid %v does not correspond to a valid RFQ quote",
scid)

return false
}

// We also want the RFQ peer and the node ID of the hop hint to match.
nodeId := hop.NodeId
return strings.Compare(peer.String(), nodeId) == 0
}

// Stop signals for an aux invoice manager to gracefully exit.
func (s *AuxInvoiceManager) Stop() error {
var stopErr error
Expand Down
Loading
Loading