Skip to content

Commit 47f213d

Browse files
authored
Merge pull request #921 from lightninglabs/enforce-min-amount
[custom channels]: enforce minimum amounts
2 parents 76e65f1 + addaa56 commit 47f213d

File tree

5 files changed

+135
-28
lines changed

5 files changed

+135
-28
lines changed

cmd/litcli/ln.go

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,16 @@ var (
182182
"set if there are multiple channels with the same " +
183183
"asset ID present",
184184
}
185+
186+
allowOverpayFlag = cli.BoolFlag{
187+
Name: "allow_overpay",
188+
Usage: "allow sending asset payments that are uneconomical " +
189+
"because the required non-dust amount for an asset " +
190+
"carrier HTLC plus one asset unit is higher than the " +
191+
"total invoice/payment amount that arrives at the " +
192+
"destination; meaning that the total amount sent " +
193+
"exceeds the total amount received plus routing fees",
194+
}
185195
)
186196

187197
// resultStreamWrapper is a wrapper around the SendPaymentClient stream that
@@ -279,7 +289,7 @@ var sendPaymentCommand = cli.Command{
279289
"--asset_amount=Y [--rfq_peer_pubkey=Z]",
280290
Flags: append(
281291
commands.SendPaymentCommand.Flags, assetIDFlag, assetAmountFlag,
282-
rfqPeerPubKeyFlag,
292+
rfqPeerPubKeyFlag, allowOverpayFlag,
283293
),
284294
Action: sendPayment,
285295
}
@@ -351,13 +361,12 @@ func sendPayment(ctx *cli.Context) error {
351361
"%w", err)
352362
}
353363

354-
// We use a constant amount of 500 to carry the asset HTLCs. In the
355-
// future, we can use the double HTLC trick here, though it consumes
356-
// more commitment space.
357-
const htlcCarrierAmt = 500
364+
// Use the smallest possible non-dust HTLC amount to carry the asset
365+
// HTLCs. In the future, we can use the double HTLC trick here, though
366+
// it consumes more commitment space.
358367
req := &routerrpc.SendPaymentRequest{
359368
Dest: destNode,
360-
Amt: htlcCarrierAmt,
369+
Amt: int64(rfqmath.DefaultOnChainHtlcSat),
361370
DestCustomRecords: make(map[uint64][]byte),
362371
}
363372

@@ -380,6 +389,7 @@ func sendPayment(ctx *cli.Context) error {
380389
rHash = hash[:]
381390

382391
req.PaymentHash = rHash
392+
allowOverpay := ctx.Bool(allowOverpayFlag.Name)
383393

384394
return commands.SendPaymentRequest(
385395
ctx, req, lndConn, tapdConn, func(ctx context.Context,
@@ -397,6 +407,7 @@ func sendPayment(ctx *cli.Context) error {
397407
AssetAmount: assetAmountToSend,
398408
PeerPubkey: rfqPeerKey,
399409
PaymentRequest: req,
410+
AllowOverpay: allowOverpay,
400411
},
401412
)
402413
if err != nil {
@@ -428,6 +439,7 @@ var payInvoiceCommand = cli.Command{
428439
},
429440
assetIDFlag,
430441
rfqPeerPubKeyFlag,
442+
allowOverpayFlag,
431443
),
432444
Action: payInvoice,
433445
}
@@ -472,15 +484,13 @@ func payInvoice(ctx *cli.Context) error {
472484
return fmt.Errorf("unable to decode assetID: %v", err)
473485
}
474486

475-
var assetID asset.ID
476-
copy(assetID[:], assetIDBytes)
477-
478487
rfqPeerKey, err := hex.DecodeString(ctx.String(rfqPeerPubKeyFlag.Name))
479488
if err != nil {
480489
return fmt.Errorf("unable to decode RFQ peer public key: "+
481490
"%w", err)
482491
}
483492

493+
allowOverpay := ctx.Bool(allowOverpayFlag.Name)
484494
req := &routerrpc.SendPaymentRequest{
485495
PaymentRequest: commands.StripPrefix(payReq),
486496
}
@@ -500,6 +510,7 @@ func payInvoice(ctx *cli.Context) error {
500510
AssetId: assetIDBytes,
501511
PeerPubkey: rfqPeerKey,
502512
PaymentRequest: req,
513+
AllowOverpay: allowOverpay,
503514
},
504515
)
505516
if err != nil {

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ require (
2323
github.com/lightninglabs/pool v0.6.5-beta.0.20241015105339-044cb451b5df
2424
github.com/lightninglabs/pool/auctioneerrpc v1.1.2
2525
github.com/lightninglabs/pool/poolrpc v1.0.0
26-
github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241213120005-7358c1b0b42a
26+
github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241216124532-967d866ecb7d
2727
github.com/lightningnetwork/lnd v0.18.4-beta.rc2.0.20241216115224-04767fe78c43
2828
github.com/lightningnetwork/lnd/cert v1.2.2
2929
github.com/lightningnetwork/lnd/fn v1.2.3

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,8 +1177,8 @@ github.com/lightninglabs/pool/poolrpc v1.0.0 h1:vvosrgNx9WXF4mcHGqLjZOW8wNM0q+BL
11771177
github.com/lightninglabs/pool/poolrpc v1.0.0/go.mod h1:ZqpEpBFRMMBAerMmilEjh27tqauSXDwLaLR0O3jvmMA=
11781178
github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display h1:w7FM5LH9Z6CpKxl13mS48idsu6F+cEZf0lkyiV+Dq9g=
11791179
github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
1180-
github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241213120005-7358c1b0b42a h1:h1ha0sK9/3Y+bSg1qD/bDAvMgvKDh5KCwyxddk3dmFM=
1181-
github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241213120005-7358c1b0b42a/go.mod h1:rkSWHSkPXX2k+PBOkEE1BA3L3qq5+Yv3m6LGkoH3tQk=
1180+
github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241216124532-967d866ecb7d h1:uYXiNlWw55+B0/iEz2e7k6p/Lu3eOkzxxETYkPsAq7g=
1181+
github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241216124532-967d866ecb7d/go.mod h1:rkSWHSkPXX2k+PBOkEE1BA3L3qq5+Yv3m6LGkoH3tQk=
11821182
github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY=
11831183
github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI=
11841184
github.com/lightningnetwork/lnd v0.18.4-beta.rc2.0.20241216115224-04767fe78c43 h1:Oqqfo54xCWlKGeA5+i2RXr4I+LKYoMl6KwYmoSs/uQE=

itest/assets_test.go

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -864,8 +864,8 @@ func payInvoiceWithSatoshi(t *testing.T, payer *HarnessNode,
864864
require.NoError(t, err)
865865

866866
result, err := getPaymentResult(stream)
867-
if cfg.expectTimeout {
868-
require.ErrorContains(t, err, "context deadline exceeded")
867+
if cfg.errSubStr != "" {
868+
require.ErrorContains(t, err, cfg.errSubStr)
869869
} else {
870870
require.NoError(t, err)
871871
require.Equal(t, cfg.payStatus, result.Status)
@@ -912,7 +912,9 @@ func payInvoiceWithSatoshiLastHop(t *testing.T, payer *HarnessNode,
912912

913913
type payConfig struct {
914914
smallShards bool
915-
expectTimeout bool
915+
errSubStr string
916+
allowOverpay bool
917+
feeLimit lnwire.MilliSatoshi
916918
payStatus lnrpc.Payment_PaymentStatus
917919
failureReason lnrpc.PaymentFailureReason
918920
rfq fn.Option[rfqmsg.ID]
@@ -921,7 +923,8 @@ type payConfig struct {
921923
func defaultPayConfig() *payConfig {
922924
return &payConfig{
923925
smallShards: false,
924-
expectTimeout: false,
926+
errSubStr: "",
927+
feeLimit: 1_000_000,
925928
payStatus: lnrpc.Payment_SUCCEEDED,
926929
failureReason: lnrpc.PaymentFailureReason_FAILURE_REASON_NONE,
927930
}
@@ -935,9 +938,9 @@ func withSmallShards() payOpt {
935938
}
936939
}
937940

938-
func withExpectTimeout() payOpt {
941+
func withPayErrSubStr(errSubStr string) payOpt {
939942
return func(c *payConfig) {
940-
c.expectTimeout = true
943+
c.errSubStr = errSubStr
941944
}
942945
}
943946

@@ -956,6 +959,18 @@ func withRFQ(rfqID rfqmsg.ID) payOpt {
956959
}
957960
}
958961

962+
func withFeeLimit(limit lnwire.MilliSatoshi) payOpt {
963+
return func(c *payConfig) {
964+
c.feeLimit = limit
965+
}
966+
}
967+
968+
func withAllowOverpay() payOpt {
969+
return func(c *payConfig) {
970+
c.allowOverpay = true
971+
}
972+
}
973+
959974
func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode,
960975
payReq string, assetID []byte,
961976
opts ...payOpt) (uint64, rfqmath.BigIntFixedPoint) {
@@ -979,7 +994,7 @@ func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode,
979994
sendReq := &routerrpc.SendPaymentRequest{
980995
PaymentRequest: payReq,
981996
TimeoutSeconds: int32(PaymentTimeout.Seconds()),
982-
FeeLimitMsat: 1_000_000,
997+
FeeLimitMsat: int64(cfg.feeLimit),
983998
}
984999

9851000
if cfg.smallShards {
@@ -997,9 +1012,20 @@ func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode,
9971012
PeerPubkey: rfqPeer.PubKey[:],
9981013
PaymentRequest: sendReq,
9991014
RfqId: rfqBytes,
1015+
AllowOverpay: cfg.allowOverpay,
10001016
})
10011017
require.NoError(t, err)
10021018

1019+
// If an error is returned by the RPC method (meaning the stream itself
1020+
// was established, no network or auth error), we expect the error to be
1021+
// returned on the first read on the stream.
1022+
if cfg.errSubStr != "" {
1023+
_, err := stream.Recv()
1024+
require.ErrorContains(t, err, cfg.errSubStr)
1025+
1026+
return 0, rfqmath.BigIntFixedPoint{}
1027+
}
1028+
10031029
var (
10041030
numUnits uint64
10051031
rateVal rfqmath.FixedPoint[rfqmath.BigInt]
@@ -1043,8 +1069,32 @@ func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode,
10431069
return numUnits, rateVal
10441070
}
10451071

1072+
type invoiceConfig struct {
1073+
errSubStr string
1074+
}
1075+
1076+
func defaultInvoiceConfig() *invoiceConfig {
1077+
return &invoiceConfig{
1078+
errSubStr: "",
1079+
}
1080+
}
1081+
1082+
type invoiceOpt func(*invoiceConfig)
1083+
1084+
func withInvoiceErrSubStr(errSubStr string) invoiceOpt {
1085+
return func(c *invoiceConfig) {
1086+
c.errSubStr = errSubStr
1087+
}
1088+
}
1089+
10461090
func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode,
1047-
assetAmount uint64, assetID []byte) *lnrpc.AddInvoiceResponse {
1091+
assetAmount uint64, assetID []byte,
1092+
opts ...invoiceOpt) *lnrpc.AddInvoiceResponse {
1093+
1094+
cfg := defaultInvoiceConfig()
1095+
for _, opt := range opts {
1096+
opt(cfg)
1097+
}
10481098

10491099
ctxb := context.Background()
10501100
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
@@ -1068,7 +1118,13 @@ func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode,
10681118
Expiry: timeoutSeconds,
10691119
},
10701120
})
1071-
require.NoError(t, err)
1121+
if cfg.errSubStr != "" {
1122+
require.ErrorContains(t, err, cfg.errSubStr)
1123+
1124+
return nil
1125+
} else {
1126+
require.NoError(t, err)
1127+
}
10721128

10731129
decodedInvoice, err := dst.DecodePayReq(ctxt, &lnrpc.PayReqString{
10741130
PayReq: resp.InvoiceResult.PaymentRequest,

itest/litd_custom_channels_test.go

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"github.com/lightninglabs/taproot-assets/proof"
1818
"github.com/lightninglabs/taproot-assets/rfqmath"
1919
"github.com/lightninglabs/taproot-assets/rfqmsg"
20-
"github.com/lightninglabs/taproot-assets/tapchannel"
2120
"github.com/lightninglabs/taproot-assets/taprpc"
2221
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
2322
oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc"
@@ -1983,20 +1982,61 @@ func testCustomChannelsLiquidityEdgeCases(ctxb context.Context,
19831982
// Yara with satoshi. This is a multi-hop payment going over 2 asset
19841983
// channels, where the total asset value is less than the default anchor
19851984
// amount of 354 sats.
1986-
invoiceResp = createAssetInvoice(t.t, dave, charlie, 1, assetID)
1987-
payInvoiceWithSatoshi(t.t, yara, invoiceResp, withFailure(
1988-
lnrpc.Payment_FAILED, failureNoRoute,
1985+
createAssetInvoice(t.t, dave, charlie, 1, assetID, withInvoiceErrSubStr(
1986+
"cannot create invoice over 1 asset units, as the minimal "+
1987+
"transportable amount",
19891988
))
19901989

19911990
logBalance(t.t, nodes, assetID, "after small payment (asset "+
19921991
"invoice, <354sats)")
19931992

1993+
// Edge case: We now create a small BTC invoice on Erin and ask Charlie
1994+
// to pay it with assets. We should get a payment failure as the amount
1995+
// is too small to be paid with assets economically. But a payment is
1996+
// still possible, since the amount is large enough to represent a
1997+
// single unit (17.1 sat per unit).
1998+
btcInvoiceResp, err := erin.AddInvoice(ctxb, &lnrpc.Invoice{
1999+
Memo: "small BTC invoice",
2000+
ValueMsat: 18_000,
2001+
})
2002+
require.NoError(t.t, err)
2003+
payInvoiceWithAssets(
2004+
t.t, charlie, dave, btcInvoiceResp.PaymentRequest, assetID,
2005+
withFeeLimit(2_000), withPayErrSubStr(
2006+
"rejecting payment of 20000 mSAT",
2007+
),
2008+
)
2009+
2010+
// When we override the uneconomical payment, it should succeed.
2011+
payInvoiceWithAssets(
2012+
t.t, charlie, dave, btcInvoiceResp.PaymentRequest, assetID,
2013+
withFeeLimit(2_000), withAllowOverpay(),
2014+
)
2015+
logBalance(
2016+
t.t, nodes, assetID, "after small payment (BTC invoice 1 sat)",
2017+
)
2018+
2019+
// When we try to pay an invoice amount that's smaller than the
2020+
// corresponding value of a single asset unit, the payment will always
2021+
// be rejected, even if we set the allow_uneconomical flag.
2022+
btcInvoiceResp, err = erin.AddInvoice(ctxb, &lnrpc.Invoice{
2023+
Memo: "very small BTC invoice",
2024+
ValueMsat: 1_000,
2025+
})
2026+
require.NoError(t.t, err)
2027+
payInvoiceWithAssets(
2028+
t.t, charlie, dave, btcInvoiceResp.PaymentRequest, assetID,
2029+
withFeeLimit(1_000), withAllowOverpay(), withPayErrSubStr(
2030+
"rejecting payment of 2000 mSAT",
2031+
),
2032+
)
2033+
19942034
// Edge case: Now Dave creates an asset invoice to be paid for by
19952035
// Yara with satoshi. For the last hop we try to settle the invoice in
19962036
// satoshi, where we will check whether Dave's strict forwarding works
19972037
// as expected. Charlie is only used as a dummy RFQ peer in this case,
19982038
// Yara totally ignored the RFQ hint and pays agnostically with sats.
1999-
invoiceResp = createAssetInvoice(t.t, charlie, dave, 1, assetID)
2039+
invoiceResp = createAssetInvoice(t.t, charlie, dave, 22, assetID)
20002040

20012041
stream, err := dave.InvoicesClient.SubscribeSingleInvoice(
20022042
ctxb, &invoicesrpc.SubscribeSingleInvoiceRequest{
@@ -2149,7 +2189,7 @@ func testCustomChannelsLiquidityEdgeCases(ctxb context.Context,
21492189
// Now Erin tries to pay the invoice. Since rfq quote cannot satisfy the
21502190
// total amount of the invoice this payment will fail.
21512191
payInvoiceWithSatoshi(
2152-
t.t, erin, iResp, withExpectTimeout(),
2192+
t.t, erin, iResp, withPayErrSubStr("context deadline exceeded"),
21532193
withFailure(lnrpc.Payment_FAILED, failureNone),
21542194
)
21552195

@@ -2702,7 +2742,7 @@ func testCustomChannelsOraclePricing(_ context.Context,
27022742
commitFeeP2WSH int64 = 2810
27032743
anchorAmount int64 = 330
27042744
assetHtlcCarryAmount = int64(
2705-
tapchannel.DefaultOnChainHtlcAmount,
2745+
rfqmath.DefaultOnChainHtlcSat,
27062746
)
27072747
unbalancedLocalAmount = channelFundingAmount - commitFeeP2TR -
27082748
anchorAmount

0 commit comments

Comments
 (0)