Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 20 additions & 9 deletions cmd/litcli/ln.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -351,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),
}

Expand All @@ -380,6 +389,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,
Expand All @@ -397,6 +407,7 @@ func sendPayment(ctx *cli.Context) error {
AssetAmount: assetAmountToSend,
PeerPubkey: rfqPeerKey,
PaymentRequest: req,
AllowOverpay: allowOverpay,
},
)
if err != nil {
Expand Down Expand Up @@ -428,6 +439,7 @@ var payInvoiceCommand = cli.Command{
},
assetIDFlag,
rfqPeerPubKeyFlag,
allowOverpayFlag,
),
Action: payInvoice,
}
Expand Down Expand Up @@ -472,15 +484,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),
}
Expand All @@ -500,6 +510,7 @@ func payInvoice(ctx *cli.Context) error {
AssetId: assetIDBytes,
PeerPubkey: rfqPeerKey,
PaymentRequest: req,
AllowOverpay: allowOverpay,
},
)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
74 changes: 65 additions & 9 deletions itest/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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]
Expand All @@ -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,
}
Expand All @@ -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
}
}

Expand All @@ -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) {
Expand All @@ -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 {
Expand All @@ -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]
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down
54 changes: 47 additions & 7 deletions itest/litd_custom_channels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
"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"
Expand Down Expand Up @@ -1983,20 +1982,61 @@
// 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{
Expand Down Expand Up @@ -2149,7 +2189,7 @@
// 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),
)

Expand Down Expand Up @@ -2662,7 +2702,7 @@
})
require.NoError(t.t, err)

// The invoice amount should come out as 100 * 1533.332.

Check failure on line 2705 in itest/litd_custom_channels_test.go

View workflow job for this annotation

GitHub Actions / check commits

undefined: tapchannel.DefaultOnChainHtlcAmount
require.EqualValues(t.t, 153_333_242, decodedInvoice.NumMsat)

numUnits, rate := payInvoiceWithAssets(
Expand Down Expand Up @@ -2702,7 +2742,7 @@
commitFeeP2WSH int64 = 2810
anchorAmount int64 = 330
assetHtlcCarryAmount = int64(
tapchannel.DefaultOnChainHtlcAmount,
rfqmath.DefaultOnChainHtlcSat,
)
unbalancedLocalAmount = channelFundingAmount - commitFeeP2TR -
anchorAmount
Expand Down
Loading