Skip to content

Commit 2d5ac44

Browse files
committed
itest: add test for zero-value utxo garbage collection
1 parent 490dc76 commit 2d5ac44

File tree

2 files changed

+369
-0
lines changed

2 files changed

+369
-0
lines changed

itest/test_list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ var allTestCases = []*testCase{
101101
name: "min relay fee bump",
102102
test: testMinRelayFeeBump,
103103
},
104+
{
105+
name: "zero value anchor sweep",
106+
test: testZeroValueAnchorSweep,
107+
},
104108
{
105109
name: "restart receiver check balance",
106110
test: testRestartReceiverCheckBalance,

itest/zero_value_anchor_test.go

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
package itest
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
taprootassets "github.com/lightninglabs/taproot-assets"
8+
"github.com/lightninglabs/taproot-assets/asset"
9+
"github.com/lightninglabs/taproot-assets/taprpc"
10+
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
// testZeroValueAnchorSweep tests that zero-value anchor outputs
15+
// are automatically swept when creating new on-chain transactions.
16+
func testZeroValueAnchorSweep(t *harnessTest) {
17+
ctxb := context.Background()
18+
19+
// First, mint some simple asset.
20+
rpcAssets := MintAssetsConfirmBatch(
21+
t.t, t.lndHarness.Miner().Client, t.tapd,
22+
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
23+
)
24+
genInfo := rpcAssets[0].AssetGenesis
25+
assetAmount := simpleAssets[0].Asset.Amount
26+
27+
// Create a second tapd node.
28+
bobLnd := t.lndHarness.NewNodeWithCoins("Bob", nil)
29+
secondTapd := setupTapdHarness(t.t, t, bobLnd, t.universeServer)
30+
defer func() {
31+
require.NoError(t.t, secondTapd.stop(!*noDelete))
32+
}()
33+
34+
bobAddr, err := secondTapd.NewAddr(ctxb, &taprpc.NewAddrRequest{
35+
AssetId: genInfo.AssetId,
36+
Amt: assetAmount,
37+
AssetVersion: rpcAssets[0].Version,
38+
})
39+
require.NoError(t.t, err)
40+
41+
// Send ALL assets to Bob, which should create a tombstone.
42+
sendResp, _ := sendAssetsToAddr(t, t.tapd, bobAddr)
43+
44+
ConfirmAndAssertOutboundTransfer(
45+
t.t, t.lndHarness.Miner().Client, t.tapd, sendResp,
46+
genInfo.AssetId,
47+
[]uint64{0, assetAmount}, 0, 1,
48+
)
49+
AssertNonInteractiveRecvComplete(t.t, secondTapd, 1)
50+
51+
// Alice should have 1 tombstone UTXO from the full-value send.
52+
AssertBalances(
53+
t.t, t.tapd, 0, WithScriptKeyType(asset.ScriptKeyTombstone),
54+
WithNumUtxos(1), WithNumAnchorUtxos(1),
55+
)
56+
57+
// Test 1: Send transaction sweeps tombstones.
58+
rpcAssets2 := MintAssetsConfirmBatch(
59+
t.t, t.lndHarness.Miner().Client, t.tapd,
60+
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
61+
)
62+
genInfo2 := rpcAssets2[0].AssetGenesis
63+
64+
// Send full amount of the new asset. This should sweep Alice's
65+
// first tombstone and create a new one.
66+
bobAddr2, err := secondTapd.NewAddr(ctxb, &taprpc.NewAddrRequest{
67+
AssetId: genInfo2.AssetId,
68+
Amt: assetAmount,
69+
AssetVersion: rpcAssets2[0].Version,
70+
})
71+
require.NoError(t.t, err)
72+
73+
sendResp2, _ := sendAssetsToAddr(t, t.tapd, bobAddr2)
74+
75+
ConfirmAndAssertOutboundTransfer(
76+
t.t, t.lndHarness.Miner().Client, t.tapd, sendResp2,
77+
genInfo2.AssetId,
78+
[]uint64{0, assetAmount}, 1, 2,
79+
)
80+
AssertNonInteractiveRecvComplete(t.t, secondTapd, 2)
81+
82+
// Check Alice's tombstone balance. The first tombstone should have been
83+
// swept (spent on-chain as an input), and a new one created. We now
84+
// have 1 tombstone UTXO (the new one from the second send).
85+
AssertBalances(
86+
t.t, t.tapd, 0, WithScriptKeyType(asset.ScriptKeyTombstone),
87+
WithNumUtxos(1), WithNumAnchorUtxos(1),
88+
)
89+
90+
// Get the new tombstone outpoint.
91+
utxosAfterSend, err := t.tapd.ListUtxos(ctxb, &taprpc.ListUtxosRequest{
92+
ScriptKeyType: &taprpc.ScriptKeyTypeQuery{
93+
Type: &taprpc.ScriptKeyTypeQuery_ExplicitType{
94+
ExplicitType: taprpc.
95+
ScriptKeyType_SCRIPT_KEY_TOMBSTONE,
96+
},
97+
},
98+
})
99+
require.NoError(t.t, err)
100+
require.Len(t.t, utxosAfterSend.ManagedUtxos, 1)
101+
102+
// Test 2: Burning transaction sweeps tombstones.
103+
rpcAssets3 := MintAssetsConfirmBatch(
104+
t.t, t.lndHarness.Miner().Client, t.tapd,
105+
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
106+
)
107+
genInfo3 := rpcAssets3[0].AssetGenesis
108+
109+
// Full burn the asset to create a zero-value burn UTXO
110+
// and sweep the second tombstone.
111+
burnResp, err := t.tapd.BurnAsset(ctxb, &taprpc.BurnAssetRequest{
112+
Asset: &taprpc.BurnAssetRequest_AssetId{
113+
AssetId: genInfo3.AssetId,
114+
},
115+
AmountToBurn: assetAmount,
116+
ConfirmationText: "assets will be destroyed",
117+
})
118+
require.NoError(t.t, err)
119+
120+
AssertAssetOutboundTransferWithOutputs(
121+
t.t, t.lndHarness.Miner().Client, t.tapd, burnResp.BurnTransfer,
122+
[][]byte{genInfo3.AssetId},
123+
[]uint64{assetAmount}, 2, 3, 1, true,
124+
)
125+
126+
// Alice should have 0 tombstones remaining and 1 burn UTXO.
127+
AssertBalances(
128+
t.t, t.tapd, 0, WithScriptKeyType(asset.ScriptKeyTombstone),
129+
WithNumUtxos(0), WithNumAnchorUtxos(0),
130+
)
131+
AssertBalances(
132+
t.t, t.tapd, assetAmount,
133+
WithScriptKeyType(asset.ScriptKeyBurn),
134+
WithNumUtxos(1), WithNumAnchorUtxos(1),
135+
)
136+
137+
// Get the burn UTXO outpoint for the next test.
138+
burnUtxos, err := t.tapd.ListUtxos(ctxb, &taprpc.ListUtxosRequest{
139+
ScriptKeyType: &taprpc.ScriptKeyTypeQuery{
140+
Type: &taprpc.ScriptKeyTypeQuery_ExplicitType{
141+
ExplicitType: taprpc.
142+
ScriptKeyType_SCRIPT_KEY_BURN,
143+
},
144+
},
145+
})
146+
require.NoError(t.t, err)
147+
require.Len(t.t, burnUtxos.ManagedUtxos, 1)
148+
149+
// Test 3: Send transactions sweeps zero-value burns.
150+
rpcAssets4 := MintAssetsConfirmBatch(
151+
t.t, t.lndHarness.Miner().Client, t.tapd,
152+
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
153+
)
154+
genInfo4 := rpcAssets4[0].AssetGenesis
155+
156+
// Send partial amount. This should NOT create a tombstone output
157+
// and sweep the burn UTXO.
158+
partialAmount := assetAmount / 2
159+
bobAddr3, err := secondTapd.NewAddr(ctxb, &taprpc.NewAddrRequest{
160+
AssetId: genInfo4.AssetId,
161+
Amt: partialAmount,
162+
AssetVersion: rpcAssets4[0].Version,
163+
})
164+
require.NoError(t.t, err)
165+
166+
sendResp3, _ := sendAssetsToAddr(t, t.tapd, bobAddr3)
167+
168+
ConfirmAndAssertOutboundTransfer(
169+
t.t, t.lndHarness.Miner().Client, t.tapd, sendResp3,
170+
genInfo4.AssetId,
171+
[]uint64{partialAmount, partialAmount}, 3, 4,
172+
)
173+
AssertNonInteractiveRecvComplete(t.t, secondTapd, 3)
174+
175+
// The burn UTXO should have been swept.
176+
AssertBalances(
177+
t.t, t.tapd, 0, WithScriptKeyType(asset.ScriptKeyBurn),
178+
WithNumUtxos(0), WithNumAnchorUtxos(0),
179+
)
180+
}
181+
182+
// testZeroValueAnchorAccumulation tests that zero-value anchor outputs
183+
// accumulate when sweeping is disabled, and are swept when the node
184+
// is restarted with sweeping enabled.
185+
func testZeroValueAnchorAccumulation(t *harnessTest) {
186+
ctxb := context.Background()
187+
188+
// Start Alice's node WITHOUT sweeping enabled.
189+
// Note: t.tapd is already started without sweeping by default.
190+
191+
// Create Bob's node with sweeping enabled for receives.
192+
bobLnd := t.lndHarness.NewNodeWithCoins("Bob", nil)
193+
bobTapd := setupTapdHarness(t.t, t, bobLnd, t.universeServer)
194+
defer func() {
195+
require.NoError(t.t, bobTapd.stop(!*noDelete))
196+
}()
197+
198+
// First, mint some assets to create zero-value UTXOs with.
199+
rpcAssets1 := MintAssetsConfirmBatch(
200+
t.t, t.lndHarness.Miner().Client, t.tapd,
201+
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
202+
)
203+
genInfo1 := rpcAssets1[0].AssetGenesis
204+
assetAmount := simpleAssets[0].Asset.Amount
205+
206+
// Test 1: Create a tombstone by sending ALL assets to Bob.
207+
bobAddr1, err := bobTapd.NewAddr(ctxb, &taprpc.NewAddrRequest{
208+
AssetId: genInfo1.AssetId,
209+
Amt: assetAmount,
210+
AssetVersion: rpcAssets1[0].Version,
211+
})
212+
require.NoError(t.t, err)
213+
214+
sendResp1, _ := sendAssetsToAddr(t, t.tapd, bobAddr1)
215+
ConfirmAndAssertOutboundTransfer(
216+
t.t, t.lndHarness.Miner().Client, t.tapd, sendResp1,
217+
genInfo1.AssetId,
218+
[]uint64{0, assetAmount}, 0, 1,
219+
)
220+
AssertNonInteractiveRecvComplete(t.t, bobTapd, 1)
221+
222+
// Alice should have 1 tombstone UTXO.
223+
AssertBalances(
224+
t.t, t.tapd, 0, WithScriptKeyType(asset.ScriptKeyTombstone),
225+
WithNumUtxos(1), WithNumAnchorUtxos(1),
226+
)
227+
228+
// Test 2: Create a burn UTXO by burning another asset fully.
229+
rpcAssets2 := MintAssetsConfirmBatch(
230+
t.t, t.lndHarness.Miner().Client, t.tapd,
231+
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
232+
)
233+
genInfo2 := rpcAssets2[0].AssetGenesis
234+
235+
// Full burn the asset to create a zero-value burn UTXO.
236+
burnResp, err := t.tapd.BurnAsset(ctxb, &taprpc.BurnAssetRequest{
237+
Asset: &taprpc.BurnAssetRequest_AssetId{
238+
AssetId: genInfo2.AssetId,
239+
},
240+
AmountToBurn: assetAmount,
241+
ConfirmationText: taprootassets.AssetBurnConfirmationText,
242+
})
243+
require.NoError(t.t, err)
244+
245+
AssertAssetOutboundTransferWithOutputs(
246+
t.t, t.lndHarness.Miner().Client, t.tapd, burnResp.BurnTransfer,
247+
[][]byte{genInfo2.AssetId},
248+
[]uint64{assetAmount}, 1, 2, 1, true,
249+
)
250+
251+
// Alice should now have 1 tombstone and 1 burn UTXO.
252+
AssertBalances(
253+
t.t, t.tapd, 0, WithScriptKeyType(asset.ScriptKeyTombstone),
254+
WithNumUtxos(1), WithNumAnchorUtxos(1),
255+
)
256+
AssertBalances(
257+
t.t, t.tapd, assetAmount,
258+
WithScriptKeyType(asset.ScriptKeyBurn),
259+
WithNumUtxos(1), WithNumAnchorUtxos(1),
260+
)
261+
262+
// Test 3: Create another tombstone with a different asset.
263+
rpcAssets3 := MintAssetsConfirmBatch(
264+
t.t, t.lndHarness.Miner().Client, t.tapd,
265+
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
266+
)
267+
genInfo3 := rpcAssets3[0].AssetGenesis
268+
269+
bobAddr2, err := bobTapd.NewAddr(ctxb, &taprpc.NewAddrRequest{
270+
AssetId: genInfo3.AssetId,
271+
Amt: assetAmount,
272+
AssetVersion: rpcAssets3[0].Version,
273+
})
274+
require.NoError(t.t, err)
275+
276+
sendResp2, _ := sendAssetsToAddr(t, t.tapd, bobAddr2)
277+
ConfirmAndAssertOutboundTransfer(
278+
t.t, t.lndHarness.Miner().Client, t.tapd, sendResp2,
279+
genInfo3.AssetId, []uint64{0, assetAmount}, 2, 3,
280+
)
281+
AssertNonInteractiveRecvComplete(t.t, bobTapd, 2)
282+
283+
// Alice should now have 2 tombstones and 1 burn UTXO.
284+
AssertBalances(
285+
t.t, t.tapd, 0, WithScriptKeyType(asset.ScriptKeyTombstone),
286+
WithNumUtxos(2), WithNumAnchorUtxos(2),
287+
)
288+
289+
// Now restart Alice's node with sweeping enabled.
290+
require.NoError(t.t, t.tapd.stop(false))
291+
292+
// Enable sweeping in the config.
293+
t.tapd.clientCfg.Wallet.SweepZeroValueAnchorUtxos = true
294+
295+
// Restart with the modified config.
296+
require.NoError(t.t, t.tapd.start(false))
297+
298+
// Wait for the node to fully sync after restart.
299+
time.Sleep(2 * time.Second)
300+
301+
// Verify that the zero-value UTXOs are still present after restart.
302+
//nolint:lll
303+
tombstoneUtxosAfterRestart, err := t.tapd.ListUtxos(ctxb, &taprpc.ListUtxosRequest{
304+
ScriptKeyType: &taprpc.ScriptKeyTypeQuery{
305+
Type: &taprpc.ScriptKeyTypeQuery_ExplicitType{
306+
ExplicitType: taprpc.
307+
ScriptKeyType_SCRIPT_KEY_TOMBSTONE,
308+
},
309+
},
310+
})
311+
require.NoError(t.t, err)
312+
require.Len(t.t, tombstoneUtxosAfterRestart.ManagedUtxos, 2)
313+
314+
//nolint:lll
315+
burnUtxosAfterRestart, err := t.tapd.ListUtxos(ctxb, &taprpc.ListUtxosRequest{
316+
ScriptKeyType: &taprpc.ScriptKeyTypeQuery{
317+
Type: &taprpc.ScriptKeyTypeQuery_ExplicitType{
318+
ExplicitType: taprpc.
319+
ScriptKeyType_SCRIPT_KEY_BURN,
320+
},
321+
},
322+
})
323+
require.NoError(t.t, err)
324+
require.Len(t.t, burnUtxosAfterRestart.ManagedUtxos, 1)
325+
326+
// Test 4: Mint and send a new asset. This should sweep all accumulated
327+
// zero-value UTXOs (2 tombstones + 1 burn).
328+
rpcAssets4 := MintAssetsConfirmBatch(
329+
t.t, t.lndHarness.Miner().Client, t.tapd,
330+
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
331+
)
332+
genInfo4 := rpcAssets4[0].AssetGenesis
333+
334+
// Send partial amount to create a normal transfer that should sweep
335+
// all zero-value UTXOs.
336+
partialAmount := assetAmount / 2
337+
bobAddr3, err := bobTapd.NewAddr(ctxb, &taprpc.NewAddrRequest{
338+
AssetId: genInfo4.AssetId,
339+
Amt: partialAmount,
340+
AssetVersion: rpcAssets4[0].Version,
341+
})
342+
require.NoError(t.t, err)
343+
344+
sendResp3, _ := sendAssetsToAddr(t, t.tapd, bobAddr3)
345+
346+
// This transfer should have swept all 3 zero-value UTXOs as inputs.
347+
// The expected number of inputs is:
348+
// 1 (new asset) + 3 (swept zero-value).
349+
ConfirmAndAssertOutboundTransfer(
350+
t.t, t.lndHarness.Miner().Client, t.tapd, sendResp3,
351+
genInfo4.AssetId,
352+
[]uint64{partialAmount, partialAmount}, 3, 4,
353+
)
354+
AssertNonInteractiveRecvComplete(t.t, bobTapd, 3)
355+
356+
// All zero-value UTXOs should have been swept.
357+
AssertBalances(
358+
t.t, t.tapd, 0, WithScriptKeyType(asset.ScriptKeyTombstone),
359+
WithNumUtxos(0), WithNumAnchorUtxos(0),
360+
)
361+
AssertBalances(
362+
t.t, t.tapd, 0, WithScriptKeyType(asset.ScriptKeyBurn),
363+
WithNumUtxos(0), WithNumAnchorUtxos(0),
364+
)
365+
}

0 commit comments

Comments
 (0)