|
| 1 | +//go:build itest |
| 2 | + |
| 3 | +package custom_channels |
| 4 | + |
| 5 | +import ( |
| 6 | + "context" |
| 7 | + "fmt" |
| 8 | + "slices" |
| 9 | + |
| 10 | + "github.com/lightninglabs/taproot-assets/itest" |
| 11 | + "github.com/lightninglabs/taproot-assets/proof" |
| 12 | + "github.com/lightninglabs/taproot-assets/taprpc" |
| 13 | + "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" |
| 14 | + tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" |
| 15 | + "github.com/lightninglabs/taproot-assets/tapscript" |
| 16 | + "github.com/lightningnetwork/lnd/lnrpc" |
| 17 | + "github.com/lightningnetwork/lnd/lntest/node" |
| 18 | + "github.com/lightningnetwork/lnd/lntest/port" |
| 19 | + "github.com/stretchr/testify/require" |
| 20 | +) |
| 21 | + |
| 22 | +// testCustomChannelsImmediateClose tests that an asset channel can be funded |
| 23 | +// and then force closed immediately after the funding transaction confirms, |
| 24 | +// without any in-channel asset movement. |
| 25 | +func testCustomChannelsImmediateClose(ctx context.Context, |
| 26 | + net *itest.IntegratedNetworkHarness, t *ccHarnessTest) { |
| 27 | + |
| 28 | + lndArgs := slices.Clone(lndArgsTemplate) |
| 29 | + tapdArgs := slices.Clone(tapdArgsTemplate) |
| 30 | + |
| 31 | + // We use Alice as the Universe proof courier and also as the funder, so |
| 32 | + // we pin her RPC listen port up front. |
| 33 | + alicePort := port.NextAvailablePort() |
| 34 | + tapdArgs = append(tapdArgs, fmt.Sprintf( |
| 35 | + "--proofcourieraddr=%s://%s", |
| 36 | + proof.UniverseRpcCourierType, |
| 37 | + fmt.Sprintf(node.ListenerFormat, alicePort), |
| 38 | + )) |
| 39 | + |
| 40 | + aliceLndArgs := slices.Clone(lndArgs) |
| 41 | + aliceLndArgs = append(aliceLndArgs, fmt.Sprintf( |
| 42 | + "--rpclisten=127.0.0.1:%d", alicePort, |
| 43 | + )) |
| 44 | + alice := net.NewNode("Alice", aliceLndArgs, tapdArgs) |
| 45 | + bob := net.NewNode("Bob", lndArgs, tapdArgs) |
| 46 | + charlie := net.NewNode("Charlie", lndArgs, tapdArgs) |
| 47 | + |
| 48 | + nodes := []*itest.IntegratedNode{alice, bob, charlie} |
| 49 | + connectAllNodes(t.t, net, nodes) |
| 50 | + fundAllNodes(t.t, net, nodes) |
| 51 | + |
| 52 | + mintedAssets := itest.MintAssetsConfirmBatch( |
| 53 | + t.t, net.Miner, asTapd(alice), |
| 54 | + []*mintrpc.MintAssetRequest{ |
| 55 | + { |
| 56 | + Asset: ccItestAsset, |
| 57 | + }, |
| 58 | + }, |
| 59 | + ) |
| 60 | + cents := mintedAssets[0] |
| 61 | + assetID := cents.AssetGenesis.AssetId |
| 62 | + |
| 63 | + t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) |
| 64 | + syncUniverses(t.t, alice, bob) |
| 65 | + syncUniverses(t.t, alice, charlie) |
| 66 | + |
| 67 | + t.Logf("Opening asset channel...") |
| 68 | + fundResp, err := asTapd(alice).FundChannel( |
| 69 | + ctx, &tchrpc.FundChannelRequest{ |
| 70 | + AssetAmount: fundingAmount, |
| 71 | + AssetId: assetID, |
| 72 | + PeerPubkey: bob.PubKey[:], |
| 73 | + FeeRateSatPerVbyte: 5, |
| 74 | + }, |
| 75 | + ) |
| 76 | + require.NoError(t.t, err) |
| 77 | + |
| 78 | + chanPoint := &lnrpc.ChannelPoint{ |
| 79 | + OutputIndex: uint32(fundResp.OutputIndex), |
| 80 | + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ |
| 81 | + FundingTxidStr: fundResp.Txid, |
| 82 | + }, |
| 83 | + } |
| 84 | + |
| 85 | + mineBlocks(t, net, 6, 1) |
| 86 | + |
| 87 | + fundingScriptTree := tapscript.NewChannelFundingScriptTree() |
| 88 | + fundingScriptKey := fundingScriptTree.TaprootKey |
| 89 | + assertUniverseProofExists( |
| 90 | + t.t, alice, assetID, nil, |
| 91 | + fundingScriptKey.SerializeCompressed(), |
| 92 | + fmt.Sprintf("%v:%v", fundResp.Txid, fundResp.OutputIndex), |
| 93 | + ) |
| 94 | + assertAssetChan( |
| 95 | + t.t, alice, bob, fundingAmount, []*taprpc.Asset{cents}, |
| 96 | + ) |
| 97 | + |
| 98 | + t.Logf("Force closing asset channel immediately after confirmation...") |
| 99 | + _, _, err = net.CloseChannel(alice, chanPoint, true) |
| 100 | + require.NoError(t.t, err) |
| 101 | + |
| 102 | + // The channel first enters waiting close until the commitment |
| 103 | + // transaction confirms. |
| 104 | + assertWaitingCloseChannelAssetData(t.t, alice, chanPoint) |
| 105 | + mineBlocks(t, net, 1, 1) |
| 106 | + |
| 107 | + // After confirmation, Alice should enter pending force close. Unlike the |
| 108 | + // cooperative close path, the local force close commitment transaction |
| 109 | + // itself is not immediately tracked as an asset transfer for Alice. |
| 110 | + assertPendingForceCloseChannelAssetData(t.t, alice, chanPoint) |
| 111 | + |
| 112 | + // With no remote asset balance, Alice eventually sweeps the local output |
| 113 | + // after the CSV delay and regains the full on-chain balance. |
| 114 | + mineBlocks(t, net, 4, 0) |
| 115 | + |
| 116 | + aliceSweepTxid, err := waitForNTxsInMempool( |
| 117 | + net.Miner, 1, ccShortTimeout, |
| 118 | + ) |
| 119 | + require.NoError(t.t, err) |
| 120 | + |
| 121 | + t.Logf("Alice sweep txid: %v", aliceSweepTxid) |
| 122 | + |
| 123 | + aliceSweepBlocks := mineBlocks(t, net, 1, 1) |
| 124 | + aliceSweepTxHash := aliceSweepBlocks[0].Transactions[1].TxHash() |
| 125 | + |
| 126 | + locateAssetTransfers(t.t, alice, aliceSweepTxHash) |
| 127 | + |
| 128 | + assertBalance( |
| 129 | + t.t, alice, ccItestAsset.Amount, itest.WithAssetID(assetID), |
| 130 | + itest.WithNumUtxos(2), |
| 131 | + ) |
| 132 | + |
| 133 | + // Finally, assert the swept asset can be spent onward to a third party. |
| 134 | + const assetSendAmount = 1000 |
| 135 | + charlieAddr, err := asTapd(charlie).NewAddr( |
| 136 | + ctx, &taprpc.NewAddrRequest{ |
| 137 | + Amt: assetSendAmount, |
| 138 | + AssetId: assetID, |
| 139 | + ProofCourierAddr: fmt.Sprintf( |
| 140 | + "%s://%s", proof.UniverseRpcCourierType, |
| 141 | + alice.RPCAddr(), |
| 142 | + ), |
| 143 | + }, |
| 144 | + ) |
| 145 | + require.NoError(t.t, err) |
| 146 | + |
| 147 | + itest.AssertAddrCreated(t.t, asTapd(charlie), cents, charlieAddr) |
| 148 | + _, err = asTapd(alice).SendAsset( |
| 149 | + ctx, &taprpc.SendAssetRequest{ |
| 150 | + TapAddrs: []string{charlieAddr.Encoded}, |
| 151 | + }, |
| 152 | + ) |
| 153 | + require.NoError(t.t, err) |
| 154 | + mineBlocks(t, net, 1, 1) |
| 155 | + itest.AssertNonInteractiveRecvComplete(t.t, asTapd(charlie), 1) |
| 156 | + |
| 157 | + assertBalance( |
| 158 | + t.t, alice, ccItestAsset.Amount-assetSendAmount, |
| 159 | + itest.WithAssetID(assetID), |
| 160 | + ) |
| 161 | + assertBalance( |
| 162 | + t.t, charlie, assetSendAmount, itest.WithAssetID(assetID), |
| 163 | + ) |
| 164 | +} |
0 commit comments