-
Notifications
You must be signed in to change notification settings - Fork 137
garbage collect zero-value UTXOs #1832
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 0-8-0-staging
Are you sure you want to change the base?
Changes from 1 commit
aef5d1f
565abe4
551d563
17e710a
280d823
b5a1f49
61a19e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. trailing |
||
| 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. | ||
darioAnongba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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) { | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could be a helper like |
||
| 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 { | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. trailing |
||
| 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. | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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; |
| 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); |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.