Skip to content

Commit c63d418

Browse files
authored
Merge pull request #1423 from lightninglabs/taprpc-groupkey-support
Support group keys on `SendPayment` & `AddInvoice`
2 parents a9ea76a + e9e1de8 commit c63d418

File tree

14 files changed

+718
-374
lines changed

14 files changed

+718
-374
lines changed

asset/asset.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,23 @@ func NewSpecifierFromGroupKey(groupPubKey btcec.PublicKey) Specifier {
348348
}
349349
}
350350

351+
// NewExlusiveSpecifier creates a specifier that may only include one of asset
352+
// ID or group key. If both are set then a specifier over the group key is
353+
// created.
354+
func NewExclusiveSpecifier(id *ID,
355+
groupPubKey *btcec.PublicKey) (Specifier, error) {
356+
357+
switch {
358+
case groupPubKey != nil:
359+
return NewSpecifierFromGroupKey(*groupPubKey), nil
360+
361+
case id != nil:
362+
return NewSpecifierFromId(*id), nil
363+
}
364+
365+
return Specifier{}, fmt.Errorf("must set either asset ID or group key")
366+
}
367+
351368
// String returns a human-readable description of the specifier.
352369
func (s *Specifier) String() string {
353370
// An unset asset ID is represented as an empty string.

rfq/manager.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import (
44
"context"
55
"encoding/hex"
66
"encoding/json"
7+
"errors"
78
"fmt"
89
"sync"
910
"time"
1011

1112
"github.com/btcsuite/btcd/btcec/v2"
13+
"github.com/btcsuite/btcd/btcec/v2/schnorr"
1214
"github.com/lightninglabs/lndclient"
15+
"github.com/lightninglabs/taproot-assets/address"
1316
"github.com/lightninglabs/taproot-assets/asset"
1417
"github.com/lightninglabs/taproot-assets/fn"
1518
"github.com/lightninglabs/taproot-assets/rfqmsg"
@@ -232,6 +235,7 @@ func (m *Manager) startSubsystems(ctx context.Context) error {
232235
HtlcInterceptor: m.cfg.HtlcInterceptor,
233236
HtlcSubscriber: m.cfg.HtlcSubscriber,
234237
AcceptHtlcEvents: m.acceptHtlcEvents,
238+
SpecifierChecker: m.AssetMatchesSpecifier,
235239
})
236240
if err != nil {
237241
return fmt.Errorf("error initializing RFQ order handler: %w",
@@ -948,6 +952,10 @@ func (m *Manager) getAssetGroupKey(ctx context.Context,
948952
// Perform the DB query.
949953
group, err := m.cfg.GroupLookup.QueryAssetGroup(ctx, id)
950954
if err != nil {
955+
if errors.Is(err, address.ErrAssetGroupUnknown) {
956+
return fn.None[btcec.PublicKey](), nil
957+
}
958+
951959
return fn.None[btcec.PublicKey](), err
952960
}
953961

@@ -971,6 +979,18 @@ func (m *Manager) AssetMatchesSpecifier(ctx context.Context,
971979

972980
switch {
973981
case specifier.HasGroupPubKey():
982+
specifierGK := specifier.UnwrapGroupKeyToPtr()
983+
984+
// Let's directly check if the ID is equal to the X coordinate
985+
// of the group key. This is used by the sender to indicate that
986+
// any asset that belongs to this group may be used.
987+
groupKeyX := schnorr.SerializePubKey(specifierGK)
988+
if asset.ID(groupKeyX) == id {
989+
return true, nil
990+
}
991+
992+
// Now let's make an actual query to find this assetID's group,
993+
// if it exists.
974994
group, err := m.getAssetGroupKey(ctx, id)
975995
if err != nil {
976996
return false, err
@@ -980,8 +1000,6 @@ func (m *Manager) AssetMatchesSpecifier(ctx context.Context,
9801000
return false, nil
9811001
}
9821002

983-
specifierGK := specifier.UnwrapGroupKeyToPtr()
984-
9851003
return group.UnwrapToPtr().IsEqual(specifierGK), nil
9861004

9871005
case specifier.HasId():

rfq/marshal.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package rfq
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/lightninglabs/taproot-assets/fn"
7+
"github.com/lightninglabs/taproot-assets/rfqmath"
8+
"github.com/lightninglabs/taproot-assets/rfqmsg"
9+
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
10+
)
11+
12+
// MarshalAcceptedSellQuoteEvent marshals a peer accepted sell quote event to
13+
// its RPC representation.
14+
func MarshalAcceptedSellQuoteEvent(
15+
event *PeerAcceptedSellQuoteEvent) *rfqrpc.PeerAcceptedSellQuote {
16+
17+
return MarshalAcceptedSellQuote(event.SellAccept)
18+
}
19+
20+
// MarshalAcceptedSellQuote marshals a peer accepted sell quote to its RPC
21+
// representation.
22+
func MarshalAcceptedSellQuote(
23+
accept rfqmsg.SellAccept) *rfqrpc.PeerAcceptedSellQuote {
24+
25+
rpcAssetRate := &rfqrpc.FixedPoint{
26+
Coefficient: accept.AssetRate.Rate.Coefficient.String(),
27+
Scale: uint32(accept.AssetRate.Rate.Scale),
28+
}
29+
30+
// Calculate the equivalent asset units for the given total BTC amount
31+
// based on the asset-to-BTC conversion rate.
32+
numAssetUnits := rfqmath.MilliSatoshiToUnits(
33+
accept.Request.PaymentMaxAmt, accept.AssetRate.Rate,
34+
)
35+
36+
minTransportableMSat := rfqmath.MinTransportableMSat(
37+
rfqmath.DefaultOnChainHtlcMSat, accept.AssetRate.Rate,
38+
)
39+
40+
return &rfqrpc.PeerAcceptedSellQuote{
41+
Peer: accept.Peer.String(),
42+
Id: accept.ID[:],
43+
Scid: uint64(accept.ShortChannelId()),
44+
BidAssetRate: rpcAssetRate,
45+
Expiry: uint64(accept.AssetRate.Expiry.Unix()),
46+
AssetAmount: numAssetUnits.ScaleTo(0).ToUint64(),
47+
MinTransportableMsat: uint64(minTransportableMSat),
48+
}
49+
}
50+
51+
// MarshalAcceptedBuyQuoteEvent marshals a peer accepted buy quote event to
52+
// its rpc representation.
53+
func MarshalAcceptedBuyQuoteEvent(
54+
event *PeerAcceptedBuyQuoteEvent) (*rfqrpc.PeerAcceptedBuyQuote,
55+
error) {
56+
57+
// We now calculate the minimum amount of asset units that can be
58+
// transported within a single HTLC for this asset at the given rate.
59+
// This corresponds to the 354 satoshi minimum non-dust HTLC value.
60+
minTransportableUnits := rfqmath.MinTransportableUnits(
61+
rfqmath.DefaultOnChainHtlcMSat, event.AssetRate.Rate,
62+
).ScaleTo(0).ToUint64()
63+
64+
return &rfqrpc.PeerAcceptedBuyQuote{
65+
Peer: event.Peer.String(),
66+
Id: event.ID[:],
67+
Scid: uint64(event.ShortChannelId()),
68+
AssetMaxAmount: event.Request.AssetMaxAmt,
69+
AskAssetRate: &rfqrpc.FixedPoint{
70+
Coefficient: event.AssetRate.Rate.Coefficient.String(),
71+
Scale: uint32(event.AssetRate.Rate.Scale),
72+
},
73+
Expiry: uint64(event.AssetRate.Expiry.Unix()),
74+
MinTransportableUnits: minTransportableUnits,
75+
}, nil
76+
}
77+
78+
// MarshalInvalidQuoteRespEvent marshals an invalid quote response event to
79+
// its rpc representation.
80+
func MarshalInvalidQuoteRespEvent(
81+
event *InvalidQuoteRespEvent) *rfqrpc.InvalidQuoteResponse {
82+
83+
peer := event.QuoteResponse.MsgPeer()
84+
id := event.QuoteResponse.MsgID()
85+
86+
return &rfqrpc.InvalidQuoteResponse{
87+
Status: rfqrpc.QuoteRespStatus(event.Status),
88+
Peer: peer.String(),
89+
Id: id[:],
90+
}
91+
}
92+
93+
// MarshalIncomingRejectQuoteEvent marshals an incoming reject quote event to
94+
// its RPC representation.
95+
func MarshalIncomingRejectQuoteEvent(
96+
event *IncomingRejectQuoteEvent) *rfqrpc.RejectedQuoteResponse {
97+
98+
return &rfqrpc.RejectedQuoteResponse{
99+
Peer: event.Peer.String(),
100+
Id: event.ID.Val[:],
101+
ErrorMessage: event.Err.Val.Msg,
102+
ErrorCode: uint32(event.Err.Val.Code),
103+
}
104+
}
105+
106+
// NewAddAssetBuyOrderResponse creates a new AddAssetBuyOrderResponse from
107+
// the given RFQ event.
108+
func NewAddAssetBuyOrderResponse(
109+
event fn.Event) (*rfqrpc.AddAssetBuyOrderResponse, error) {
110+
111+
resp := &rfqrpc.AddAssetBuyOrderResponse{}
112+
113+
switch e := event.(type) {
114+
case *PeerAcceptedBuyQuoteEvent:
115+
acceptedQuote, err := MarshalAcceptedBuyQuoteEvent(e)
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
resp.Response = &rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote{
121+
AcceptedQuote: acceptedQuote,
122+
}
123+
return resp, nil
124+
125+
case *InvalidQuoteRespEvent:
126+
resp.Response = &rfqrpc.AddAssetBuyOrderResponse_InvalidQuote{
127+
InvalidQuote: MarshalInvalidQuoteRespEvent(e),
128+
}
129+
return resp, nil
130+
131+
case *IncomingRejectQuoteEvent:
132+
resp.Response = &rfqrpc.AddAssetBuyOrderResponse_RejectedQuote{
133+
RejectedQuote: MarshalIncomingRejectQuoteEvent(e),
134+
}
135+
return resp, nil
136+
137+
default:
138+
return nil, fmt.Errorf("unknown AddAssetBuyOrder event "+
139+
"type: %T", e)
140+
}
141+
}
142+
143+
// NewAddAssetSellOrderResponse creates a new AddAssetSellOrderResponse from
144+
// the given RFQ event.
145+
func NewAddAssetSellOrderResponse(
146+
event fn.Event) (*rfqrpc.AddAssetSellOrderResponse, error) {
147+
148+
resp := &rfqrpc.AddAssetSellOrderResponse{}
149+
150+
switch e := event.(type) {
151+
case *PeerAcceptedSellQuoteEvent:
152+
resp.Response = &rfqrpc.AddAssetSellOrderResponse_AcceptedQuote{
153+
AcceptedQuote: MarshalAcceptedSellQuoteEvent(e),
154+
}
155+
return resp, nil
156+
157+
case *InvalidQuoteRespEvent:
158+
resp.Response = &rfqrpc.AddAssetSellOrderResponse_InvalidQuote{
159+
InvalidQuote: MarshalInvalidQuoteRespEvent(e),
160+
}
161+
return resp, nil
162+
163+
case *IncomingRejectQuoteEvent:
164+
resp.Response = &rfqrpc.AddAssetSellOrderResponse_RejectedQuote{
165+
RejectedQuote: MarshalIncomingRejectQuoteEvent(e),
166+
}
167+
return resp, nil
168+
169+
default:
170+
return nil, fmt.Errorf("unknown AddAssetSellOrder event "+
171+
"type: %T", e)
172+
}
173+
}

rfq/order.go

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"sync"
88
"time"
99

10+
"github.com/btcsuite/btcd/btcec/v2/schnorr"
1011
"github.com/davecgh/go-spew/spew"
1112
"github.com/lightninglabs/lndclient"
1213
"github.com/lightninglabs/taproot-assets/asset"
@@ -58,7 +59,8 @@ type SerialisedScid = rfqmsg.SerialisedScid
5859
type Policy interface {
5960
// CheckHtlcCompliance returns an error if the given HTLC intercept
6061
// descriptor does not satisfy the subject policy.
61-
CheckHtlcCompliance(htlc lndclient.InterceptedHtlc) error
62+
CheckHtlcCompliance(ctx context.Context, htlc lndclient.InterceptedHtlc,
63+
specifierChecker rfqmsg.SpecifierChecker) error
6264

6365
// Expiry returns the policy's expiry time as a unix timestamp.
6466
Expiry() uint64
@@ -145,8 +147,8 @@ func NewAssetSalePolicy(quote rfqmsg.BuyAccept) *AssetSalePolicy {
145147
// included as a hop hint within the invoice. The SCID is the only piece of
146148
// information used to determine the policy applicable to the HTLC. As a result,
147149
// HTLC custom records are not expected to be present.
148-
func (c *AssetSalePolicy) CheckHtlcCompliance(
149-
htlc lndclient.InterceptedHtlc) error {
150+
func (c *AssetSalePolicy) CheckHtlcCompliance(_ context.Context,
151+
htlc lndclient.InterceptedHtlc, _ rfqmsg.SpecifierChecker) error {
150152

151153
// Since we will be reading CurrentAmountMsat value we acquire a read
152154
// lock.
@@ -248,11 +250,23 @@ func (c *AssetSalePolicy) GenerateInterceptorResponse(
248250

249251
outgoingAmt := rfqmath.DefaultOnChainHtlcMSat
250252

251-
// Unpack asset ID.
252-
assetID, err := c.AssetSpecifier.UnwrapIdOrErr()
253-
if err != nil {
254-
return nil, fmt.Errorf("asset sale policy has no asset ID: %w",
255-
err)
253+
var assetID asset.ID
254+
255+
// We have performed checks for the asset IDs inside the HTLC against
256+
// the specifier's group key in a previous step. Here we just need to
257+
// provide a dummy value as the asset ID. The real asset IDs will be
258+
// carefully picked in a later step in the process. What really matters
259+
// now is the total amount.
260+
switch {
261+
case c.AssetSpecifier.HasGroupPubKey():
262+
groupKey := c.AssetSpecifier.UnwrapGroupKeyToPtr()
263+
groupKeyX := schnorr.SerializePubKey(groupKey)
264+
265+
assetID = asset.ID(groupKeyX)
266+
267+
case c.AssetSpecifier.HasId():
268+
specifierID := *c.AssetSpecifier.UnwrapIdToPtr()
269+
copy(assetID[:], specifierID[:])
256270
}
257271

258272
// Compute the outgoing asset amount given the msat outgoing amount and
@@ -341,8 +355,9 @@ func NewAssetPurchasePolicy(quote rfqmsg.SellAccept) *AssetPurchasePolicy {
341355

342356
// CheckHtlcCompliance returns an error if the given HTLC intercept descriptor
343357
// does not satisfy the subject policy.
344-
func (c *AssetPurchasePolicy) CheckHtlcCompliance(
345-
htlc lndclient.InterceptedHtlc) error {
358+
func (c *AssetPurchasePolicy) CheckHtlcCompliance(ctx context.Context,
359+
htlc lndclient.InterceptedHtlc,
360+
specifierChecker rfqmsg.SpecifierChecker) error {
346361

347362
// Since we will be reading CurrentAmountMsat value we acquire a read
348363
// lock.
@@ -368,7 +383,9 @@ func (c *AssetPurchasePolicy) CheckHtlcCompliance(
368383
}
369384

370385
// Sum the asset balance in the HTLC record.
371-
assetAmt, err := htlcRecord.SumAssetBalance(c.AssetSpecifier)
386+
assetAmt, err := htlcRecord.SumAssetBalance(
387+
ctx, c.AssetSpecifier, specifierChecker,
388+
)
372389
if err != nil {
373390
return fmt.Errorf("error summing asset balance: %w", err)
374391
}
@@ -523,15 +540,19 @@ func NewAssetForwardPolicy(incoming, outgoing Policy) (*AssetForwardPolicy,
523540

524541
// CheckHtlcCompliance returns an error if the given HTLC intercept descriptor
525542
// does not satisfy the subject policy.
526-
func (a *AssetForwardPolicy) CheckHtlcCompliance(
527-
htlc lndclient.InterceptedHtlc) error {
543+
func (a *AssetForwardPolicy) CheckHtlcCompliance(ctx context.Context,
544+
htlc lndclient.InterceptedHtlc, sChk rfqmsg.SpecifierChecker) error {
528545

529-
if err := a.incomingPolicy.CheckHtlcCompliance(htlc); err != nil {
546+
if err := a.incomingPolicy.CheckHtlcCompliance(
547+
ctx, htlc, sChk,
548+
); err != nil {
530549
return fmt.Errorf("error checking forward policy, inbound "+
531550
"HTLC does not comply with policy: %w", err)
532551
}
533552

534-
if err := a.outgoingPolicy.CheckHtlcCompliance(htlc); err != nil {
553+
if err := a.outgoingPolicy.CheckHtlcCompliance(
554+
ctx, htlc, sChk,
555+
); err != nil {
535556
return fmt.Errorf("error checking forward policy, outbound "+
536557
"HTLC does not comply with policy: %w", err)
537558
}
@@ -642,6 +663,10 @@ type OrderHandlerCfg struct {
642663
// HtlcSubscriber is a subscriber that is used to retrieve live HTLC
643664
// event updates.
644665
HtlcSubscriber HtlcSubscriber
666+
667+
// SpecifierChecker is an interface that contains methods for
668+
// checking certain properties related to asset specifiers.
669+
SpecifierChecker rfqmsg.SpecifierChecker
645670
}
646671

647672
// OrderHandler orchestrates management of accepted quote bundles. It monitors
@@ -684,7 +709,7 @@ func NewOrderHandler(cfg OrderHandlerCfg) (*OrderHandler, error) {
684709
//
685710
// NOTE: This function must be thread safe. It is used by an external
686711
// interceptor service.
687-
func (h *OrderHandler) handleIncomingHtlc(_ context.Context,
712+
func (h *OrderHandler) handleIncomingHtlc(ctx context.Context,
688713
htlc lndclient.InterceptedHtlc) (*lndclient.InterceptedHtlcResponse,
689714
error) {
690715

@@ -716,7 +741,7 @@ func (h *OrderHandler) handleIncomingHtlc(_ context.Context,
716741
// At this point, we know that a policy exists and has not expired
717742
// whilst sitting in the local cache. We can now check that the HTLC
718743
// complies with the policy.
719-
err = policy.CheckHtlcCompliance(htlc)
744+
err = policy.CheckHtlcCompliance(ctx, htlc, h.cfg.SpecifierChecker)
720745
if err != nil {
721746
log.Warnf("HTLC does not comply with policy: %v "+
722747
"(HTLC=%v, policy=%v)", err, htlc, policy)

0 commit comments

Comments
 (0)