From a6f49b5602c42b2ad4f4138ef57e50266a2d1eb0 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 13 Dec 2024 14:05:39 +0100 Subject: [PATCH 1/4] mod: bump tapd version --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6b64591b4..dc6b5adf9 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/lightninglabs/pool v0.6.5-beta.0.20241015105339-044cb451b5df github.com/lightninglabs/pool/auctioneerrpc v1.1.2 github.com/lightninglabs/pool/poolrpc v1.0.0 - github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241213120005-7358c1b0b42a + github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241216124532-967d866ecb7d github.com/lightningnetwork/lnd v0.18.4-beta.rc2.0.20241216115224-04767fe78c43 github.com/lightningnetwork/lnd/cert v1.2.2 github.com/lightningnetwork/lnd/fn v1.2.3 diff --git a/go.sum b/go.sum index d3455417d..0385397e8 100644 --- a/go.sum +++ b/go.sum @@ -1177,8 +1177,8 @@ github.com/lightninglabs/pool/poolrpc v1.0.0 h1:vvosrgNx9WXF4mcHGqLjZOW8wNM0q+BL github.com/lightninglabs/pool/poolrpc v1.0.0/go.mod h1:ZqpEpBFRMMBAerMmilEjh27tqauSXDwLaLR0O3jvmMA= github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display h1:w7FM5LH9Z6CpKxl13mS48idsu6F+cEZf0lkyiV+Dq9g= github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241213120005-7358c1b0b42a h1:h1ha0sK9/3Y+bSg1qD/bDAvMgvKDh5KCwyxddk3dmFM= -github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241213120005-7358c1b0b42a/go.mod h1:rkSWHSkPXX2k+PBOkEE1BA3L3qq5+Yv3m6LGkoH3tQk= +github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241216124532-967d866ecb7d h1:uYXiNlWw55+B0/iEz2e7k6p/Lu3eOkzxxETYkPsAq7g= +github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241216124532-967d866ecb7d/go.mod h1:rkSWHSkPXX2k+PBOkEE1BA3L3qq5+Yv3m6LGkoH3tQk= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI= github.com/lightningnetwork/lnd v0.18.4-beta.rc2.0.20241216115224-04767fe78c43 h1:Oqqfo54xCWlKGeA5+i2RXr4I+LKYoMl6KwYmoSs/uQE= From 153a55bcddf3badc4e9b79eacdf06ff1232474db Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 13 Dec 2024 13:40:44 +0100 Subject: [PATCH 2/4] cmd/litcli: add allow_overpay flag to payment cmds --- cmd/litcli/ln.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/cmd/litcli/ln.go b/cmd/litcli/ln.go index a1cfcca89..6f1aab9a3 100644 --- a/cmd/litcli/ln.go +++ b/cmd/litcli/ln.go @@ -182,6 +182,16 @@ var ( "set if there are multiple channels with the same " + "asset ID present", } + + allowOverpayFlag = cli.BoolFlag{ + Name: "allow_overpay", + Usage: "allow sending asset payments that are uneconomical " + + "because the required non-dust amount for an asset " + + "carrier HTLC plus one asset unit is higher than the " + + "total invoice/payment amount that arrives at the " + + "destination; meaning that the total amount sent " + + "exceeds the total amount received plus routing fees", + } ) // resultStreamWrapper is a wrapper around the SendPaymentClient stream that @@ -279,7 +289,7 @@ var sendPaymentCommand = cli.Command{ "--asset_amount=Y [--rfq_peer_pubkey=Z]", Flags: append( commands.SendPaymentCommand.Flags, assetIDFlag, assetAmountFlag, - rfqPeerPubKeyFlag, + rfqPeerPubKeyFlag, allowOverpayFlag, ), Action: sendPayment, } @@ -380,6 +390,7 @@ func sendPayment(ctx *cli.Context) error { rHash = hash[:] req.PaymentHash = rHash + allowOverpay := ctx.Bool(allowOverpayFlag.Name) return commands.SendPaymentRequest( ctx, req, lndConn, tapdConn, func(ctx context.Context, @@ -397,6 +408,7 @@ func sendPayment(ctx *cli.Context) error { AssetAmount: assetAmountToSend, PeerPubkey: rfqPeerKey, PaymentRequest: req, + AllowOverpay: allowOverpay, }, ) if err != nil { @@ -428,6 +440,7 @@ var payInvoiceCommand = cli.Command{ }, assetIDFlag, rfqPeerPubKeyFlag, + allowOverpayFlag, ), Action: payInvoice, } @@ -472,15 +485,13 @@ func payInvoice(ctx *cli.Context) error { return fmt.Errorf("unable to decode assetID: %v", err) } - var assetID asset.ID - copy(assetID[:], assetIDBytes) - rfqPeerKey, err := hex.DecodeString(ctx.String(rfqPeerPubKeyFlag.Name)) if err != nil { return fmt.Errorf("unable to decode RFQ peer public key: "+ "%w", err) } + allowOverpay := ctx.Bool(allowOverpayFlag.Name) req := &routerrpc.SendPaymentRequest{ PaymentRequest: commands.StripPrefix(payReq), } @@ -500,6 +511,7 @@ func payInvoice(ctx *cli.Context) error { AssetId: assetIDBytes, PeerPubkey: rfqPeerKey, PaymentRequest: req, + AllowOverpay: allowOverpay, }, ) if err != nil { From 4be9737ae1e213533c66af08ba5ed23baa3c33df Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 13 Dec 2024 13:41:03 +0100 Subject: [PATCH 3/4] cmd/litcli: change keysend HTLC carrier amount to 354 sats --- cmd/litcli/ln.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cmd/litcli/ln.go b/cmd/litcli/ln.go index 6f1aab9a3..67ef137e6 100644 --- a/cmd/litcli/ln.go +++ b/cmd/litcli/ln.go @@ -361,13 +361,12 @@ func sendPayment(ctx *cli.Context) error { "%w", err) } - // We use a constant amount of 500 to carry the asset HTLCs. In the - // future, we can use the double HTLC trick here, though it consumes - // more commitment space. - const htlcCarrierAmt = 500 + // Use the smallest possible non-dust HTLC amount to carry the asset + // HTLCs. In the future, we can use the double HTLC trick here, though + // it consumes more commitment space. req := &routerrpc.SendPaymentRequest{ Dest: destNode, - Amt: htlcCarrierAmt, + Amt: int64(rfqmath.DefaultOnChainHtlcSat), DestCustomRecords: make(map[uint64][]byte), } From addaa568eac3e1dd8fb17d488074a0e7fe47997a Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 13 Dec 2024 14:05:45 +0100 Subject: [PATCH 4/4] itest: fix assertions, add new test cases --- itest/assets_test.go | 74 ++++++++++++++++++++++++++---- itest/litd_custom_channels_test.go | 54 +++++++++++++++++++--- 2 files changed, 112 insertions(+), 16 deletions(-) diff --git a/itest/assets_test.go b/itest/assets_test.go index dd66c747f..b46c5f367 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -864,8 +864,8 @@ func payInvoiceWithSatoshi(t *testing.T, payer *HarnessNode, require.NoError(t, err) result, err := getPaymentResult(stream) - if cfg.expectTimeout { - require.ErrorContains(t, err, "context deadline exceeded") + if cfg.errSubStr != "" { + require.ErrorContains(t, err, cfg.errSubStr) } else { require.NoError(t, err) require.Equal(t, cfg.payStatus, result.Status) @@ -912,7 +912,9 @@ func payInvoiceWithSatoshiLastHop(t *testing.T, payer *HarnessNode, type payConfig struct { smallShards bool - expectTimeout bool + errSubStr string + allowOverpay bool + feeLimit lnwire.MilliSatoshi payStatus lnrpc.Payment_PaymentStatus failureReason lnrpc.PaymentFailureReason rfq fn.Option[rfqmsg.ID] @@ -921,7 +923,8 @@ type payConfig struct { func defaultPayConfig() *payConfig { return &payConfig{ smallShards: false, - expectTimeout: false, + errSubStr: "", + feeLimit: 1_000_000, payStatus: lnrpc.Payment_SUCCEEDED, failureReason: lnrpc.PaymentFailureReason_FAILURE_REASON_NONE, } @@ -935,9 +938,9 @@ func withSmallShards() payOpt { } } -func withExpectTimeout() payOpt { +func withPayErrSubStr(errSubStr string) payOpt { return func(c *payConfig) { - c.expectTimeout = true + c.errSubStr = errSubStr } } @@ -956,6 +959,18 @@ func withRFQ(rfqID rfqmsg.ID) payOpt { } } +func withFeeLimit(limit lnwire.MilliSatoshi) payOpt { + return func(c *payConfig) { + c.feeLimit = limit + } +} + +func withAllowOverpay() payOpt { + return func(c *payConfig) { + c.allowOverpay = true + } +} + func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode, payReq string, assetID []byte, opts ...payOpt) (uint64, rfqmath.BigIntFixedPoint) { @@ -979,7 +994,7 @@ func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode, sendReq := &routerrpc.SendPaymentRequest{ PaymentRequest: payReq, TimeoutSeconds: int32(PaymentTimeout.Seconds()), - FeeLimitMsat: 1_000_000, + FeeLimitMsat: int64(cfg.feeLimit), } if cfg.smallShards { @@ -997,9 +1012,20 @@ func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode, PeerPubkey: rfqPeer.PubKey[:], PaymentRequest: sendReq, RfqId: rfqBytes, + AllowOverpay: cfg.allowOverpay, }) require.NoError(t, err) + // If an error is returned by the RPC method (meaning the stream itself + // was established, no network or auth error), we expect the error to be + // returned on the first read on the stream. + if cfg.errSubStr != "" { + _, err := stream.Recv() + require.ErrorContains(t, err, cfg.errSubStr) + + return 0, rfqmath.BigIntFixedPoint{} + } + var ( numUnits uint64 rateVal rfqmath.FixedPoint[rfqmath.BigInt] @@ -1043,8 +1069,32 @@ func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode, return numUnits, rateVal } +type invoiceConfig struct { + errSubStr string +} + +func defaultInvoiceConfig() *invoiceConfig { + return &invoiceConfig{ + errSubStr: "", + } +} + +type invoiceOpt func(*invoiceConfig) + +func withInvoiceErrSubStr(errSubStr string) invoiceOpt { + return func(c *invoiceConfig) { + c.errSubStr = errSubStr + } +} + func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, - assetAmount uint64, assetID []byte) *lnrpc.AddInvoiceResponse { + assetAmount uint64, assetID []byte, + opts ...invoiceOpt) *lnrpc.AddInvoiceResponse { + + cfg := defaultInvoiceConfig() + for _, opt := range opts { + opt(cfg) + } ctxb := context.Background() ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) @@ -1068,7 +1118,13 @@ func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, Expiry: timeoutSeconds, }, }) - require.NoError(t, err) + if cfg.errSubStr != "" { + require.ErrorContains(t, err, cfg.errSubStr) + + return nil + } else { + require.NoError(t, err) + } decodedInvoice, err := dst.DecodePayReq(ctxt, &lnrpc.PayReqString{ PayReq: resp.InvoiceResult.PaymentRequest, diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go index bfb810017..398f66811 100644 --- a/itest/litd_custom_channels_test.go +++ b/itest/litd_custom_channels_test.go @@ -17,7 +17,6 @@ import ( "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/rfqmath" "github.com/lightninglabs/taproot-assets/rfqmsg" - "github.com/lightninglabs/taproot-assets/tapchannel" "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" @@ -1983,20 +1982,61 @@ func testCustomChannelsLiquidityEdgeCases(ctxb context.Context, // Yara with satoshi. This is a multi-hop payment going over 2 asset // channels, where the total asset value is less than the default anchor // amount of 354 sats. - invoiceResp = createAssetInvoice(t.t, dave, charlie, 1, assetID) - payInvoiceWithSatoshi(t.t, yara, invoiceResp, withFailure( - lnrpc.Payment_FAILED, failureNoRoute, + createAssetInvoice(t.t, dave, charlie, 1, assetID, withInvoiceErrSubStr( + "cannot create invoice over 1 asset units, as the minimal "+ + "transportable amount", )) logBalance(t.t, nodes, assetID, "after small payment (asset "+ "invoice, <354sats)") + // Edge case: We now create a small BTC invoice on Erin and ask Charlie + // to pay it with assets. We should get a payment failure as the amount + // is too small to be paid with assets economically. But a payment is + // still possible, since the amount is large enough to represent a + // single unit (17.1 sat per unit). + btcInvoiceResp, err := erin.AddInvoice(ctxb, &lnrpc.Invoice{ + Memo: "small BTC invoice", + ValueMsat: 18_000, + }) + require.NoError(t.t, err) + payInvoiceWithAssets( + t.t, charlie, dave, btcInvoiceResp.PaymentRequest, assetID, + withFeeLimit(2_000), withPayErrSubStr( + "rejecting payment of 20000 mSAT", + ), + ) + + // When we override the uneconomical payment, it should succeed. + payInvoiceWithAssets( + t.t, charlie, dave, btcInvoiceResp.PaymentRequest, assetID, + withFeeLimit(2_000), withAllowOverpay(), + ) + logBalance( + t.t, nodes, assetID, "after small payment (BTC invoice 1 sat)", + ) + + // When we try to pay an invoice amount that's smaller than the + // corresponding value of a single asset unit, the payment will always + // be rejected, even if we set the allow_uneconomical flag. + btcInvoiceResp, err = erin.AddInvoice(ctxb, &lnrpc.Invoice{ + Memo: "very small BTC invoice", + ValueMsat: 1_000, + }) + require.NoError(t.t, err) + payInvoiceWithAssets( + t.t, charlie, dave, btcInvoiceResp.PaymentRequest, assetID, + withFeeLimit(1_000), withAllowOverpay(), withPayErrSubStr( + "rejecting payment of 2000 mSAT", + ), + ) + // Edge case: Now Dave creates an asset invoice to be paid for by // Yara with satoshi. For the last hop we try to settle the invoice in // satoshi, where we will check whether Dave's strict forwarding works // as expected. Charlie is only used as a dummy RFQ peer in this case, // Yara totally ignored the RFQ hint and pays agnostically with sats. - invoiceResp = createAssetInvoice(t.t, charlie, dave, 1, assetID) + invoiceResp = createAssetInvoice(t.t, charlie, dave, 22, assetID) stream, err := dave.InvoicesClient.SubscribeSingleInvoice( ctxb, &invoicesrpc.SubscribeSingleInvoiceRequest{ @@ -2149,7 +2189,7 @@ func testCustomChannelsLiquidityEdgeCases(ctxb context.Context, // Now Erin tries to pay the invoice. Since rfq quote cannot satisfy the // total amount of the invoice this payment will fail. payInvoiceWithSatoshi( - t.t, erin, iResp, withExpectTimeout(), + t.t, erin, iResp, withPayErrSubStr("context deadline exceeded"), withFailure(lnrpc.Payment_FAILED, failureNone), ) @@ -2702,7 +2742,7 @@ func testCustomChannelsOraclePricing(_ context.Context, commitFeeP2WSH int64 = 2810 anchorAmount int64 = 330 assetHtlcCarryAmount = int64( - tapchannel.DefaultOnChainHtlcAmount, + rfqmath.DefaultOnChainHtlcSat, ) unbalancedLocalAmount = channelFundingAmount - commitFeeP2TR - anchorAmount