Skip to content

Commit

Permalink
Merge pull request #1157 from lightninglabs/rfqmsg-request-fields-bli…
Browse files Browse the repository at this point in the history
…p-align

rfqmsg: request fields blip align
  • Loading branch information
ffranr authored Nov 5, 2024
2 parents e6b78bd + c5d356e commit ab90081
Show file tree
Hide file tree
Showing 26 changed files with 1,126 additions and 678 deletions.
63 changes: 63 additions & 0 deletions asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,41 @@ type Specifier struct {
groupKey fn.Option[btcec.PublicKey]
}

// NewSpecifier creates a new Specifier instance based on the provided
// parameters.
//
// The Specifier identifies an asset using either an asset ID, a group public
// key, or a group key. At least one of these must be specified if the
// `mustBeSpecified` parameter is set to true.
func NewSpecifier(id *ID, groupPubKey *btcec.PublicKey, groupKey *GroupKey,
mustBeSpecified bool) (Specifier, error) {

// Return an error if the asset ID, group public key, and group key are
// all nil and at least one of them must be specified.
isAnySpecified := id != nil || groupPubKey != nil || groupKey != nil
if !isAnySpecified && mustBeSpecified {
return Specifier{}, fmt.Errorf("at least one of the asset ID "+
"or asset group key fields must be specified "+
"(id=%v, groupPubKey=%v, groupKey=%v)",
id, groupPubKey, groupKey)
}

// Create an option for the asset ID.
optId := fn.MaybeSome(id)

// Create an option for the group public key.
optGroupPubKey := fn.MaybeSome(groupPubKey)

if groupKey != nil {
optGroupPubKey = fn.Some(groupKey.GroupPubKey)
}

return Specifier{
id: optId,
groupKey: optGroupPubKey,
}, nil
}

// NewSpecifierOptionalGroupPubKey creates a new specifier that specifies an
// asset by its ID and an optional group public key.
func NewSpecifierOptionalGroupPubKey(id ID,
Expand Down Expand Up @@ -308,6 +343,23 @@ func NewSpecifierFromGroupKey(groupPubKey btcec.PublicKey) Specifier {
}
}

// String returns a human-readable description of the specifier.
func (s *Specifier) String() string {
// An unset asset ID is represented as an empty string.
var assetIdStr string
s.WhenId(func(id ID) {
assetIdStr = id.String()
})

var groupKeyBytes []byte
s.WhenGroupPubKey(func(key btcec.PublicKey) {
groupKeyBytes = key.SerializeCompressed()
})

return fmt.Sprintf("AssetSpecifier(id=%s, group_pub_key=%x)",
assetIdStr, groupKeyBytes)
}

// AsBytes returns the asset ID and group public key as byte slices.
func (s *Specifier) AsBytes() ([]byte, []byte) {
var assetIDBytes, groupKeyBytes []byte
Expand All @@ -333,6 +385,11 @@ func (s *Specifier) HasGroupPubKey() bool {
return s.groupKey.IsSome()
}

// IsSome returns true if the specifier is set.
func (s *Specifier) IsSome() bool {
return s.HasId() || s.HasGroupPubKey()
}

// WhenId executes the given function if the ID field is specified.
func (s *Specifier) WhenId(f func(ID)) {
s.id.WhenSome(f)
Expand Down Expand Up @@ -365,6 +422,12 @@ func (s *Specifier) UnwrapGroupKeyToPtr() *btcec.PublicKey {
return s.groupKey.UnwrapToPtr()
}

// UnwrapToPtr unwraps the asset ID and asset group public key fields,
// returning them as pointers.
func (s *Specifier) UnwrapToPtr() (*ID, *btcec.PublicKey) {
return s.UnwrapIdToPtr(), s.UnwrapGroupKeyToPtr()
}

// Type denotes the asset types supported by the Taproot Asset protocol.
type Type uint8

Expand Down
68 changes: 49 additions & 19 deletions itest/rfq_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"time"

"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/taproot-assets/asset"
"github.com/lightninglabs/taproot-assets/fn"
"github.com/lightninglabs/taproot-assets/rfqmsg"
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
Expand All @@ -19,6 +21,7 @@ import (
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -190,8 +193,8 @@ func testRfqAssetBuyHtlcIntercept(t *harnessTest) {
t.Log("Alice payment sent")

// At this point Bob should have received a HTLC with the asset transfer
// specific scid. We'll wait for Bob to publish an accept HTLC event and
// then validate it against the accepted quote.
// specific scid. We'll wait for Bob to validate the HTLC against the
// accepted quote and publish a HTLC accept event.
BeforeTimeout(t.t, func() {
t.Log("Waiting for Bob to receive HTLC")

Expand Down Expand Up @@ -229,7 +232,11 @@ func testRfqAssetSellHtlcIntercept(t *harnessTest) {
t.t, t.lndHarness.Miner().Client, ts.AliceTapd,
[]*mintrpc.MintAssetRequest{issuableAssets[0]},
)
mintedAssetId := rpcAssets[0].AssetGenesis.AssetId
mintedAssetIdBytes := rpcAssets[0].AssetGenesis.AssetId

// Type convert the asset ID bytes to an `asset.ID`.
var mintedAssetId asset.ID
copy(mintedAssetId[:], mintedAssetIdBytes[:])

ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
Expand All @@ -241,7 +248,7 @@ func testRfqAssetSellHtlcIntercept(t *harnessTest) {
ctxt, &rfqrpc.AddAssetBuyOfferRequest{
AssetSpecifier: &rfqrpc.AssetSpecifier{
Id: &rfqrpc.AssetSpecifier_AssetId{
AssetId: mintedAssetId,
AssetId: mintedAssetIdBytes,
},
},
MaxUnits: 1000,
Expand All @@ -257,20 +264,18 @@ func testRfqAssetSellHtlcIntercept(t *harnessTest) {

// Alice sends a sell order to Bob for some amount of the newly minted
// asset.
purchaseAssetAmt := uint64(200)
askAmt := uint64(42000)
sellOrderExpiry := uint64(time.Now().Add(24 * time.Hour).Unix())

_, err = ts.AliceTapd.AddAssetSellOrder(
ctxt, &rfqrpc.AddAssetSellOrderRequest{
AssetSpecifier: &rfqrpc.AssetSpecifier{
Id: &rfqrpc.AssetSpecifier_AssetId{
AssetId: mintedAssetId,
AssetId: mintedAssetIdBytes,
},
},
MaxAssetAmount: purchaseAssetAmt,
MinAsk: askAmt,
Expiry: sellOrderExpiry,
PaymentMaxAmt: askAmt,
Expiry: sellOrderExpiry,

// Here we explicitly specify Bob as the destination
// peer for the sell order. This will prompt Alice's
Expand Down Expand Up @@ -303,6 +308,10 @@ func testRfqAssetSellHtlcIntercept(t *harnessTest) {

acceptedQuote := acceptedQuotes.SellQuotes[0]

// Type cast the accepted quote ID bytes to an `rfqmsg.ID`.
var acceptedQuoteId rfqmsg.ID
copy(acceptedQuoteId[:], acceptedQuote.Id[:])

// Register to receive RFQ events from Bob's tapd node. We'll use this
// to wait for Bob to receive the HTLC with the asset transfer specific
// scid.
Expand Down Expand Up @@ -337,19 +346,40 @@ func testRfqAssetSellHtlcIntercept(t *harnessTest) {

// Send the payment to the route.
t.Log("Alice paying invoice")
var htlcRfqIDTlvType rfqmsg.HtlcRfqIDType

// Construct first hop custom records for payment.
//
// The custom records will contain the accepted quote ID and the asset
// amounts that Alice will pay to Bob.
//
// We select an asset amount which is sufficient to cover the invoice
// amount.
paymentAssetAmount := uint64(42)
assetAmounts := []*rfqmsg.AssetBalance{
rfqmsg.NewAssetBalance(mintedAssetId, paymentAssetAmount),
}

htlcCustomRecords := rfqmsg.NewHtlc(
assetAmounts, fn.Some(acceptedQuoteId),
)

// Convert the custom records to a TLV map for inclusion in
// SendToRouteRequest.
firstHopCustomRecords, err := tlv.RecordsToMap(
htlcCustomRecords.Records(),
)
require.NoError(t.t, err)

routeReq := routerrpc.SendToRouteRequest{
PaymentHash: invoice.RHash,
Route: routeBuildResp.Route,
FirstHopCustomRecords: map[uint64][]byte{
uint64(htlcRfqIDTlvType.TypeVal()): acceptedQuote.Id[:],
},
PaymentHash: invoice.RHash,
Route: routeBuildResp.Route,
FirstHopCustomRecords: firstHopCustomRecords,
}
sendAttempt := ts.AliceLnd.RPC.SendToRouteV2(&routeReq)

// The payment will fail since it doesn't transport the correct amount
// of the asset.
require.Equal(t.t, lnrpc.HTLCAttempt_FAILED, sendAttempt.Status)
// The payment will succeed since it the asset amount transport is
// sufficient to cover the invoice amount.
require.Equal(t.t, lnrpc.HTLCAttempt_SUCCEEDED, sendAttempt.Status)

// At this point Bob should have received a HTLC with the asset transfer
// specific scid. We'll wait for Bob to publish an accept HTLC event and
Expand All @@ -365,7 +395,7 @@ func testRfqAssetSellHtlcIntercept(t *harnessTest) {

// Confirm that Carol receives the lightning payment from Alice via Bob.
invoice = ts.CarolLnd.RPC.LookupInvoice(addInvoiceResp.RHash)
require.Equal(t.t, invoice.State, lnrpc.Invoice_OPEN)
require.Equal(t.t, lnrpc.Invoice_SETTLED, invoice.State)

// Close event notification streams.
err = aliceEventNtfns.CloseSend()
Expand Down
27 changes: 17 additions & 10 deletions rfq/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ func (m *Manager) handleIncomingMessage(incomingMsg rfqmsg.IncomingMsg) error {
// and compare it to the one in the invoice.
err := m.addScidAlias(
uint64(msg.ShortChannelId()),
*msg.Request.AssetID, msg.Peer,
msg.Request.AssetSpecifier, msg.Peer,
)
if err != nil {
m.handleError(
Expand Down Expand Up @@ -483,8 +483,8 @@ func (m *Manager) handleOutgoingMessage(outgoingMsg rfqmsg.OutgoingMsg) error {
// make sure we can identify the forwarded asset payment by the
// outgoing SCID alias within the onion packet.
err := m.addScidAlias(
uint64(msg.ShortChannelId()), *msg.Request.AssetID,
msg.Peer,
uint64(msg.ShortChannelId()),
msg.Request.AssetSpecifier, msg.Peer,
)
if err != nil {
return fmt.Errorf("error adding local alias: %w", err)
Expand Down Expand Up @@ -514,7 +514,7 @@ func (m *Manager) handleOutgoingMessage(outgoingMsg rfqmsg.OutgoingMsg) error {
}

// addScidAlias adds a SCID alias to the alias manager.
func (m *Manager) addScidAlias(scidAlias uint64, assetID asset.ID,
func (m *Manager) addScidAlias(scidAlias uint64, assetSpecifier asset.Specifier,
peer route.Vertex) error {

// Retrieve all local channels.
Expand All @@ -536,6 +536,12 @@ func (m *Manager) addScidAlias(scidAlias uint64, assetID asset.ID,

// Identify the correct channel to use as the base SCID for the alias
// by inspecting the asset data in the custom channel data.
assetID, err := assetSpecifier.UnwrapIdOrErr()
if err != nil {
return fmt.Errorf("asset ID must be specified when adding "+
"alias: %w", err)
}

var (
assetIDStr = assetID.String()
baseSCID uint64
Expand Down Expand Up @@ -733,14 +739,15 @@ type SellOrder struct {
// AssetGroupKey is the public key of the asset group to sell.
AssetGroupKey *btcec.PublicKey

// MaxAssetAmount is the maximum amount of the asset that can be sold as
// part of the order.
MaxAssetAmount uint64

// MinAsk is the minimum ask price that the seller is willing to accept.
MinAsk lnwire.MilliSatoshi
// PaymentMaxAmt is the maximum msat amount that the responding peer
// must agree to pay.
PaymentMaxAmt lnwire.MilliSatoshi

// Expiry is the unix timestamp at which the order expires.
//
// TODO(ffranr): This is the invoice expiry unix timestamp in seconds.
// We should make use of this field to ensure quotes are valid for the
// duration of the invoice.
Expiry uint64

// Peer is the peer that the buy order is intended for. This field is
Expand Down
Loading

0 comments on commit ab90081

Please sign in to comment.