Skip to content

Commit f55b6c5

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

File tree

11 files changed

+679
-19
lines changed

11 files changed

+679
-19
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: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
// The second mint should sweep the previous tombstone outpoint as a BTC
63+
// input of its anchor transaction.
64+
// Re-query managed transactions and assert usage indirectly by checking
65+
// that a new mint happened (already ensured) and later the following send
66+
// can select the same anchor input if needed.
67+
68+
recvAddr2 := rpcassert.NewAddrRPC(t.t, ctxt, bobTapd, nil, &taprpc.NewAddrRequest{
69+
AssetId: secondMint[0].AssetGenesis.AssetId,
70+
Amt: secondMint[0].Amount,
71+
AssetVersion: secondMint[0].Version,
72+
})
73+
74+
sendResp2, sendEvents2 := sendAsset(
75+
t, t.tapd, withReceiverAddresses(recvAddr2), withSkipProofCourierPingCheck(),
76+
)
77+
defer sendEvents2.Cancel()
78+
79+
found := false
80+
for _, in := range sendResp2.Transfer.Inputs {
81+
if in.AnchorPoint == tombstoneOutpoint {
82+
found = true
83+
break
84+
}
85+
}
86+
require.Truef(t.t, found, "zero value anchor %v not swept", tombstoneOutpoint)
87+
}

tapcfg/server.go

Lines changed: 2 additions & 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

@@ -697,6 +698,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger,
697698
IgnoreChecker: ignoreCheckerOpt,
698699
MintSupplyCommitter: supplyCommitManager,
699700
DelegationKeyChecker: addrBook,
701+
AnchorLister: assetStore,
700702
},
701703
ChainParams: tapChainParams,
702704
ProofUpdates: proofArchive,

tapdb/assets_store.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/lightninglabs/taproot-assets/proof"
2323
"github.com/lightninglabs/taproot-assets/tapdb/sqlc"
2424
"github.com/lightninglabs/taproot-assets/tapfreighter"
25+
"github.com/lightninglabs/taproot-assets/tapgarden"
2526
"github.com/lightninglabs/taproot-assets/tappsbt"
2627
"github.com/lightninglabs/taproot-assets/tapsend"
2728
"github.com/lightningnetwork/lnd/clock"
@@ -1320,6 +1321,116 @@ func (a *AssetStore) FetchManagedUTXOs(ctx context.Context) (
13201321
return managedUtxos, nil
13211322
}
13221323

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

2590+
for _, zeroAnchor := range spend.ZeroValueAnchors {
2591+
anchorPointBytes, err := encodeOutpoint(zeroAnchor)
2592+
if err != nil {
2593+
return err
2594+
}
2595+
2596+
err = q.UpdateUTXOLease(ctx, UpdateUTXOLease{
2597+
LeaseOwner: finalLeaseOwner[:],
2598+
LeaseExpiry: sql.NullTime{
2599+
Time: finalLeaseExpiry.UTC(),
2600+
Valid: true,
2601+
},
2602+
Outpoint: anchorPointBytes,
2603+
})
2604+
if err != nil {
2605+
return fmt.Errorf("unable to lease zero value "+
2606+
"anchor: %w", err)
2607+
}
2608+
}
2609+
24792610
// Then the passive assets.
24802611
if len(spend.PassiveAssets) > 0 {
24812612
if spend.PassiveAssetsAnchor == nil {

tapfreighter/chain_porter.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,7 +1589,7 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
15891589
"assets: %w", err)
15901590
}
15911591

1592-
anchorTx, err := wallet.AnchorVirtualTransactions(
1592+
anchorTx, sweptAnchors, err := wallet.AnchorVirtualTransactions(
15931593
ctx, &AnchorVTxnsParams{
15941594
FeeRate: feeRate,
15951595
ActivePackets: currentPkg.VirtualPackets,
@@ -1608,6 +1608,7 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
16081608
// signing process with a copy to avoid clearing the info on
16091609
// finalization.
16101610
currentPkg.AnchorTx = anchorTx
1611+
currentPkg.ZeroValueAnchors = sweptAnchors
16111612

16121613
// For the final validation, we need to also supply the assets
16131614
// that were committed to the input tree but pruned because they
@@ -1655,6 +1656,13 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
16551656
// local means the lnd node connected to this daemon knows how
16561657
// to derive the key.
16571658
isLocalKey := func(key asset.ScriptKey) (bool, error) {
1659+
// Tombstone outputs use the NUMS key and are always controlled by
1660+
// the local daemon so we treat them as local regardless of whether
1661+
// the key was explicitly declared before.
1662+
if key.PubKey != nil && key.PubKey.IsEqual(asset.NUMSPubKey) {
1663+
return true, nil
1664+
}
1665+
16581666
// To make sure we have the correct internal key with
16591667
// the family and index set, we attempt to fetch it
16601668
// from the database. If it exists, then we know we
@@ -1695,7 +1703,7 @@ func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
16951703
parcel, err := ConvertToTransfer(
16961704
currentHeight, currentPkg.VirtualPackets,
16971705
currentPkg.AnchorTx, currentPkg.PassiveAssets,
1698-
isLocalKey, currentPkg.Label,
1706+
currentPkg.ZeroValueAnchors, isLocalKey, currentPkg.Label,
16991707
currentPkg.SkipAnchorTxBroadcast,
17001708
)
17011709
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
}

0 commit comments

Comments
 (0)