From 2f3f95aa407b63e07985395649421d0206bb94bd Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Thu, 25 Jun 2026 13:06:00 +0000 Subject: [PATCH 1/2] build: bump lnd for immediate close itest --- go.mod | 2 ++ go.sum | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index ea2192a33..4f0120d15 100644 --- a/go.mod +++ b/go.mod @@ -232,3 +232,5 @@ replace github.com/prometheus/common => github.com/prometheus/common v0.66.1 // Note this is a temporary replace and will be removed when taprpc is tagged. replace github.com/lightninglabs/taproot-assets/taprpc => ./taprpc + +replace github.com/lightningnetwork/lnd => github.com/GeorgeTsagk/lnd v0.0.0-20260716113926-b0a25b9831e7 diff --git a/go.sum b/go.sum index 8d102e4d5..a8ad213d1 100644 --- a/go.sum +++ b/go.sum @@ -603,6 +603,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GeorgeTsagk/lnd v0.0.0-20260716113926-b0a25b9831e7 h1:yKt+XlpQb3pqKyrP1ZomTmkZnrQxOq8medP1QDl9mqA= +github.com/GeorgeTsagk/lnd v0.0.0-20260716113926-b0a25b9831e7/go.mod h1:LWfQaNNXlZghUJqIIh+JsgfxiVtC+ecZkrIe5bm9o4U= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= @@ -1082,8 +1084,6 @@ github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display h1:w7FM5LH9 github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= github.com/lightningnetwork/lightning-onion v1.4.0 h1:qWE1icOH4AKXRcq1KCzt6P/TesqptgBTP++V7wowTc0= github.com/lightningnetwork/lightning-onion v1.4.0/go.mod h1:YDPkvVTVQ6FBBE6Yj93tDd7zA3iTSrryi9xq46i7bKE= -github.com/lightningnetwork/lnd v0.21.0-beta.rc2.0.20260630214209-40c64f9db30d h1:mDcB9mwuwJXRbDmxps6mBgGs318Iz5ZbOrt0ieEx0os= -github.com/lightningnetwork/lnd v0.21.0-beta.rc2.0.20260630214209-40c64f9db30d/go.mod h1:LWfQaNNXlZghUJqIIh+JsgfxiVtC+ecZkrIe5bm9o4U= github.com/lightningnetwork/lnd/actor v0.0.6 h1:Ge8N2wivARG+27qJBwTlB0vwsypStZYZy8vk4Zl38sU= github.com/lightningnetwork/lnd/actor v0.0.6/go.mod h1:YAsoniSbY/cAM9HTVNfZLvt7RI6swDxy6wzPspTcMZg= github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI= From 17b5586853bb43e7c0c601b2c3e092c4a9f5f92a Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Thu, 25 Jun 2026 13:07:24 +0000 Subject: [PATCH 2/2] itest: add immediate force close coverage --- itest/custom_channels/custom_channels_test.go | 4 + itest/custom_channels/immediate_close_test.go | 164 ++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 itest/custom_channels/immediate_close_test.go diff --git a/itest/custom_channels/custom_channels_test.go b/itest/custom_channels/custom_channels_test.go index 40d66ac15..5ad70df80 100644 --- a/itest/custom_channels/custom_channels_test.go +++ b/itest/custom_channels/custom_channels_test.go @@ -35,6 +35,10 @@ var testCases = []*ccTestCase{ name: "force close", test: testCustomChannelsForceClose, }, + { + name: "immediate close", + test: testCustomChannelsImmediateClose, + }, { name: "group tranches force close", test: testCustomChannelsGroupTranchesForceClose, diff --git a/itest/custom_channels/immediate_close_test.go b/itest/custom_channels/immediate_close_test.go new file mode 100644 index 000000000..cfa601698 --- /dev/null +++ b/itest/custom_channels/immediate_close_test.go @@ -0,0 +1,164 @@ +//go:build itest + +package custom_channels + +import ( + "context" + "fmt" + "slices" + + "github.com/lightninglabs/taproot-assets/itest" + "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/taprpc" + "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" + tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" + "github.com/lightninglabs/taproot-assets/tapscript" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lntest/node" + "github.com/lightningnetwork/lnd/lntest/port" + "github.com/stretchr/testify/require" +) + +// testCustomChannelsImmediateClose tests that an asset channel can be funded +// and then force closed immediately after the funding transaction confirms, +// without any in-channel asset movement. +func testCustomChannelsImmediateClose(ctx context.Context, + net *itest.IntegratedNetworkHarness, t *ccHarnessTest) { + + lndArgs := slices.Clone(lndArgsTemplate) + tapdArgs := slices.Clone(tapdArgsTemplate) + + // We use Alice as the Universe proof courier and also as the funder, so + // we pin her RPC listen port up front. + alicePort := port.NextAvailablePort() + tapdArgs = append(tapdArgs, fmt.Sprintf( + "--proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, + fmt.Sprintf(node.ListenerFormat, alicePort), + )) + + aliceLndArgs := slices.Clone(lndArgs) + aliceLndArgs = append(aliceLndArgs, fmt.Sprintf( + "--rpclisten=127.0.0.1:%d", alicePort, + )) + alice := net.NewNode("Alice", aliceLndArgs, tapdArgs) + bob := net.NewNode("Bob", lndArgs, tapdArgs) + charlie := net.NewNode("Charlie", lndArgs, tapdArgs) + + nodes := []*itest.IntegratedNode{alice, bob, charlie} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, net.Miner, asTapd(alice), + []*mintrpc.MintAssetRequest{ + { + Asset: ccItestAsset, + }, + }, + ) + cents := mintedAssets[0] + assetID := cents.AssetGenesis.AssetId + + t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) + syncUniverses(t.t, alice, bob) + syncUniverses(t.t, alice, charlie) + + t.Logf("Opening asset channel...") + fundResp, err := asTapd(alice).FundChannel( + ctx, &tchrpc.FundChannelRequest{ + AssetAmount: fundingAmount, + AssetId: assetID, + PeerPubkey: bob.PubKey[:], + FeeRateSatPerVbyte: 5, + }, + ) + require.NoError(t.t, err) + + chanPoint := &lnrpc.ChannelPoint{ + OutputIndex: uint32(fundResp.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: fundResp.Txid, + }, + } + + mineBlocks(t, net, 6, 1) + + fundingScriptTree := tapscript.NewChannelFundingScriptTree() + fundingScriptKey := fundingScriptTree.TaprootKey + assertUniverseProofExists( + t.t, alice, assetID, nil, + fundingScriptKey.SerializeCompressed(), + fmt.Sprintf("%v:%v", fundResp.Txid, fundResp.OutputIndex), + ) + assertAssetChan( + t.t, alice, bob, fundingAmount, []*taprpc.Asset{cents}, + ) + + t.Logf("Force closing asset channel immediately after confirmation...") + _, _, err = net.CloseChannel(alice, chanPoint, true) + require.NoError(t.t, err) + + // The channel first enters waiting close until the commitment + // transaction confirms. + assertWaitingCloseChannelAssetData(t.t, alice, chanPoint) + mineBlocks(t, net, 1, 1) + + // After confirmation, Alice should enter pending force close. Unlike the + // cooperative close path, the local force close commitment transaction + // itself is not immediately tracked as an asset transfer for Alice. + assertPendingForceCloseChannelAssetData(t.t, alice, chanPoint) + + // With no remote asset balance, Alice eventually sweeps the local output + // after the CSV delay and regains the full on-chain balance. + mineBlocks(t, net, 4, 0) + + aliceSweepTxid, err := waitForNTxsInMempool( + net.Miner, 1, ccShortTimeout, + ) + require.NoError(t.t, err) + + t.Logf("Alice sweep txid: %v", aliceSweepTxid) + + aliceSweepBlocks := mineBlocks(t, net, 1, 1) + aliceSweepTxHash := aliceSweepBlocks[0].Transactions[1].TxHash() + + locateAssetTransfers(t.t, alice, aliceSweepTxHash) + + assertBalance( + t.t, alice, ccItestAsset.Amount, itest.WithAssetID(assetID), + itest.WithNumUtxos(2), + ) + + // Finally, assert the swept asset can be spent onward to a third party. + const assetSendAmount = 1000 + charlieAddr, err := asTapd(charlie).NewAddr( + ctx, &taprpc.NewAddrRequest{ + Amt: assetSendAmount, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + alice.RPCAddr(), + ), + }, + ) + require.NoError(t.t, err) + + itest.AssertAddrCreated(t.t, asTapd(charlie), cents, charlieAddr) + _, err = asTapd(alice).SendAsset( + ctx, &taprpc.SendAssetRequest{ + TapAddrs: []string{charlieAddr.Encoded}, + }, + ) + require.NoError(t.t, err) + mineBlocks(t, net, 1, 1) + itest.AssertNonInteractiveRecvComplete(t.t, asTapd(charlie), 1) + + assertBalance( + t.t, alice, ccItestAsset.Amount-assetSendAmount, + itest.WithAssetID(assetID), + ) + assertBalance( + t.t, charlie, assetSendAmount, itest.WithAssetID(assetID), + ) +}