From 247c9f80288758bde0a91d5110967f55e6e63616 Mon Sep 17 00:00:00 2001 From: Nate Maninger Date: Wed, 2 Aug 2023 08:48:06 -0600 Subject: [PATCH 1/4] sqlite: fix underflow panic --- persist/sqlite/contracts.go | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/persist/sqlite/contracts.go b/persist/sqlite/contracts.go index 981fd52c..1b7071ba 100644 --- a/persist/sqlite/contracts.go +++ b/persist/sqlite/contracts.go @@ -92,18 +92,23 @@ func (u *updateContractsTxn) ConfirmResolution(id types.FileContractID, height u var dbID int64 if err := u.tx.QueryRow(query, height, sqlHash256(id)).Scan(&dbID); err != nil { return fmt.Errorf("failed to confirm resolution: %w", err) - } else if err := setContractStatus(u.tx, id, contracts.ContractStatusSuccessful); err != nil { - return fmt.Errorf("failed to set contract status to ended: %w", err) } - // reduce the host's locked and risked collateral contract, err := getContract(u.tx, id) if err != nil { return fmt.Errorf("failed to get contract: %w", err) - } else if err := incrementCurrencyStat(u.tx, metricLockedCollateral, contract.LockedCollateral, true, time.Now()); err != nil { - return fmt.Errorf("failed to increment locked collateral stat: %w", err) - } else if err := incrementCurrencyStat(u.tx, metricRiskedCollateral, contract.Usage.RiskedCollateral, true, time.Now()); err != nil { - return fmt.Errorf("failed to increment risked collateral stat: %w", err) + } + // only decrement the collateral if the contract was previously active + if contract.Status != contracts.ContractStatusSuccessful && contract.Status != contracts.ContractStatusFailed { + if err := incrementCurrencyStat(u.tx, metricLockedCollateral, contract.LockedCollateral, true, time.Now()); err != nil { + return fmt.Errorf("failed to increment locked collateral stat: %w", err) + } else if err := incrementCurrencyStat(u.tx, metricRiskedCollateral, contract.Usage.RiskedCollateral, true, time.Now()); err != nil { + return fmt.Errorf("failed to increment risked collateral stat: %w", err) + } + } + // set the contract status to successful + if err := setContractStatus(u.tx, id, contracts.ContractStatusSuccessful); err != nil { + return fmt.Errorf("failed to set contract status to ended: %w", err) } return nil } @@ -444,10 +449,14 @@ func (s *Store) ExpireContract(id types.FileContractID, status contracts.Contrac contract, err := getContract(tx, id) if err != nil { return fmt.Errorf("failed to get contract: %w", err) - } else if err := incrementCurrencyStat(tx, metricLockedCollateral, contract.LockedCollateral, true, time.Now()); err != nil { - return fmt.Errorf("failed to increment locked collateral stat: %w", err) - } else if err := incrementCurrencyStat(tx, metricRiskedCollateral, contract.Usage.RiskedCollateral, true, time.Now()); err != nil { - return fmt.Errorf("failed to increment risked collateral stat: %w", err) + } + // only decrement if the contract is not already successful or failed + if contract.Status != contracts.ContractStatusSuccessful && contract.Status != contracts.ContractStatusFailed { + if err := incrementCurrencyStat(tx, metricLockedCollateral, contract.LockedCollateral, true, time.Now()); err != nil { + return fmt.Errorf("failed to increment locked collateral stat: %w", err) + } else if err := incrementCurrencyStat(tx, metricRiskedCollateral, contract.Usage.RiskedCollateral, true, time.Now()); err != nil { + return fmt.Errorf("failed to increment risked collateral stat: %w", err) + } } return nil }) From d00d4ee9c62b56748b77511ea438a88ca5132de2 Mon Sep 17 00:00:00 2001 From: Nate Maninger Date: Wed, 2 Aug 2023 09:09:20 -0600 Subject: [PATCH 2/4] contracts: don't expire contract without proof until window end --- host/contracts/actions.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/host/contracts/actions.go b/host/contracts/actions.go index 44f26f6e..f4966459 100644 --- a/host/contracts/actions.go +++ b/host/contracts/actions.go @@ -156,9 +156,6 @@ func (cm *ContractManager) handleContractAction(id types.FileContractID, height validPayout, missedPayout := contract.Revision.ValidHostPayout(), contract.Revision.MissedHostPayout() if missedPayout.Cmp(validPayout) >= 0 { log.Info("skipping storage proof, no benefit to host", zap.String("validPayout", validPayout.ExactString()), zap.String("missedPayout", missedPayout.ExactString())) - if err := cm.store.ExpireContract(id, ContractStatusSuccessful); err != nil { - log.Error("failed to set contract status", zap.Error(err)) - } return } @@ -244,11 +241,10 @@ func (cm *ContractManager) handleContractAction(id types.FileContractID, height log.Error("contract failed, revenue lost", zap.Uint64("windowStart", contract.Revision.WindowStart), zap.Uint64("windowEnd", contract.Revision.WindowEnd), zap.String("validPayout", validPayout.ExactString()), zap.String("missedPayout", missedPayout.ExactString())) return } - // note: this should always be a no-op, but it's good to be explicit if err := cm.store.ExpireContract(id, ContractStatusSuccessful); err != nil { log.Error("failed to set contract status", zap.Error(err)) } - log.Info("contract expired") + log.Info("contract successful", zap.String("validPayout", validPayout.ExactString()), zap.String("missedPayout", missedPayout.ExactString())) default: log.Panic("unrecognized contract action", zap.Stack("stack")) } From 0843d29a91e321ea6132a33d9b8ecd59ba2ba9d1 Mon Sep 17 00:00:00 2001 From: Nate Maninger Date: Wed, 2 Aug 2023 09:32:37 -0600 Subject: [PATCH 3/4] sqlite: clearer contract metric logic --- persist/sqlite/contracts.go | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/persist/sqlite/contracts.go b/persist/sqlite/contracts.go index 1b7071ba..38f6d7ae 100644 --- a/persist/sqlite/contracts.go +++ b/persist/sqlite/contracts.go @@ -98,8 +98,9 @@ func (u *updateContractsTxn) ConfirmResolution(id types.FileContractID, height u if err != nil { return fmt.Errorf("failed to get contract: %w", err) } - // only decrement the collateral if the contract was previously active - if contract.Status != contracts.ContractStatusSuccessful && contract.Status != contracts.ContractStatusFailed { + // rejected, successful, and failed contracts have already had their + // collateral and revenue removed + if contract.Status == contracts.ContractStatusActive || contract.Status == contracts.ContractStatusPending { if err := incrementCurrencyStat(u.tx, metricLockedCollateral, contract.LockedCollateral, true, time.Now()); err != nil { return fmt.Errorf("failed to increment locked collateral stat: %w", err) } else if err := incrementCurrencyStat(u.tx, metricRiskedCollateral, contract.Usage.RiskedCollateral, true, time.Now()); err != nil { @@ -441,23 +442,24 @@ func (s *Store) ContractFormationSet(id types.FileContractID) ([]types.Transacti // if the contract is active or pending. func (s *Store) ExpireContract(id types.FileContractID, status contracts.ContractStatus) error { return s.transaction(func(tx txn) error { - if err := setContractStatus(tx, id, status); err != nil { - return fmt.Errorf("failed to set contract status: %w", err) - } - // reduce the locked and risked collateral metrics contract, err := getContract(tx, id) if err != nil { return fmt.Errorf("failed to get contract: %w", err) } - // only decrement if the contract is not already successful or failed - if contract.Status != contracts.ContractStatusSuccessful && contract.Status != contracts.ContractStatusFailed { + // successful, failed, and rejected contracts should have already had their + // collateral removed from the metrics + if contract.Status == contracts.ContractStatusActive || contract.Status == contracts.ContractStatusPending { if err := incrementCurrencyStat(tx, metricLockedCollateral, contract.LockedCollateral, true, time.Now()); err != nil { return fmt.Errorf("failed to increment locked collateral stat: %w", err) } else if err := incrementCurrencyStat(tx, metricRiskedCollateral, contract.Usage.RiskedCollateral, true, time.Now()); err != nil { return fmt.Errorf("failed to increment risked collateral stat: %w", err) } } + // update the contract status + if err := setContractStatus(tx, id, status); err != nil { + return fmt.Errorf("failed to set contract status: %w", err) + } return nil }) } @@ -1023,13 +1025,13 @@ func scanContract(row scanner) (c contracts.Contract, err error) { return } -func updateContractMetrics(tx txn, prev, current contracts.ContractStatus) error { - if prev == current { +func updateContractMetrics(tx txn, current, next contracts.ContractStatus) error { + if current == next { return nil } var initialMetric, finalMetric string - switch prev { + switch current { case contracts.ContractStatusPending: initialMetric = metricPendingContracts case contracts.ContractStatusRejected: @@ -1043,7 +1045,7 @@ func updateContractMetrics(tx txn, prev, current contracts.ContractStatus) error default: return fmt.Errorf("invalid prev contract status: %v", current) } - switch current { + switch next { case contracts.ContractStatusPending: finalMetric = metricPendingContracts case contracts.ContractStatusRejected: From ca13440dd6aec9f929d0e1337ccead5319be3bf0 Mon Sep 17 00:00:00 2001 From: Nate Maninger Date: Wed, 2 Aug 2023 09:35:15 -0600 Subject: [PATCH 4/4] sqlite: add migration to recalc collateral --- persist/sqlite/init.sql | 2 +- persist/sqlite/migrations.go | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/persist/sqlite/init.sql b/persist/sqlite/init.sql index 86980419..23e5e62b 100644 --- a/persist/sqlite/init.sql +++ b/persist/sqlite/init.sql @@ -220,4 +220,4 @@ CREATE TABLE global_settings ( contracts_height INTEGER -- height of the contract manager as of the last processed change ); -INSERT INTO global_settings (id, db_version) VALUES (0, 10); -- version must be updated when the schema changes \ No newline at end of file +INSERT INTO global_settings (id, db_version) VALUES (0, 11); -- version must be updated when the schema changes \ No newline at end of file diff --git a/persist/sqlite/migrations.go b/persist/sqlite/migrations.go index 34d1cf9c..b2808916 100644 --- a/persist/sqlite/migrations.go +++ b/persist/sqlite/migrations.go @@ -8,6 +8,32 @@ import ( "go.sia.tech/hostd/host/contracts" ) +// migrateVersion11 recalculates the contract collateral metrics for existing contracts. +func migrateVersion11(tx txn) error { + rows, err := tx.Query(`SELECT locked_collateral, risked_collateral FROM contracts WHERE contract_status IN (?, ?)`, contracts.ContractStatusPending, contracts.ContractStatusActive) + if err != nil { + return fmt.Errorf("failed to query contracts: %w", err) + } + defer rows.Close() + var totalLocked, totalRisked types.Currency + for rows.Next() { + var locked, risked types.Currency + if err := rows.Scan((*sqlCurrency)(&locked), (*sqlCurrency)(&risked)); err != nil { + return fmt.Errorf("failed to scan contract: %w", err) + } + totalLocked = totalLocked.Add(locked) + totalRisked = totalRisked.Add(risked) + } + + if err := setCurrencyStat(tx, metricLockedCollateral, totalLocked, time.Now()); err != nil { + return fmt.Errorf("failed to increment locked collateral: %w", err) + } else if err := setCurrencyStat(tx, metricRiskedCollateral, totalRisked, time.Now()); err != nil { + return fmt.Errorf("failed to increment risked collateral: %w", err) + } + return nil +} + +// migrateVersion10 drops the log_lines table. func migrateVersion10(tx txn) error { _, err := tx.Exec(`DROP TABLE log_lines;`) return err @@ -238,4 +264,5 @@ var migrations = []func(tx txn) error{ migrateVersion8, migrateVersion9, migrateVersion10, + migrateVersion11, }