diff --git a/itest/assets_test.go b/itest/assets_test.go index 49ac0ca71..4c244cab9 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -983,6 +983,14 @@ func assertChannelSatBalance(t *testing.T, node *HarnessNode, func assertChannelAssetBalance(t *testing.T, node *HarnessNode, chanPoint *lnrpc.ChannelPoint, local, remote uint64) { + assertChannelAssetBalanceWithDelta( + t, node, chanPoint, local, remote, 1, + ) +} + +func assertChannelAssetBalanceWithDelta(t *testing.T, node *HarnessNode, + chanPoint *lnrpc.ChannelPoint, local, remote uint64, delta float64) { + targetChan := fetchChannel(t, node, chanPoint) var assetBalance rfqmsg.JsonAssetChannel @@ -991,8 +999,8 @@ func assertChannelAssetBalance(t *testing.T, node *HarnessNode, require.Len(t, assetBalance.FundingAssets, 1) - require.InDelta(t, local, assetBalance.LocalBalance, 1) - require.InDelta(t, remote, assetBalance.RemoteBalance, 1) + require.InDelta(t, local, assetBalance.LocalBalance, delta) + require.InDelta(t, remote, assetBalance.RemoteBalance, delta) } // addRoutingFee adds the default routing fee (1 part per million fee rate plus @@ -1152,10 +1160,12 @@ func payPayReqWithSatoshi(t *testing.T, payer *HarnessNode, payReq string, defer cancel() sendReq := &routerrpc.SendPaymentRequest{ - PaymentRequest: payReq, - TimeoutSeconds: int32(PaymentTimeout.Seconds()), - FeeLimitMsat: 1_000_000, - MaxParts: cfg.maxShards, + PaymentRequest: payReq, + TimeoutSeconds: int32(PaymentTimeout.Seconds()), + FeeLimitMsat: 1_000_000, + MaxParts: cfg.maxShards, + OutgoingChanIds: cfg.outgoingChanIDs, + AllowSelfPayment: cfg.allowSelfPayment, } if cfg.smallShards { @@ -1243,6 +1253,8 @@ type payConfig struct { failureReason lnrpc.PaymentFailureReason rfq fn.Option[rfqmsg.ID] groupKey []byte + outgoingChanIDs []uint64 + allowSelfPayment bool } func defaultPayConfig() *payConfig { @@ -1314,6 +1326,18 @@ func withGroupKey(groupKey []byte) payOpt { } } +func withOutgoingChanIDs(ids []uint64) payOpt { + return func(c *payConfig) { + c.outgoingChanIDs = ids + } +} + +func withAllowSelfPayment() payOpt { + return func(c *payConfig) { + c.allowSelfPayment = true + } +} + func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode, payReq string, assetID []byte, opts ...payOpt) (uint64, rfqmath.BigIntFixedPoint) { diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go index c51470136..f5829befb 100644 --- a/itest/litd_custom_channels_test.go +++ b/itest/litd_custom_channels_test.go @@ -7,6 +7,7 @@ import ( "math" "math/big" "slices" + "strconv" "time" "github.com/btcsuite/btcd/btcec/v2" @@ -4339,3 +4340,179 @@ func testCustomChannelsDecodeAssetInvoice(ctx context.Context, const expectedUnits = 100_000_000_000 require.Equal(t.t, int64(expectedUnits), int64(decodeResp.AssetAmount)) } + +// testCustomChannelsSelfPayment tests that circular self-payments can be made +// to re-balance between BTC and assets. +func testCustomChannelsSelfPayment(ctx context.Context, net *NetworkHarness, + t *harnessTest) { + + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplate) + + // We use Alice as the proof courier. But in order for Alice to also + // use itself, we need to define its port upfront. + alicePort := port.NextAvailablePort() + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, + fmt.Sprintf(node.ListenerFormat, alicePort), + )) + + // Next, we'll make Alice and Bob, who will be the main nodes under + // test. + alice, err := net.NewNodeWithPort( + t.t, "Alice", lndArgs, false, true, alicePort, litdArgs..., + ) + require.NoError(t.t, err) + bob, err := net.NewNode( + t.t, "Bob", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + // Now we'll connect all nodes, and also fund them with some coins. + nodes := []*HarnessNode{alice, bob} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + aliceTap := newTapClient(t.t, alice) + + // Next, we'll mint an asset for Alice, who will be the node that opens + // the channel outbound. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, aliceTap, + []*mintrpc.MintAssetRequest{ + { + Asset: itestAsset, + }, + }, + ) + cents := mintedAssets[0] + assetID := cents.AssetGenesis.AssetId + + t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) + syncUniverses(t.t, aliceTap, bob) + t.Logf("Universes synced between all nodes, distributing assets...") + + // With the assets created, and synced -- we'll now open the channel + // between Alice and Bob. + t.Logf("Opening asset channel...") + assetFundResp, err := aliceTap.FundChannel( + ctx, &tchrpc.FundChannelRequest{ + AssetAmount: fundingAmount, + AssetId: assetID, + PeerPubkey: bob.PubKey[:], + FeeRateSatPerVbyte: 5, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded asset channel between Alice and Bob: %v", assetFundResp) + + assetChanPoint := &lnrpc.ChannelPoint{ + OutputIndex: uint32(assetFundResp.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: assetFundResp.Txid, + }, + } + + // With the channel open, mine a block to confirm it. + mineBlocks(t, net, 6, 1) + + // Before we start sending out payments, let's make sure each node can + // see the other one in the graph and has all required features. + require.NoError(t.t, t.lndHarness.AssertNodeKnown(alice, bob)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(bob, alice)) + + t.Logf("Opening normal channel between Alice and Bob...") + satChanPoint := openChannelAndAssert( + t, net, alice, bob, lntest.OpenChannelParams{ + Amt: 10_000_000, + SatPerVByte: 5, + }, + ) + defer closeChannelAndAssert(t, net, alice, satChanPoint, false) + + satChan := fetchChannel(t.t, alice, satChanPoint) + satChanSCID := satChan.ChanId + + t.Logf("Alice pubkey: %x", alice.PubKey[:]) + t.Logf("Bob pubkey: %x", bob.PubKey[:]) + t.Logf("Outgoing channel SCID: %d", satChanSCID) + logBalance(t.t, nodes, assetID, "initial") + + t.Logf("Key sending 15k assets from Alice to Bob...") + const ( + assetKeySendAmount = 15_000 + numInvoicePayments = 10 + assetInvoiceAmount = 1_234 + btcKeySendAmount = 200_000 + btcReserveAmount = 2000 + btcHtlcCost = numInvoicePayments * 354 + ) + sendAssetKeySendPayment( + t.t, alice, bob, assetKeySendAmount, assetID, + fn.Some[int64](btcReserveAmount+btcHtlcCost), + ) + + // We also send 200k sats from Alice to Bob, to make sure the BTC + // channel has liquidity in both directions. + sendKeySendPayment(t.t, alice, bob, btcKeySendAmount) + logBalance(t.t, nodes, assetID, "after keysend") + + // We now do a series of small payments. They should all succeed and the + // balances should be updated accordingly. + aliceAssetBalance := uint64(fundingAmount - assetKeySendAmount) + bobAssetBalance := uint64(assetKeySendAmount) + for i := 0; i < numInvoicePayments; i++ { + // The BTC balance of Alice before we start the payment. We + // expect that to go down by at least the invoice amount. + btcBalanceAliceBefore := fetchChannel( + t.t, alice, satChanPoint, + ).LocalBalance + + invoiceResp := createAssetInvoice( + t.t, bob, alice, assetInvoiceAmount, assetID, + ) + payInvoiceWithSatoshi( + t.t, alice, invoiceResp, withOutgoingChanIDs( + []uint64{satChanSCID}, + ), withAllowSelfPayment(), + ) + + logBalance( + t.t, nodes, assetID, + "after paying invoice "+strconv.Itoa(i), + ) + + // The accumulated delta from the rounding of multiple sends. + // We basically allow the balance to be off by one unit for each + // payment. + delta := float64(i + 1) + + // We now expect the channel balance to have decreased in the + // BTC channel and increased in the assets channel. + assertChannelAssetBalanceWithDelta( + t.t, alice, assetChanPoint, + aliceAssetBalance+assetInvoiceAmount, + bobAssetBalance-assetInvoiceAmount, delta, + ) + aliceAssetBalance += assetInvoiceAmount + bobAssetBalance -= assetInvoiceAmount + + btcBalanceAliceAfter := fetchChannel( + t.t, alice, satChanPoint, + ).LocalBalance + + // The difference between the two balances should be at least + // the invoice amount. + decodedInvoice, err := alice.DecodePayReq( + context.Background(), &lnrpc.PayReqString{ + PayReq: invoiceResp.PaymentRequest, + }, + ) + require.NoError(t.t, err) + require.GreaterOrEqual( + t.t, btcBalanceAliceBefore-btcBalanceAliceAfter, + decodedInvoice.NumSatoshis, + ) + } +} diff --git a/itest/litd_test_list_on_test.go b/itest/litd_test_list_on_test.go index 41ccc8937..179b1d2ce 100644 --- a/itest/litd_test_list_on_test.go +++ b/itest/litd_test_list_on_test.go @@ -114,4 +114,8 @@ var allTestCases = []*testCase{ test: testCustomChannelsDecodeAssetInvoice, noAliceBob: true, }, + { + name: "test custom channels self-payment", + test: testCustomChannelsSelfPayment, + }, }