Skip to content

Commit f55c185

Browse files
committed
Merge branch 'itest/logdir' into feat/zero-value-utxo-selection
2 parents d4b078a + 5f3ef95 commit f55c185

21 files changed

+1375
-1132
lines changed

docs/release-notes/release-notes-0.7.0.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@
179179
configured with public read access. This matches the behavior of the
180180
existing FetchSupplyCommit RPC endpoint.
181181

182+
- The [`ListUtxos` RPC now returns a `Swept` field](https://github.com/lightninglabs/taproot-assets/pull/1832)
183+
indicating whether the output is spent.
184+
182185
## tapcli Additions
183186

184187
- [Rename](https://github.com/lightninglabs/taproot-assets/pull/1682) the mint
@@ -230,6 +233,9 @@
230233
- Enable [burning the full amount of an asset](https://github.com/lightninglabs/taproot-assets/pull/1791)
231234
when it is the sole one anchored to a Bitcoin UTXO.
232235

236+
- [Garbage collection of zero-value UTXOs](https://github.com/lightninglabs/taproot-assets/pull/1832)
237+
by sweeping tombstones and burn outputs when executing onchain transactions.
238+
233239
## RPC Updates
234240

235241
## tapcli Updates

itest/zero_value_anchor_test.go

Lines changed: 147 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,16 @@ import (
88
"github.com/stretchr/testify/require"
99
)
1010

11-
// testZeroValueAnchorSweep tests that zero-value anchor outputs (tombstones)
11+
// testZeroValueAnchorSweep tests that zero-value anchor outputs
1212
// are automatically swept when creating new on-chain transactions.
1313
func testZeroValueAnchorSweep(t *harnessTest) {
1414
ctxb := context.Background()
1515

16-
// First, mint some simple assets.
16+
// First, mint some simple asset.
1717
rpcAssets := MintAssetsConfirmBatch(
1818
t.t, t.lndHarness.Miner().Client, t.tapd,
1919
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
2020
)
21-
2221
genInfo := rpcAssets[0].AssetGenesis
2322
assetAmount := simpleAssets[0].Asset.Amount
2423

@@ -29,88 +28,194 @@ func testZeroValueAnchorSweep(t *harnessTest) {
2928
require.NoError(t.t, secondTapd.stop(!*noDelete))
3029
}()
3130

32-
// Create an address for Bob to receive ALL assets (full value send).
33-
// This should create a tombstone output on Alice's side.
3431
bobAddr, err := secondTapd.NewAddr(ctxb, &taprpc.NewAddrRequest{
3532
AssetId: genInfo.AssetId,
36-
Amt: assetAmount, // Send ALL
33+
Amt: assetAmount,
3734
AssetVersion: rpcAssets[0].Version,
3835
})
3936
require.NoError(t.t, err)
4037

41-
// Send ALL assets to Bob, which should create a tombstone output.
38+
// Send ALL assets to Bob, which should create a tombstone.
4239
sendResp, _ := sendAssetsToAddr(t, t.tapd, bobAddr)
4340

44-
// Confirm the send and wait for completion.
4541
ConfirmAndAssertOutboundTransfer(
4642
t.t, t.lndHarness.Miner().Client, t.tapd, sendResp,
4743
genInfo.AssetId,
4844
[]uint64{0, assetAmount}, 0, 1,
4945
)
5046
AssertNonInteractiveRecvComplete(t.t, secondTapd, 1)
5147

52-
// At this point, Alice should have a tombstone UTXO.
53-
// Check Alice's UTXOs to see if there are any without assets (tombstones).
54-
aliceUtxos, err := t.tapd.ListUtxos(ctxb, &taprpc.ListUtxosRequest{})
48+
// Check Alice's UTXOs for tombstone script keys.
49+
utxos, err := t.tapd.ListUtxos(ctxb, &taprpc.ListUtxosRequest{
50+
ScriptKeyType: &taprpc.ScriptKeyTypeQuery{
51+
Type: &taprpc.ScriptKeyTypeQuery_ExplicitType{
52+
ExplicitType: taprpc.
53+
ScriptKeyType_SCRIPT_KEY_TOMBSTONE,
54+
},
55+
},
56+
})
5557
require.NoError(t.t, err)
58+
require.Len(t.t, utxos.ManagedUtxos, 1)
5659

57-
tombstoneCount := 0
58-
for _, utxo := range aliceUtxos.ManagedUtxos {
59-
if len(utxo.Assets) == 0 {
60-
tombstoneCount++
61-
t.t.Logf("Found tombstone UTXO: %s, value=%d",
62-
utxo.OutPoint, utxo.AmtSat)
60+
tombstoneOp := ""
61+
for outpoint, utxo := range utxos.ManagedUtxos {
62+
if !utxo.Swept {
63+
tombstoneOp = outpoint
64+
break
6365
}
6466
}
6567

66-
t.t.Logf("Alice has %d tombstone UTXOs before sweep", tombstoneCount)
67-
require.Greater(t.t, tombstoneCount, 0, "Should have at least one tombstone UTXO")
68-
69-
// Now mint more assets for Alice so she can create another transaction
70-
// that should sweep the tombstones.
68+
// Test 1: Send transaction sweeps tombstones.
7169
rpcAssets2 := MintAssetsConfirmBatch(
7270
t.t, t.lndHarness.Miner().Client, t.tapd,
73-
[]*mintrpc.MintAssetRequest{simpleAssets[1]},
71+
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
7472
)
75-
7673
genInfo2 := rpcAssets2[0].AssetGenesis
77-
assetAmount2 := simpleAssets[1].Asset.Amount
7874

79-
// Create another address for Bob.
75+
// Send full amount of the new asset. This should sweep Alice's
76+
// first tombstone and create a new one.
8077
bobAddr2, err := secondTapd.NewAddr(ctxb, &taprpc.NewAddrRequest{
8178
AssetId: genInfo2.AssetId,
82-
Amt: assetAmount2,
79+
Amt: assetAmount,
8380
AssetVersion: rpcAssets2[0].Version,
8481
})
8582
require.NoError(t.t, err)
8683

87-
// Send the new assets. This should sweep Alice's tombstone UTXOs.
8884
sendResp2, _ := sendAssetsToAddr(t, t.tapd, bobAddr2)
8985

90-
// Confirm the send.
9186
ConfirmAndAssertOutboundTransfer(
9287
t.t, t.lndHarness.Miner().Client, t.tapd, sendResp2,
9388
genInfo2.AssetId,
94-
[]uint64{0, assetAmount2}, 1, 2,
89+
[]uint64{0, assetAmount}, 1, 2,
9590
)
9691
AssertNonInteractiveRecvComplete(t.t, secondTapd, 2)
9792

98-
// Check Alice's UTXOs again. The tombstones should have been swept.
99-
finalAliceUtxos, err := t.tapd.ListUtxos(ctxb, &taprpc.ListUtxosRequest{})
93+
// Check Alice's UTXOs again. The tombstone should have been swept.
94+
utxosAfterSend, err := t.tapd.ListUtxos(ctxb, &taprpc.ListUtxosRequest{
95+
ScriptKeyType: &taprpc.ScriptKeyTypeQuery{
96+
Type: &taprpc.ScriptKeyTypeQuery_ExplicitType{
97+
ExplicitType: taprpc.
98+
ScriptKeyType_SCRIPT_KEY_TOMBSTONE,
99+
},
100+
},
101+
})
100102
require.NoError(t.t, err)
103+
require.Len(t.t, utxosAfterSend.ManagedUtxos, 2)
104+
105+
// After the sweep, check that the first tombstone
106+
// is now marked as swept.
107+
sweptUtxo, ok := utxosAfterSend.ManagedUtxos[tombstoneOp]
108+
require.True(t.t, ok)
109+
require.True(t.t, sweptUtxo.Swept)
110+
111+
tombstoneOp2 := ""
112+
for outpoint, utxo := range utxosAfterSend.ManagedUtxos {
113+
if !utxo.Swept {
114+
tombstoneOp2 = outpoint
115+
break
116+
}
117+
}
118+
require.NotEmpty(t.t, tombstoneOp2)
101119

102-
finalTombstoneCount := 0
103-
for _, utxo := range finalAliceUtxos.ManagedUtxos {
104-
if len(utxo.Assets) == 0 {
105-
finalTombstoneCount++
106-
t.t.Logf("Found tombstone UTXO after sweep: %s, value=%d",
107-
utxo.OutPoint, utxo.AmtSat)
120+
// Test 2: Burning transaction sweeps tombstones.
121+
rpcAssets3 := MintAssetsConfirmBatch(
122+
t.t, t.lndHarness.Miner().Client, t.tapd,
123+
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
124+
)
125+
genInfo3 := rpcAssets3[0].AssetGenesis
126+
127+
// Full burn the asset to create a zero-value burn UTXO
128+
// and sweep the second tombstone.
129+
burnResp, err := t.tapd.BurnAsset(ctxb, &taprpc.BurnAssetRequest{
130+
Asset: &taprpc.BurnAssetRequest_AssetId{
131+
AssetId: genInfo3.AssetId,
132+
},
133+
AmountToBurn: assetAmount,
134+
ConfirmationText: "assets will be destroyed",
135+
})
136+
require.NoError(t.t, err)
137+
138+
AssertAssetOutboundTransferWithOutputs(
139+
t.t, t.lndHarness.Miner().Client, t.tapd, burnResp.BurnTransfer,
140+
[][]byte{genInfo3.AssetId},
141+
[]uint64{assetAmount}, 2, 3, 1, true,
142+
)
143+
144+
// Second tombstone should be swept.
145+
utxosAfterBurn, err := t.tapd.ListUtxos(ctxb, &taprpc.ListUtxosRequest{
146+
ScriptKeyType: &taprpc.ScriptKeyTypeQuery{
147+
Type: &taprpc.ScriptKeyTypeQuery_ExplicitType{
148+
ExplicitType: taprpc.
149+
ScriptKeyType_SCRIPT_KEY_TOMBSTONE,
150+
},
151+
},
152+
},
153+
)
154+
require.NoError(t.t, err)
155+
156+
sweptTombstone2, ok := utxosAfterBurn.ManagedUtxos[tombstoneOp2]
157+
require.True(t.t, ok)
158+
require.True(t.t, sweptTombstone2.Swept)
159+
160+
burnUtxos, err := t.tapd.ListUtxos(ctxb, &taprpc.ListUtxosRequest{
161+
ScriptKeyType: &taprpc.ScriptKeyTypeQuery{
162+
Type: &taprpc.ScriptKeyTypeQuery_ExplicitType{
163+
ExplicitType: taprpc.
164+
ScriptKeyType_SCRIPT_KEY_BURN,
165+
},
166+
},
167+
})
168+
require.NoError(t.t, err)
169+
require.Len(t.t, burnUtxos.ManagedUtxos, 1)
170+
171+
burnOutpoint := ""
172+
for outpoint, utxo := range burnUtxos.ManagedUtxos {
173+
if !utxo.Swept {
174+
burnOutpoint = outpoint
175+
break
108176
}
109177
}
178+
require.NotEmpty(t.t, burnOutpoint)
179+
180+
// Test 3: Send transactions sweeps zero-value burns.
181+
rpcAssets4 := MintAssetsConfirmBatch(
182+
t.t, t.lndHarness.Miner().Client, t.tapd,
183+
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
184+
)
185+
genInfo4 := rpcAssets4[0].AssetGenesis
186+
187+
// Send partial amouunt. This should NOT create a tombstone output
188+
// and sweep the burn UTXO.
189+
partialAmount := assetAmount / 2
190+
bobAddr3, err := secondTapd.NewAddr(ctxb, &taprpc.NewAddrRequest{
191+
AssetId: genInfo4.AssetId,
192+
Amt: partialAmount,
193+
AssetVersion: rpcAssets4[0].Version,
194+
})
195+
require.NoError(t.t, err)
196+
197+
sendResp3, _ := sendAssetsToAddr(t, t.tapd, bobAddr3)
110198

111-
t.t.Logf("Alice has %d tombstone UTXOs after sweep", finalTombstoneCount)
199+
ConfirmAndAssertOutboundTransfer(
200+
t.t, t.lndHarness.Miner().Client, t.tapd, sendResp3,
201+
genInfo4.AssetId,
202+
[]uint64{partialAmount, partialAmount}, 3, 4,
203+
)
204+
AssertNonInteractiveRecvComplete(t.t, secondTapd, 3)
205+
206+
// Burn UTXO should be swept.
207+
finalBurnUtxos, err := t.tapd.ListUtxos(ctxb, &taprpc.ListUtxosRequest{
208+
ScriptKeyType: &taprpc.ScriptKeyTypeQuery{
209+
Type: &taprpc.ScriptKeyTypeQuery_ExplicitType{
210+
ExplicitType: taprpc.
211+
ScriptKeyType_SCRIPT_KEY_BURN,
212+
},
213+
},
214+
})
215+
require.NoError(t.t, err)
216+
require.Len(t.t, finalBurnUtxos.ManagedUtxos, 1)
112217

113-
// We expect no tombstones after the sweep transaction.
114-
require.Equal(t.t, 0, finalTombstoneCount,
115-
"All tombstones should be swept")
218+
sweptBurnUtxo, ok := finalBurnUtxos.ManagedUtxos[burnOutpoint]
219+
require.True(t.t, ok)
220+
require.True(t.t, sweptBurnUtxo.Swept)
116221
}

rpcserver.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1403,6 +1403,7 @@ func (r *rpcServer) ListUtxos(ctx context.Context,
14031403
MerkleRoot: u.MerkleRoot,
14041404
LeaseOwner: u.LeaseOwner[:],
14051405
LeaseExpiryUnix: u.LeaseExpiry.Unix(),
1406+
Swept: u.Swept,
14061407
}
14071408
}
14081409

@@ -1419,6 +1420,14 @@ func (r *rpcServer) ListUtxos(ctx context.Context,
14191420
utxos[op] = utxo
14201421
}
14211422

1423+
// As a final pass, we'll prune out any UTXOs that don't have any
1424+
// assets, as these may be in the DB just for record keeping.
1425+
for _, utxo := range utxos {
1426+
if len(utxo.Assets) == 0 {
1427+
delete(utxos, utxo.OutPoint)
1428+
}
1429+
}
1430+
14221431
return &taprpc.ListUtxosResponse{
14231432
ManagedUtxos: utxos,
14241433
}, nil
@@ -2612,8 +2621,17 @@ func (r *rpcServer) AnchorVirtualPsbts(ctx context.Context,
26122621
prevID.OutPoint.String())
26132622
}
26142623

2624+
// Fetch zero-value UTXOs that should be swept as additional inputs.
2625+
zeroValueInputs, err := r.cfg.AssetStore.FetchZeroValueAnchorUTXOs(ctx)
2626+
if err != nil {
2627+
return nil, fmt.Errorf("unable to fetch zero-value "+
2628+
"UTXOs: %w", err)
2629+
}
2630+
26152631
resp, err := r.cfg.ChainPorter.RequestShipment(
2616-
tapfreighter.NewPreSignedParcel(vPackets, inputCommitments, ""),
2632+
tapfreighter.NewPreSignedParcel(
2633+
vPackets, inputCommitments, zeroValueInputs, "",
2634+
),
26172635
)
26182636
if err != nil {
26192637
return nil, fmt.Errorf("error requesting delivery: %w", err)
@@ -3726,7 +3744,8 @@ func (r *rpcServer) BurnAsset(ctx context.Context,
37263744

37273745
resp, err := r.cfg.ChainPorter.RequestShipment(
37283746
tapfreighter.NewPreSignedParcel(
3729-
fundResp.VPackets, fundResp.InputCommitments, in.Note,
3747+
fundResp.VPackets, fundResp.InputCommitments,
3748+
fundResp.ZeroValueInputs, in.Note,
37303749
),
37313750
)
37323751
if err != nil {

0 commit comments

Comments
 (0)