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)
|
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
|
// findSpentTickets attempts to find transactions that vote/revoke the provided
|
||||||
// tickets by matching the payment script of the ticket's commitment address
|
// tickets by matching the payment script of the ticket's commitment address
|
||||||
// against the block filters of the mainchain blocks between the provided start
|
// 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 {
|
func (v *vspd) checkDatabaseIntegrity() error {
|
||||||
err := v.checkPurchaseHeights()
|
err := v.checkPurchaseHeights()
|
||||||
if err != nil {
|
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
|
return nil
|
||||||
@ -270,6 +275,62 @@ func (v *vspd) checkPurchaseHeights() error {
|
|||||||
return nil
|
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 is called once when vspd starts up, and once each time a
|
||||||
// blockconnected notification is received from dcrd.
|
// blockconnected notification is received from dcrd.
|
||||||
func (v *vspd) blockConnected() {
|
func (v *vspd) blockConnected() {
|
||||||
@ -502,10 +563,13 @@ func (v *vspd) blockConnected() {
|
|||||||
for _, spentTicket := range spent {
|
for _, spentTicket := range spent {
|
||||||
dbTicket := spentTicket.dbTicket
|
dbTicket := spentTicket.dbTicket
|
||||||
|
|
||||||
if spentTicket.voted() {
|
switch {
|
||||||
|
case spentTicket.voted():
|
||||||
dbTicket.Outcome = database.Voted
|
dbTicket.Outcome = database.Voted
|
||||||
} else {
|
case spentTicket.missed():
|
||||||
dbTicket.Outcome = database.Revoked
|
dbTicket.Outcome = database.Missed
|
||||||
|
default:
|
||||||
|
dbTicket.Outcome = database.Expired
|
||||||
}
|
}
|
||||||
|
|
||||||
err = v.db.UpdateTicket(dbTicket)
|
err = v.db.UpdateTicket(dbTicket)
|
||||||
|
|||||||
@ -31,11 +31,17 @@ const (
|
|||||||
type TicketOutcome string
|
type TicketOutcome string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Revoked indicates the ticket has been revoked, either because it was
|
// Expired indicates the ticket expired and has been revoked.
|
||||||
// missed or it expired.
|
Expired TicketOutcome = "expired"
|
||||||
Revoked TicketOutcome = "revoked"
|
// Missed indicates the ticket was missed and has been revoked.
|
||||||
|
Missed TicketOutcome = "missed"
|
||||||
// Voted indicates the ticket has already voted.
|
// Voted indicates the ticket has already voted.
|
||||||
Voted TicketOutcome = "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.
|
// 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)) {
|
switch TicketOutcome(tBkt.Get(outcomeK)) {
|
||||||
case Voted:
|
case Voted:
|
||||||
voted++
|
voted++
|
||||||
case Revoked:
|
case Revoked, Expired, Missed:
|
||||||
revoked++
|
revoked++
|
||||||
default:
|
default:
|
||||||
voting++
|
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
|
// GetMissingPurchaseHeight returns tickets which are confirmed but do not have
|
||||||
// a purchase height.
|
// a purchase height.
|
||||||
func (vdb *VspDatabase) GetMissingPurchaseHeight() (TicketList, error) {
|
func (vdb *VspDatabase) GetMissingPurchaseHeight() (TicketList, error) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user