@@ -2,6 +2,7 @@ package itest
22
33import (
44 "context"
5+ "time"
56
67 "github.com/lightninglabs/taproot-assets/asset"
78 "github.com/lightninglabs/taproot-assets/taprpc"
@@ -176,3 +177,217 @@ func testZeroValueAnchorSweep(t *harnessTest) {
176177 WithNumUtxos (0 ), WithNumAnchorUtxos (0 ),
177178 )
178179}
180+
181+ // testZeroValueAnchorAccumulation tests that zero-value anchor outputs
182+ // accumulate when sweeping is disabled, and are swept when the node
183+ // is restarted with sweeping enabled.
184+ func testZeroValueAnchorAccumulation (t * harnessTest ) {
185+ ctxb := context .Background ()
186+
187+ // Start Alice's node WITHOUT sweeping enabled.
188+ // Note: t.tapd is already started without sweeping by default.
189+
190+ // Create Bob's node with sweeping enabled for receives.
191+ bobLnd := t .lndHarness .NewNodeWithCoins ("Bob" , nil )
192+ bobTapd := setupTapdHarness (t .t , t , bobLnd , t .universeServer )
193+ defer func () {
194+ require .NoError (t .t , bobTapd .stop (! * noDelete ))
195+ }()
196+
197+ // First, mint some assets to create zero-value UTXOs with.
198+ rpcAssets1 := MintAssetsConfirmBatch (
199+ t .t , t .lndHarness .Miner ().Client , t .tapd ,
200+ []* mintrpc.MintAssetRequest {simpleAssets [0 ]},
201+ )
202+ genInfo1 := rpcAssets1 [0 ].AssetGenesis
203+ assetAmount := simpleAssets [0 ].Asset .Amount
204+
205+ // Test 1: Create a tombstone by sending ALL assets to Bob.
206+ bobAddr1 , err := bobTapd .NewAddr (ctxb , & taprpc.NewAddrRequest {
207+ AssetId : genInfo1 .AssetId ,
208+ Amt : assetAmount ,
209+ AssetVersion : rpcAssets1 [0 ].Version ,
210+ })
211+ require .NoError (t .t , err )
212+
213+ sendResp1 , _ := sendAssetsToAddr (t , t .tapd , bobAddr1 )
214+ ConfirmAndAssertOutboundTransfer (
215+ t .t , t .lndHarness .Miner ().Client , t .tapd , sendResp1 ,
216+ genInfo1 .AssetId ,
217+ []uint64 {0 , assetAmount }, 0 , 1 ,
218+ )
219+ AssertNonInteractiveRecvComplete (t .t , bobTapd , 1 )
220+
221+ // Alice should have 1 tombstone UTXO.
222+ AssertBalances (
223+ t .t , t .tapd , 0 , WithScriptKeyType (asset .ScriptKeyTombstone ),
224+ WithNumUtxos (1 ), WithNumAnchorUtxos (1 ),
225+ )
226+
227+ // Test 2: Create a burn UTXO by burning another asset fully.
228+ rpcAssets2 := MintAssetsConfirmBatch (
229+ t .t , t .lndHarness .Miner ().Client , t .tapd ,
230+ []* mintrpc.MintAssetRequest {simpleAssets [0 ]},
231+ )
232+ genInfo2 := rpcAssets2 [0 ].AssetGenesis
233+
234+ // Full burn the asset to create a zero-value burn UTXO.
235+ burnResp , err := t .tapd .BurnAsset (ctxb , & taprpc.BurnAssetRequest {
236+ Asset : & taprpc.BurnAssetRequest_AssetId {
237+ AssetId : genInfo2 .AssetId ,
238+ },
239+ AmountToBurn : assetAmount ,
240+ ConfirmationText : "assets will be destroyed" ,
241+ })
242+ require .NoError (t .t , err )
243+
244+ AssertAssetOutboundTransferWithOutputs (
245+ t .t , t .lndHarness .Miner ().Client , t .tapd , burnResp .BurnTransfer ,
246+ [][]byte {genInfo2 .AssetId },
247+ []uint64 {assetAmount }, 1 , 2 , 1 , true ,
248+ )
249+
250+ // Alice should now have 1 tombstone and 1 burn UTXO.
251+ AssertBalances (
252+ t .t , t .tapd , 0 , WithScriptKeyType (asset .ScriptKeyTombstone ),
253+ WithNumUtxos (1 ), WithNumAnchorUtxos (1 ),
254+ )
255+ AssertBalances (
256+ t .t , t .tapd , assetAmount ,
257+ WithScriptKeyType (asset .ScriptKeyBurn ),
258+ WithNumUtxos (1 ), WithNumAnchorUtxos (1 ),
259+ )
260+
261+ // Test 3: Create another tombstone with a different asset.
262+ rpcAssets3 := MintAssetsConfirmBatch (
263+ t .t , t .lndHarness .Miner ().Client , t .tapd ,
264+ []* mintrpc.MintAssetRequest {simpleAssets [0 ]},
265+ )
266+ genInfo3 := rpcAssets3 [0 ].AssetGenesis
267+
268+ bobAddr2 , err := bobTapd .NewAddr (ctxb , & taprpc.NewAddrRequest {
269+ AssetId : genInfo3 .AssetId ,
270+ Amt : assetAmount ,
271+ AssetVersion : rpcAssets3 [0 ].Version ,
272+ })
273+ require .NoError (t .t , err )
274+
275+ sendResp2 , _ := sendAssetsToAddr (t , t .tapd , bobAddr2 )
276+ ConfirmAndAssertOutboundTransfer (
277+ t .t , t .lndHarness .Miner ().Client , t .tapd , sendResp2 ,
278+ genInfo3 .AssetId , []uint64 {0 , assetAmount }, 2 , 3 ,
279+ )
280+ AssertNonInteractiveRecvComplete (t .t , bobTapd , 2 )
281+
282+ // Alice should now have 2 tombstones and 1 burn UTXO.
283+ AssertBalances (
284+ t .t , t .tapd , 0 , WithScriptKeyType (asset .ScriptKeyTombstone ),
285+ WithNumUtxos (2 ), WithNumAnchorUtxos (2 ),
286+ )
287+
288+ // Now restart Alice's node with sweeping enabled.
289+
290+ // Save the base directory to preserve the database.
291+ baseDir := t .tapd .cfg .BaseDir
292+ lndNode := t .tapd .cfg .LndNode
293+ require .NoError (t .t , t .tapd .stop (false ))
294+
295+ // Create a new tapd config with the same base directory to preserve
296+ // state.
297+ tapdCfg := tapdConfig {
298+ NetParams : harnessNetParams ,
299+ LndNode : lndNode ,
300+ BaseDir : baseDir ,
301+ }
302+
303+ // Create a new harness with sweeping enabled.
304+ aliceTapd , err := newTapdHarness (
305+ t .t , t , tapdCfg ,
306+ func (ho * harnessOpts ) {
307+ ho .sweepZeroValueAnchorUtxos = true
308+ ho .proofCourier = t .proofCourier
309+ },
310+ )
311+ require .NoError (t .t , err )
312+
313+ // Start the new harness.
314+ err = aliceTapd .start (false )
315+ require .NoError (t .t , err )
316+
317+ // Replace the test harness's tapd reference.
318+ oldTapd := t .tapd
319+ t .tapd = aliceTapd
320+ defer func () {
321+ // Stop the new tapd and restore original for cleanup.
322+ require .NoError (t .t , aliceTapd .stop (false ))
323+ t .tapd = oldTapd
324+ }()
325+
326+ // Wait for the node to fully sync after restart.
327+ time .Sleep (2 * time .Second )
328+
329+ // Verify that the zero-value UTXOs are still present after restart.
330+ //nolint:lll
331+ tombstoneUtxosAfterRestart , err := t .tapd .ListUtxos (ctxb , & taprpc.ListUtxosRequest {
332+ ScriptKeyType : & taprpc.ScriptKeyTypeQuery {
333+ Type : & taprpc.ScriptKeyTypeQuery_ExplicitType {
334+ ExplicitType : taprpc .
335+ ScriptKeyType_SCRIPT_KEY_TOMBSTONE ,
336+ },
337+ },
338+ })
339+ require .NoError (t .t , err )
340+ require .Len (t .t , tombstoneUtxosAfterRestart .ManagedUtxos , 2 )
341+
342+ //nolint:lll
343+ burnUtxosAfterRestart , err := t .tapd .ListUtxos (ctxb , & taprpc.ListUtxosRequest {
344+ ScriptKeyType : & taprpc.ScriptKeyTypeQuery {
345+ Type : & taprpc.ScriptKeyTypeQuery_ExplicitType {
346+ ExplicitType : taprpc .
347+ ScriptKeyType_SCRIPT_KEY_BURN ,
348+ },
349+ },
350+ })
351+ require .NoError (t .t , err )
352+ require .Len (t .t , burnUtxosAfterRestart .ManagedUtxos , 1 )
353+
354+ // Test 4: Mint and send a new asset. This should sweep all accumulated
355+ // zero-value UTXOs (2 tombstones + 1 burn).
356+ rpcAssets4 := MintAssetsConfirmBatch (
357+ t .t , t .lndHarness .Miner ().Client , t .tapd ,
358+ []* mintrpc.MintAssetRequest {simpleAssets [0 ]},
359+ )
360+ genInfo4 := rpcAssets4 [0 ].AssetGenesis
361+
362+ // Send partial amount to create a normal transfer that should sweep
363+ // all zero-value UTXOs.
364+ partialAmount := assetAmount / 2
365+ bobAddr3 , err := bobTapd .NewAddr (ctxb , & taprpc.NewAddrRequest {
366+ AssetId : genInfo4 .AssetId ,
367+ Amt : partialAmount ,
368+ AssetVersion : rpcAssets4 [0 ].Version ,
369+ })
370+ require .NoError (t .t , err )
371+
372+ sendResp3 , _ := sendAssetsToAddr (t , t .tapd , bobAddr3 )
373+
374+ // This transfer should have swept all 3 zero-value UTXOs as inputs.
375+ // The expected number of inputs is:
376+ // 1 (new asset) + 3 (swept zero-value).
377+ ConfirmAndAssertOutboundTransfer (
378+ t .t , t .lndHarness .Miner ().Client , t .tapd , sendResp3 ,
379+ genInfo4 .AssetId ,
380+ []uint64 {partialAmount , partialAmount }, 3 , 4 ,
381+ )
382+ AssertNonInteractiveRecvComplete (t .t , bobTapd , 3 )
383+
384+ // All zero-value UTXOs should have been swept.
385+ AssertBalances (
386+ t .t , t .tapd , 0 , WithScriptKeyType (asset .ScriptKeyTombstone ),
387+ WithNumUtxos (0 ), WithNumAnchorUtxos (0 ),
388+ )
389+ AssertBalances (
390+ t .t , t .tapd , 0 , WithScriptKeyType (asset .ScriptKeyBurn ),
391+ WithNumUtxos (0 ), WithNumAnchorUtxos (0 ),
392+ )
393+ }
0 commit comments