From ee0b04ec855135d16147d7078d0d9b66af83304a Mon Sep 17 00:00:00 2001 From: ZZiigguurraatt Date: Mon, 3 Mar 2025 10:12:47 -0500 Subject: [PATCH 1/5] litd_custom_channels_test.go: better commenting of test cases --- itest/litd_custom_channels_test.go | 85 ++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 15 deletions(-) diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go index 69d38dd75..e6d38018f 100644 --- a/itest/litd_custom_channels_test.go +++ b/itest/litd_custom_channels_test.go @@ -419,8 +419,11 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, logBalance(t.t, nodes, assetID, "initial") // ------------ - // Test case 1: Send a direct keysend payment from Charlie to Dave, - // sending the whole balance. + // Test case 1: Send a direct asset keysend payment from Charlie to Dave, + // sending the whole asset balance. + // + // Charlie --[assets]--> Dave + // // ------------ keySendAmount := charlieFundingAmount sendAssetKeySendPayment( @@ -466,7 +469,7 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, sendKeySendPayment(t.t, charlie, dave, 2000) logBalance(t.t, nodes, assetID, "after BTC only keysend") - // Let's keysend the rest of the balance back to Charlie. + // Let's keysend the rest of the asset balance back to Charlie. sendAssetKeySendPayment( t.t, dave, charlie, charlieFundingAmount-charlieInvoiceAmount, assetID, fn.None[int64](), @@ -477,9 +480,18 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, daveAssetBalance -= charlieFundingAmount - charlieInvoiceAmount // ------------ - // Test case 2: Pay a normal invoice from Dave by Charlie, making it - // a direct channel invoice payment with no RFQ SCID present in the - // invoice. + // Test case 2: Pay a normal sats invoice from Dave by + // Charlie using an asset, + // making it a direct channel invoice payment with no RFQ SCID present in + // the invoice (but an RFQ is used when trying to send the payment). In this + // case, Charlie gets to choose if he wants to pay Dave using assets or + // sats. In contrast, test case 3.5 we have the opposite scenario where + // an asset invoice is used and Charlie must pay with assets and not have + // a choice (and that case is supposed to fail because Charlie tries to + // pay with sats instead). + // + // Charlie --[assets]--> Dave + // // ------------ createAndPayNormalInvoice( t.t, charlie, dave, dave, 20_000, assetID, withSmallShards(), @@ -489,6 +501,7 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, // We should also be able to do a multi-hop BTC only payment, paying an // invoice from Erin by Charlie. + // Charlie --[assets]--> Dave --[sats]--> Erin createAndPayNormalInvoiceWithBtc(t.t, charlie, erin, 2000) logBalance(t.t, nodes, assetID, "after BTC only invoice") @@ -496,6 +509,9 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, // Test case 3: Pay an asset invoice from Dave by Charlie, making it // a direct channel invoice payment with an RFQ SCID present in the // invoice. + // + // Charlie --[assets]--> Dave + // // ------------ const daveInvoiceAssetAmount = 2_000 invoiceResp = createAssetInvoice( @@ -511,10 +527,20 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, daveAssetBalance += daveInvoiceAssetAmount // ------------ - // Test case 3.5: Pay an asset invoice from Dave by Charlie with normal + // Test case 3.5: Pay an asset invoice with an RFQ SCID present from Dave + // by Charlie with normal // satoshi payment flow. We expect that payment to fail, since it's a // direct channel payment and the invoice is for assets, not sats. So // without a conversion, it is rejected by the receiver. + // Normally, sats is the standard and we can always pay a taproot assets + // invoice with sats, but this special case where it is a direct channel + // payment, the fact that Dave requested specifically to receive a + // taproot asset from Charlie, that must be honored because Charlie did + // an RFQ with Dave when that invoice was created agreeing that when it + // was paid that Dave would receive taproot asset instead of sats. + // + // Charlie --[assets]--> Dave + // // ------------ invoiceResp = createAssetInvoice( t.t, charlie, dave, daveInvoiceAssetAmount, assetID, @@ -530,7 +556,11 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, // as the invoice payment failed. // ------------ - // Test case 4: Pay a normal invoice from Erin by Charlie. + // Test case 4: Pay a normal sats invoice from Erin by Charlie + // using an asset. + // + // Charlie --[assets]--> Dave --[sats]--> Erin + // // ------------ paidAssetAmount := createAndPayNormalInvoice( t.t, charlie, dave, erin, 20_000, assetID, withSmallShards(), @@ -542,7 +572,10 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, // ------------ // Test case 5: Create an asset invoice on Fabia and pay it from - // Charlie. + // Charlie using an asset. + // + // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia + // // ------------ const fabiaInvoiceAssetAmount1 = 1000 invoiceResp = createAssetInvoice( @@ -563,7 +596,13 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, // Test case 6: Create an asset invoice on Fabia and pay it with just // BTC from Dave, making sure it ends up being a multipart payment (we // set the maximum shard size to 80k sat and 15k asset units will be - // more than a single shard). + // more than a single shard). The purpose here is to force testing of multi + // part payments so that we can do so with a simple network instead of + // building a more complicated one that actually needs multi part paymemts + // in order to get the payment to successfully route. + // + // Dave --[sats]--> Erin --[assets]--> Fabia + // // ------------ const fabiaInvoiceAssetAmount2 = 15_000 invoiceResp = createAssetInvoice( @@ -579,7 +618,11 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, // Test case 7: Create an asset invoice on Fabia and pay it with assets // from Charlie, making sure it ends up being a multipart payment as // well, with the high amount of asset units to send and the hard coded - // 80k sat max shard size. + // 80k sat max shard size. Again, as in test case 6 above, we are doing + // this here to test multi part payments in a simpler way. + // + // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia + // // ------------ const fabiaInvoiceAssetAmount3 = 10_000 invoiceResp = createAssetInvoice( @@ -599,6 +642,15 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, // ------------ // Test case 8: An invoice payment over two channels that are both asset // channels. + // + // Charlie --[assets]--> Dave + // | + // | + // [assets] + // | + // v + // Yara + // // ------------ logBalance(t.t, nodes, assetID, "before asset-to-asset") @@ -616,8 +668,11 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, yaraAssetBalance += yaraInvoiceAssetAmount1 // ------------ - // Test case 8: Now we'll close each of the channels, starting with the + // Test case 9: Now we'll close each of the channels, starting with the // Charlie -> Dave custom channel. + // + // Charlie --[assets]--> Dave + // // ------------ t.Logf("Closing Charlie -> Dave channel") closeAssetChannelAndAssert( @@ -1044,7 +1099,7 @@ func testCustomChannelsGroupedAsset(ctx context.Context, net *NetworkHarness, yaraAssetBalance += yaraInvoiceAssetAmount1 // ------------ - // Test case 8: Now we'll close each of the channels, starting with the + // Test case 9: Now we'll close each of the channels, starting with the // Charlie -> Dave custom channel. // ------------ t.Logf("Closing Charlie -> Dave channel") @@ -2899,8 +2954,8 @@ func testCustomChannelsOraclePricing(ctx context.Context, net *NetworkHarness, const charlieInvoiceAmount = 104_081_638 require.EqualValues(t.t, charlieInvoiceAmount, numUnits) - // The default routing fees are 1ppm + 1msat per hop, and we have 2 - // hops in total. + // The default routing fees are 1ppm + 1msat per hop, and we have 3 + // hops in total, but only 1 hop where routing fees are collected in sats. charliePaidMSat := addRoutingFee(addRoutingFee(lnwire.MilliSatoshi( decodedInvoice.NumMsat, ))) From 901221a4a3705e6d2a923c81c1f35bc6190ab62d Mon Sep 17 00:00:00 2001 From: ZZiigguurraatt Date: Thu, 13 Mar 2025 10:55:35 -0400 Subject: [PATCH 2/5] fix errors in new comments, add links to related github issues --- itest/litd_custom_channels_test.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go index e6d38018f..061a6feee 100644 --- a/itest/litd_custom_channels_test.go +++ b/itest/litd_custom_channels_test.go @@ -483,11 +483,13 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, // Test case 2: Pay a normal sats invoice from Dave by // Charlie using an asset, // making it a direct channel invoice payment with no RFQ SCID present in - // the invoice (but an RFQ is used when trying to send the payment). In this - // case, Charlie gets to choose if he wants to pay Dave using assets or - // sats. In contrast, test case 3.5 we have the opposite scenario where + // the invoice. This case should fail because Charlie can't choose + // to send something that dave is not expecting. More details about this + // scenario are discussed in + // https://github.com/lightninglabs/taproot-assets/issues/1421#issuecomment-2707614141 + // In contrast, in test case 3.5 we have the opposite scenario where // an asset invoice is used and Charlie must pay with assets and not have - // a choice (and that case is supposed to fail because Charlie tries to + // a choice (and that case is also supposed to fail because Charlie tries to // pay with sats instead). // // Charlie --[assets]--> Dave @@ -538,6 +540,8 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, // taproot asset from Charlie, that must be honored because Charlie did // an RFQ with Dave when that invoice was created agreeing that when it // was paid that Dave would receive taproot asset instead of sats. + // More information about this scenario is discussed in + // https://github.com/lightninglabs/taproot-assets/issues/1430 // // Charlie --[assets]--> Dave // From cafb54cad3f8a52c0442e0e6c9883a9bc174fc5c Mon Sep 17 00:00:00 2001 From: ZZiigguurraatt Date: Thu, 13 Mar 2025 10:21:45 -0400 Subject: [PATCH 3/5] itest/litd_custom_channels_test.go: introduce the function `printExpectedRoute` --- itest/assets_test.go | 130 +++++++++++++++++++++++++++++ itest/litd_custom_channels_test.go | 43 ++++++++-- 2 files changed, 168 insertions(+), 5 deletions(-) diff --git a/itest/assets_test.go b/itest/assets_test.go index 49fe394cf..bf6ae3534 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -2009,6 +2009,136 @@ func assetExists(t *testing.T, client *tapClient, assetID []byte, "found in list, got: %v", amount, toProtoJSON(t, resp)) } +func getAssetFromAssetList(assets []rfqmsg.JsonAssetChanInfo, + assetID []byte) rfqmsg.JsonAssetChanInfo { + for _, asset := range assets { + if asset.AssetInfo.AssetGenesis.AssetID == hex.EncodeToString(assetID) { + return asset + } + } + return rfqmsg.JsonAssetChanInfo{} +} + +// use printChannels to print balances along a specified route. +// This function is better than logBalance because it shows only the channels +// that participate in the route so that you can distinguish between balances +// in channels that are with nodes that aren't participating in the route. +func printExpectedRoute(t *testing.T, route []*HarnessNode, satsToSend uint64, + assetsToSend uint64, assetID []byte, title string) { + expectedRouteNames := make([]string, 0) + for _, node := range route { + expectedRouteNames = append(expectedRouteNames, node.Cfg.Name) + } + t.Logf("actual channel capacities (" + title + "):") + t.Logf("expected route: %v", expectedRouteNames) + for i := 0; i < len(route)-1; i++ { + printChannels(t, route[i], route[i+1], satsToSend, assetsToSend, + assetID) + } +} + +// Print channel sat and asset (if defined) balances between two nodes taking +// into account the channel reserve. +// Check (shows (✔) or (x) )to see if satsToSend and assetsToSend are available +// to send/forward in the channel (if non-zero). +// Check to see if the channel is active (A) or inactive (I). +func printChannels(t *testing.T, node *HarnessNode, peer *HarnessNode, + satsToSend uint64, assetsToSend uint64, assetID []byte) { + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + channelResp, err := node.ListChannels(ctxt, &lnrpc.ListChannelsRequest{ + Peer: peer.PubKey[:], + }) + require.NoError(t, err) + + // var targetChan *lnrpc.Channel + // note: the only field of the response is `channels` + for _, channel := range channelResp.Channels { + + LocalSpendable := uint64(channel.LocalBalance) - + channel.LocalConstraints.ChanReserveSat + RemoteSpendable := uint64(channel.RemoteBalance) - + channel.RemoteConstraints.ChanReserveSat + + var state string + var satsBalanceStatus string + + // check to see if the channel should have the ability to actually + // make the sats payment + if satsToSend == 0 { + satsBalanceStatus = "" + } else if LocalSpendable >= satsToSend { + satsBalanceStatus = "(✔) " + } else { + satsBalanceStatus = "(x) " + } + + // check if the channel is active or inactive + // this helps troubleshooting channels that have gone inactive + // due to mission control putting them in an inactive state due + // to some previous error. + if channel.Active { + state = "A" + } else { + state = "I" + } + + // now actually print the sats channel details + t.Logf(satsBalanceStatus+"("+state+") cap: %v sat, "+ + node.Cfg.Name+"->[bal: %v sat|res: %v sat|spend: %v sat], "+ + peer.Cfg.Name+"->[bal: %v sat|res: %v sat|spend: %v sat], "+ + "commit_fee: %v", + channel.Capacity, + channel.LocalBalance, + channel.LocalConstraints.ChanReserveSat, + LocalSpendable, + channel.RemoteBalance, + channel.RemoteConstraints.ChanReserveSat, + RemoteSpendable, + channel.CommitFee, + ) + + // if this channel has data defining an asset in it, read it too + if len(channel.CustomChannelData) > 0 { + + var custom_channel_data rfqmsg.JsonAssetChannel + err = json.Unmarshal(channel.CustomChannelData, + &custom_channel_data) + require.NoError(t, err) + + asset := getAssetFromAssetList(custom_channel_data.Assets, assetID) + + var assetsBalanceStatus string + + // check to see if the channel should have the ability to actually + // send the asset payment + if assetsToSend == 0 { + assetsBalanceStatus = "" + } else if asset.LocalBalance >= assetsToSend { + assetsBalanceStatus = "(✔) " + } else { + assetsBalanceStatus = "(x) " + } + + // note: taproot assets channels don't currently have a concept of + // reserve like normal sats channels, so this printout is simpler + // than the sats channel printed above + t.Logf(assetsBalanceStatus+"("+state+") cap: %v " + + asset.AssetInfo.AssetGenesis.Name + + ", "+node.Cfg.Name+"->[bal: %v " + + asset.AssetInfo.AssetGenesis.Name+"], " + + peer.Cfg.Name+"->[bal: %v " + + asset.AssetInfo.AssetGenesis.Name+"]", + asset.Capacity, + asset.LocalBalance, + asset.RemoteBalance, + ) + } + } +} + func logBalance(t *testing.T, nodes []*HarnessNode, assetID []byte, occasion string) { diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go index 061a6feee..b5e4829d1 100644 --- a/itest/litd_custom_channels_test.go +++ b/itest/litd_custom_channels_test.go @@ -415,9 +415,6 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) - // Print initial channel balances. - logBalance(t.t, nodes, assetID, "initial") - // ------------ // Test case 1: Send a direct asset keysend payment from Charlie to Dave, // sending the whole asset balance. @@ -426,11 +423,19 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, // // ------------ keySendAmount := charlieFundingAmount + + // Print initial channel balances. + printExpectedRoute(t.t, []*HarnessNode{charlie, dave}, uint64(0), + keySendAmount, assetID, "initial") + sendAssetKeySendPayment( t.t, charlie, dave, charlieFundingAmount, assetID, fn.None[int64](), ) - logBalance(t.t, nodes, assetID, "after keysend") + + // Print channel balances after sending. + printExpectedRoute(t.t, []*HarnessNode{charlie, dave}, uint64(0), + uint64(0), assetID, "after keysend") charlieAssetBalance -= keySendAmount daveAssetBalance += keySendAmount @@ -442,11 +447,29 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, invoiceResp := createAssetInvoice( t.t, dave, charlie, charlieInvoiceAmount, assetID, ) + + + // Now that we have our payment request, we'll see how many assets are + // to be sent from dave's perspective + daveTapd := newTapClient(t.t, dave) + decodeResp, err := daveTapd.DecodeAssetPayReq( + ctx, &tapchannelrpc.AssetPayReq{ + AssetId: assetID, + PayReqString: invoiceResp.PaymentRequest, + }, + ) + + // Print initial channel balances. + printExpectedRoute(t.t, []*HarnessNode{dave, charlie}, uint64(0), + decodeResp.AssetAmount, assetID, "initial") + payInvoiceWithAssets( t.t, dave, charlie, invoiceResp.PaymentRequest, assetID, withSmallShards(), ) - logBalance(t.t, nodes, assetID, "after invoice back") + // Print channel balances after sending. + printExpectedRoute(t.t, []*HarnessNode{dave, charlie}, uint64(0), + uint64(0), assetID, "after invoice back") // Make sure the invoice on the receiver side and the payment on the // sender side show the individual HTLCs that arrived for it and that @@ -662,12 +685,22 @@ func testCustomChannels(ctx context.Context, net *NetworkHarness, invoiceResp = createAssetInvoice( t.t, dave, yara, yaraInvoiceAssetAmount1, assetID, ) + + // Print initial channel balances. + printExpectedRoute(t.t, []*HarnessNode{charlie, dave, yara}, uint64(0), + yaraInvoiceAssetAmount1, assetID, + "before asset-to-asset") + payInvoiceWithAssets( t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, withSmallShards(), ) logBalance(t.t, nodes, assetID, "after asset-to-asset") + // Print channel balances after sending. + printExpectedRoute(t.t, []*HarnessNode{charlie, dave, yara}, uint64(0), + uint64(0), assetID, "after asset-to-asset") + charlieAssetBalance -= yaraInvoiceAssetAmount1 yaraAssetBalance += yaraInvoiceAssetAmount1 From ac14dc0195ac03baa0952832af1627d037933212 Mon Sep 17 00:00:00 2001 From: ZZiigguurraatt Date: Fri, 21 Mar 2025 13:55:32 -0400 Subject: [PATCH 4/5] printChannels: don't print a negative spendable amount if the channel was funded by the peer and the balance is less than the reserve because not enough payments were received yet --- itest/assets_test.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/itest/assets_test.go b/itest/assets_test.go index bf6ae3534..3d9a1c6e6 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -2057,10 +2057,17 @@ func printChannels(t *testing.T, node *HarnessNode, peer *HarnessNode, // note: the only field of the response is `channels` for _, channel := range channelResp.Channels { - LocalSpendable := uint64(channel.LocalBalance) - - channel.LocalConstraints.ChanReserveSat - RemoteSpendable := uint64(channel.RemoteBalance) - - channel.RemoteConstraints.ChanReserveSat + LocalSpendable := channel.LocalBalance - + int64(channel.LocalConstraints.ChanReserveSat) + + RemoteSpendable := channel.RemoteBalance - + int64(channel.RemoteConstraints.ChanReserveSat) + + // Balance can be less than the reserve if funded by the peer and + // not enough payments received yet, so limit to 0 and don't let + // the Spendable value go negative. + if (LocalSpendable < 0) {LocalSpendable = 0} + if (RemoteSpendable < 0) {RemoteSpendable = 0} var state string var satsBalanceStatus string @@ -2069,7 +2076,7 @@ func printChannels(t *testing.T, node *HarnessNode, peer *HarnessNode, // make the sats payment if satsToSend == 0 { satsBalanceStatus = "" - } else if LocalSpendable >= satsToSend { + } else if LocalSpendable >= int64(satsToSend) { satsBalanceStatus = "(✔) " } else { satsBalanceStatus = "(x) " From a63aa363e70d356d065f7981e74817357594840e Mon Sep 17 00:00:00 2001 From: ZZiigguurraatt Date: Fri, 21 Mar 2025 13:58:40 -0400 Subject: [PATCH 5/5] printChannels: if no assetID defined, don't try to print asset balances and if assetID not found, give a log message. --- itest/assets_test.go | 62 +++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/itest/assets_test.go b/itest/assets_test.go index 3d9a1c6e6..b83f33d9f 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -2115,33 +2115,43 @@ func printChannels(t *testing.T, node *HarnessNode, peer *HarnessNode, &custom_channel_data) require.NoError(t, err) - asset := getAssetFromAssetList(custom_channel_data.Assets, assetID) - - var assetsBalanceStatus string - - // check to see if the channel should have the ability to actually - // send the asset payment - if assetsToSend == 0 { - assetsBalanceStatus = "" - } else if asset.LocalBalance >= assetsToSend { - assetsBalanceStatus = "(✔) " - } else { - assetsBalanceStatus = "(x) " + if len(assetID) > 0 { + + asset := getAssetFromAssetList(custom_channel_data.Assets, + assetID) + + if (asset != rfqmsg.JsonAssetChanInfo{}) { + + var assetsBalanceStatus string + + // check to see if the channel should have the ability + // to actually send the asset payment + if assetsToSend == 0 { + assetsBalanceStatus = "" + } else if asset.LocalBalance >= assetsToSend { + assetsBalanceStatus = "(✔) " + } else { + assetsBalanceStatus = "(x) " + } + + // note: taproot assets channels don't currently have + // a concept of reserve like normal sats channels, so + // this printout is simpler than the sats channel + // printed above + t.Logf(assetsBalanceStatus+"("+state+") cap: %v " + + asset.AssetInfo.AssetGenesis.Name + + ", "+node.Cfg.Name+"->[bal: %v " + + asset.AssetInfo.AssetGenesis.Name+"], " + + peer.Cfg.Name+"->[bal: %v " + + asset.AssetInfo.AssetGenesis.Name+"]", + asset.Capacity, + asset.LocalBalance, + asset.RemoteBalance, + ) + } else { + t.Logf("assetID %v not found", assetID) + } } - - // note: taproot assets channels don't currently have a concept of - // reserve like normal sats channels, so this printout is simpler - // than the sats channel printed above - t.Logf(assetsBalanceStatus+"("+state+") cap: %v " + - asset.AssetInfo.AssetGenesis.Name + - ", "+node.Cfg.Name+"->[bal: %v " + - asset.AssetInfo.AssetGenesis.Name+"], " + - peer.Cfg.Name+"->[bal: %v " + - asset.AssetInfo.AssetGenesis.Name+"]", - asset.Capacity, - asset.LocalBalance, - asset.RemoteBalance, - ) } } }