vspd/internal/vspd/databaseintegrity.go
jholdstock c4de3d5d95 vspd: Pass dcrd RPC to findSpentTickets as a param
This allows RPC clients to be reused more easily in the case that the
caller of findSpentTickets already has a connected dcrd RPC client.
2023-09-19 18:01:43 +01:00

135 lines
3.6 KiB
Go

// Copyright (c) 2020-2023 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package vspd
import (
"context"
"fmt"
"github.com/decred/vspd/database"
)
// checkDatabaseIntegrity starts the process of ensuring that all data expected
// to be in the database is present and up to date.
func (v *Vspd) checkDatabaseIntegrity(ctx context.Context) error {
err := v.checkPurchaseHeights()
if err != nil {
return fmt.Errorf("checkPurchaseHeights error: %w", err)
}
err = v.checkRevoked(ctx)
if err != nil {
return fmt.Errorf("checkRevoked error: %w", err)
}
return nil
}
// checkPurchaseHeights ensures a purchase height is recorded for all confirmed
// tickets in the database. This is necessary because of an old bug which, in
// some circumstances, would prevent purchase height from being stored.
func (v *Vspd) checkPurchaseHeights() error {
missing, err := v.db.GetMissingPurchaseHeight()
if err != nil {
// Cannot proceed if this fails, return.
return fmt.Errorf("db.GetMissingPurchaseHeight error: %w", err)
}
if len(missing) == 0 {
// Nothing to do, return.
return nil
}
v.log.Warnf("%d tickets are missing purchase heights", len(missing))
dcrdClient, _, err := v.dcrd.Client()
if err != nil {
// Cannot proceed if this fails, return.
return err
}
fixed := 0
for _, ticket := range missing {
tktTx, err := dcrdClient.GetRawTransaction(ticket.Hash)
if err != nil {
// Just log and continue, other tickets might succeed.
v.log.Errorf("Could not get raw tx for ticket %s: %v", ticket.Hash, err)
continue
}
ticket.PurchaseHeight = tktTx.BlockHeight
err = v.db.UpdateTicket(ticket)
if err != nil {
// Just log and continue, other tickets might succeed.
v.log.Errorf("Could not insert purchase height for ticket %s: %v", ticket.Hash, err)
continue
}
fixed++
}
v.log.Infof("Added missing purchase height to %d tickets", fixed)
return nil
}
// checkRevoked ensures that any tickets in the database with outcome set to
// revoked are updated to either expired or missed.
func (v *Vspd) checkRevoked(ctx context.Context) error {
revoked, err := v.db.GetRevokedTickets()
if err != nil {
return fmt.Errorf("db.GetRevoked error: %w", err)
}
if len(revoked) == 0 {
// Nothing to do, return.
return nil
}
v.log.Warnf("Updating %s in revoked status, this may take a while...",
pluralize(len(revoked), "ticket"))
dcrdClient, _, err := v.dcrd.Client()
if err != nil {
return err
}
// Search for the transactions which spend these tickets, starting at the
// earliest height one of them matured.
startHeight := revoked.EarliestPurchaseHeight() + int64(v.network.TicketMaturity)
spent, _, err := v.findSpentTickets(ctx, dcrdClient, revoked, startHeight)
if err != nil {
return fmt.Errorf("findSpentTickets error: %w", err)
}
fixedMissed := 0
fixedExpired := 0
// Update database with correct voted status.
for hash, spentTicket := range spent {
switch {
case spentTicket.voted():
v.log.Errorf("Ticket voted but was recorded as revoked. Please contact "+
"developers so this can be investigated (ticketHash=%s)", hash)
continue
case spentTicket.missed():
spentTicket.dbTicket.Outcome = database.Missed
fixedMissed++
default:
spentTicket.dbTicket.Outcome = database.Expired
fixedExpired++
}
err = v.db.UpdateTicket(spentTicket.dbTicket)
if err != nil {
v.log.Errorf("Could not update status of ticket %s: %v", hash, err)
}
}
v.log.Infof("%s updated (%d missed, %d expired)",
pluralize(fixedExpired+fixedMissed, "revoked ticket"),
fixedMissed, fixedExpired)
return nil
}