Skip to content

Commit faf184a

Browse files
committed
tapcfg+tapfreighter: configure sweeping with wallet flag
1 parent 5f8ba65 commit faf184a

File tree

12 files changed

+343
-64
lines changed

12 files changed

+343
-64
lines changed

config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,10 @@ type Config struct {
190190

191191
ChainPorter tapfreighter.Porter
192192

193+
// SweepZeroValueAnchorUtxos toggles sweeping zero-value UTXOs into
194+
// anchor transactions for sends and burns.
195+
SweepZeroValueAnchorUtxos bool
196+
193197
// FsmDaemonAdapters is a set of adapters that allow a state machine to
194198
// interact with external daemons.
195199
FsmDaemonAdapters *lndservices.LndFsmDaemonAdapters

docs/release-notes/release-notes-0.8.0.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
- [Garbage collection of zero-value UTXOs](https://github.com/lightninglabs/taproot-assets/pull/1832)
3636
by sweeping tombstones and burn outputs when executing onchain transactions.
3737
Garbage collection will be executed on every burn, transfer or call to
38-
`AnchorVirtualPsbts`.
38+
`AnchorVirtualPsbts`. A new configuration is available to control the sweeping
39+
via the flag `wallet.sweep-zero-value-anchor-utxos`.
3940

4041
## RPC Updates
4142

itest/integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func TestTaprootAssetsDaemon(t *testing.T) {
119119
// assure no state is taken over between runs.
120120
tapdHarness, uniHarness, proofCourier := setupHarnesses(
121121
t1, ht, lndHarness, uniServerLndHarness,
122-
testCase.proofCourierType,
122+
testCase.proofCourierType, testCase.tapdOptions...,
123123
)
124124

125125
ht := ht.newHarnessTest(

itest/tapd_harness.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ type harnessOpts struct {
139139
// the local oracle to the counterparty when requesting a quote.
140140
sendPriceHint bool
141141

142+
// sweepZeroValueAnchorUtxos indicates whether zero-value anchor UTXOs
143+
// should be swept into anchor transactions.
144+
sweepZeroValueAnchorUtxos bool
145+
142146
// disableSupplyVerifierChainWatch when true prevents the supply
143147
// verifier from starting state machines to watch on-chain outputs for
144148
// spends. This option is intended for universe servers, where supply
@@ -197,6 +201,8 @@ func newTapdHarness(t *testing.T, ht *harnessTest, cfg tapdConfig,
197201
tapCfg.ChainConf.Network = cfg.NetParams.Name
198202
tapCfg.TapdDir = cfg.BaseDir
199203
tapCfg.DebugLevel = *logLevel
204+
tapCfg.Wallet.SweepZeroValueAnchorUtxos =
205+
opts.sweepZeroValueAnchorUtxos
200206

201207
// Enable universe proof courier RPC endpoints. These endpoints are
202208
// also used within some tests for transferring proofs.

itest/test_harness.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ type testCase struct {
7070
name string
7171
test func(t *harnessTest)
7272
proofCourierType proof.CourierType
73+
tapdOptions []Option
7374
}
7475

7576
// harnessTest wraps a regular testing.T providing enhanced error detection
@@ -270,7 +271,7 @@ func (h *harnessTest) addFederationServer(host string, target *tapdHarness) {
270271
// to each other through an in-memory gRPC connection.
271272
func setupHarnesses(t *testing.T, ht *harnessTest,
272273
lndHarness *lntest.HarnessTest, uniServerLndHarness *node.HarnessNode,
273-
proofCourierType proof.CourierType) (*tapdHarness,
274+
proofCourierType proof.CourierType, tapdOpts ...Option) (*tapdHarness,
274275
*universeServerHarness, proof.CourierHarness) {
275276

276277
// Create a new universe server harness and start it.
@@ -306,10 +307,11 @@ func setupHarnesses(t *testing.T, ht *harnessTest,
306307
alice := lndHarness.NewNodeWithCoins("Alice", nil)
307308

308309
// Create a tapd that uses Alice and connect it to the universe server.
310+
tapdOptions := append(tapdOpts, func(params *tapdHarnessParams) {
311+
params.proofCourier = proofCourier
312+
})
309313
tapdHarness := setupTapdHarness(
310-
t, ht, alice, universeServer, func(params *tapdHarnessParams) {
311-
params.proofCourier = proofCourier
312-
},
314+
t, ht, alice, universeServer, tapdOptions...,
313315
)
314316
return tapdHarness, universeServer, proofCourier
315317
}
@@ -372,6 +374,10 @@ type tapdHarnessParams struct {
372374
// sendPriceHint indicates whether the tapd should send price hints from
373375
// the local oracle to the counterparty when requesting a quote.
374376
sendPriceHint bool
377+
378+
// sweepZeroValueAnchorUtxos indicates whether zero-value anchor UTXOs
379+
// should be swept into anchor transactions.
380+
sweepZeroValueAnchorUtxos bool
375381
}
376382

377383
// Option is a tapd harness option.
@@ -400,6 +406,14 @@ func WithSendPriceHint() Option {
400406
}
401407
}
402408

409+
// WithSweepZeroValueAnchorUtxos enables sweeping zero-value anchor UTXOs for
410+
// the tapd harness created with this option.
411+
func WithSweepZeroValueAnchorUtxos() Option {
412+
return func(th *tapdHarnessParams) {
413+
th.sweepZeroValueAnchorUtxos = true
414+
}
415+
}
416+
403417
// setupTapdHarness creates a new tapd that connects to the given lnd node
404418
// and to the given universe server.
405419
func setupTapdHarness(t *testing.T, ht *harnessTest,
@@ -434,6 +448,7 @@ func setupTapdHarness(t *testing.T, ht *harnessTest,
434448
ho.disableSyncCache = params.disableSyncCache
435449
ho.oracleServerAddress = params.oracleServerAddress
436450
ho.sendPriceHint = params.sendPriceHint
451+
ho.sweepZeroValueAnchorUtxos = params.sweepZeroValueAnchorUtxos
437452
}
438453

439454
tapdCfg := tapdConfig{

itest/test_list_on_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ var allTestCases = []*testCase{
104104
{
105105
name: "zero value anchor sweep",
106106
test: testZeroValueAnchorSweep,
107+
tapdOptions: []Option{
108+
WithSweepZeroValueAnchorUtxos(),
109+
},
110+
},
111+
{
112+
name: "zero value anchor accumulation",
113+
test: testZeroValueAnchorAccumulation,
107114
},
108115
{
109116
name: "restart receiver check balance",

itest/zero_value_anchor_test.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package itest
22

33
import (
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+
}

rpcserver.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2671,11 +2671,19 @@ func (r *rpcServer) AnchorVirtualPsbts(ctx context.Context,
26712671
prevID.OutPoint.String())
26722672
}
26732673

2674-
// Fetch zero-value UTXOs that should be swept as additional inputs.
2675-
zeroValueInputs, err := r.cfg.AssetStore.FetchZeroValueAnchorUTXOs(ctx)
2676-
if err != nil {
2677-
return nil, fmt.Errorf("unable to fetch zero-value "+
2678-
"UTXOs: %w", err)
2674+
// Fetch zero-value UTXOs that should be swept as additional inputs if
2675+
// the feature is enabled.
2676+
var (
2677+
zeroValueInputs []*tapfreighter.ZeroValueInput
2678+
err error
2679+
)
2680+
if r.cfg.SweepZeroValueAnchorUtxos {
2681+
zeroValueInputs, err = r.cfg.AssetStore.
2682+
FetchZeroValueAnchorUTXOs(ctx)
2683+
if err != nil {
2684+
return nil, fmt.Errorf("unable to fetch zero-value "+
2685+
"UTXOs: %w", err)
2686+
}
26792687
}
26802688

26812689
resp, err := r.cfg.ChainPorter.RequestShipment(

sample-tapd.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,10 @@
412412
; Value must be a valid float ranging from 0.00 to 1.00.
413413
; wallet.psbt-max-fee-ratio=0.75
414414

415+
; Sweep zero-value anchor UTXOs into send and burn anchor transactions.
416+
; Disabled by default.
417+
; wallet.sweep-zero-value-anchor-utxos=false
418+
415419
[address]
416420

417421
; If true, tapd will not try to sync issuance proofs for unknown assets when

0 commit comments

Comments
 (0)