Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/release-notes/release-notes-0.7.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@
configured with public read access. This matches the behavior of the
existing FetchSupplyCommit RPC endpoint.

- The [`ListUtxos` RPC now returns a `Swept` field](https://github.com/lightninglabs/taproot-assets/pull/1832)
indicating whether the output is spent.

- [PR#1839](https://github.com/lightninglabs/taproot-assets/pull/1839) The
`FetchSupplyLeaves` and `FetchSupplyCommit` RPC endpoints now
include a new `block_headers` field. This field is a map from block
Expand Down Expand Up @@ -249,6 +252,9 @@
- Enable [burning the full amount of an asset](https://github.com/lightninglabs/taproot-assets/pull/1791)
when it is the sole one anchored to a Bitcoin UTXO.

- [Garbage collection of zero-value UTXOs](https://github.com/lightninglabs/taproot-assets/pull/1832)
by sweeping tombstones and burn outputs when executing onchain transactions.

## RPC Updates

## tapcli Updates
Expand Down
4 changes: 2 additions & 2 deletions itest/burn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,8 @@ func testBurnAssets(t *harnessTest) {
AssertSendEventsComplete(t.t, fullSendAddr.ScriptKey, sendEvents)

AssertBalances(
t.t, t.tapd, burnAmt+simpleCollectible.Amount,
WithNumUtxos(2), WithNumAnchorUtxos(2),
t.t, t.tapd, simpleCollectible.Amount,
WithNumUtxos(1), WithNumAnchorUtxos(1),
WithScriptKeyType(asset.ScriptKeyBurn),
)

Expand Down
5 changes: 3 additions & 2 deletions itest/full_value_split_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,11 @@ func testFullValueSend(t *harnessTest) {
1, 2,
)

// Alice should have one more zero-value tombstones in her wallet.
// After the second run, Alice's previous tombstons were swept. She now
// has 1 new tombstone UTXO from the last full-value send.
AssertBalances(
t.t, t.tapd, 0, WithScriptKeyType(asset.ScriptKeyTombstone),
WithNumUtxos(3), WithNumAnchorUtxos(3),
WithNumUtxos(1), WithNumAnchorUtxos(1),
)
AssertBalances(
t.t, secondTapd, mintedAsset.Amount+mintedGroupAsset.Amount,
Expand Down
4 changes: 4 additions & 0 deletions itest/test_list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ var allTestCases = []*testCase{
name: "min relay fee bump",
test: testMinRelayFeeBump,
},
{
name: "zero value anchor sweep",
test: testZeroValueAnchorSweep,
},
{
name: "restart receiver check balance",
test: testRestartReceiverCheckBalance,
Expand Down
196 changes: 196 additions & 0 deletions itest/zero_value_anchor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package itest

import (
"context"

"github.com/lightninglabs/taproot-assets/asset"
"github.com/lightninglabs/taproot-assets/taprpc"
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
"github.com/stretchr/testify/require"
)

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

// First, mint some simple asset.
rpcAssets := MintAssetsConfirmBatch(
t.t, t.lndHarness.Miner().Client, t.tapd,
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
)
genInfo := rpcAssets[0].AssetGenesis
assetAmount := simpleAssets[0].Asset.Amount

// Create a second tapd node.
bobLnd := t.lndHarness.NewNodeWithCoins("Bob", nil)
secondTapd := setupTapdHarness(t.t, t, bobLnd, t.universeServer)
defer func() {
require.NoError(t.t, secondTapd.stop(!*noDelete))
}()

bobAddr, err := secondTapd.NewAddr(ctxb, &taprpc.NewAddrRequest{
AssetId: genInfo.AssetId,
Amt: assetAmount,
AssetVersion: rpcAssets[0].Version,
})
require.NoError(t.t, err)

// Send ALL assets to Bob, which should create a tombstone.
sendResp, _ := sendAssetsToAddr(t, t.tapd, bobAddr)

ConfirmAndAssertOutboundTransfer(
t.t, t.lndHarness.Miner().Client, t.tapd, sendResp,
genInfo.AssetId,
[]uint64{0, assetAmount}, 0, 1,
)
AssertNonInteractiveRecvComplete(t.t, secondTapd, 1)

// Alice should have 1 tombstone UTXO from the full-value send.
AssertBalances(
t.t, t.tapd, 0, WithScriptKeyType(asset.ScriptKeyTombstone),
WithNumUtxos(1), WithNumAnchorUtxos(1),
)

// Test 1: Send transaction sweeps tombstones.
rpcAssets2 := MintAssetsConfirmBatch(
t.t, t.lndHarness.Miner().Client, t.tapd,
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
)
genInfo2 := rpcAssets2[0].AssetGenesis

// Send full amount of the new asset. This should sweep Alice's
// first tombstone and create a new one.
bobAddr2, err := secondTapd.NewAddr(ctxb, &taprpc.NewAddrRequest{
AssetId: genInfo2.AssetId,
Amt: assetAmount,
AssetVersion: rpcAssets2[0].Version,
})
require.NoError(t.t, err)

sendResp2, _ := sendAssetsToAddr(t, t.tapd, bobAddr2)

ConfirmAndAssertOutboundTransfer(
t.t, t.lndHarness.Miner().Client, t.tapd, sendResp2,
genInfo2.AssetId,
[]uint64{0, assetAmount}, 1, 2,
)
AssertNonInteractiveRecvComplete(t.t, secondTapd, 2)

// Check Alice's tombstone balance. The first tombstone should have been
// swept (spent on-chain as an input), and a new one created. We now
// have 1 tombstone UTXO (the new one from the second send).
AssertBalances(
t.t, t.tapd, 0, WithScriptKeyType(asset.ScriptKeyTombstone),
WithNumUtxos(1), WithNumAnchorUtxos(1),
)

// Get the new tombstone outpoint.
utxosAfterSend, err := t.tapd.ListUtxos(ctxb, &taprpc.ListUtxosRequest{
ScriptKeyType: &taprpc.ScriptKeyTypeQuery{
Type: &taprpc.ScriptKeyTypeQuery_ExplicitType{
ExplicitType: taprpc.
ScriptKeyType_SCRIPT_KEY_TOMBSTONE,
},
},
})
require.NoError(t.t, err)
require.Len(t.t, utxosAfterSend.ManagedUtxos, 1)

tombstoneOp2 := ""
for outpoint, utxo := range utxosAfterSend.ManagedUtxos {
if !utxo.Swept {
tombstoneOp2 = outpoint
break
}
}
require.NotEmpty(t.t, tombstoneOp2)

// Test 2: Burning transaction sweeps tombstones.
rpcAssets3 := MintAssetsConfirmBatch(
t.t, t.lndHarness.Miner().Client, t.tapd,
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
)
genInfo3 := rpcAssets3[0].AssetGenesis

// Full burn the asset to create a zero-value burn UTXO
// and sweep the second tombstone.
burnResp, err := t.tapd.BurnAsset(ctxb, &taprpc.BurnAssetRequest{
Asset: &taprpc.BurnAssetRequest_AssetId{
AssetId: genInfo3.AssetId,
},
AmountToBurn: assetAmount,
ConfirmationText: "assets will be destroyed",
})
require.NoError(t.t, err)

AssertAssetOutboundTransferWithOutputs(
t.t, t.lndHarness.Miner().Client, t.tapd, burnResp.BurnTransfer,
[][]byte{genInfo3.AssetId},
[]uint64{assetAmount}, 2, 3, 1, true,
)

// Alice should have 0 tombstones remaining and 1 burn UTXO.
AssertBalances(
t.t, t.tapd, 0, WithScriptKeyType(asset.ScriptKeyTombstone),
WithNumUtxos(0), WithNumAnchorUtxos(0),
)
AssertBalances(
t.t, t.tapd, assetAmount,
WithScriptKeyType(asset.ScriptKeyBurn),
WithNumUtxos(1), WithNumAnchorUtxos(1),
)

// Get the burn UTXO outpoint for the next test.
burnUtxos, err := t.tapd.ListUtxos(ctxb, &taprpc.ListUtxosRequest{
ScriptKeyType: &taprpc.ScriptKeyTypeQuery{
Type: &taprpc.ScriptKeyTypeQuery_ExplicitType{
ExplicitType: taprpc.
ScriptKeyType_SCRIPT_KEY_BURN,
},
},
})
require.NoError(t.t, err)
require.Len(t.t, burnUtxos.ManagedUtxos, 1)

burnOutpoint := ""
for outpoint, utxo := range burnUtxos.ManagedUtxos {
if !utxo.Swept {
burnOutpoint = outpoint
break
}
}
require.NotEmpty(t.t, burnOutpoint)

// Test 3: Send transactions sweeps zero-value burns.
rpcAssets4 := MintAssetsConfirmBatch(
t.t, t.lndHarness.Miner().Client, t.tapd,
[]*mintrpc.MintAssetRequest{simpleAssets[0]},
)
genInfo4 := rpcAssets4[0].AssetGenesis

// Send partial amouunt. This should NOT create a tombstone output
// and sweep the burn UTXO.
partialAmount := assetAmount / 2
bobAddr3, err := secondTapd.NewAddr(ctxb, &taprpc.NewAddrRequest{
AssetId: genInfo4.AssetId,
Amt: partialAmount,
AssetVersion: rpcAssets4[0].Version,
})
require.NoError(t.t, err)

sendResp3, _ := sendAssetsToAddr(t, t.tapd, bobAddr3)

ConfirmAndAssertOutboundTransfer(
t.t, t.lndHarness.Miner().Client, t.tapd, sendResp3,
genInfo4.AssetId,
[]uint64{partialAmount, partialAmount}, 3, 4,
)
AssertNonInteractiveRecvComplete(t.t, secondTapd, 3)

// The burn UTXO should have been swept.
AssertBalances(
t.t, t.tapd, 0, WithScriptKeyType(asset.ScriptKeyBurn),
WithNumUtxos(0), WithNumAnchorUtxos(0),
)
}
15 changes: 13 additions & 2 deletions rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1403,6 +1403,7 @@ func (r *rpcServer) ListUtxos(ctx context.Context,
MerkleRoot: u.MerkleRoot,
LeaseOwner: u.LeaseOwner[:],
LeaseExpiryUnix: u.LeaseExpiry.Unix(),
Swept: u.Swept,
}
}

Expand Down Expand Up @@ -2620,8 +2621,17 @@ func (r *rpcServer) AnchorVirtualPsbts(ctx context.Context,
prevID.OutPoint.String())
}

// Fetch zero-value UTXOs that should be swept as additional inputs.
zeroValueInputs, err := r.cfg.AssetStore.FetchZeroValueAnchorUTXOs(ctx)
if err != nil {
return nil, fmt.Errorf("unable to fetch zero-value "+
"UTXOs: %w", err)
}

resp, err := r.cfg.ChainPorter.RequestShipment(
tapfreighter.NewPreSignedParcel(vPackets, inputCommitments, ""),
tapfreighter.NewPreSignedParcel(
vPackets, inputCommitments, zeroValueInputs, "",
),
)
if err != nil {
return nil, fmt.Errorf("error requesting delivery: %w", err)
Expand Down Expand Up @@ -3734,7 +3744,8 @@ func (r *rpcServer) BurnAsset(ctx context.Context,

resp, err := r.cfg.ChainPorter.RequestShipment(
tapfreighter.NewPreSignedParcel(
fundResp.VPackets, fundResp.InputCommitments, in.Note,
fundResp.VPackets, fundResp.InputCommitments,
fundResp.ZeroValueInputs, in.Note,
),
)
if err != nil {
Expand Down
Loading
Loading