Skip to content

Commit 3639f16

Browse files
committed
tapdb: add support for zero-value utxo sweeping
This includes the addition of a field in the table with the corresponding migration and the and functions.
1 parent e892603 commit 3639f16

File tree

9 files changed

+264
-11
lines changed

9 files changed

+264
-11
lines changed

tapdb/assets_store.go

Lines changed: 230 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,39 @@ 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
504+
}
505+
506+
// GetOutPoint returns the outpoint of the zero-value UTXO.
507+
func (m *ManagedUTXO) GetOutPoint() wire.OutPoint {
508+
return m.OutPoint
509+
}
510+
511+
// GetOutputValue returns the satoshi value of the zero-value UTXO.
512+
func (m *ManagedUTXO) GetOutputValue() btcutil.Amount {
513+
return m.OutputValue
514+
}
515+
516+
// GetInternalKey returns the internal key descriptor for the zero-value UTXO.
517+
func (m *ManagedUTXO) GetInternalKey() keychain.KeyDescriptor {
518+
return m.InternalKey
519+
}
520+
521+
// GetMerkleRoot returns the taproot merkle root for the zero-value UTXO.
522+
func (m *ManagedUTXO) GetMerkleRoot() []byte {
523+
return m.MerkleRoot
524+
}
525+
526+
// GetPkScript returns the pkScript of the anchor output.
527+
func (m *ManagedUTXO) GetPkScript() []byte {
528+
return m.PkScript
492529
}
493530

494531
// AssetHumanReadable is a subset of the base asset struct that only includes
@@ -1309,6 +1346,7 @@ func (a *AssetStore) FetchManagedUTXOs(ctx context.Context) (
13091346
MerkleRoot: u.MerkleRoot,
13101347
TapscriptSibling: u.TapscriptSibling,
13111348
LeaseOwner: u.LeaseOwner,
1349+
Swept: u.Swept,
13121350
}
13131351
if u.LeaseExpiry.Valid {
13141352
utxo.LeaseExpiry = u.LeaseExpiry.Time
@@ -1320,6 +1358,179 @@ func (a *AssetStore) FetchManagedUTXOs(ctx context.Context) (
13201358
return managedUtxos, nil
13211359
}
13221360

1361+
// MarkManagedUTXOAsSwept marks a managed UTXO as swept, indicating it has been
1362+
// spent in a Bitcoin transaction.
1363+
func (a *AssetStore) MarkManagedUTXOAsSwept(ctx context.Context,
1364+
outpoint wire.OutPoint) error {
1365+
1366+
outpointBytes, err := encodeOutpoint(outpoint)
1367+
if err != nil {
1368+
return fmt.Errorf("unable to encode outpoint: %w", err)
1369+
}
1370+
1371+
var writeTxOpts AssetStoreTxOptions
1372+
return a.db.ExecTx(ctx, &writeTxOpts, func(q ActiveAssetsStore) error {
1373+
return q.MarkManagedUTXOAsSwept(ctx, outpointBytes)
1374+
})
1375+
}
1376+
1377+
// FetchZeroValueAnchorUTXOs fetches all managed UTXOs that contain only
1378+
// zero-value assets (tombstones and burns).
1379+
func (a *AssetStore) FetchZeroValueAnchorUTXOs(ctx context.Context) (
1380+
[]tapfreighter.ZeroValueInput, error) {
1381+
1382+
// Strategy: fetch all managed UTXOs and filter in-memory.
1383+
// A UTXO is a "zero-value anchor" if all assets are either tombstones
1384+
// (NUMS key with amount 0) or burns.
1385+
// We exclude leased and spent UTXOs.
1386+
1387+
var results []tapfreighter.ZeroValueInput
1388+
1389+
readOpts := NewAssetStoreReadTx()
1390+
now := a.clock.Now().UTC()
1391+
1392+
dbErr := a.db.ExecTx(ctx, &readOpts, func(q ActiveAssetsStore) error {
1393+
utxos, err := q.FetchManagedUTXOs(ctx)
1394+
if err != nil {
1395+
return err
1396+
}
1397+
1398+
for _, u := range utxos {
1399+
// Skip UTXOs that are leased.
1400+
if len(u.LeaseOwner) > 0 &&
1401+
u.LeaseExpiry.Valid &&
1402+
u.LeaseExpiry.Time.UTC().After(now) {
1403+
1404+
continue
1405+
}
1406+
1407+
var anchorPoint wire.OutPoint
1408+
err := readOutPoint(
1409+
bytes.NewReader(u.Outpoint), 0, 0, &anchorPoint)
1410+
if err != nil {
1411+
return err
1412+
}
1413+
1414+
// Skip UTXOs that have already been swept.
1415+
if u.Swept {
1416+
continue
1417+
}
1418+
1419+
// Query all assets anchored at this outpoint.
1420+
// We include spent assets here because tombstones are
1421+
// marked as spent when created.
1422+
anchorPointBytes, err := encodeOutpoint(anchorPoint)
1423+
if err != nil {
1424+
return err
1425+
}
1426+
1427+
assetsAtAnchor, err := a.queryChainAssets(
1428+
ctx, q, QueryAssetFilters{
1429+
AnchorPoint: anchorPointBytes,
1430+
Now: sql.NullTime{
1431+
Time: now,
1432+
Valid: true,
1433+
},
1434+
},
1435+
)
1436+
if err != nil {
1437+
return err
1438+
}
1439+
1440+
// Skip If we don't have any assets recorded.
1441+
if len(assetsAtAnchor) == 0 {
1442+
continue
1443+
}
1444+
1445+
// Determine if all assets are tombstones or burns.
1446+
// A tombstone asset is marked as "spent" at the asset
1447+
// level but its anchor UTXO may still be unspent
1448+
// on-chain and available for sweeping.
1449+
allZeroValue := true
1450+
for _, chainAsset := range assetsAtAnchor {
1451+
aAsset := chainAsset.Asset
1452+
1453+
isTombstone := aAsset.Amount == 0 &&
1454+
aAsset.ScriptKey.PubKey.IsEqual(
1455+
asset.NUMSPubKey,
1456+
)
1457+
1458+
isBurn := len(aAsset.PrevWitnesses) > 0 &&
1459+
asset.IsBurnKey(
1460+
aAsset.ScriptKey.PubKey,
1461+
aAsset.PrevWitnesses[0],
1462+
)
1463+
1464+
if !isTombstone && !isBurn {
1465+
allZeroValue = false
1466+
break
1467+
}
1468+
}
1469+
1470+
if !allZeroValue {
1471+
continue
1472+
}
1473+
1474+
log.Debugf("Anchor %v is zero-value, adding to "+
1475+
"sweep list", anchorPoint)
1476+
1477+
internalKey, err := btcec.ParsePubKey(u.RawKey)
1478+
if err != nil {
1479+
return err
1480+
}
1481+
1482+
// Fetch the chain transaction to get the actual
1483+
// pkScript.
1484+
chainTx, err := q.FetchChainTx(ctx, anchorPoint.Hash[:])
1485+
if err != nil {
1486+
log.Warnf("Failed to fetch chain tx for "+
1487+
"%v: %v, skipping", anchorPoint, err)
1488+
continue
1489+
}
1490+
1491+
// Extract the pkScript from the transaction.
1492+
var tx wire.MsgTx
1493+
if err := tx.Deserialize(
1494+
bytes.NewReader(chainTx.RawTx),
1495+
); err != nil {
1496+
log.Warnf("Failed to deserialize tx for "+
1497+
"%v: %v, skipping", anchorPoint, err)
1498+
continue
1499+
}
1500+
pkScript := tx.TxOut[anchorPoint.Index].PkScript
1501+
1502+
mu := &ManagedUTXO{
1503+
OutPoint: anchorPoint,
1504+
OutputValue: btcutil.Amount(u.AmtSats),
1505+
InternalKey: keychain.KeyDescriptor{
1506+
PubKey: internalKey,
1507+
KeyLocator: keychain.KeyLocator{
1508+
Index: uint32(u.KeyIndex),
1509+
Family: keychain.KeyFamily(
1510+
u.KeyFamily,
1511+
),
1512+
},
1513+
},
1514+
TaprootAssetRoot: u.TaprootAssetRoot,
1515+
MerkleRoot: u.MerkleRoot,
1516+
TapscriptSibling: u.TapscriptSibling,
1517+
LeaseOwner: u.LeaseOwner,
1518+
PkScript: pkScript,
1519+
LeaseExpiry: u.LeaseExpiry.Time,
1520+
}
1521+
1522+
results = append(results, mu)
1523+
}
1524+
1525+
return nil
1526+
})
1527+
if dbErr != nil {
1528+
return nil, dbErr
1529+
}
1530+
1531+
return results, nil
1532+
}
1533+
13231534
// FetchAssetProofsSizes fetches the sizes of the proofs in the db.
13241535
func (a *AssetStore) FetchAssetProofsSizes(
13251536
ctx context.Context) ([]AssetProofSize, error) {
@@ -3302,9 +3513,25 @@ func (a *AssetStore) LogAnchorTxConfirm(ctx context.Context,
33023513
// Keep the old proofs as a reference for when we list past
33033514
// transfers.
33043515

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.
3516+
// Mark all zero-value UTXOs as swept since they were spent
3517+
// as additional inputs to the Bitcoin transaction.
3518+
for _, zeroValueInput := range conf.ZeroValueInputs {
3519+
outpoint := zeroValueInput.GetOutPoint()
3520+
outpointBytes, err := encodeOutpoint(outpoint)
3521+
if err != nil {
3522+
return fmt.Errorf("failed to encode "+
3523+
"zero-value outpoint: %w", err)
3524+
}
3525+
3526+
err = q.MarkManagedUTXOAsSwept(ctx, outpointBytes)
3527+
if err != nil {
3528+
return fmt.Errorf("unable to mark zero-value "+
3529+
"UTXO as swept: %w", err)
3530+
}
3531+
3532+
log.Infof("Marked zero-value UTXO %v as swept",
3533+
outpoint)
3534+
}
33083535

33093536
// We now insert in the DB any burns that may have been present
33103537
// 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)