8000 Implement wallet lock/unlock utxos by peterjan · Pull Request #1896 · SiaFoundation/renterd · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Implement wallet lock/unlock utxos #1896

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

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

# Extend store with utxo lock/unlock methods to implement the SingleAddressStore interface
5 changes: 5 additions & 0 deletions .changeset/tmp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

# tmp
2 changes: 1 addition & 1 deletion bus/bus.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ type (
SignTransaction(txn *types.Transaction, toSign []types.Hash256, cf types.CoveredFields)
SignV2Inputs(txn *types.V2Transaction, toSign []int)
SpendableOutputs() ([]types.SiacoinElement, error)
Tip() types.ChainIndex
Tip() (types.ChainIndex, error)
UnconfirmedEvents() ([]wallet.Event, error)
UpdateChainState(tx wallet.UpdateTx, reverted []chain.RevertUpdate, applied []chain.ApplyUpdate) error
Events(offset, limit int) ([]wallet.Event, error)
Expand Down
7 changes: 6 additions & 1 deletion bus/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,10 +363,15 @@ func (b *Bus) walletHandler(jc jape.Context) {
return
}

tip, err := b.w.Tip()
if jc.Check("couldn't fetch wallet tip", err) != nil {
return
}

api.WriteResponse(jc, api.WalletResponse{
Balance: balance,
Address: address,
ScanHeight: b.w.Tip().Height,
ScanHeight: tip.Height,
})
}

Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ require (
github.com/montanaflynn/stats v0.7.1
github.com/shopspring/decimal v1.4.0
go.sia.tech/core v0.13.1
go.sia.tech/coreutils v0.15.2
go.sia.tech/coreutils v0.15.3-0.20250604114829-b3300929a670
go.sia.tech/gofakes3 v0.0.5
go.sia.tech/hostd/v2 v2.2.3
go.sia.tech/hostd/v2 v2.2.4-0.20250604115849-44d1cd1df570
go.sia.tech/jape v0.14.0
go.sia.tech/mux v1.4.0
go.sia.tech/web/renterd v0.79.0
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,20 @@ go.sia.tech/core v0.13.1 h1:dBKzZBhWZsgdV7qZa6qiaZtTDj5evvSqYWpeGNenlRI=
go.sia.tech/core v0.13.1/go.mod h1:oMOgHT4bf9VSXUCOgtt9w4MFns/pY0LRUgwyMXdxW5w=
go.sia.tech/coreutils v0.15.2 h1:2oEe8wpsmU5WVNfe0x75URhno+lPSXc+ozRtZNgjzu4=
go.sia.tech/coreutils v0.15.2/go.mod h1:Kz/VQViqymnR1EW7DDdKrQru8dMFxiY44/qmjriilWs=
go.sia.tech/coreutils v0.15.3-0.2025060307 6D47 5522-331930111593 h1:xvF/E+QbLLGRNCdQ68MtiRjLObF9tA2Hpf2fNE9EdqU=
go.sia.tech/coreutils v0.15.3-0.20250603075522-331930111593/go.mod h1:Kz/VQViqymnR1EW7DDdKrQru8dMFxiY44/qmjriilWs=
go.sia.tech/coreutils v0.15.3-0.20250603082923-201fa2c389dc h1:jlpNcPiQyD9rcD1ch61P+VFx1rRtlyvr42OnYtyxeks=
go.sia.tech/coreutils v0.15.3-0.20250603082923-201fa2c389dc/go.mod h1:Kz/VQViqymnR1EW7DDdKrQru8dMFxiY44/qmjriilWs=
go.sia.tech/coreutils v0.15.3-0.20250604114829-b3300929a670 h1:5QHhLKGWuRVfLuyJmhcyxVIL7/bl+F86hkZ46rUO0Fo=
go.sia.tech/coreutils v0.15.3-0.20250604114829-b3300929a670/go.mod h1:Kz/VQViqymnR1EW7DDdKrQru8dMFxiY44/qmjriilWs=
go.sia.tech/gofakes3 v0.0.5 h1:vFhVBUFbKE9ZplvLE2w4TQxFMQyF8qvgxV4TaTph+Vw=
go.sia.tech/gofakes3 v0.0.5/go.mod h1:LXEzwGw+OHysWLmagleCttX93cJZlT9rBu/icOZjQ54=
go.sia.tech/hostd/v2 v2.2.3 h1:GUILm15ZFWukIk2hOH7SNKm/9IrHNX33EW2TOGlVvxA=
go.sia.tech/hostd/v2 v2.2.3/go.mod h1:nooL0MwckWudt+Qv1EJdQvxbi32xFfcGHq2xYl99uD0=
go.sia.tech/hostd/v2 v2.2.4-0.20250603081001-f34e0830b49d h1:+cPKCK/jcCJWynaDQkJo6nA+IPYBa9kIHobimCULd00=
go.sia.tech/hostd/v2 v2.2.4-0.20250603081001-f34e0830b49d/go.mod h1:TgjGGQWKqb1EnyWUzqVMKO4Psbunaxy4tenLeHdRQ08=
go.sia.tech/hostd/v2 v2.2.4-0.20250604115849-44d1cd1df570 h1:D4tpJgEUPSEO3WJ1OgatjYhkH2N0Hd7o5XGff5svPIE=
go.sia.tech/hostd/v2 v2.2.4-0.20250604115849-44d1cd1df570/go.mod h1:eRgQHyiA46r14YWC2PvmhCVO1VnwiN7WInqb88Hobdc=
go.sia.tech/jape v0.14.0 h1:hyocTKqvcji+rC1vDE1djINlpErQQVDS6zoLMmxW3Xs=
go.sia.tech/jape v0.14.0/go.mod h1:tONxoKrNr0iQWzBCygwlTkGoGjuEhyVpLGInvGd2mGY=
go.sia.tech/mux v1.4.0 h1:LgsLHtn7l+25MwrgaPaUCaS8f2W2/tfvHIdXps04sVo=
Expand Down
6 changes: 6 additions & 0 deletions internal/sql/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,12 @@ var (
return performMigration(ctx, tx, migrationsFs, dbIdentifier, "00035_fix_ns_ms", log)
},
},
{
ID: "00036_wallet_locked_outputs",
Migrate: func(tx Tx) error {
return performMigration(ctx, tx, migrationsFs, dbIdentifier, "00036_wallet_locked_outputs", log)
},
},
}
}
MetricsMigrations = func(ctx context.Context, migrationsFs embed.FS, log *zap.SugaredLogger) []Migration {
Expand Down
2 changes: 2 additions & 0 deletions internal/test/e2e/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,8 @@ func (c *TestCluster) MineBlocks(n uint64) {
}

func (c *TestCluster) sync() {
c.tt.Helper()

tip := c.cm.Tip()
c.tt.Retry(10000, time.Millisecond, func() error {
cs, err := c.Bus.ConsensusState(context.Background())
Expand Down
4 changes: 2 additions & 2 deletions stores/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ func TestProcessChainUpdate(t *testing.T) {
}

// assert wallet state elements
sces, err := ss.UnspentSiacoinElements()
_, sces, err := ss.UnspentSiacoinElements()
if err != nil {
t.Fatal("unexpected error", err)
} else if len(sces) != 1 {
Expand Down Expand Up @@ -323,7 +323,7 @@ func TestProcessChainUpdate(t *testing.T) {
}

// assert wallet state elements
sces, err = ss.UnspentSiacoinElements()
_, sces, err = ss.UnspentSiacoinElements()
if err != nil {
t.Fatal("unexpected error", err)
} else if len(sces) != 1 {
Expand Down
16 changes: 15 additions & 1 deletion stores/sql/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ type (
Tip(ctx context.Context) (types.ChainIndex, error)

// UnspentSiacoinElements returns all wallet outputs in the database.
UnspentSiacoinElements(ctx context.Context) ([]types.SiacoinElement, error)
UnspentSiacoinElements(ctx context.Context) ([]types.SiacoinElement, types.ChainIndex, error)

// UpdateAutopilotConfig updates the autopilot config in the database.
UpdateAutopilotConfig(ctx context.Context, ap api.AutopilotConfig) error
Expand Down Expand Up @@ -361,6 +361,20 @@ type (

// WalletEventCount returns the total number of events in the database.
WalletEventCount(ctx context.Context) (uint64, error)

// WalletLockOutputs locks the given output until the given unlock time.
// If the output is already locked, it is updated. The unlock time
// should be in the future.
WalletLockOutputs(ctx context.Context, scois []types.SiacoinOutputID, until time.Time) error

// WalletLockedOutputs returns the IDs of all locked output. A locked
// output is one that has an unlock timestamp greater than the given
// threshold.
WalletLockedOutputs(ctx context.Context, threshold time.Time) ([]types.SiacoinOutputID, error)

// WalletReleaseOutputs unlocks the given outputs. If the outputs is not
// locked, it is ignored.
WalletReleaseOutputs(ctx context.Context, scois []types.SiacoinOutputID) error
}

MetricsDatabase interface {
Expand Down
48 changes: 45 additions & 3 deletions stores/sql/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2137,17 +2137,22 @@ func UpdateSlab(ctx context.Context, tx Tx, key object.EncryptionKey, updated []
return tx.UpsertContractSectors(ctx, upsert)
}

func UnspentSiacoinElements(ctx context.Context, tx sql.Tx) (elements []types.SiacoinElement, err error) {
func UnspentSiacoinElements(ctx context.Context, tx sql.Tx) (elements []types.SiacoinElement, tip types.ChainIndex, err error) {
tip, err = Tip(ctx, tx)
if err != nil {
return nil, types.ChainIndex{}, fmt.Errorf("failed to fetch chain tip: %w", err)
}

rows, err := tx.Query(ctx, "SELECT output_id, leaf_index, merkle_proof, address, value, maturity_height FROM wallet_outputs")
if err != nil {
return nil, fmt.Errorf("failed to fetch wallet events: %w", err)
return nil, types.ChainIndex{}, fmt.Errorf("failed to fetch wallet events: %w", err)
}
defer rows.Close()

for rows.Next() {
element, err := scanSiacoinElement(rows)
if err != nil {
return nil, fmt.Errorf("failed to scan wallet event: %w", err)
return nil, types.ChainIndex{}, fmt.Errorf("failed to scan wallet event: %w", err)
}
elements = append(elements, element)
}
Expand Down Expand Up @@ -2285,6 +2290,43 @@ func WalletEventCount(ctx context.Context, tx sql.Tx) (count uint64, err error)
return uint64(n), nil
}

func WalletLockedOutputs(ctx context.Context, tx sql.Tx, threshold time.Time) ([]types.SiacoinOutputID, error) {
rows, err := tx.Query(ctx, `SELECT output_id FROM wallet_locked_outputs WHERE unlock_timestamp > ?`, UnixTimeMS(threshold))
if err != nil {
return nil, fmt.Errorf("failed to query locked outputs: %w", err)
}
defer rows.Close()

var locked []types.SiacoinOutputID
for rows.Next() {
var id types.SiacoinOutputID
if err := rows.Scan((*Hash256)(&id)); err != nil {
return nil, fmt.Errorf("failed to scan locked output: %w", err)
}
locked = append(locked, id)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("failed to iterate locked outputs: %w", err)
}

return locked, nil
}

func WalletReleaseOutputs(ctx context.Context, tx sql.Tx, scois []types.SiacoinOutputID) error {
if len(scois) == 0 {
return nil
}

var args []any
for _, id := range scois {
args = append(args, Hash256(id))
}
args = append(args, UnixTimeMS(time.Now()))

_, err := tx.Exec(ctx, fmt.Sprintf(`DELETE FROM wallet_locked_outputs WHERE output_id IN (%s) OR unlock_timestamp < ?`, strings.Repeat("?, ", len(scois)-1)+"?"), args...)
return err
}

func scanBucket(s Scanner) (api.Bucket, error) {
var createdAt time.Time
var name, policy string
Expand Down
27 changes: 26 additions & 1 deletion stores/sql/mysql/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -919,7 +919,7 @@ func (tx *MainDatabaseTx) Tip(ctx context.Context) (types.ChainIndex, error) {
return ssql.Tip(ctx, tx.Tx)
}

func (tx *MainDatabaseTx) UnspentSiacoinElements(ctx context.Context) (elements []types.SiacoinElement, err error) {
func (tx *MainDatabaseTx) UnspentSiacoinElements(ctx context.Context) (elements []types.SiacoinElement, tip types.ChainIndex, err error) {
return ssql.UnspentSiacoinElements(ctx, tx.Tx)
}

Expand Down Expand Up @@ -1177,6 +1177,31 @@ func (tx *MainDatabaseTx) WalletEventCount(ctx context.Context) (count uint64, e
return ssql.WalletEventCount(ctx, tx.Tx)
}

func (tx *MainDatabaseTx) WalletLockOutputs(ctx context.Context, scois []types.SiacoinOutputID, until time.Time) error {
stmt, err := tx.Prepare(ctx, `INSERT INTO wallet_locked_outputs (created_at, output_id, unlock_timestamp) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE unlock_timestamp = VALUES(unlock_timestamp)`)
if err != nil {
return fmt.Errorf("failed to prepare statement to lock outputs: %w", err)
}
defer stmt.Close()

for _, id := range scois {
if _, err := stmt.Exec(ctx, time.Now(), ssql.Hash256(id), ssql.UnixTimeMS(until)); err != nil {
return fmt.Errorf("failed to lock output %s: %w", id, err)
}
}

_, err = tx.Exec(ctx, `DELETE FROM wallet_locked_outputs WHERE unlock_timestamp < ?`, ssql.UnixTimeMS(time.Now()))
return err
}

func (tx *MainDatabaseTx) WalletLockedOutputs(ctx context.Context, threshold time.Time) ([]types.SiacoinOutputID, error) {
return ssql.WalletLockedOutputs(ctx, tx.Tx, threshold)
}

func (tx *MainDatabaseTx) WalletReleaseOutputs(ctx context.Context, scois []types.SiacoinOutputID) error {
return ssql.WalletReleaseOutputs(ctx, tx.Tx, scois)
}

func (tx *MainDatabaseTx) insertSlabs(ctx context.Context, objID, partID *int64, slices object.SlabSlices) error {
if (objID == nil) == (partID == nil) {
return errors.New("exactly one of objID and partID must be set")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE `wallet_locked_outputs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`created_at` datetime(3) DEFAULT NULL,
`output_id` varbinary(32) NOT NULL,
`unlock_timestamp` bigint NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_wallet_locked_outputs_output_id` (`output_id`),
KEY `idx_wallet_locked_outputs_unlock_timestamp` (`unlock_timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
11 changes: 11 additions & 0 deletions stores/sql/mysql/migrations/main/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,17 @@ CREATE TABLE `wallet_outputs` (
KEY `idx_wallet_outputs_maturity_height` (`maturity_height`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- locked outputs
CREATE TABLE `wallet_locked_outputs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`created_at` datetime(3) DEFAULT NULL,
`output_id` varbinary(32) NOT NULL,
`unlock_timestamp` bigint NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_wallet_locked_outputs_output_id` (`output_id`),
KEY `idx_wallet_locked_outputs_unlock_timestamp` (`unlock_timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- contract elements
CREATE TABLE `contract_elements` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
Expand Down
27 changes: 26 additions & 1 deletion stores/sql/sqlite/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,7 @@ func (tx *MainDatabaseTx) Tip(ctx context.Context) (types.ChainIndex, error) {
return ssql.Tip(ctx, tx.Tx)
}

func (tx *MainDatabaseTx) UnspentSiacoinElements(ctx context.Context) (elements []types.SiacoinElement, err error) {
func (tx *MainDatabaseTx) UnspentSiacoinElements(ctx context.Context) (elements []types.SiacoinElement, tip types.ChainIndex, err error) {
return ssql.UnspentSiacoinElements(ctx, tx.Tx)
}

Expand Down Expand Up @@ -1175,6 +1175,31 @@ func (tx *MainDatabaseTx) WalletEventCount(ctx context.Context) (count uint64, e
return ssql.WalletEventCount(ctx, tx.Tx)
}

func (tx *MainDatabaseTx) WalletLockOutputs(ctx context.Context, scois []types.SiacoinOutputID, until time.Time) error {
stmt, err := tx.Prepare(ctx, `INSERT INTO wallet_locked_outputs (created_at, output_id, unlock_timestamp) VALUES (?, ?, ?) ON CONFLICT (output_id) DO UPDATE SET unlock_timestamp=EXCLUDED.unlock_timestamp`)
if err != nil {
return fmt.Errorf("failed to prepare statement to lock outputs: %w", err)
}
defer stmt.Close()

for _, id := range scois {
if _, err := stmt.Exec(ctx, time.Now(), ssql.Hash256(id), ssql.UnixTimeMS(until)); err != nil {
return fmt.Errorf("failed to lock output %s: %w", id, err)
}
}

_, err = tx.Exec(ctx, `DELETE FROM wallet_locked_outputs WHERE unlock_timestamp < ?`, ssql.UnixTimeMS(time.Now()))
return err
}

func (tx *MainDatabaseTx) WalletLockedOutputs(ctx context.Context, threshold time.Time) ([]types.SiacoinOutputID, error) {
return ssql.WalletLockedOutputs(ctx, tx.Tx, threshold)
}

func (tx *MainDatabaseTx) WalletReleaseOutputs(ctx context.Context, scois []types.SiacoinOutputID) error {
return ssql.WalletReleaseOutputs(ctx, tx.Tx, scois)
}

func (tx *MainDatabaseTx) insertSlabs(ctx context.Context, objID, partID *int64, slices object.SlabSlices) error {
if (objID == nil) == (partID == nil) {
return errors.New("exactly one of objID and partID must be set")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CREATE TABLE `wallet_locked_outputs` (`id` integer PRIMARY KEY AUTOINCREMENT,`created_at` datetime, `output_id` blob NOT NULL, `unlock_timestamp` integer NOT NULL);
CREATE UNIQUE INDEX `idx_wallet_locked_outputs_output_id` ON `wallet_locked_outputs`(`output_id`);
CREATE INDEX `idx_wallet_locked_outputs_unlock_timestamp` ON `wallet_locked_outputs`(`unlock_timestamp`);
5 changes: 5 additions & 0 deletions stores/sql/sqlite/migrations/main/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ CREATE TABLE `wallet_outputs` (`id` integer PRIMARY KEY AUTOINCREMENT,`created_a
CREATE UNIQUE INDEX `idx_wallet_outputs_output_id` ON `wallet_outputs`(`output_id`);
CREATE INDEX `idx_wallet_outputs_maturity_height` ON `wallet_outputs`(`maturity_height`);

-- locked outputs
CREATE TABLE `wallet_locked_outputs` (`id` integer PRIMARY KEY AUTOINCREMENT,`created_at` datetime, `output_id` blob NOT NULL, `unlock_timestamp` integer NOT NULL);
CREATE UNIQUE INDEX `idx_wallet_locked_outputs_output_id` ON `wallet_locked_outputs`(`output_id`);
CREATE INDEX `idx_wallet_locked_outputs_unlock_timestamp` ON `wallet_locked_outputs`(`unlock_timestamp`);

-- contract elements
CREATE TABLE `contract_elements` (
`id` integer PRIMARY KEY AUTOINCREMENT,
Expand Down
27 changes: 20 additions & 7 deletions stores/wallet.go
8AFD
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ func (s *SQLStore) Tip() (ci types.ChainIndex, err error) {
}

// UnspentSiacoinElements returns a list of all unspent siacoin outputs
func (s *SQLStore) UnspentSiacoinElements() (elements []types.SiacoinElement, err error) {
err = s.db.Transaction(context.Background(), func(tx sql.DatabaseTx) (err error) {
elements, err = tx.UnspentSiacoinElements(context.Background())
func (s *SQLStore) UnspentSiacoinElements() (tip types.ChainIndex, elements []types.SiacoinElement, err error) {
err = s.db.Transaction(s.shutdownCtx, func(tx sql.DatabaseTx) (err error) {
elements, tip, err = tx.UnspentSiacoinElements(context.Background())
return
})
return
Expand All @@ -51,14 +51,27 @@ func (s *SQLStore) WalletEventCount() (count uint64, err error) {
return
}

// LockUTXOs locks the specified UTXOs until the specified time.
func (s *SQLStore) LockUTXOs(scois []types.SiacoinOutputID, until time.Time) error {
return nil
return s.db.Transaction(s.shutdownCtx, func(tx sql.DatabaseTx) error {
return tx.WalletLockOutputs(s.shutdownCtx, scois, until)
})
}

func (s *SQLStore) LockedUTXOs(t time.Time) ([]types.SiacoinOutputID, error) {
return nil, nil
// LockedUTXOs returns a list of UTXOs that are currently locked until the
// specified time.
func (s *SQLStore) LockedUTXOs(t time.Time) (utxos []types.SiacoinOutputID, err error) {
err = s.db.Transaction(s.shutdownCtx, func(tx sql.DatabaseTx) (err error) {
utxos, err = tx.WalletLockedOutputs(s.shutdownCtx, t)
return
})
return
}

// ReleaseUTXOs releases the specified UTXOs, making them available for use
// again. If the UTXOs are not locked, this is a no-op.
func (s *SQLStore) ReleaseUTXOs(scois []types.SiacoinOutputID) error {
return nil
return s.db.Transaction(s.shutdownCtx, func(tx sql.DatabaseTx) error {
return tx.WalletReleaseOutputs(s.shutdownCtx, scois)
})
}
Loading
Loading
0