multi: Update revoked tickets to expired/missed.
Any tickets in the database which are currently revoked should be updated to either expired or missed. This is achieved with a heuristic based on the expiry height and revoke height of the tickets. It is not guaranteed to be 100% correct but should be pretty close. The web api is not yet updated to reflect this change, missed/expired tickets will continue to be counted as revoked.
This commit is contained in:
parent
2fc60321e0
commit
a52034cda7
@ -25,6 +25,39 @@ func (s *spentTicket) voted() bool {
|
||||
return stake.IsSSGen(s.spendingTx)
|
||||
}
|
||||
|
||||
func (s *spentTicket) missed() bool {
|
||||
// The following switch statement is a heuristic to estimate whether a
|
||||
// ticket was missed or expired based on its revoke height. Absolute
|
||||
// precision is not needed here as this status is only used to report VSP
|
||||
// stats via /vspinfo, which could be forged by a malicious VSP operator
|
||||
// anyway.
|
||||
switch {
|
||||
case s.heightSpent < s.expiryHeight:
|
||||
// A ticket revoked before expiry height was definitely missed.
|
||||
return true
|
||||
case s.heightSpent == s.expiryHeight:
|
||||
// If a ticket was revoked on exactly expiry height, assume it expired.
|
||||
// This might be incorrect if DCP-0009 was not active and a missed
|
||||
// ticket was coincidentally revoked on exactly the expiry height.
|
||||
return false
|
||||
case s.heightSpent == s.expiryHeight+1:
|
||||
// Revoking after the expiry height was only possible before DCP-0009
|
||||
// activated. Cannot be certain if missed or expired, but if it was
|
||||
// revoked exactly in the first block an expired ticket could have
|
||||
// possibly been revoked, there is a high probability the voter was
|
||||
// online and didn't miss the vote, so assume expired.
|
||||
return false
|
||||
default:
|
||||
// Revoking after the expiry height was only possible before DCP-0009
|
||||
// activated. Cannot be certain if missed or expired, but if it was
|
||||
// revoked later than the first block an expired ticket could have
|
||||
// possibly been revoked, it is probably because the voter was offline
|
||||
// and there is a much higher probability that the ticket was missed, so
|
||||
// assume missed.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// findSpentTickets attempts to find transactions that vote/revoke the provided
|
||||
// tickets by matching the payment script of the ticket's commitment address
|
||||
// against the block filters of the mainchain blocks between the provided start
|
||||
|
||||
@ -219,7 +219,12 @@ func (v *vspd) run() int {
|
||||
func (v *vspd) checkDatabaseIntegrity() error {
|
||||
err := v.checkPurchaseHeights()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("checkPurchaseHeights error: %w", err)
|
||||
}
|
||||
|
||||
err = v.checkRevoked()
|
||||
if err != nil {
|
||||
return fmt.Errorf("checkRevoked error: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -270,6 +275,62 @@ func (v *vspd) checkPurchaseHeights() error {
|
||||
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() 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"))
|
||||
|
||||
// Search for the transactions which spend these tickets, starting at the
|
||||
// earliest height one of them matured.
|
||||
startHeight := revoked.EarliestPurchaseHeight() + int64(v.cfg.netParams.TicketMaturity)
|
||||
|
||||
spent, _, err := v.findSpentTickets(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
|
||||
}
|
||||
|
||||
// blockConnected is called once when vspd starts up, and once each time a
|
||||
// blockconnected notification is received from dcrd.
|
||||
func (v *vspd) blockConnected() {
|
||||
@ -502,10 +563,13 @@ func (v *vspd) blockConnected() {
|
||||
for _, spentTicket := range spent {
|
||||
dbTicket := spentTicket.dbTicket
|
||||
|
||||
if spentTicket.voted() {
|
||||
switch {
|
||||
case spentTicket.voted():
|
||||
dbTicket.Outcome = database.Voted
|
||||
} else {
|
||||
dbTicket.Outcome = database.Revoked
|
||||
case spentTicket.missed():
|
||||
dbTicket.Outcome = database.Missed
|
||||
default:
|
||||
dbTicket.Outcome = database.Expired
|
||||
}
|
||||
|
||||
err = v.db.UpdateTicket(dbTicket)
|
||||
|
||||
@ -31,11 +31,17 @@ const (
|
||||
type TicketOutcome string
|
||||
|
||||
const (
|
||||
// Revoked indicates the ticket has been revoked, either because it was
|
||||
// missed or it expired.
|
||||
Revoked TicketOutcome = "revoked"
|
||||
// Expired indicates the ticket expired and has been revoked.
|
||||
Expired TicketOutcome = "expired"
|
||||
// Missed indicates the ticket was missed and has been revoked.
|
||||
Missed TicketOutcome = "missed"
|
||||
// Voted indicates the ticket has already voted.
|
||||
Voted TicketOutcome = "voted"
|
||||
|
||||
// Revoked is a deprecated status which should no longer be used. It was
|
||||
// used before vspd was able to distinguish between expired and missed
|
||||
// tickets.
|
||||
Revoked TicketOutcome = "revoked"
|
||||
)
|
||||
|
||||
// The keys used to store ticket values in the database.
|
||||
@ -319,7 +325,7 @@ func (vdb *VspDatabase) CountTickets() (int64, int64, int64, error) {
|
||||
switch TicketOutcome(tBkt.Get(outcomeK)) {
|
||||
case Voted:
|
||||
voted++
|
||||
case Revoked:
|
||||
case Revoked, Expired, Missed:
|
||||
revoked++
|
||||
default:
|
||||
voting++
|
||||
@ -380,6 +386,13 @@ func (vdb *VspDatabase) GetVotedTickets() (TicketList, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetRevokedTickets returns all tickets which have outcome == revoked.
|
||||
func (vdb *VspDatabase) GetRevokedTickets() (TicketList, error) {
|
||||
return vdb.filterTickets(func(t *bolt.Bucket) bool {
|
||||
return TicketOutcome(t.Get(outcomeK)) == Revoked
|
||||
})
|
||||
}
|
||||
|
||||
// GetMissingPurchaseHeight returns tickets which are confirmed but do not have
|
||||
// a purchase height.
|
||||
func (vdb *VspDatabase) GetMissingPurchaseHeight() (TicketList, error) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user