Skip to content

Commit 3bd769d

Browse files
committed
single commit to bundle changes. draft
1 parent 614acdc commit 3bd769d

File tree

9 files changed

+612
-27
lines changed

9 files changed

+612
-27
lines changed

itest/test_list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ var allTestCases = []*testCase{
167167
name: "full value send",
168168
test: testFullValueSend,
169169
},
170+
{
171+
name: "zero value anchor sweep",
172+
test: testZeroValueAnchorSweep,
173+
},
170174
{
171175
name: "collectible send hashmail courier",
172176
test: testCollectibleSend,

itest/zero_value_anchor_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//go:build itest
2+
3+
package itest
4+
5+
import (
6+
"context"
7+
8+
"github.com/lightninglabs/taproot-assets/itest/rpcassert"
9+
"github.com/lightninglabs/taproot-assets/taprpc"
10+
mintrpc "github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func testZeroValueAnchorSweep(t *harnessTest) {
15+
t.t.Helper()
16+
17+
ctxb := context.Background()
18+
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
19+
defer cancel()
20+
21+
miner := t.lndHarness.Miner().Client
22+
23+
firstMint := MintAssetsConfirmBatch(
24+
t.t, miner, t.tapd, []*mintrpc.MintAssetRequest{CopyRequest(simpleAssets[0])},
25+
)
26+
27+
bobLnd := t.lndHarness.NewNodeWithCoins("bob-zero-anchor", nil)
28+
bobTapd := setupTapdHarness(t.t, t, bobLnd, t.universeServer)
29+
defer func() {
30+
require.NoError(t.t, bobTapd.stop(!*noDelete))
31+
bobLnd.Cleanup(nil)
32+
}()
33+
34+
recvAddr := rpcassert.NewAddrRPC(t.t, ctxt, bobTapd, nil, &taprpc.NewAddrRequest{
35+
AssetId: firstMint[0].AssetGenesis.AssetId,
36+
Amt: firstMint[0].Amount,
37+
AssetVersion: firstMint[0].Version,
38+
})
39+
40+
sendResp1, sendEvents1 := sendAsset(
41+
t, t.tapd, withReceiverAddresses(recvAddr), withSkipProofCourierPingCheck(),
42+
)
43+
defer sendEvents1.Cancel()
44+
45+
var tombstoneOutpoint string
46+
for _, out := range sendResp1.Transfer.Outputs {
47+
// We don't require ScriptKeyIsLocal on tombstones (NUMS key is not
48+
// controlled by us). We only check for zero-amount, split-root type.
49+
if out.Amount == 0 {
50+
tombstoneOutpoint = out.Anchor.Outpoint
51+
break
52+
}
53+
}
54+
require.NotEmpty(t.t, tombstoneOutpoint, "expected tombstone output not found")
55+
56+
MineBlocks(t.t, miner, 1, 1)
57+
58+
secondMint := MintAssetsConfirmBatch(
59+
t.t, miner, t.tapd, []*mintrpc.MintAssetRequest{CopyRequest(simpleAssets[0])},
60+
)
61+
62+
recvAddr2 := rpcassert.NewAddrRPC(t.t, ctxt, bobTapd, nil, &taprpc.NewAddrRequest{
63+
AssetId: secondMint[0].AssetGenesis.AssetId,
64+
Amt: secondMint[0].Amount,
65+
AssetVersion: secondMint[0].Version,
66+
})
67+
68+
sendResp2, sendEvents2 := sendAsset(
69+
t, t.tapd, withReceiverAddresses(recvAddr2), withSkipProofCourierPingCheck(),
70+
)
71+
defer sendEvents2.Cancel()
72+
73+
found := false
74+
for _, in := range sendResp2.Transfer.Inputs {
75+
if in.AnchorPoint == tombstoneOutpoint {
76+
found = true
77+
break
78+
}
79+
}
80+
require.Truef(t.t, found, "zero value anchor %v not swept", tombstoneOutpoint)
81+
}

tapcfg/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger,
425425
GroupVerifier: groupVerifier,
426426
IgnoreChecker: ignoreCheckerOpt,
427427
Wallet: walletAnchor,
428+
AnchorLister: assetStore,
428429
ChainParams: &tapChainParams,
429430
})
430431

tapdb/assets_store.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,6 +1320,93 @@ func (a *AssetStore) FetchManagedUTXOs(ctx context.Context) (
13201320
return managedUtxos, nil
13211321
}
13221322

1323+
// ListZeroValueAnchors returns the set of managed anchor UTXOs that only
1324+
// contain tombstone/burn commitments and therefore have zero effective asset
1325+
// value.
1326+
//
1327+
// NOTE: This implements the tapfreighter.ZeroValueAnchorLister interface.
1328+
// ListZeroValueAnchors implements both the tapfreighter and tapgarden lister
1329+
// flavors by returning tapfreighter.ZeroValueAnchor; tapgarden wraps it via a
1330+
// thin adapter in planter.
1331+
func (a *AssetStore) ListZeroValueAnchors(ctx context.Context) (
1332+
[]*tapfreighter.ZeroValueAnchor, error) {
1333+
1334+
managedUtxos, err := a.FetchManagedUTXOs(ctx)
1335+
if err != nil {
1336+
return nil, err
1337+
}
1338+
1339+
now := a.clock.Now().UTC()
1340+
anchors := make([]*tapfreighter.ZeroValueAnchor, 0)
1341+
1342+
for _, utxo := range managedUtxos {
1343+
// Skip entries that are currently leased.
1344+
if len(utxo.LeaseOwner) != 0 {
1345+
if utxo.LeaseExpiry.IsZero() || !utxo.LeaseExpiry.Before(now) {
1346+
continue
1347+
}
1348+
}
1349+
1350+
anchorPointBytes, err := encodeOutpoint(utxo.OutPoint)
1351+
if err != nil {
1352+
return nil, err
1353+
}
1354+
1355+
filter := QueryAssetFilters{
1356+
AnchorPoint: anchorPointBytes,
1357+
Spent: sqlBool(false),
1358+
Leased: sqlBool(false),
1359+
Now: sql.NullTime{
1360+
Time: now,
1361+
Valid: true,
1362+
},
1363+
}
1364+
1365+
commitments, err := a.queryCommitments(ctx, filter)
1366+
var (
1367+
anchorCommitment *commitment.TapCommitment
1368+
assets []*asset.Asset
1369+
)
1370+
1371+
switch {
1372+
case errors.Is(err, tapfreighter.ErrMatchingAssetsNotFound):
1373+
// No spendable assets anchored here, which is exactly the
1374+
// situation we want to sweep.
1375+
case err != nil:
1376+
return nil, err
1377+
default:
1378+
if len(commitments) > 0 {
1379+
anchorCommitment = commitments[0].Commitment
1380+
assets = anchorCommitment.CommittedAssets()
1381+
}
1382+
}
1383+
1384+
zeroValue := len(assets) == 0
1385+
for _, asset := range assets {
1386+
if asset.Amount > 0 && !asset.IsBurn() {
1387+
zeroValue = false
1388+
break
1389+
}
1390+
}
1391+
1392+
if !zeroValue {
1393+
continue
1394+
}
1395+
1396+
anchors = append(anchors, &tapfreighter.ZeroValueAnchor{
1397+
OutPoint: utxo.OutPoint,
1398+
Value: utxo.OutputValue,
1399+
InternalKey: utxo.InternalKey,
1400+
Commitment: anchorCommitment,
1401+
TaprootAssetRoot: append([]byte(nil), utxo.TaprootAssetRoot...),
1402+
MerkleRoot: append([]byte(nil), utxo.MerkleRoot...),
1403+
TapscriptSibling: append([]byte(nil), utxo.TapscriptSibling...),
1404+
})
1405+
}
1406+
1407+
return anchors, nil
1408+
}
1409+
13231410
// FetchAssetProofsSizes fetches the sizes of the proofs in the db.
13241411
func (a *AssetStore) FetchAssetProofsSizes(
13251412
ctx context.Context) ([]AssetProofSize, error) {
@@ -2476,6 +2563,26 @@ func (a *AssetStore) LogPendingParcel(ctx context.Context,
24762563
}
24772564
}
24782565

2566+
for _, zeroAnchor := range spend.ZeroValueAnchors {
2567+
anchorPointBytes, err := encodeOutpoint(zeroAnchor)
2568+
if err != nil {
2569+
return err
2570+
}
2571+
2572+
err = q.UpdateUTXOLease(ctx, UpdateUTXOLease{
2573+
LeaseOwner: finalLeaseOwner[:],
2574+
LeaseExpiry: sql.NullTime{
2575+
Time: finalLeaseExpiry.UTC(),
2576+
Valid: true,
2577+
},
2578+
Outpoint: anchorPointBytes,
2579+
})
2580+
if err != nil {
2581+
return fmt.Errorf("unable to lease zero value "+
2582+
"anchor: %w", err)
2583+
}
2584+
}
2585+
24792586
// Then the passive assets.
24802587
if len(spend.PassiveAssets) > 0 {
24812588
if spend.PassiveAssetsAnchor == nil {

tapfreighter/chain_porter.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,15 +1245,16 @@ func (p *ChainPorter) importLocalAddresses(ctx context.Context,
12451245

12461246
// We'll need to extract the output public key from the tx out that does
12471247
// the send. We'll use this shortly below as a step before broadcast.
1248+
//
1249+
// For outbound parcels, we always control the anchor output internal
1250+
// keys, regardless of whether the asset script key is local or not.
1251+
// This includes tombstone outputs (which use NUMS script keys but have
1252+
// local anchor internal keys). We need to import all these anchor
1253+
// outputs into lnd's wallet so they can be spent later (e.g., for
1254+
// sweeping zero-value anchors).
12481255
for idx := range parcel.Outputs {
12491256
out := &parcel.Outputs[idx]
12501257

1251-
// Skip non-local outputs, those are going to a receiver outside
1252-
// of this daemon.
1253-
if !out.ScriptKeyLocal {
1254-
continue
1255-
}
1256-
12571258
anchorOutputIndex := out.Anchor.OutPoint.Index
12581259
anchorOutput := parcel.AnchorTx.TxOut[anchorOutputIndex]
12591260
_, witProgram, err := txscript.ExtractWitnessProgramInfo(
@@ -1589,7 +1590,7 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
15891590
"assets: %w", err)
15901591
}
15911592

1592-
anchorTx, err := wallet.AnchorVirtualTransactions(
1593+
anchorTx, sweptAnchors, err := wallet.AnchorVirtualTransactions(
15931594
ctx, &AnchorVTxnsParams{
15941595
FeeRate: feeRate,
15951596
ActivePackets: currentPkg.VirtualPackets,
@@ -1608,6 +1609,7 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
16081609
// signing process with a copy to avoid clearing the info on
16091610
// finalization.
16101611
currentPkg.AnchorTx = anchorTx
1612+
currentPkg.ZeroValueAnchors = sweptAnchors
16111613

16121614
// For the final validation, we need to also supply the assets
16131615
// that were committed to the input tree but pruned because they
@@ -1655,6 +1657,13 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
16551657
// local means the lnd node connected to this daemon knows how
16561658
// to derive the key.
16571659
isLocalKey := func(key asset.ScriptKey) (bool, error) {
1660+
// Tombstone outputs use the NUMS key and are always controlled by
1661+
// the local daemon so we treat them as local regardless of whether
1662+
// the key was explicitly declared before.
1663+
if key.PubKey != nil && key.PubKey.IsEqual(asset.NUMSPubKey) {
1664+
return true, nil
1665+
}
1666+
16581667
// To make sure we have the correct internal key with
16591668
// the family and index set, we attempt to fetch it
16601669
// from the database. If it exists, then we know we
@@ -1695,7 +1704,7 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
16951704
parcel, err := ConvertToTransfer(
16961705
currentHeight, currentPkg.VirtualPackets,
16971706
currentPkg.AnchorTx, currentPkg.PassiveAssets,
1698-
isLocalKey, currentPkg.Label,
1707+
currentPkg.ZeroValueAnchors, isLocalKey, currentPkg.Label,
16991708
currentPkg.SkipAnchorTxBroadcast,
17001709
)
17011710
if err != nil {

tapfreighter/interface.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,12 @@ type CoinSelector interface {
192192
maxVersion commitment.TapCommitmentVersion,
193193
) ([]*AnchoredCommitment, error)
194194

195+
// LeaseCoins leases/locks/reserves coins for the given lease owner
196+
// until the given expiry. This is used to prevent multiple concurrent
197+
// coin selection attempts from selecting the same coin(s).
198+
LeaseCoins(ctx context.Context, leaseOwner [32]byte, expiry time.Time,
199+
utxoOutpoints ...wire.OutPoint) error
200+
195201
// ReleaseCoins releases/unlocks coins that were previously leased and
196202
// makes them available for coin selection again.
197203
ReleaseCoins(ctx context.Context, utxoOutpoints ...wire.OutPoint) error
@@ -244,6 +250,44 @@ type Anchor struct {
244250
PkScript []byte
245251
}
246252

253+
// ZeroValueAnchor describes a managed anchor UTXO whose commitment only holds
254+
// tombstones/burns and therefore carries zero effective asset value.
255+
type ZeroValueAnchor struct {
256+
// OutPoint is the BTC outpoint of the anchor UTXO.
257+
OutPoint wire.OutPoint
258+
259+
// Value is the BTC value locked in the anchor output.
260+
Value btcutil.Amount
261+
262+
// InternalKey is the internal key that anchors the commitment in the
263+
// outpoint.
264+
InternalKey keychain.KeyDescriptor
265+
266+
// Commitment is the full Taproot Asset commitment anchored at the
267+
// outpoint if one can be reconstructed from the asset data stored on
268+
// disk. For anchors that only contain tombstones/burns, this might be
269+
// nil.
270+
Commitment *commitment.TapCommitment
271+
272+
// TaprootAssetRoot is the Taproot Asset commitment root hash committed
273+
// to by this outpoint.
274+
TaprootAssetRoot []byte
275+
276+
// MerkleRoot is the tapscript merkle root that includes the Taproot
277+
// Asset commitment and an optional sibling.
278+
MerkleRoot []byte
279+
280+
// TapscriptSibling is the serialized tapscript sibling preimage for the
281+
// anchor output (if present).
282+
TapscriptSibling []byte
283+
}
284+
285+
// ZeroValueAnchorLister lists managed anchor UTXOs whose asset commitments
286+
// carry no spendable value.
287+
type ZeroValueAnchorLister interface {
288+
ListZeroValueAnchors(ctx context.Context) ([]*ZeroValueAnchor, error)
289+
}
290+
247291
// OutputIdentifier is a key that can be used to uniquely identify a transfer
248292
// output.
249293
type OutputIdentifier [32]byte
@@ -467,6 +511,10 @@ type OutboundParcel struct {
467511
// during the parcel confirmation process.
468512
PassiveAssets []*tappsbt.VPacket
469513

514+
// ZeroValueAnchors lists the tombstone/burn anchors that were swept as
515+
// additional BTC inputs when constructing the anchor transaction.
516+
ZeroValueAnchors []wire.OutPoint
517+
470518
// PassiveAssetsAnchor is the anchor point for the passive assets. This
471519
// might be a distinct anchor from any active transfer in case the
472520
// active transfers don't create any change going back to us.
@@ -498,6 +546,7 @@ func (o *OutboundParcel) Copy() *OutboundParcel {
498546
ChainFees: o.ChainFees,
499547
Inputs: fn.CopySlice(o.Inputs),
500548
Outputs: fn.CopySlice(o.Outputs),
549+
ZeroValueAnchors: fn.CopySlice(o.ZeroValueAnchors),
501550
Label: o.Label,
502551
SkipAnchorTxBroadcast: o.SkipAnchorTxBroadcast,
503552
}

tapfreighter/parcel.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,10 @@ type sendPackage struct {
494494
// PassiveAssets is the data used in re-anchoring passive assets.
495495
PassiveAssets []*tappsbt.VPacket
496496

497+
// ZeroValueAnchors keeps track of tombstone/burn anchors that were added
498+
// as extra BTC inputs to the anchor transaction for garbage collection.
499+
ZeroValueAnchors []wire.OutPoint
500+
497501
// Parcel is the asset transfer request that kicked off this transfer.
498502
Parcel Parcel
499503

@@ -546,6 +550,7 @@ type sendPackage struct {
546550
// they were already committed at.
547551
func ConvertToTransfer(currentHeight uint32, activeTransfers []*tappsbt.VPacket,
548552
anchorTx *tapsend.AnchorTransaction, passiveAssets []*tappsbt.VPacket,
553+
zeroValueAnchors []wire.OutPoint,
549554
isLocalKey func(asset.ScriptKey) (bool, error), label string,
550555
skipAnchorTxBroadcast bool) (*OutboundParcel, error) {
551556

@@ -584,6 +589,7 @@ func ConvertToTransfer(currentHeight uint32, activeTransfers []*tappsbt.VPacket,
584589
),
585590
PassiveAssets: passiveAssets,
586591
PassiveAssetsAnchor: passiveAssetAnchor,
592+
ZeroValueAnchors: fn.CopySlice(zeroValueAnchors),
587593
Label: label,
588594
SkipAnchorTxBroadcast: skipAnchorTxBroadcast,
589595
}

0 commit comments

Comments
 (0)