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

rfqmsg: request fields blip align #1157

Merged
merged 24 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
640ea7b
rfqmsg: reorder requestWireMsgData fields for clarity
ffranr Oct 22, 2024
a907cad
rfqmsg: re-name request msg field SuggestedAssetRate to InAssetRateHint
ffranr Oct 22, 2024
c12991c
rfqmsg: add request msg field OutAssetRateHint and use in SellRequest
ffranr Oct 22, 2024
8253285
rfqmsg: rename request field AssetMaxAmount to MaxInAsset
ffranr Oct 22, 2024
e6ba958
asset: introduce NewSpecifier constructor for asset.Specifier
ffranr Oct 23, 2024
2e78ef0
asset: add String method to asset.Specifier
ffranr Oct 23, 2024
d701056
asset: add Specifier.UnwrapToPtr method
ffranr Oct 23, 2024
49ff032
rfq+rfqmsg: add AssetSpecifier field to SellRequest
ffranr Oct 23, 2024
39d4024
rfqmath: add NewBigIntFromUint64 constructor
ffranr Oct 23, 2024
1e20488
rfqmsg: refactor unit test for easier extensibility
ffranr Oct 23, 2024
54200c1
rfqmsg: add Htlc.SumAssetBalance method
ffranr Oct 23, 2024
163a0b0
rfqmath: add FixedPoint.SetIntValue method
ffranr Oct 25, 2024
5440228
rfq: validate in-flight HTLC asset amounts against HTLC out msat
ffranr Oct 23, 2024
4ac78fb
rfq: improve AssetSalePolicy.CheckHtlcCompliance comments
ffranr Oct 25, 2024
26aa3bf
rfq: refactor AssetSalePolicy field names for clarity
ffranr Oct 25, 2024
a33a027
multi: rename field BuyRequest.AssetAmount to AssetMaxAmt
ffranr Oct 25, 2024
8f5fb5f
rfq+rfqmsg: add AssetSpecifier field to BuyRequest
ffranr Oct 26, 2024
f922c40
rfqmsg: add request fields MinInAsset and MinOutAsset
ffranr Oct 26, 2024
d755237
rfq+asset: carry asset specifier through to price oracle query
ffranr Oct 30, 2024
51f2389
rfq+priceoraclerpc: add payment_asset_max_amount field to oracle request
ffranr Oct 30, 2024
ecc8450
rfq: reformulate oracle `assetAmount` to `assetMaxAmt` with Option type
ffranr Oct 30, 2024
eae8f82
rfq: add field SellRequest.PaymentMaxAmt
ffranr Oct 31, 2024
9e8c663
multi: revise rfq.SellOrder fields
ffranr Oct 31, 2024
c5d356e
rfq: add PaymentMaxAmt check to AssetPurchasePolicy
ffranr Oct 31, 2024
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
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)",
ffranr marked this conversation as resolved.
Show resolved Hide resolved
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,
Copy link
Member

Choose a reason for hiding this comment

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

nit: could squash this and 2 previous commits to something like "asset: add helper methods"

// 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
Loading