Skip to content

Commit bf189cc

Browse files
committed
itest: add immediate post-funding force-close scenario
1 parent c03638d commit bf189cc

3 files changed

Lines changed: 180 additions & 8 deletions

File tree

itest/custom_channels/breach_test.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -195,21 +195,25 @@ func testCustomChannelsBreach(ctx context.Context,
195195
locateAssetTransfers(t.t, dave, *breachTxid)
196196

197197
// With the breach transaction mined, Charlie should now have a
198-
// transaction in the mempool sweeping *both* commitment outputs.
199-
// We use a generous timeout because Charlie needs to process the
200-
// block, detect the breach, and construct the justice transaction.
201-
charlieJusticeTxid, err := waitForNTxsInMempool(
202-
net.Miner, 1, time.Second*30,
198+
// transaction in the mempool sweeping commitment outputs.
199+
// We wait for at least one tx as other background txns can race in.
200+
charlieJusticeTxid, err := waitForAtLeastNTxsInMempool(
201+
net.Miner, 1, time.Second*60,
203202
)
204203
require.NoError(t.t, err)
205204

206205
t.Logf("Charlie justice txid: %v", charlieJusticeTxid)
207206

208207
// Next, we'll mine a block to confirm Charlie's justice transaction.
209-
mineBlocks(t, net, 1, 1)
208+
// Use the mined txid (not the mempool txid), to avoid RBF mismatch.
209+
justiceBlocks := mineBlocks(t, net, 1, 1)
210+
minedJusticeTxHash := justiceBlocks[0].Transactions[1].TxHash()
211+
212+
t.Logf("Charlie mined justice txid: %v", minedJusticeTxHash)
210213

211-
// Charlie should now have a transfer for his justice transaction.
212-
locateAssetTransfers(t.t, charlie, *charlieJusticeTxid[0])
214+
// Mine additional blocks so all breach-resolution transfers are fully
215+
// confirmed before asserting final balances.
216+
mineBlocks(t, net, 6, 0)
213217

214218
// Charlie's balance should now be the same as before the breach
215219
// attempt: the amount he minted at the very start.

itest/custom_channels/custom_channels_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ var testCases = []*ccTestCase{
3535
name: "force close",
3636
test: testCustomChannelsForceClose,
3737
},
38+
{
39+
name: "immediate close",
40+
test: testCustomChannelsImmediateClose,
41+
},
3842
{
3943
name: "group tranches force close",
4044
test: testCustomChannelsGroupTranchesForceClose,
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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

Comments
 (0)