diff --git a/cmd/hostd/main.go b/cmd/hostd/main.go index cefd7978..e7f9ff4e 100644 --- a/cmd/hostd/main.go +++ b/cmd/hostd/main.go @@ -23,6 +23,7 @@ import ( "go.sia.tech/hostd/build" "go.sia.tech/hostd/config" "go.sia.tech/hostd/internal/explorer" + "go.sia.tech/hostd/persist/sqlite" "go.sia.tech/jape" "go.sia.tech/web/hostd" "go.uber.org/zap" @@ -231,6 +232,33 @@ func main() { case "config": buildConfig() return + case "rebuild": + fp := flag.Arg(1) + + levelStr := cfg.Log.StdOut.Level + if levelStr == "" { + levelStr = cfg.Log.Level + } + + level := parseLogLevel(levelStr) + core := zapcore.NewCore(humanEncoder(cfg.Log.StdOut.EnableANSI), zapcore.Lock(os.Stdout), level) + log := zap.New(core, zap.AddCaller()) + defer log.Sync() + + db, err := sqlite.OpenDatabase(fp, log) + if err != nil { + log.Fatal("failed to open database", zap.Error(err)) + } + defer db.Close() + + if err := db.CheckContractAccountFunding(); err != nil { + log.Fatal("failed to check contract account funding", zap.Error(err)) + } else if err := db.RecalcContractAccountFunding(); err != nil { + log.Fatal("failed to recalculate contract account funding", zap.Error(err)) + } else if err := db.CheckContractAccountFunding(); err != nil { + log.Fatal("failed to check contract account funding", zap.Error(err)) + } + return } ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) diff --git a/persist/sqlite/recalc.go b/persist/sqlite/recalc.go new file mode 100644 index 00000000..cbb68950 --- /dev/null +++ b/persist/sqlite/recalc.go @@ -0,0 +1,93 @@ +package sqlite + +import ( + "fmt" + + "go.sia.tech/core/types" + "go.uber.org/zap" +) + +func checkContractAccountFunding(tx txn, log *zap.Logger) error { + rows, err := tx.Query(`SELECT contract_id, amount FROM contract_account_funding`) + if err != nil { + return fmt.Errorf("failed to query contract account funding: %w", err) + } + defer rows.Close() + + contractFunding := make(map[int64]types.Currency) + for rows.Next() { + var contractID int64 + var amount types.Currency + if err := rows.Scan(&contractID, (*sqlCurrency)(&amount)); err != nil { + return fmt.Errorf("failed to scan contract account funding: %w", err) + } + contractFunding[contractID] = contractFunding[contractID].Add(amount) + } + + if err := rows.Err(); err != nil { + return fmt.Errorf("failed to iterate contract account funding: %w", err) + } else if err := rows.Close(); err != nil { + return fmt.Errorf("failed to close contract account funding: %w", err) + } + + for contractID, amount := range contractFunding { + var actualAmount types.Currency + err := tx.QueryRow(`SELECT account_funding FROM contracts WHERE id=$1`, contractID).Scan((*sqlCurrency)(&actualAmount)) + if err != nil { + return fmt.Errorf("failed to query contract account funding: %w", err) + } + + if !actualAmount.Equals(amount) { + log.Debug("incorrect contract account funding", zap.Int64("contractID", contractID), zap.Stringer("expected", amount), zap.Stringer("actual", actualAmount)) + } + } + return nil +} + +func recalcContractAccountFunding(tx txn, _ *zap.Logger) error { + rows, err := tx.Query(`SELECT contract_id, amount FROM contract_account_funding`) + if err != nil { + return fmt.Errorf("failed to query contract account funding: %w", err) + } + defer rows.Close() + + contractFunding := make(map[int64]types.Currency) + for rows.Next() { + var contractID int64 + var amount types.Currency + if err := rows.Scan(&contractID, (*sqlCurrency)(&amount)); err != nil { + return fmt.Errorf("failed to scan contract account funding: %w", err) + } + contractFunding[contractID] = contractFunding[contractID].Add(amount) + } + + if err := rows.Err(); err != nil { + return fmt.Errorf("failed to iterate contract account funding: %w", err) + } else if err := rows.Close(); err != nil { + return fmt.Errorf("failed to close contract account funding: %w", err) + } + + for contractID, amount := range contractFunding { + res, err := tx.Exec(`UPDATE contracts SET account_funding=$1 WHERE id=$2`, sqlCurrency(amount), contractID) + if err != nil { + return fmt.Errorf("failed to query contract account funding: %w", err) + } else if rowsAffected, err := res.RowsAffected(); err != nil { + return fmt.Errorf("failed to query contract account funding: %w", err) + } else if rowsAffected != 1 { + return fmt.Errorf("failed to update contract account funding: %w", err) + } + } + return nil +} + +func (s *Store) CheckContractAccountFunding() error { + return s.transaction(func(tx txn) error { + return checkContractAccountFunding(tx, s.log) + }) +} + +func (s *Store) RecalcContractAccountFunding() error { + return s.transaction(func(tx txn) error { + return recalcContractAccountFunding(tx, s.log) + }) +}