multi: Display expired/missed tickets on webpage.

Revoked ticket count is replaced by separate counts for expired/missed
tickets.

/vspinfo API response remains unchanged.
This commit is contained in:
jholdstock 2023-09-04 16:01:56 +01:00 committed by Jamie Holdstock
parent fc1f7f2955
commit 54e243526e
5 changed files with 84 additions and 35 deletions

View File

@ -308,13 +308,14 @@ func (vdb *VspDatabase) Size() (uint64, error) {
return size, err return size, err
} }
// CountTickets returns the total number of voted, revoked, and currently voting // CountTickets returns the total number of voted, expired, missed, and
// tickets. This func iterates over every ticket so should be used sparingly. // currently voting tickets. This func iterates over every ticket so should be
func (vdb *VspDatabase) CountTickets() (int64, int64, int64, error) { // used sparingly.
func (vdb *VspDatabase) CountTickets() (int64, int64, int64, int64, error) {
vdb.ticketsMtx.RLock() vdb.ticketsMtx.RLock()
defer vdb.ticketsMtx.RUnlock() defer vdb.ticketsMtx.RUnlock()
var voting, voted, revoked int64 var voting, voted, expired, missed int64
err := vdb.db.View(func(tx *bolt.Tx) error { err := vdb.db.View(func(tx *bolt.Tx) error {
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK) ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
@ -325,8 +326,15 @@ 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, Expired, Missed: case Expired:
revoked++ expired++
case Missed:
missed++
case Revoked:
// There shouldn't be any revoked tickets in the db, they
// should have been updated to expired/missed. Give benefit
// of doubt to VSP admin and count these as expired.
expired++
default: default:
voting++ voting++
} }
@ -336,7 +344,7 @@ func (vdb *VspDatabase) CountTickets() (int64, int64, int64, error) {
}) })
}) })
return voting, voted, revoked, err return voting, voted, expired, missed, err
} }
// GetUnconfirmedTickets returns tickets which are not yet confirmed. // GetUnconfirmedTickets returns tickets which are not yet confirmed.

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2022 The Decred developers // Copyright (c) 2020-2023 The Decred developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -228,8 +228,8 @@ func testFilterTickets(t *testing.T) {
} }
func testCountTickets(t *testing.T) { func testCountTickets(t *testing.T) {
count := func(test string, expectedVoting, expectedVoted, expectedRevoked int64) { count := func(test string, expectedVoting, expectedVoted, expectedExpired, expectedMissed int64) {
voting, voted, revoked, err := db.CountTickets() voting, voted, expired, missed, err := db.CountTickets()
if err != nil { if err != nil {
t.Fatalf("error counting tickets: %v", err) t.Fatalf("error counting tickets: %v", err)
} }
@ -242,14 +242,18 @@ func testCountTickets(t *testing.T) {
t.Fatalf("test %s: expected %d voted tickets, got %d", t.Fatalf("test %s: expected %d voted tickets, got %d",
test, expectedVoted, voted) test, expectedVoted, voted)
} }
if revoked != expectedRevoked { if expired != expectedExpired {
t.Fatalf("test %s: expected %d revoked tickets, got %d", t.Fatalf("test %s: expected %d expired tickets, got %d",
test, expectedRevoked, revoked) test, expectedExpired, expired)
}
if missed != expectedMissed {
t.Fatalf("test %s: expected %d missed tickets, got %d",
test, expectedMissed, missed)
} }
} }
// Initial counts should all be zero. // Initial counts should all be zero.
count("empty db", 0, 0, 0) count("empty db", 0, 0, 0, 0)
// Insert a ticket with non-confirmed fee into the database. // Insert a ticket with non-confirmed fee into the database.
// This should not be counted. // This should not be counted.
@ -260,7 +264,7 @@ func testCountTickets(t *testing.T) {
t.Fatalf("error storing ticket in database: %v", err) t.Fatalf("error storing ticket in database: %v", err)
} }
count("unconfirmed fee", 0, 0, 0) count("unconfirmed fee", 0, 0, 0, 0)
// Insert a ticket with confirmed fee into the database. // Insert a ticket with confirmed fee into the database.
// This should be counted. // This should be counted.
@ -271,7 +275,7 @@ func testCountTickets(t *testing.T) {
t.Fatalf("error storing ticket in database: %v", err) t.Fatalf("error storing ticket in database: %v", err)
} }
count("confirmed fee", 1, 0, 0) count("confirmed fee", 1, 0, 0, 0)
// Insert a voted ticket into the database. // Insert a voted ticket into the database.
// This should be counted. // This should be counted.
@ -283,17 +287,41 @@ func testCountTickets(t *testing.T) {
t.Fatalf("error storing ticket in database: %v", err) t.Fatalf("error storing ticket in database: %v", err)
} }
count("voted", 1, 1, 0) count("voted", 1, 1, 0, 0)
// Insert a revoked ticket into the database. // Insert an expired ticket into the database.
// This should be counted. // This should be counted.
ticket4 := exampleTicket() ticket4 := exampleTicket()
ticket4.FeeTxStatus = FeeConfirmed ticket4.FeeTxStatus = FeeConfirmed
ticket4.Outcome = Revoked ticket4.Outcome = Expired
err = db.InsertNewTicket(ticket4) err = db.InsertNewTicket(ticket4)
if err != nil { if err != nil {
t.Fatalf("error storing ticket in database: %v", err) t.Fatalf("error storing ticket in database: %v", err)
} }
count("revoked", 1, 1, 1) count("expired", 1, 1, 1, 0)
// Insert a missed ticket into the database.
// This should be counted.
ticket5 := exampleTicket()
ticket5.FeeTxStatus = FeeConfirmed
ticket5.Outcome = Missed
err = db.InsertNewTicket(ticket5)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
count("missed", 1, 1, 1, 1)
// Insert a revoked ticket into the database.
// This should be counted as expired.
ticket6 := exampleTicket()
ticket6.FeeTxStatus = FeeConfirmed
ticket6.Outcome = Revoked
err = db.InsertNewTicket(ticket6)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
count("revoked", 1, 1, 2, 1)
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2022 The Decred developers // Copyright (c) 2020-2023 The Decred developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -31,12 +31,14 @@ type cacheData struct {
DatabaseSize string DatabaseSize string
Voting int64 Voting int64
Voted int64 Voted int64
Revoked int64 Expired int64
Missed int64
VotingWalletsOnline int64 VotingWalletsOnline int64
TotalVotingWallets int64 TotalVotingWallets int64
BlockHeight uint32 BlockHeight uint32
NetworkProportion float32 NetworkProportion float32
RevokedProportion float32 ExpiredProportion float32
MissedProportion float32
} }
func (c *cache) getData() cacheData { func (c *cache) getData() cacheData {
@ -66,8 +68,8 @@ func (c *cache) update(db *database.VspDatabase, dcrd rpc.DcrdConnect,
return err return err
} }
// Get latest counts of voting, voted and revoked tickets. // Get latest counts of voting, voted, expired and missed tickets.
voting, voted, revoked, err := db.CountTickets() voting, voted, expired, missed, err := db.CountTickets()
if err != nil { if err != nil {
return err return err
} }
@ -104,17 +106,20 @@ func (c *cache) update(db *database.VspDatabase, dcrd rpc.DcrdConnect,
c.data.Voted = voted c.data.Voted = voted
c.data.TotalVotingWallets = int64(len(clients) + len(failedConnections)) c.data.TotalVotingWallets = int64(len(clients) + len(failedConnections))
c.data.VotingWalletsOnline = int64(len(clients)) c.data.VotingWalletsOnline = int64(len(clients))
c.data.Revoked = revoked c.data.Expired = expired
c.data.Missed = missed
c.data.BlockHeight = bestBlock.Height c.data.BlockHeight = bestBlock.Height
c.data.NetworkProportion = float32(voting) / float32(bestBlock.PoolSize) c.data.NetworkProportion = float32(voting) / float32(bestBlock.PoolSize)
total := voted + revoked total := voted + expired + missed
// Prevent dividing by zero when pool has no voted/revoked tickets. // Prevent dividing by zero when pool has no voted/expired/missed tickets.
if total == 0 { if total == 0 {
c.data.RevokedProportion = 0 c.data.ExpiredProportion = 0
c.data.MissedProportion = 0
} else { } else {
c.data.RevokedProportion = float32(revoked) / float32(total) c.data.ExpiredProportion = float32(expired) / float32(total)
c.data.MissedProportion = float32(missed) / float32(total)
} }
return nil return nil

View File

@ -13,10 +13,18 @@
</div> </div>
<div class="col-6 col-sm-4 col-lg-2 py-3"> <div class="col-6 col-sm-4 col-lg-2 py-3">
<div class="stat-title">Revoked tickets</div> <div class="stat-title">Expired tickets</div>
<div class="stat-value"> <div class="stat-value">
{{ comma .WebApiCache.Revoked }} {{ comma .WebApiCache.Expired }}
<span class="text-muted">({{ float32ToPercent .WebApiCache.RevokedProportion }})</span> <span class="text-muted">({{ float32ToPercent .WebApiCache.ExpiredProportion }})</span>
</div>
</div>
<div class="col-6 col-sm-4 col-lg-2 py-3">
<div class="stat-title">Missed tickets</div>
<div class="stat-value">
{{ comma .WebApiCache.Missed }}
<span class="text-muted">({{ float32ToPercent .WebApiCache.MissedProportion }})</span>
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
// Copyright (c) 2020-2022 The Decred developers // Copyright (c) 2020-2023 The Decred developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -28,7 +28,7 @@ func (s *Server) vspInfo(c *gin.Context) {
Voted: cachedStats.Voted, Voted: cachedStats.Voted,
TotalVotingWallets: cachedStats.TotalVotingWallets, TotalVotingWallets: cachedStats.TotalVotingWallets,
VotingWalletsOnline: cachedStats.VotingWalletsOnline, VotingWalletsOnline: cachedStats.VotingWalletsOnline,
Revoked: cachedStats.Revoked, Revoked: cachedStats.Expired + cachedStats.Missed,
BlockHeight: cachedStats.BlockHeight, BlockHeight: cachedStats.BlockHeight,
NetworkProportion: cachedStats.NetworkProportion, NetworkProportion: cachedStats.NetworkProportion,
}, c) }, c)