Skip to content

Commit af847c4

Browse files
authored
Merge pull request #1382 from lightninglabs/rfq-negotiation-groupkey
Support group keys for RFQ negotiation flows
2 parents dc3af8e + 6cf7190 commit af847c4

File tree

8 files changed

+326
-89
lines changed

8 files changed

+326
-89
lines changed

asset/asset.go

+10
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,16 @@ func (s *Specifier) UnwrapToPtr() (*ID, *btcec.PublicKey) {
438438
return s.UnwrapIdToPtr(), s.UnwrapGroupKeyToPtr()
439439
}
440440

441+
// AssertNotEmpty checks whether the specifier is empty, returning an error if
442+
// so.
443+
func (s *Specifier) AssertNotEmpty() error {
444+
if !s.HasId() && !s.HasGroupPubKey() {
445+
return fmt.Errorf("asset specifier is empty")
446+
}
447+
448+
return nil
449+
}
450+
441451
// Type denotes the asset types supported by the Taproot Asset protocol.
442452
type Type uint8
443453

itest/rfq_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,112 @@ func testRfqAssetSellHtlcIntercept(t *harnessTest) {
424424
require.NoError(t.t, err)
425425
}
426426

427+
// testRfqNegotiationGroupKey checks that two nodes can negotiate and register
428+
// quotes based on a specifier that only uses a group key.
429+
func testRfqNegotiationGroupKey(t *harnessTest) {
430+
// Initialize a new test scenario.
431+
ts := newRfqTestScenario(t)
432+
433+
// Mint an asset with Alice's tapd node.
434+
rpcAssets := MintAssetsConfirmBatch(
435+
t.t, t.lndHarness.Miner().Client, ts.AliceTapd,
436+
[]*mintrpc.MintAssetRequest{issuableAssets[0]},
437+
)
438+
439+
mintedAssetGroupKey := rpcAssets[0].AssetGroup.TweakedGroupKey
440+
441+
ctxb := context.Background()
442+
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
443+
defer cancel()
444+
445+
// Subscribe to Alice's RFQ events stream.
446+
aliceEventNtfns, err := ts.AliceTapd.SubscribeRfqEventNtfns(
447+
ctxb, &rfqrpc.SubscribeRfqEventNtfnsRequest{},
448+
)
449+
require.NoError(t.t, err)
450+
451+
// Alice sends a sell order to Bob for some amount of the newly minted
452+
// asset.
453+
askAmt := uint64(42000)
454+
sellOrderExpiry := uint64(time.Now().Add(24 * time.Hour).Unix())
455+
456+
// We first try to add a sell order without specifying the asset skip
457+
// flag. That should result in an error, since we only have a normal
458+
// channel and not an asset channel.
459+
sellReq := &rfqrpc.AddAssetSellOrderRequest{
460+
AssetSpecifier: &rfqrpc.AssetSpecifier{
461+
Id: &rfqrpc.AssetSpecifier_GroupKey{
462+
GroupKey: mintedAssetGroupKey,
463+
},
464+
},
465+
PaymentMaxAmt: askAmt,
466+
Expiry: sellOrderExpiry,
467+
468+
// Here we explicitly specify Bob as the destination
469+
// peer for the sell order. This will prompt Alice's
470+
// tapd node to send a request for quote message to
471+
// Bob's node.
472+
PeerPubKey: ts.BobLnd.PubKey[:],
473+
474+
TimeoutSeconds: uint32(rfqTimeout.Seconds()),
475+
}
476+
_, err = ts.AliceTapd.AddAssetSellOrder(ctxt, sellReq)
477+
require.ErrorContains(
478+
t.t, err, "no asset channel balance found",
479+
)
480+
481+
// Now we set the skip flag and we shouldn't get an error anymore.
482+
sellReq.SkipAssetChannelCheck = true
483+
_, err = ts.AliceTapd.AddAssetSellOrder(ctxt, sellReq)
484+
require.NoError(t.t, err, "unable to upsert asset sell order")
485+
486+
// Wait until Alice receives an incoming sell quote accept message (sent
487+
// from Bob) RFQ event notification.
488+
BeforeTimeout(t.t, func() {
489+
event, err := aliceEventNtfns.Recv()
490+
require.NoError(t.t, err)
491+
492+
_, ok := event.Event.(*rfqrpc.RfqEvent_PeerAcceptedSellQuote)
493+
require.True(t.t, ok, "unexpected event: %v", event)
494+
}, rfqTimeout)
495+
496+
// We now repeat the same flow, where Alice is making a BuyOrderRequest.
497+
assetMaxAmt := uint64(1000)
498+
buyOrderExpiry := sellOrderExpiry
499+
500+
buyReq := &rfqrpc.AddAssetBuyOrderRequest{
501+
AssetSpecifier: &rfqrpc.AssetSpecifier{
502+
Id: &rfqrpc.AssetSpecifier_GroupKey{
503+
GroupKey: mintedAssetGroupKey,
504+
},
505+
},
506+
AssetMaxAmt: assetMaxAmt,
507+
Expiry: buyOrderExpiry,
508+
PeerPubKey: ts.BobLnd.PubKey[:],
509+
TimeoutSeconds: uint32(rfqTimeout.Seconds()),
510+
}
511+
512+
_, err = ts.AliceTapd.AddAssetBuyOrder(ctxt, buyReq)
513+
require.ErrorContains(
514+
t.t, err, "no asset channel balance found",
515+
)
516+
517+
// Now we set the skip flag and we shouldn't get an error anymore.
518+
buyReq.SkipAssetChannelCheck = true
519+
_, err = ts.AliceTapd.AddAssetBuyOrder(ctxt, buyReq)
520+
require.NoError(t.t, err)
521+
522+
// Wait until Alice receives an incoming buy quote accept message (sent
523+
// from Bob) RFQ event notification.
524+
BeforeTimeout(t.t, func() {
525+
event, err := aliceEventNtfns.Recv()
526+
require.NoError(t.t, err)
527+
528+
_, ok := event.Event.(*rfqrpc.RfqEvent_PeerAcceptedBuyQuote)
529+
require.True(t.t, ok, "unexpected event: %v", event)
530+
}, rfqTimeout)
531+
}
532+
427533
// rfqTestScenario is a struct which holds test scenario helper infra.
428534
type rfqTestScenario struct {
429535
testHarness *harnessTest

itest/test_list_on_test.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,10 @@ var testCases = []*testCase{
315315
name: "rfq asset sell htlc intercept",
316316
test: testRfqAssetSellHtlcIntercept,
317317
},
318-
318+
{
319+
name: "rfq negotiation group key",
320+
test: testRfqNegotiationGroupKey,
321+
},
319322
{
320323
name: "multi signature on all levels",
321324
test: testMultiSignature,

rfq/manager.go

+129-20
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package rfq
22

33
import (
44
"context"
5+
"encoding/hex"
56
"encoding/json"
67
"fmt"
78
"sync"
@@ -61,6 +62,14 @@ type (
6162
SellAcceptMap map[SerialisedScid]rfqmsg.SellAccept
6263
)
6364

65+
// GroupLookup is an interface that helps us look up a group of an asset based
66+
// on the asset ID.
67+
type GroupLookup interface {
68+
// QueryAssetGroup fetches the group information of an asset, if it
69+
// belongs in a group.
70+
QueryAssetGroup(context.Context, asset.ID) (*asset.AssetGroup, error)
71+
}
72+
6473
// ManagerCfg is a struct that holds the configuration parameters for the RFQ
6574
// manager.
6675
type ManagerCfg struct {
@@ -84,6 +93,10 @@ type ManagerCfg struct {
8493
// determine the available channels for routing.
8594
ChannelLister ChannelLister
8695

96+
// GroupLookup is an interface that helps us querry asset groups by
97+
// asset IDs.
98+
GroupLookup GroupLookup
99+
87100
// AliasManager is the SCID alias manager. This component is injected
88101
// into the manager once lnd and tapd are hooked together.
89102
AliasManager ScidAliasManager
@@ -165,6 +178,12 @@ type Manager struct {
165178
SerialisedScid, rfqmsg.SellAccept,
166179
]
167180

181+
// groupKeyLookupCache is a map that helps us quickly perform an
182+
// in-memory look up of the group an asset belongs to. Since this
183+
// information is static and generated during minting, it is not
184+
// possible for an asset to change groups.
185+
groupKeyLookupCache lnutils.SyncMap[asset.ID, *btcec.PublicKey]
186+
168187
// subscribers is a map of components that want to be notified on new
169188
// events, keyed by their subscription ID.
170189
subscribers lnutils.SyncMap[uint64, *fn.EventReceiver[fn.Event]]
@@ -539,18 +558,7 @@ func (m *Manager) addScidAlias(scidAlias uint64, assetSpecifier asset.Specifier,
539558
return c.PubKeyBytes == peer
540559
}, localChans)
541560

542-
// Identify the correct channel to use as the base SCID for the alias
543-
// by inspecting the asset data in the custom channel data.
544-
assetID, err := assetSpecifier.UnwrapIdOrErr()
545-
if err != nil {
546-
return fmt.Errorf("asset ID must be specified when adding "+
547-
"alias: %w", err)
548-
}
549-
550-
var (
551-
assetIDStr = assetID.String()
552-
baseSCID uint64
553-
)
561+
var baseSCID uint64
554562
for _, localChan := range peerChannels {
555563
if len(localChan.CustomChannelData) == 0 {
556564
continue
@@ -564,12 +572,20 @@ func (m *Manager) addScidAlias(scidAlias uint64, assetSpecifier asset.Specifier,
564572
continue
565573
}
566574

567-
for _, channelAsset := range assetData.Assets {
568-
gen := channelAsset.AssetInfo.AssetGenesis
569-
if gen.AssetID == assetIDStr {
570-
baseSCID = localChan.ChannelID
571-
break
572-
}
575+
match, err := m.ChannelCompatible(
576+
ctxb, assetData.Assets, assetSpecifier,
577+
)
578+
if err != nil {
579+
return err
580+
}
581+
582+
// TODO(george): Instead of returning the first result,
583+
// try to pick the best channel for what we're trying to
584+
// do (receive/send). Binding a baseSCID means we're
585+
// also binding the asset liquidity on that channel.
586+
if match {
587+
baseSCID = localChan.ChannelID
588+
break
573589
}
574590
}
575591

@@ -583,8 +599,8 @@ func (m *Manager) addScidAlias(scidAlias uint64, assetSpecifier asset.Specifier,
583599
// At this point, if the base SCID is still not found, we return an
584600
// error. We can't map the SCID alias to a base SCID.
585601
if baseSCID == 0 {
586-
return fmt.Errorf("add alias: base SCID not found for asset: "+
587-
"%v", assetID)
602+
return fmt.Errorf("add alias: base SCID not found for %s",
603+
&assetSpecifier)
588604
}
589605

590606
log.Debugf("Adding SCID alias %d for base SCID %d", scidAlias, baseSCID)
@@ -917,6 +933,99 @@ func (m *Manager) RemoveSubscriber(
917933
return nil
918934
}
919935

936+
// getAssetGroupKey retrieves the group key of an asset based on its ID.
937+
func (m *Manager) getAssetGroupKey(ctx context.Context,
938+
id asset.ID) (fn.Option[btcec.PublicKey], error) {
939+
940+
// First, see if we have already queried our DB for this ID.
941+
v, ok := m.groupKeyLookupCache.Load(id)
942+
if ok {
943+
return fn.Some(*v), nil
944+
}
945+
946+
// Perform the DB query.
947+
group, err := m.cfg.GroupLookup.QueryAssetGroup(ctx, id)
948+
if err != nil {
949+
return fn.None[btcec.PublicKey](), err
950+
}
951+
952+
// If the asset does not belong to a group, return early with no error
953+
// or response.
954+
if group == nil || group.GroupKey == nil {
955+
return fn.None[btcec.PublicKey](), nil
956+
}
957+
958+
// Store the result for future calls.
959+
m.groupKeyLookupCache.Store(id, &group.GroupPubKey)
960+
961+
return fn.Some(group.GroupPubKey), nil
962+
}
963+
964+
// AssetMatchesSpecifier checks if the provided asset satisfies the provided
965+
// specifier. If the specifier includes a group key, we will check if the asset
966+
// belongs to that group.
967+
func (m *Manager) AssetMatchesSpecifier(ctx context.Context,
968+
specifier asset.Specifier, id asset.ID) (bool, error) {
969+
970+
switch {
971+
case specifier.HasGroupPubKey():
972+
group, err := m.getAssetGroupKey(ctx, id)
973+
if err != nil {
974+
return false, err
975+
}
976+
977+
if group.IsNone() {
978+
return false, nil
979+
}
980+
981+
specifierGK := specifier.UnwrapGroupKeyToPtr()
982+
983+
return group.UnwrapToPtr().IsEqual(specifierGK), nil
984+
985+
case specifier.HasId():
986+
specifierID := specifier.UnwrapIdToPtr()
987+
988+
return *specifierID == id, nil
989+
990+
default:
991+
return false, fmt.Errorf("specifier is empty")
992+
}
993+
}
994+
995+
// ChannelCompatible checks a channel's assets against an asset specifier. If
996+
// the specifier is an asset ID, then all assets must be of that specific ID,
997+
// if the specifier is a group key, then all assets in the channel must belong
998+
// to that group.
999+
func (m *Manager) ChannelCompatible(ctx context.Context,
1000+
jsonAssets []rfqmsg.JsonAssetChanInfo, specifier asset.Specifier) (bool,
1001+
error) {
1002+
1003+
for _, chanAsset := range jsonAssets {
1004+
gen := chanAsset.AssetInfo.AssetGenesis
1005+
assetIDBytes, err := hex.DecodeString(
1006+
gen.AssetID,
1007+
)
1008+
if err != nil {
1009+
return false, fmt.Errorf("error decoding asset ID: %w",
1010+
err)
1011+
}
1012+
1013+
var assetID asset.ID
1014+
copy(assetID[:], assetIDBytes)
1015+
1016+
match, err := m.AssetMatchesSpecifier(ctx, specifier, assetID)
1017+
if err != nil {
1018+
return false, err
1019+
}
1020+
1021+
if !match {
1022+
return false, err
1023+
}
1024+
}
1025+
1026+
return true, nil
1027+
}
1028+
9201029
// publishSubscriberEvent publishes an event to all subscribers.
9211030
func (m *Manager) publishSubscriberEvent(event fn.Event) {
9221031
// Iterate over the subscribers and deliver the event to each one.

rfqmsg/buy_request.go

+4-6
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,9 @@ func NewBuyRequestFromWire(wireMsg WireMessage,
159159
// Validate ensures that the buy request is valid.
160160
func (q *BuyRequest) Validate() error {
161161
// Ensure that the asset specifier is set.
162-
//
163-
// TODO(ffranr): For now, the asset ID must be set. We do not currently
164-
// support group keys.
165-
if !q.AssetSpecifier.HasId() {
166-
return fmt.Errorf("asset id not specified in BuyRequest")
162+
err := q.AssetSpecifier.AssertNotEmpty()
163+
if err != nil {
164+
return err
167165
}
168166

169167
// Ensure that the message version is supported.
@@ -173,7 +171,7 @@ func (q *BuyRequest) Validate() error {
173171
}
174172

175173
// Ensure that the suggested asset rate has not expired.
176-
err := fn.MapOptionZ(q.AssetRateHint, func(rate AssetRate) error {
174+
err = fn.MapOptionZ(q.AssetRateHint, func(rate AssetRate) error {
177175
if rate.Expiry.Before(time.Now()) {
178176
return fmt.Errorf("suggested asset rate has expired")
179177
}

rfqmsg/sell_request.go

+4-6
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,10 @@ func NewSellRequestFromWire(wireMsg WireMessage,
152152

153153
// Validate ensures that the quote request is valid.
154154
func (q *SellRequest) Validate() error {
155-
// Ensure that the asset specifier is set.
156-
//
157-
// TODO(ffranr): For now, the asset ID must be set. We do not currently
158-
// support group keys.
159-
if !q.AssetSpecifier.HasId() {
160-
return fmt.Errorf("asset id not specified in SellRequest")
155+
// Ensure that the asset specifier is not empty.
156+
err := q.AssetSpecifier.AssertNotEmpty()
157+
if err != nil {
158+
return err
161159
}
162160

163161
// Ensure that the message version is supported.

0 commit comments

Comments
 (0)