Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
10 changes: 10 additions & 0 deletions asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -1803,6 +1803,16 @@ func (a *Asset) IsBurn() bool {
return IsBurnKey(a.ScriptKey.PubKey, a.PrevWitnesses[0])
}

// IsTombstone returns true if an asset uses the NUMS script key and has zero
// value.
func (a *Asset) IsTombstone() bool {
if a.ScriptKey.PubKey == nil {
return false
}

return a.Amount == 0 && a.ScriptKey.PubKey.IsEqual(NUMSPubKey)
}

// PrimaryPrevID returns the primary prev ID of an asset. This is the prev ID of
// the first witness, unless the first witness is a split-commitment witness,
// in which case it is the prev ID of the first witness of the root asset.
Expand Down
207 changes: 204 additions & 3 deletions tapdb/assets_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ type (
// ManagedUTXORow wraps a managed UTXO listing row.
ManagedUTXORow = sqlc.FetchManagedUTXOsRow

// MarkManagedUTXOAsSweptParams wraps the params needed to associate a
// managed UTXO with the chain transaction that swept it.
MarkManagedUTXOAsSweptParams = sqlc.MarkManagedUTXOAsSweptParams

// UpdateUTXOLease wraps the params needed to lease a managed UTXO.
UpdateUTXOLease = sqlc.UpdateUTXOLeaseParams

Expand Down Expand Up @@ -279,6 +283,11 @@ type ActiveAssetsStore interface {
// serialized outpoint.
DeleteManagedUTXO(ctx context.Context, outpoint []byte) error

// MarkManagedUTXOAsSwept marks a managed UTXO as swept by recording
// the chain transaction that spent it.
MarkManagedUTXOAsSwept(ctx context.Context,
arg MarkManagedUTXOAsSweptParams) error

// UpdateUTXOLease leases a managed UTXO identified by the passed
// serialized outpoint.
UpdateUTXOLease(ctx context.Context, arg UpdateUTXOLease) error
Expand Down Expand Up @@ -1320,6 +1329,154 @@ func (a *AssetStore) FetchManagedUTXOs(ctx context.Context) (
return managedUtxos, nil
}

// FetchZeroValueAnchorUTXOs fetches all managed UTXOs that contain only
// zero-value assets (tombstones and burns).
func (a *AssetStore) FetchZeroValueAnchorUTXOs(ctx context.Context) (
[]*tapfreighter.ZeroValueInput, error) {

// Strategy: fetch all managed UTXOs and filter in-memory.
// A UTXO is a "zero-value anchor" if all assets are either tombstones
// (NUMS key with amount 0) or burns.
// We exclude leased and spent UTXOs.

var results []*tapfreighter.ZeroValueInput

readOpts := NewAssetStoreReadTx()
now := a.clock.Now().UTC()

dbErr := a.db.ExecTx(ctx, &readOpts, func(q ActiveAssetsStore) error {
utxos, err := q.FetchManagedUTXOs(ctx)
if err != nil {
return fmt.Errorf("failed to fetch managed "+
"utxos: %w", err)
}

for _, u := range utxos {
if len(u.LeaseOwner) > 0 &&
u.LeaseExpiry.Valid &&
u.LeaseExpiry.Time.UTC().After(now) {

continue
}

if u.SweptTxnID.Valid {
continue
}

var anchorPoint wire.OutPoint
err := readOutPoint(
bytes.NewReader(u.Outpoint), 0, 0, &anchorPoint)
Comment on lines +1367 to +1368
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trailing ) should be on a new line

if err != nil {
return fmt.Errorf("failed to read "+
"outpoint: %w", err)
}

// Query all assets anchored at this outpoint.
// We include spent assets here because tombstones are
// marked as spent when created.
assetsAtAnchor, err := a.queryChainAssets(
ctx, q, QueryAssetFilters{
AnchorPoint: u.Outpoint,
Now: sql.NullTime{
Time: now,
Valid: true,
},
},
)
if err != nil {
return fmt.Errorf("failed to query chain "+
"assets at anchor: %w", err)
}

if len(assetsAtAnchor) == 0 {
continue
}

// Determine if all assets are tombstones or burns.
// A tombstone asset is marked as "spent" at the asset
// level but its anchor UTXO may still be unspent
// on-chain and available for sweeping.
allZeroValue := true
for _, chainAsset := range assetsAtAnchor {
aAsset := chainAsset.Asset

if !aAsset.IsTombstone() && !aAsset.IsBurn() {
allZeroValue = false
break
}
}

if !allZeroValue {
continue
}

log.DebugS(ctx, "adding orphan utxo to sweep list",
"outpoint", anchorPoint.String())

internalKey, err := btcec.ParsePubKey(u.RawKey)
if err != nil {
return fmt.Errorf("failed to parse internal"+
"key: %w", err)
}

// Fetch the chain transaction to get the actual
// pkScript.
chainTx, err := q.FetchChainTx(ctx, anchorPoint.Hash[:])
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
log.Warnf("chain tx not found for "+
"outpoint%v: %w, skipping",
anchorPoint, err)

continue
}

return fmt.Errorf("failed to fetch chain tx "+
"for outpoint %v: %w", anchorPoint, err)
}

// Extract the pkScript from the transaction.
var tx wire.MsgTx
err = tx.Deserialize(
bytes.NewReader(chainTx.RawTx),
)
if err != nil {
return fmt.Errorf("failed to deserialize "+
"chain tx for outpoint %v: %w",
anchorPoint, err)
}

pkScript := tx.TxOut[anchorPoint.Index].PkScript

mu := &tapfreighter.ZeroValueInput{
OutPoint: anchorPoint,
OutputValue: btcutil.Amount(u.AmtSats),
InternalKey: keychain.KeyDescriptor{
PubKey: internalKey,
KeyLocator: keychain.KeyLocator{
Index: uint32(u.KeyIndex),
Family: keychain.KeyFamily(
u.KeyFamily,
),
},
},
MerkleRoot: u.MerkleRoot,
PkScript: pkScript,
}

results = append(results, mu)
}

return nil
})
if dbErr != nil {
return nil, fmt.Errorf("failed to fetch orphan "+
"utxos: %w", dbErr)
}

return results, nil
}

// FetchAssetProofsSizes fetches the sizes of the proofs in the db.
func (a *AssetStore) FetchAssetProofsSizes(
ctx context.Context) ([]AssetProofSize, error) {
Expand Down Expand Up @@ -2476,6 +2633,30 @@ func (a *AssetStore) LogPendingParcel(ctx context.Context,
}
}

// Also extend leases for any zero-value UTXOs being swept.
for _, zeroValueInput := range spend.ZeroValueInputs {
outpointBytes, err := encodeOutpoint(
zeroValueInput.OutPoint,
)
if err != nil {
return fmt.Errorf("unable to encode "+
"zero-value outpoint: %w", err)
}

err = q.UpdateUTXOLease(ctx, UpdateUTXOLease{
LeaseOwner: finalLeaseOwner[:],
LeaseExpiry: sql.NullTime{
Time: finalLeaseExpiry.UTC(),
Valid: true,
},
Comment on lines +2648 to +2651
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be a helper like sqlInt16

Outpoint: outpointBytes,
})
if err != nil {
return fmt.Errorf("unable to extend "+
" zero-value UTXO lease: %w", err)
}
}

// Then the passive assets.
if len(spend.PassiveAssets) > 0 {
if spend.PassiveAssetsAnchor == nil {
Expand Down Expand Up @@ -3302,9 +3483,29 @@ func (a *AssetStore) LogAnchorTxConfirm(ctx context.Context,
// Keep the old proofs as a reference for when we list past
// transfers.

// At this point we could delete the managed UTXO since it's no
// longer an unspent output, however we'll keep it in order to
// be able to reconstruct transfer history.
// Mark all zero-value UTXOs as swept since they were spent
// as additional inputs to the Bitcoin transaction.
for _, zeroValueInput := range conf.ZeroValueInputs {
outpoint := zeroValueInput.OutPoint
outpointBytes, err := encodeOutpoint(outpoint)
if err != nil {
return fmt.Errorf("failed to encode "+
"zero-value outpoint: %w", err)
}

err = q.MarkManagedUTXOAsSwept(ctx,
MarkManagedUTXOAsSweptParams{
Outpoint: outpointBytes,
SweepingTxid: conf.AnchorTXID[:],
})
Comment on lines +3496 to +3500
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trailing ) should be on a new line

if err != nil {
return fmt.Errorf("unable to mark zero-value "+
"UTXO as swept: %w", err)
}

log.Debugf("Marked zero-value UTXO %v as swept",
outpoint)
}

// We now insert in the DB any burns that may have been present
// in the transfer.
Expand Down
2 changes: 1 addition & 1 deletion tapdb/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const (
// daemon.
//
// NOTE: This MUST be updated when a new migration is added.
LatestMigrationVersion = 47
LatestMigrationVersion = 48
)

// DatabaseBackend is an interface that contains all methods our different
Expand Down
29 changes: 27 additions & 2 deletions tapdb/sqlc/assets.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions tapdb/sqlc/migrations/000048_add_swept_flag.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Remove swept transaction reference from managed_utxos table.
ALTER TABLE managed_utxos DROP COLUMN swept_txn_id;
3 changes: 3 additions & 0 deletions tapdb/sqlc/migrations/000048_add_swept_flag.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Record which chain transaction swept a managed UTXO.
ALTER TABLE managed_utxos
ADD COLUMN swept_txn_id BIGINT REFERENCES chain_txns(txn_id);
1 change: 1 addition & 0 deletions tapdb/sqlc/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tapdb/sqlc/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions tapdb/sqlc/queries/assets.sql
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,16 @@ ORDER BY witness_index;
DELETE FROM managed_utxos
WHERE outpoint = $1;

-- name: MarkManagedUTXOAsSwept :exec
WITH spending_tx AS (
SELECT txn_id
FROM chain_txns
WHERE txid = @sweeping_txid
)
UPDATE managed_utxos
SET swept_txn_id = (SELECT txn_id FROM spending_tx)
WHERE outpoint = @outpoint;

-- name: UpdateUTXOLease :exec
UPDATE managed_utxos
SET lease_owner = @lease_owner, lease_expiry = @lease_expiry
Expand Down
2 changes: 1 addition & 1 deletion tapdb/sqlc/schemas/generated_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ CREATE TABLE managed_utxos (
-- expiry is NULL or the timestamp is in the past, then the lease is not
-- valid and the UTXO is available for coin selection.
lease_expiry TIMESTAMP
, root_version SMALLINT);
, root_version SMALLINT, swept_txn_id BIGINT REFERENCES chain_txns(txn_id));

CREATE INDEX mint_anchor_uni_commitments_outpoint_idx
ON mint_supply_pre_commits(outpoint)
Expand Down