Revoked ticket count is replaced by separate counts for expired/missed tickets. /vspinfo API response remains unchanged.
438 lines
13 KiB
Go
438 lines
13 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 database
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
bolt "go.etcd.io/bbolt"
|
|
)
|
|
|
|
// FeeStatus represents the current state of a ticket fee payment.
|
|
type FeeStatus string
|
|
|
|
const (
|
|
// NoFee indicates no fee tx has been received yet.
|
|
NoFee FeeStatus = "none"
|
|
// FeeReceieved indicates fee tx has been received but not broadcast.
|
|
FeeReceieved FeeStatus = "received"
|
|
// FeeBroadcast indicates fee tx has been broadcast but not confirmed.
|
|
FeeBroadcast FeeStatus = "broadcast"
|
|
// FeeConfirmed indicates fee tx has been broadcast and confirmed.
|
|
FeeConfirmed FeeStatus = "confirmed"
|
|
// FeeError indicates fee tx could not be broadcast due to an error.
|
|
FeeError FeeStatus = "error"
|
|
)
|
|
|
|
// TicketOutcome describes the reason a ticket is no longer votable.
|
|
type TicketOutcome string
|
|
|
|
const (
|
|
// 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.
|
|
var (
|
|
hashK = []byte("Hash")
|
|
purchaseHeightK = []byte("PurchaseHeight")
|
|
commitmentAddressK = []byte("CommitmentAddress")
|
|
feeAddressIndexK = []byte("FeeAddressIndex")
|
|
feeAddressK = []byte("FeeAddress")
|
|
feeAmountK = []byte("FeeAmount")
|
|
feeExpirationK = []byte("FeeExpiration")
|
|
confirmedK = []byte("Confirmed")
|
|
votingWIFK = []byte("VotingWIF")
|
|
voteChoicesK = []byte("VoteChoices")
|
|
tSpendPolicyK = []byte("TSpendPolicy")
|
|
treasuryPolicyK = []byte("TreasuryPolicy")
|
|
feeTxHexK = []byte("FeeTxHex")
|
|
feeTxHashK = []byte("FeeTxHash")
|
|
feeTxStatusK = []byte("FeeTxStatus")
|
|
outcomeK = []byte("Outcome")
|
|
)
|
|
|
|
type Ticket struct {
|
|
Hash string
|
|
PurchaseHeight int64
|
|
CommitmentAddress string
|
|
FeeAddressIndex uint32
|
|
FeeAddress string
|
|
FeeAmount int64
|
|
FeeExpiration int64
|
|
|
|
// Confirmed will be set when the ticket has 6+ confirmations.
|
|
Confirmed bool
|
|
|
|
// VotingWIF is set in /payfee.
|
|
VotingWIF string
|
|
|
|
// VoteChoices, TSpendPolicy and TreasuryPolicy are initially set in
|
|
// /payfee, but can be updated in /setvotechoices.
|
|
VoteChoices map[string]string
|
|
TSpendPolicy map[string]string
|
|
TreasuryPolicy map[string]string
|
|
|
|
// FeeTxHex and FeeTxHash will be set when the fee tx has been received.
|
|
FeeTxHex string
|
|
FeeTxHash string
|
|
|
|
// FeeTxStatus indicates the current state of the fee transaction.
|
|
FeeTxStatus FeeStatus
|
|
|
|
// Outcome is set once a ticket is either voted or revoked. An empty outcome
|
|
// indicates that a ticket is still votable.
|
|
Outcome TicketOutcome
|
|
}
|
|
|
|
type TicketList []Ticket
|
|
|
|
// EarliestPurchaseHeight returns the lowest non-zero purchase height in the
|
|
// list of tickets. Zero will be returned if the list is empty, or if every
|
|
// ticket in the list has zero purchase height.
|
|
func (t TicketList) EarliestPurchaseHeight() int64 {
|
|
var oldestHeight int64
|
|
for _, ticket := range t {
|
|
// Skip unconfirmed tickets.
|
|
if ticket.PurchaseHeight == 0 {
|
|
continue
|
|
}
|
|
if oldestHeight == 0 || oldestHeight > ticket.PurchaseHeight {
|
|
oldestHeight = ticket.PurchaseHeight
|
|
}
|
|
}
|
|
return oldestHeight
|
|
}
|
|
|
|
func (t *Ticket) FeeExpired() bool {
|
|
now := time.Now()
|
|
return now.After(time.Unix(t.FeeExpiration, 0))
|
|
}
|
|
|
|
// InsertNewTicket will insert the provided ticket into the database. Returns an
|
|
// error if either the ticket hash or fee address already exist.
|
|
func (vdb *VspDatabase) InsertNewTicket(ticket Ticket) error {
|
|
vdb.ticketsMtx.Lock()
|
|
defer vdb.ticketsMtx.Unlock()
|
|
|
|
return vdb.db.Update(func(tx *bolt.Tx) error {
|
|
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
|
|
|
|
// Create a bucket for the new ticket. Returns an error if bucket
|
|
// already exists.
|
|
newTicketBkt, err := ticketBkt.CreateBucket([]byte(ticket.Hash))
|
|
if err != nil {
|
|
return fmt.Errorf("could not create bucket for ticket: %w", err)
|
|
}
|
|
|
|
err = putTicketInBucket(newTicketBkt, ticket)
|
|
if err != nil {
|
|
return fmt.Errorf("putting ticket in bucket failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// putTicketInBucket encodes each of the fields of the provided ticket as a byte
|
|
// array, and stores them as values within the provided db bucket.
|
|
func putTicketInBucket(bkt *bolt.Bucket, ticket Ticket) error {
|
|
var err error
|
|
if err = bkt.Put(hashK, []byte(ticket.Hash)); err != nil {
|
|
return err
|
|
}
|
|
if err = bkt.Put(commitmentAddressK, []byte(ticket.CommitmentAddress)); err != nil {
|
|
return err
|
|
}
|
|
if err = bkt.Put(feeAddressK, []byte(ticket.FeeAddress)); err != nil {
|
|
return err
|
|
}
|
|
if err = bkt.Put(votingWIFK, []byte(ticket.VotingWIF)); err != nil {
|
|
return err
|
|
}
|
|
if err = bkt.Put(feeTxHexK, []byte(ticket.FeeTxHex)); err != nil {
|
|
return err
|
|
}
|
|
if err = bkt.Put(feeTxHashK, []byte(ticket.FeeTxHash)); err != nil {
|
|
return err
|
|
}
|
|
if err = bkt.Put(feeTxStatusK, []byte(ticket.FeeTxStatus)); err != nil {
|
|
return err
|
|
}
|
|
if err = bkt.Put(outcomeK, []byte(ticket.Outcome)); err != nil {
|
|
return err
|
|
}
|
|
if err = bkt.Put(purchaseHeightK, int64ToBytes(ticket.PurchaseHeight)); err != nil {
|
|
return err
|
|
}
|
|
if err = bkt.Put(feeAddressIndexK, uint32ToBytes(ticket.FeeAddressIndex)); err != nil {
|
|
return err
|
|
}
|
|
if err = bkt.Put(feeAmountK, int64ToBytes(ticket.FeeAmount)); err != nil {
|
|
return err
|
|
}
|
|
if err = bkt.Put(feeExpirationK, int64ToBytes(ticket.FeeExpiration)); err != nil {
|
|
return err
|
|
}
|
|
if err = bkt.Put(confirmedK, boolToBytes(ticket.Confirmed)); err != nil {
|
|
return err
|
|
}
|
|
if err = bkt.Put(tSpendPolicyK, stringMapToBytes(ticket.TSpendPolicy)); err != nil {
|
|
return err
|
|
}
|
|
if err = bkt.Put(treasuryPolicyK, stringMapToBytes(ticket.TreasuryPolicy)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return bkt.Put(voteChoicesK, stringMapToBytes(ticket.VoteChoices))
|
|
}
|
|
|
|
func getTicketFromBkt(bkt *bolt.Bucket) (Ticket, error) {
|
|
var ticket Ticket
|
|
|
|
ticket.Hash = string(bkt.Get(hashK))
|
|
ticket.CommitmentAddress = string(bkt.Get(commitmentAddressK))
|
|
ticket.FeeAddress = string(bkt.Get(feeAddressK))
|
|
ticket.VotingWIF = string(bkt.Get(votingWIFK))
|
|
ticket.FeeTxHex = string(bkt.Get(feeTxHexK))
|
|
ticket.FeeTxHash = string(bkt.Get(feeTxHashK))
|
|
ticket.FeeTxStatus = FeeStatus(bkt.Get(feeTxStatusK))
|
|
ticket.Outcome = TicketOutcome(bkt.Get(outcomeK))
|
|
|
|
ticket.PurchaseHeight = bytesToInt64(bkt.Get(purchaseHeightK))
|
|
ticket.FeeAddressIndex = bytesToUint32(bkt.Get(feeAddressIndexK))
|
|
ticket.FeeAmount = bytesToInt64(bkt.Get(feeAmountK))
|
|
ticket.FeeExpiration = bytesToInt64(bkt.Get(feeExpirationK))
|
|
|
|
ticket.Confirmed = bytesToBool(bkt.Get(confirmedK))
|
|
|
|
var err error
|
|
ticket.VoteChoices, err = bytesToStringMap(bkt.Get(voteChoicesK))
|
|
if err != nil {
|
|
return ticket, fmt.Errorf("unmarshal VoteChoices err: %w", err)
|
|
}
|
|
|
|
ticket.TSpendPolicy, err = bytesToStringMap(bkt.Get(tSpendPolicyK))
|
|
if err != nil {
|
|
return ticket, fmt.Errorf("unmarshal TSpendPolicy err: %w", err)
|
|
}
|
|
|
|
ticket.TreasuryPolicy, err = bytesToStringMap(bkt.Get(treasuryPolicyK))
|
|
if err != nil {
|
|
return ticket, fmt.Errorf("unmarshal TreasuryPolicy err: %w", err)
|
|
}
|
|
|
|
return ticket, nil
|
|
}
|
|
|
|
func (vdb *VspDatabase) DeleteTicket(ticket Ticket) error {
|
|
vdb.ticketsMtx.Lock()
|
|
defer vdb.ticketsMtx.Unlock()
|
|
|
|
return vdb.db.Update(func(tx *bolt.Tx) error {
|
|
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
|
|
|
|
err := ticketBkt.DeleteBucket([]byte(ticket.Hash))
|
|
if err != nil {
|
|
return fmt.Errorf("could not delete ticket: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (vdb *VspDatabase) UpdateTicket(ticket Ticket) error {
|
|
vdb.ticketsMtx.Lock()
|
|
defer vdb.ticketsMtx.Unlock()
|
|
|
|
return vdb.db.Update(func(tx *bolt.Tx) error {
|
|
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
|
|
|
|
bkt := ticketBkt.Bucket([]byte(ticket.Hash))
|
|
|
|
if bkt == nil {
|
|
return fmt.Errorf("ticket does not exist with hash %s", ticket.Hash)
|
|
}
|
|
|
|
return putTicketInBucket(bkt, ticket)
|
|
})
|
|
}
|
|
|
|
func (vdb *VspDatabase) GetTicketByHash(ticketHash string) (Ticket, bool, error) {
|
|
vdb.ticketsMtx.RLock()
|
|
defer vdb.ticketsMtx.RUnlock()
|
|
|
|
var ticket Ticket
|
|
var found bool
|
|
err := vdb.db.View(func(tx *bolt.Tx) error {
|
|
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK).Bucket([]byte(ticketHash))
|
|
|
|
if ticketBkt == nil {
|
|
return nil
|
|
}
|
|
|
|
var err error
|
|
ticket, err = getTicketFromBkt(ticketBkt)
|
|
if err != nil {
|
|
return fmt.Errorf("could not get ticket: %w", err)
|
|
}
|
|
|
|
found = true
|
|
|
|
return nil
|
|
})
|
|
|
|
return ticket, found, err
|
|
}
|
|
|
|
// Size returns the current size of the database in bytes. Note that this may
|
|
// not exactly match the size of the database file stored on disk.
|
|
func (vdb *VspDatabase) Size() (uint64, error) {
|
|
var size uint64
|
|
err := vdb.db.View(func(tx *bolt.Tx) error {
|
|
size = uint64(tx.Size())
|
|
return nil
|
|
})
|
|
return size, err
|
|
}
|
|
|
|
// CountTickets returns the total number of voted, expired, missed, and
|
|
// currently voting tickets. This func iterates over every ticket so should be
|
|
// used sparingly.
|
|
func (vdb *VspDatabase) CountTickets() (int64, int64, int64, int64, error) {
|
|
vdb.ticketsMtx.RLock()
|
|
defer vdb.ticketsMtx.RUnlock()
|
|
|
|
var voting, voted, expired, missed int64
|
|
err := vdb.db.View(func(tx *bolt.Tx) error {
|
|
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
|
|
|
|
return ticketBkt.ForEachBucket(func(k []byte) error {
|
|
tBkt := ticketBkt.Bucket(k)
|
|
|
|
if FeeStatus(tBkt.Get(feeTxStatusK)) == FeeConfirmed {
|
|
switch TicketOutcome(tBkt.Get(outcomeK)) {
|
|
case Voted:
|
|
voted++
|
|
case Expired:
|
|
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:
|
|
voting++
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
})
|
|
|
|
return voting, voted, expired, missed, err
|
|
}
|
|
|
|
// GetUnconfirmedTickets returns tickets which are not yet confirmed.
|
|
func (vdb *VspDatabase) GetUnconfirmedTickets() (TicketList, error) {
|
|
vdb.ticketsMtx.RLock()
|
|
defer vdb.ticketsMtx.RUnlock()
|
|
|
|
return vdb.filterTickets(func(t *bolt.Bucket) bool {
|
|
return !bytesToBool(t.Get(confirmedK))
|
|
})
|
|
}
|
|
|
|
// GetPendingFees returns tickets which are confirmed and have a fee tx which is
|
|
// not yet broadcast.
|
|
func (vdb *VspDatabase) GetPendingFees() (TicketList, error) {
|
|
vdb.ticketsMtx.RLock()
|
|
defer vdb.ticketsMtx.RUnlock()
|
|
|
|
return vdb.filterTickets(func(t *bolt.Bucket) bool {
|
|
return bytesToBool(t.Get(confirmedK)) && FeeStatus(t.Get(feeTxStatusK)) == FeeReceieved
|
|
})
|
|
}
|
|
|
|
// GetUnconfirmedFees returns tickets with a fee tx that is broadcast but not
|
|
// confirmed yet.
|
|
func (vdb *VspDatabase) GetUnconfirmedFees() (TicketList, error) {
|
|
vdb.ticketsMtx.RLock()
|
|
defer vdb.ticketsMtx.RUnlock()
|
|
|
|
return vdb.filterTickets(func(t *bolt.Bucket) bool {
|
|
return FeeStatus(t.Get(feeTxStatusK)) == FeeBroadcast
|
|
})
|
|
}
|
|
|
|
// GetVotableTickets returns tickets with a confirmed fee tx and no outcome (ie.
|
|
// not expired/voted/missed).
|
|
func (vdb *VspDatabase) GetVotableTickets() (TicketList, error) {
|
|
return vdb.filterTickets(func(t *bolt.Bucket) bool {
|
|
return FeeStatus(t.Get(feeTxStatusK)) == FeeConfirmed && TicketOutcome(t.Get(outcomeK)) == ""
|
|
})
|
|
}
|
|
|
|
// GetVotedTickets returns tickets with a confirmed fee tx and outcome == voted.
|
|
func (vdb *VspDatabase) GetVotedTickets() (TicketList, error) {
|
|
return vdb.filterTickets(func(t *bolt.Bucket) bool {
|
|
return FeeStatus(t.Get(feeTxStatusK)) == FeeConfirmed && TicketOutcome(t.Get(outcomeK)) == Voted
|
|
})
|
|
}
|
|
|
|
// 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) {
|
|
return vdb.filterTickets(func(t *bolt.Bucket) bool {
|
|
return bytesToBool(t.Get(confirmedK)) && bytesToInt64(t.Get(purchaseHeightK)) == 0
|
|
})
|
|
}
|
|
|
|
// filterTickets accepts a filter function and returns all tickets from the
|
|
// database which match the filter.
|
|
//
|
|
// This function must be called with the lock held.
|
|
func (vdb *VspDatabase) filterTickets(filter func(*bolt.Bucket) bool) (TicketList, error) {
|
|
var tickets TicketList
|
|
err := vdb.db.View(func(tx *bolt.Tx) error {
|
|
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
|
|
|
|
return ticketBkt.ForEachBucket(func(k []byte) error {
|
|
ticketBkt := ticketBkt.Bucket(k)
|
|
|
|
if filter(ticketBkt) {
|
|
ticket, err := getTicketFromBkt(ticketBkt)
|
|
if err != nil {
|
|
return fmt.Errorf("could not get ticket: %w", err)
|
|
}
|
|
tickets = append(tickets, ticket)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
})
|
|
|
|
return tickets, err
|
|
}
|