diff --git a/go.mod b/go.mod index 9d373d303..842c82163 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/lightninglabs/pool v0.6.5-beta.0.20250305125211-4e860ec4e77f github.com/lightninglabs/pool/auctioneerrpc v1.1.3-0.20250305125211-4e860ec4e77f github.com/lightninglabs/pool/poolrpc v1.0.1-0.20250305125211-4e860ec4e77f - github.com/lightninglabs/taproot-assets v0.6.0-rc1.0.20250515090148-95af3680134e + github.com/lightninglabs/taproot-assets v0.6.0-rc1.0.20250515152718-48f9b0d008ab github.com/lightninglabs/taproot-assets/taprpc v1.0.3 github.com/lightningnetwork/lnd v0.19.0-beta.rc3 github.com/lightningnetwork/lnd/cert v1.2.2 diff --git a/go.sum b/go.sum index 2140225ac..036551c8c 100644 --- a/go.sum +++ b/go.sum @@ -506,8 +506,8 @@ github.com/lightninglabs/pool/poolrpc v1.0.1-0.20250305125211-4e860ec4e77f h1:5p github.com/lightninglabs/pool/poolrpc v1.0.1-0.20250305125211-4e860ec4e77f/go.mod h1:lGs2hSVZ+GFpdv3btaIl9icG5/gz7BBRfvmD2iqqNl0= 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.6.0-rc1.0.20250515090148-95af3680134e h1:l6UObl0hucKdrfdla/FC+UWON9v2VfrN3BTtHnUySo4= -github.com/lightninglabs/taproot-assets v0.6.0-rc1.0.20250515090148-95af3680134e/go.mod h1:OdeFcj2bnJf6aaYjBB5c8KdNI3aDaEMQpsSu2EqvMlw= +github.com/lightninglabs/taproot-assets v0.6.0-rc1.0.20250515152718-48f9b0d008ab h1:rZ1QgCYi9jY0CMn/qsFx1F7DWNhBcu16sdux53ewwpg= +github.com/lightninglabs/taproot-assets v0.6.0-rc1.0.20250515152718-48f9b0d008ab/go.mod h1:OdeFcj2bnJf6aaYjBB5c8KdNI3aDaEMQpsSu2EqvMlw= github.com/lightninglabs/taproot-assets/taprpc v1.0.3 h1:Vt9vKNwAFGfJ/I29C1gSEwD0pcNeI53pFRCPf/WBgHI= github.com/lightninglabs/taproot-assets/taprpc v1.0.3/go.mod h1:Ccq0t2GsXzOtC8qF0U1ux/yTF5HcBbVrhCb0tb/jObM= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY= diff --git a/itest/assets_test.go b/itest/assets_test.go index e6e065800..9800225a2 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -38,6 +38,7 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" + "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/rpc" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/lightningnetwork/lnd/lntypes" @@ -66,6 +67,260 @@ var ( failureNone = lnrpc.PaymentFailureReason_FAILURE_REASON_NONE ) +// itestNode is a wrapper around a lnd/tapd node. +type itestNode struct { + Lnd *HarnessNode + Tapd *tapClient +} + +// multiRfqNodes contains all the itest nodes that are required to set up the +// multi RFQ network topology. +type multiRfqNodes struct { + charlie, dave, erin, fabia, yara itestNode + universeTap *tapClient +} + +// createTestMultiRFQAssetNetwork creates a lightning network topology which +// consists of both bitcoin and asset channels. It focuses on the property of +// having multiple channels available on both the sender and receiver side. +// +// The topology we are going for looks like the following: +// +// /---[sats]--> Erin --[assets]--\ +// / \ +// / \ +// +// Charlie -----[sats]--> Dave --[assets]---->Fabia +// +// \ / +// \ / +// \---[sats]--> Yara --[assets]--/ +func createTestMultiRFQAssetNetwork(t *harnessTest, net *NetworkHarness, + nodes multiRfqNodes, mintedAsset *taprpc.Asset, assetSendAmount, + fundingAmount uint64, pushSat int64) (*lnrpc.ChannelPoint, + *lnrpc.ChannelPoint, *lnrpc.ChannelPoint) { + + charlie, charlieTap := nodes.charlie.Lnd, nodes.charlie.Tapd + dave, daveTap := nodes.dave.Lnd, nodes.dave.Tapd + erin, erinTap := nodes.erin.Lnd, nodes.erin.Tapd + _, fabiaTap := nodes.fabia.Lnd, nodes.fabia.Tapd + yara, yaraTap := nodes.yara.Lnd, nodes.yara.Tapd + universeTap := nodes.universeTap + + // Let's open the normal sats channels between Charlie and the routing + // peers. + _ = openChannelAndAssert( + t, net, charlie, erin, lntest.OpenChannelParams{ + Amt: 10_000_000, + SatPerVByte: 5, + }, + ) + + _ = openChannelAndAssert( + t, net, charlie, dave, lntest.OpenChannelParams{ + Amt: 10_000_000, + SatPerVByte: 5, + }, + ) + + _ = openChannelAndAssert( + t, net, charlie, yara, lntest.OpenChannelParams{ + Amt: 10_000_000, + SatPerVByte: 5, + }, + ) + + ctxb := context.Background() + assetID := mintedAsset.AssetGenesis.AssetId + var groupKey []byte + if mintedAsset.AssetGroup != nil { + groupKey = mintedAsset.AssetGroup.TweakedGroupKey + } + + fundingScriptTree := tapscript.NewChannelFundingScriptTree() + fundingScriptKey := fundingScriptTree.TaprootKey + fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() + + // We need to send some assets to Dave, so he can fund an asset channel + // with Fabia. + daveAddr, err := daveTap.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: assetSendAmount, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + charlieTap.node.Cfg.LitAddr(), + ), + }) + require.NoError(t.t, err) + + t.Logf("Sending %v asset units to Dave...", assetSendAmount) + + // Send the assets to Dave. + itest.AssertAddrCreated(t.t, daveTap, mintedAsset, daveAddr) + sendResp, err := charlieTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ + TapAddrs: []string{daveAddr.Encoded}, + }) + require.NoError(t.t, err) + itest.ConfirmAndAssertOutboundTransfer( + t.t, t.lndHarness.Miner.Client, charlieTap, sendResp, assetID, + []uint64{mintedAsset.Amount - assetSendAmount, assetSendAmount}, + 0, 1, + ) + itest.AssertNonInteractiveRecvComplete(t.t, daveTap, 1) + + // We need to send some assets to Erin, so he can fund an asset channel + // with Fabia. + erinAddr, err := erinTap.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: assetSendAmount, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + charlieTap.node.Cfg.LitAddr(), + ), + }) + require.NoError(t.t, err) + + t.Logf("Sending %v asset units to Erin...", assetSendAmount) + + // Send the assets to Erin. + itest.AssertAddrCreated(t.t, erinTap, mintedAsset, erinAddr) + sendResp, err = charlieTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ + TapAddrs: []string{erinAddr.Encoded}, + }) + require.NoError(t.t, err) + itest.ConfirmAndAssertOutboundTransfer( + t.t, t.lndHarness.Miner.Client, charlieTap, sendResp, assetID, + []uint64{ + mintedAsset.Amount - 2*assetSendAmount, assetSendAmount, + }, 1, 2, + ) + itest.AssertNonInteractiveRecvComplete(t.t, erinTap, 1) + + // We need to send some assets to Yara, so he can fund an asset channel + // with Fabia. + yaraAddr, err := yaraTap.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: assetSendAmount, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + charlieTap.node.Cfg.LitAddr(), + ), + }) + require.NoError(t.t, err) + + t.Logf("Sending %v asset units to Yara...", assetSendAmount) + + // Send the assets to Yara. + itest.AssertAddrCreated(t.t, yaraTap, mintedAsset, yaraAddr) + sendResp, err = charlieTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ + TapAddrs: []string{yaraAddr.Encoded}, + }) + require.NoError(t.t, err) + itest.ConfirmAndAssertOutboundTransfer( + t.t, t.lndHarness.Miner.Client, charlieTap, sendResp, assetID, + []uint64{ + mintedAsset.Amount - 3*assetSendAmount, assetSendAmount, + }, 2, 3, + ) + itest.AssertNonInteractiveRecvComplete(t.t, yaraTap, 1) + + // We fund the Dave->Fabia channel. + fundRespDF, err := daveTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: fundingAmount, + AssetId: assetID, + PeerPubkey: fabiaTap.node.PubKey[:], + FeeRateSatPerVbyte: 5, + PushSat: pushSat, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Dave and Fabia: %v", fundRespDF) + + // We fund the Erin->Fabia channel. + fundRespEF, err := erinTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: fundingAmount, + AssetId: assetID, + PeerPubkey: fabiaTap.node.PubKey[:], + FeeRateSatPerVbyte: 5, + PushSat: pushSat, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Erin and Fabia: %v", fundRespEF) + + // We fund the Yara->Fabia channel. + fundRespYF, err := yaraTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: fundingAmount, + AssetId: assetID, + PeerPubkey: fabiaTap.node.PubKey[:], + FeeRateSatPerVbyte: 5, + PushSat: pushSat, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Yara and Fabia: %v", fundRespYF) + + // Make sure the pending channel shows up in the list and has the + // custom records set as JSON. + assertPendingChannels( + t.t, daveTap.node, mintedAsset, 1, fundingAmount, 0, + ) + assertPendingChannels( + t.t, erinTap.node, mintedAsset, 1, fundingAmount, 0, + ) + assertPendingChannels( + t.t, yaraTap.node, mintedAsset, 1, fundingAmount, 0, + ) + + // Now that we've looked at the pending channels, let's actually confirm + // all three of them. + mineBlocks(t, net, 6, 3) + + // We'll be tracking the expected asset balances throughout the test, so + // we can assert it after each action. + charlieAssetBalance := mintedAsset.Amount - 3*assetSendAmount + daveAssetBalance := assetSendAmount - fundingAmount + erinAssetBalance := assetSendAmount - fundingAmount + yaraAssetBalance := assetSendAmount - fundingAmount + + itest.AssertBalances( + t.t, charlieTap, charlieAssetBalance, + itest.WithAssetID(assetID), itest.WithNumUtxos(1), + ) + + itest.AssertBalances( + t.t, daveTap, daveAssetBalance, itest.WithAssetID(assetID), + ) + + itest.AssertBalances( + t.t, erinTap, erinAssetBalance, itest.WithAssetID(assetID), + ) + + itest.AssertBalances( + t.t, yaraTap, yaraAssetBalance, itest.WithAssetID(assetID), + ) + + // Assert that the proofs for both channels has been uploaded to the + // designated Universe server. + assertUniverseProofExists( + t.t, universeTap, assetID, groupKey, fundingScriptTreeBytes, + fmt.Sprintf("%v:%v", fundRespDF.Txid, fundRespDF.OutputIndex), + ) + assertUniverseProofExists( + t.t, universeTap, assetID, groupKey, fundingScriptTreeBytes, + fmt.Sprintf("%v:%v", fundRespEF.Txid, fundRespEF.OutputIndex), + ) + assertUniverseProofExists( + t.t, universeTap, assetID, groupKey, fundingScriptTreeBytes, + fmt.Sprintf("%v:%v", fundRespYF.Txid, fundRespYF.OutputIndex), + ) + + return nil, nil, nil +} + // createTestAssetNetwork sends asset funds from Charlie to Dave and Erin, so // they can fund asset channels with Yara and Fabia, respectively. So the asset // channels created are Charlie->Dave, Dave->Yara, Erin->Fabia. The channels @@ -1110,7 +1365,7 @@ func sendKeySendPayment(t *testing.T, src, dst *HarnessNode, stream, err := src.RouterClient.SendPaymentV2(ctxt, req) require.NoError(t, err) - result, err := getFinalPaymentResult(stream) + result, err := getPaymentResult(stream, false) require.NoError(t, err) require.Equal(t, lnrpc.Payment_SUCCEEDED, result.Status) } @@ -1170,6 +1425,12 @@ func payPayReqWithSatoshi(t *testing.T, payer *HarnessNode, payReq string, ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) defer cancel() + shardSize := uint64(0) + + if cfg.smallShards { + shardSize = 80_000_000 + } + sendReq := &routerrpc.SendPaymentRequest{ PaymentRequest: payReq, TimeoutSeconds: int32(PaymentTimeout.Seconds()), @@ -1177,6 +1438,7 @@ func payPayReqWithSatoshi(t *testing.T, payer *HarnessNode, payReq string, MaxParts: cfg.maxShards, OutgoingChanIds: cfg.outgoingChanIDs, AllowSelfPayment: cfg.allowSelfPayment, + MaxShardSizeMsat: shardSize, } if cfg.smallShards { @@ -1186,17 +1448,9 @@ func payPayReqWithSatoshi(t *testing.T, payer *HarnessNode, payReq string, stream, err := payer.RouterClient.SendPaymentV2(ctxt, sendReq) require.NoError(t, err) - if cfg.payStatus == lnrpc.Payment_IN_FLIGHT { - t.Logf("Waiting for initial stream response...") - result, err := stream.Recv() - require.NoError(t, err) - - require.Equal(t, cfg.payStatus, result.Status) - - return - } - - result, err := getFinalPaymentResult(stream) + result, err := getPaymentResult( + stream, cfg.payStatus == lnrpc.Payment_IN_FLIGHT, + ) if cfg.errSubStr != "" { require.ErrorContains(t, err, cfg.errSubStr) } else { @@ -1506,16 +1760,21 @@ func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, timeoutSeconds := int64(rfq.DefaultInvoiceExpiry.Seconds()) - t.Logf("Asking peer %x for quote to buy assets to receive for "+ - "invoice over %d units; waiting up to %ds", - dstRfqPeer.PubKey[:], assetAmount, timeoutSeconds) + var peerPubKey []byte + if dstRfqPeer != nil { + peerPubKey = dstRfqPeer.PubKey[:] + + t.Logf("Asking peer %x for quote to buy assets to receive for "+ + "invoice over %d units; waiting up to %ds", + dstRfqPeer.PubKey[:], assetAmount, timeoutSeconds) + } dstTapd := newTapClient(t, dst) request := &tchrpc.AddInvoiceRequest{ GroupKey: cfg.groupKey, AssetAmount: assetAmount, - PeerPubkey: dstRfqPeer.PubKey[:], + PeerPubkey: peerPubKey, InvoiceRequest: &lnrpc.Invoice{ Memo: fmt.Sprintf("this is an asset invoice for "+ "%d units", assetAmount), @@ -1570,7 +1829,7 @@ func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, t.Logf("Got quote for %d mSats at %3f msat/unit from peer %x with "+ "SCID %d", decodedInvoice.NumMsat, mSatPerUnit, - dstRfqPeer.PubKey[:], resp.AcceptedBuyQuote.Scid) + resp.AcceptedBuyQuote.Peer, resp.AcceptedBuyQuote.Scid) return resp.InvoiceResult } @@ -1704,9 +1963,15 @@ func createAssetHodlInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, timeoutSeconds := int64(rfq.DefaultInvoiceExpiry.Seconds()) + var rfqPeer []byte + + if dstRfqPeer != nil { + rfqPeer = dstRfqPeer.PubKey[:] + } + t.Logf("Asking peer %x for quote to buy assets to receive for "+ "invoice for %d units; waiting up to %ds", - dstRfqPeer.PubKey[:], assetAmount, timeoutSeconds) + rfqPeer, assetAmount, timeoutSeconds) dstTapd := newTapClient(t, dst) @@ -1719,7 +1984,7 @@ func createAssetHodlInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, payHash := preimage.Hash() request := &tchrpc.AddInvoiceRequest{ AssetAmount: assetAmount, - PeerPubkey: dstRfqPeer.PubKey[:], + PeerPubkey: rfqPeer, InvoiceRequest: &lnrpc.Invoice{ Memo: fmt.Sprintf("this is an asset invoice for "+ "%d units", assetAmount), @@ -1757,7 +2022,7 @@ func createAssetHodlInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, require.EqualValues(t, uint64(numMSats), uint64(decodedInvoice.NumMsat)) t.Logf("Got quote for %d msat at %v msat/unit from peer %x with SCID "+ - "%d", decodedInvoice.NumMsat, mSatPerUnit, dstRfqPeer.PubKey[:], + "%d", decodedInvoice.NumMsat, mSatPerUnit, rfqPeer, resp.AcceptedBuyQuote.Scid) return assetHodlInvoice{ diff --git a/itest/litd_accounts_test.go b/itest/litd_accounts_test.go index f8729d977..918f2e9a1 100644 --- a/itest/litd_accounts_test.go +++ b/itest/litd_accounts_test.go @@ -416,13 +416,13 @@ func payNode(invoiceCtx, paymentCtx context.Context, t *harnessTest, stream, err := from.SendPaymentV2(paymentCtx, sendReq) require.NoError(t.t, err) - result, err := getFinalPaymentResult(stream) + result, err := getPaymentResult(stream, false) require.NoError(t.t, err) require.Equal(t.t, result.Status, lnrpc.Payment_SUCCEEDED) } -func getFinalPaymentResult(stream routerrpc.Router_SendPaymentV2Client) ( - *lnrpc.Payment, error) { +func getPaymentResult(stream routerrpc.Router_SendPaymentV2Client, + isHodl bool) (*lnrpc.Payment, error) { for { payment, err := stream.Recv() @@ -430,7 +430,14 @@ func getFinalPaymentResult(stream routerrpc.Router_SendPaymentV2Client) ( return nil, err } - if payment.Status != lnrpc.Payment_IN_FLIGHT { + // If this is a hodl payment, then we'll return the first + // expected response. Otherwise, we'll wait until the in flight + // clears to we can observe the other payment states. + switch { + case isHodl: + return payment, nil + + case payment.Status != lnrpc.Payment_IN_FLIGHT: return payment, nil } } diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go index c1fcbf024..13ba05fb5 100644 --- a/itest/litd_custom_channels_test.go +++ b/itest/litd_custom_channels_test.go @@ -2561,7 +2561,7 @@ func testCustomChannelsLiquidityEdgeCasesCore(ctx context.Context, // amount of 354 sats. createAssetInvoice( t.t, dave, charlie, 1, assetID, withInvoiceErrSubStr( - "1 asset units, as the minimal transportable amount", + "could not create any quotes for the invoice", ), withInvGroupKey(groupID), ) @@ -2892,6 +2892,144 @@ func testCustomChannelsLiquidityEdgeCasesGroup(ctx context.Context, testCustomChannelsLiquidityEdgeCasesCore(ctx, net, t, true) } +// testCustomChannelsMultiRFQReceive tests that a node creating an invoice with +// multiple RFQ quotes can actually guide the payer into using multiple private +// taproot asset channels to pay the invoice. +func testCustomChannelsMultiRFQReceive(ctx context.Context, net *NetworkHarness, + t *harnessTest) { + + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplate) + + charlie, err := net.NewNode( + t.t, "Charlie", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, charlie.Cfg.LitAddr(), + )) + + dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + fabia, err := net.NewNode( + t.t, "Fabia", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + yara, err := net.NewNode( + t.t, "Yara", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + // Let's create the tap clients. + charlieTap := newTapClient(t.t, charlie) + daveTap := newTapClient(t.t, dave) + erinTap := newTapClient(t.t, erin) + fabiaTap := newTapClient(t.t, fabia) + yaraTap := newTapClient(t.t, yara) + + assetReq := itest.CopyRequest(&mintrpc.MintAssetRequest{ + Asset: itestAsset, + }) + + assetReq.Asset.NewGroupedAsset = true + + // Mint an asset on Charlie and sync all nodes to Charlie as the + // universe. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, charlieTap, + []*mintrpc.MintAssetRequest{assetReq}, + ) + cents := mintedAssets[0] + assetID := cents.AssetGenesis.AssetId + groupID := cents.GetAssetGroup().GetTweakedGroupKey() + + syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) + + multiRfqNodes := multiRfqNodes{ + charlie: itestNode{ + Lnd: charlie, + Tapd: charlieTap, + }, + dave: itestNode{ + Lnd: dave, + Tapd: daveTap, + }, + erin: itestNode{ + Lnd: erin, + Tapd: erinTap, + }, + fabia: itestNode{ + Lnd: fabia, + Tapd: fabiaTap, + }, + yara: itestNode{ + Lnd: yara, + Tapd: yaraTap, + }, + universeTap: charlieTap, + } + + createTestMultiRFQAssetNetwork( + t, net, multiRfqNodes, cents, 10_000, 10_000, 10_000, + ) + + logBalance(t.t, nodes, assetID, "before multi-rfq receive") + + hodlInv := createAssetHodlInvoice(t.t, nil, fabia, 20_000, assetID) + + payInvoiceWithSatoshi( + t.t, charlie, &lnrpc.AddInvoiceResponse{ + PaymentRequest: hodlInv.payReq, + }, + withGroupKey(groupID), + withFailure(lnrpc.Payment_IN_FLIGHT, failureNone), + ) + + logBalance(t.t, nodes, assetID, "after inflight multi-rfq") + + // Assert that some HTLCs are present from Fabia's point of view. + assertMinNumHtlcs(t.t, fabia, 1) + + // Assert that Charlie also has at least one outgoing HTLC as a sanity + // check. + assertMinNumHtlcs(t.t, charlie, 1) + + // Now let's cancel the invoice and assert that all inbound channels + // have cleared their HTLCs. + payHash := hodlInv.preimage.Hash() + _, err = fabia.InvoicesClient.CancelInvoice( + ctx, &invoicesrpc.CancelInvoiceMsg{ + PaymentHash: payHash[:], + }, + ) + require.NoError(t.t, err) + + assertNumHtlcs(t.t, dave, 0) + assertNumHtlcs(t.t, erin, 0) + assertNumHtlcs(t.t, yara, 0) + + logBalance(t.t, nodes, assetID, "after cancelled hodl") + + // Now let's create a normal invoice that will be settled once all the + // HTLCs have been received. This is only possible because the payer + // uses multiple bolt11 hop hints to reach the destination. + invoiceResp := createAssetInvoice(t.t, nil, fabia, 15_000, assetID) + + payInvoiceWithSatoshi( + t.t, charlie, invoiceResp, withGroupKey(groupID), + ) + + logBalance(t.t, nodes, assetID, "after multi-rfq receive") +} + // testCustomChannelsStrictForwarding is a test that tests the strict forwarding // behavior of a node when it comes to paying asset invoices with assets and // BTC invoices with satoshis. diff --git a/itest/litd_test_list_on_test.go b/itest/litd_test_list_on_test.go index 8d0a074fb..c81385951 100644 --- a/itest/litd_test_list_on_test.go +++ b/itest/litd_test_list_on_test.go @@ -119,4 +119,9 @@ var allTestCases = []*testCase{ test: testCustomChannelsSelfPayment, noAliceBob: true, }, + { + name: "custom channels multi rfq", + test: testCustomChannelsMultiRFQReceive, + noAliceBob: true, + }, }