Skip to content

Commit 223d830

Browse files
committed
tapdb: add support for zero-value utxo sweeping
This includes the addition of a "swept" field in the "managed_utxos" table with the corresponding migration and the "MarkManagedUTXOAsSwept" function.
1 parent 80083ec commit 223d830

File tree

10 files changed

+253
-11
lines changed

10 files changed

+253
-11
lines changed

asset/asset.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1803,6 +1803,16 @@ func (a *Asset) IsBurn() bool {
18031803
return IsBurnKey(a.ScriptKey.PubKey, a.PrevWitnesses[0])
18041804
}
18051805

1806+
// IsTombstone returns true if an asset uses the NUMS script key and has zero
1807+
// value.
1808+
func (a *Asset) IsTombstone() bool {
1809+
if a.ScriptKey.PubKey == nil {
1810+
return false
1811+
}
1812+
1813+
return a.Amount == 0 && a.ScriptKey.PubKey.IsEqual(NUMSPubKey)
1814+
}
1815+
18061816
// PrimaryPrevID returns the primary prev ID of an asset. This is the prev ID of
18071817
// the first witness, unless the first witness is a split-commitment witness,
18081818
// in which case it is the prev ID of the first witness of the root asset.

tapdb/assets_store.go

Lines changed: 209 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,10 @@ type ActiveAssetsStore interface {
279279
// serialized outpoint.
280280
DeleteManagedUTXO(ctx context.Context, outpoint []byte) error
281281

282+
// MarkManagedUTXOAsSwept marks a managed UTXO as swept, indicating
283+
// it has been spent in a Bitcoin transaction.
284+
MarkManagedUTXOAsSwept(ctx context.Context, outpoint []byte) error
285+
282286
// UpdateUTXOLease leases a managed UTXO identified by the passed
283287
// serialized outpoint.
284288
UpdateUTXOLease(ctx context.Context, arg UpdateUTXOLease) error
@@ -489,6 +493,14 @@ type ManagedUTXO struct {
489493
// LeaseExpiry is the expiry time of the lease on this UTXO. If the
490494
// zero, then this UTXO isn't leased.
491495
LeaseExpiry time.Time
496+
497+
// PkScript is the pkScript of the anchor output. This is populated
498+
// when fetching zero-value anchor UTXOs to enable PSBT creation.
499+
PkScript []byte
500+
501+
// Swept indicates whether this UTXO has been used as input
502+
// in a Bitcoin transaction.
503+
Swept bool
492504
}
493505

494506
// AssetHumanReadable is a subset of the base asset struct that only includes
@@ -1309,6 +1321,7 @@ func (a *AssetStore) FetchManagedUTXOs(ctx context.Context) (
13091321
MerkleRoot: u.MerkleRoot,
13101322
TapscriptSibling: u.TapscriptSibling,
13111323
LeaseOwner: u.LeaseOwner,
1324+
Swept: u.Swept,
13121325
}
13131326
if u.LeaseExpiry.Valid {
13141327
utxo.LeaseExpiry = u.LeaseExpiry.Time
@@ -1320,6 +1333,159 @@ func (a *AssetStore) FetchManagedUTXOs(ctx context.Context) (
13201333
return managedUtxos, nil
13211334
}
13221335

1336+
// MarkManagedUTXOAsSwept marks a managed UTXO as swept, indicating it has been
1337+
// spent in a Bitcoin transaction.
1338+
func (a *AssetStore) MarkManagedUTXOAsSwept(ctx context.Context,
1339+
outpoint wire.OutPoint) error {
1340+
1341+
outpointBytes, err := encodeOutpoint(outpoint)
1342+
if err != nil {
1343+
return fmt.Errorf("unable to encode outpoint: %w", err)
1344+
}
1345+
1346+
var writeTxOpts AssetStoreTxOptions
1347+
return a.db.ExecTx(ctx, &writeTxOpts, func(q ActiveAssetsStore) error {
1348+
return q.MarkManagedUTXOAsSwept(ctx, outpointBytes)
1349+
})
1350+
}
1351+
1352+
// FetchZeroValueAnchorUTXOs fetches all managed UTXOs that contain only
1353+
// zero-value assets (tombstones and burns).
1354+
func (a *AssetStore) FetchZeroValueAnchorUTXOs(ctx context.Context) (
1355+
[]*tapfreighter.ZeroValueInput, error) {
1356+
1357+
// Strategy: fetch all managed UTXOs and filter in-memory.
1358+
// A UTXO is a "zero-value anchor" if all assets are either tombstones
1359+
// (NUMS key with amount 0) or burns.
1360+
// We exclude leased and spent UTXOs.
1361+
1362+
var results []*tapfreighter.ZeroValueInput
1363+
1364+
readOpts := NewAssetStoreReadTx()
1365+
now := a.clock.Now().UTC()
1366+
1367+
dbErr := a.db.ExecTx(ctx, &readOpts, func(q ActiveAssetsStore) error {
1368+
utxos, err := q.FetchManagedUTXOs(ctx)
1369+
if err != nil {
1370+
return err
1371+
}
1372+
1373+
for _, u := range utxos {
1374+
if len(u.LeaseOwner) > 0 &&
1375+
u.LeaseExpiry.Valid &&
1376+
u.LeaseExpiry.Time.UTC().After(now) {
1377+
1378+
continue
1379+
}
1380+
1381+
if u.Swept {
1382+
continue
1383+
}
1384+
1385+
var anchorPoint wire.OutPoint
1386+
err := readOutPoint(
1387+
bytes.NewReader(u.Outpoint), 0, 0, &anchorPoint)
1388+
if err != nil {
1389+
return err
1390+
}
1391+
1392+
// Query all assets anchored at this outpoint.
1393+
// We include spent assets here because tombstones are
1394+
// marked as spent when created.
1395+
assetsAtAnchor, err := a.queryChainAssets(
1396+
ctx, q, QueryAssetFilters{
1397+
AnchorPoint: u.Outpoint,
1398+
Now: sql.NullTime{
1399+
Time: now,
1400+
Valid: true,
1401+
},
1402+
},
1403+
)
1404+
if err != nil {
1405+
return fmt.Errorf("failed to query assets at "+
1406+
"anchor: %w", err)
1407+
}
1408+
1409+
if len(assetsAtAnchor) == 0 {
1410+
continue
1411+
}
1412+
1413+
// Determine if all assets are tombstones or burns.
1414+
// A tombstone asset is marked as "spent" at the asset
1415+
// level but its anchor UTXO may still be unspent
1416+
// on-chain and available for sweeping.
1417+
allZeroValue := true
1418+
for _, chainAsset := range assetsAtAnchor {
1419+
aAsset := chainAsset.Asset
1420+
1421+
if !aAsset.IsTombstone() && !aAsset.IsBurn() {
1422+
allZeroValue = false
1423+
break
1424+
}
1425+
}
1426+
1427+
if !allZeroValue {
1428+
continue
1429+
}
1430+
1431+
log.Debugf("Adding zero-value anchor to sweep list "+
1432+
"(outpoint=%s)", anchorPoint.String())
1433+
1434+
internalKey, err := btcec.ParsePubKey(u.RawKey)
1435+
if err != nil {
1436+
return err
1437+
}
1438+
1439+
// Fetch the chain transaction to get the actual
1440+
// pkScript.
1441+
chainTx, err := q.FetchChainTx(ctx, anchorPoint.Hash[:])
1442+
if err != nil {
1443+
log.Warnf("Failed to fetch chain tx for "+
1444+
"%v: %v, skipping", anchorPoint, err)
1445+
continue
1446+
}
1447+
1448+
// Extract the pkScript from the transaction.
1449+
var tx wire.MsgTx
1450+
err = tx.Deserialize(
1451+
bytes.NewReader(chainTx.RawTx),
1452+
)
1453+
if err != nil {
1454+
log.Warnf("Failed to deserialize tx for "+
1455+
"%v: %v, skipping", anchorPoint, err)
1456+
continue
1457+
}
1458+
1459+
pkScript := tx.TxOut[anchorPoint.Index].PkScript
1460+
1461+
mu := &tapfreighter.ZeroValueInput{
1462+
OutPoint: anchorPoint,
1463+
OutputValue: btcutil.Amount(u.AmtSats),
1464+
InternalKey: keychain.KeyDescriptor{
1465+
PubKey: internalKey,
1466+
KeyLocator: keychain.KeyLocator{
1467+
Index: uint32(u.KeyIndex),
1468+
Family: keychain.KeyFamily(
1469+
u.KeyFamily,
1470+
),
1471+
},
1472+
},
1473+
MerkleRoot: u.MerkleRoot,
1474+
PkScript: pkScript,
1475+
}
1476+
1477+
results = append(results, mu)
1478+
}
1479+
1480+
return nil
1481+
})
1482+
if dbErr != nil {
1483+
return nil, dbErr
1484+
}
1485+
1486+
return results, nil
1487+
}
1488+
13231489
// FetchAssetProofsSizes fetches the sizes of the proofs in the db.
13241490
func (a *AssetStore) FetchAssetProofsSizes(
13251491
ctx context.Context) ([]AssetProofSize, error) {
@@ -2476,6 +2642,30 @@ func (a *AssetStore) LogPendingParcel(ctx context.Context,
24762642
}
24772643
}
24782644

2645+
// Also extend leases for any zero-value UTXOs being swept.
2646+
for _, zeroValueInput := range spend.ZeroValueInputs {
2647+
outpointBytes, err := encodeOutpoint(
2648+
zeroValueInput.OutPoint,
2649+
)
2650+
if err != nil {
2651+
return fmt.Errorf("unable to encode "+
2652+
"zero-value outpoint: %w", err)
2653+
}
2654+
2655+
err = q.UpdateUTXOLease(ctx, UpdateUTXOLease{
2656+
LeaseOwner: finalLeaseOwner[:],
2657+
LeaseExpiry: sql.NullTime{
2658+
Time: finalLeaseExpiry.UTC(),
2659+
Valid: true,
2660+
},
2661+
Outpoint: outpointBytes,
2662+
})
2663+
if err != nil {
2664+
return fmt.Errorf("unable to extend "+
2665+
" zero-value UTXO lease: %w", err)
2666+
}
2667+
}
2668+
24792669
// Then the passive assets.
24802670
if len(spend.PassiveAssets) > 0 {
24812671
if spend.PassiveAssetsAnchor == nil {
@@ -3302,9 +3492,25 @@ func (a *AssetStore) LogAnchorTxConfirm(ctx context.Context,
33023492
// Keep the old proofs as a reference for when we list past
33033493
// transfers.
33043494

3305-
// At this point we could delete the managed UTXO since it's no
3306-
// longer an unspent output, however we'll keep it in order to
3307-
// be able to reconstruct transfer history.
3495+
// Mark all zero-value UTXOs as swept since they were spent
3496+
// as additional inputs to the Bitcoin transaction.
3497+
for _, zeroValueInput := range conf.ZeroValueInputs {
3498+
outpoint := zeroValueInput.OutPoint
3499+
outpointBytes, err := encodeOutpoint(outpoint)
3500+
if err != nil {
3501+
return fmt.Errorf("failed to encode "+
3502+
"zero-value outpoint: %w", err)
3503+
}
3504+
3505+
err = q.MarkManagedUTXOAsSwept(ctx, outpointBytes)
3506+
if err != nil {
3507+
return fmt.Errorf("unable to mark zero-value "+
3508+
"UTXO as swept: %w", err)
3509+
}
3510+
3511+
log.Debugf("Marked zero-value UTXO %v as swept",
3512+
outpoint)
3513+
}
33083514

33093515
// We now insert in the DB any burns that may have been present
33103516
// in the transfer.

tapdb/migrations.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const (
2424
// daemon.
2525
//
2626
// NOTE: This MUST be updated when a new migration is added.
27-
LatestMigrationVersion = 47
27+
LatestMigrationVersion = 48
2828
)
2929

3030
// DatabaseBackend is an interface that contains all methods our different

tapdb/sqlc/assets.sql.go

Lines changed: 19 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- Remove swept flag from managed_utxos table
2+
ALTER TABLE managed_utxos DROP COLUMN swept;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- Add swept flag to managed_utxos table to track when UTXOs have been swept
2+
ALTER TABLE managed_utxos ADD COLUMN swept BOOLEAN NOT NULL DEFAULT FALSE;

tapdb/sqlc/models.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tapdb/sqlc/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)