// Copyright (c) 2020 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 ( "encoding/json" "errors" "fmt" "time" bolt "go.etcd.io/bbolt" ) // FeeStatus represents the current state of a ticket fee payment. type FeeStatus string const ( // No fee transaction has been received yet. NoFee FeeStatus = "none" // Fee transaction has been received but not broadcast. FeeReceieved FeeStatus = "received" // Fee transaction has been broadcast but not confirmed. FeeBroadcast FeeStatus = "broadcast" // Fee transaction has been broadcast and confirmed. FeeConfirmed FeeStatus = "confirmed" // Fee transaction 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 ( // Ticket has been revoked, either because it was missed or it expired. Revoked TicketOutcome = "revoked" // Ticket has already voted. Voted TicketOutcome = "voted" ) // Ticket is serialized to json and stored in bbolt db. The json keys are // deliberately kept short because they are duplicated many times in the db. type Ticket struct { Hash string `json:"hsh"` PurchaseHeight int64 `json:"phgt"` CommitmentAddress string `json:"cmtaddr"` FeeAddressIndex uint32 `json:"faddridx"` FeeAddress string `json:"faddr"` FeeAmount int64 `json:"famt"` FeeExpiration int64 `json:"fexp"` // Confirmed will be set when the ticket has 6+ confirmations. Confirmed bool `json:"conf"` // VotingWIF is set in /payfee. VotingWIF string `json:"vwif"` // VoteChoices is initially set in /payfee, but can be updated in // /setvotechoices. VoteChoices map[string]string `json:"vchces"` // FeeTxHex and FeeTxHash will be set when the fee tx has been received. FeeTxHex string `json:"fhex"` FeeTxHash string `json:"fhsh"` // FeeTxStatus indicates the current state of the fee transaction. FeeTxStatus FeeStatus `json:"fsts"` // Outcome is set once a ticket is either voted or revoked. An empty outcome // indicates that a ticket is still votable. Outcome TicketOutcome `json:"otcme"` } func (t *Ticket) FeeExpired() bool { now := time.Now() return now.After(time.Unix(t.FeeExpiration, 0)) } var ( ErrNoTicketFound = errors.New("no ticket found") ) 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) hashBytes := []byte(ticket.Hash) if ticketBkt.Get(hashBytes) != nil { return fmt.Errorf("ticket already exists with hash %s", ticket.Hash) } // Error if a ticket already exists with the same fee address. err := ticketBkt.ForEach(func(k, v []byte) error { var t Ticket err := json.Unmarshal(v, &t) if err != nil { return fmt.Errorf("could not unmarshal ticket: %w", err) } if t.FeeAddress == ticket.FeeAddress { return fmt.Errorf("ticket with fee address %s already exists", t.FeeAddress) } if t.FeeAddressIndex == ticket.FeeAddressIndex { return fmt.Errorf("ticket with fee address index %d already exists", t.FeeAddressIndex) } return nil }) if err != nil { return err } ticketBytes, err := json.Marshal(ticket) if err != nil { return fmt.Errorf("could not marshal ticket: %w", err) } return ticketBkt.Put(hashBytes, ticketBytes) }) } 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.Delete([]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) hashBytes := []byte(ticket.Hash) if ticketBkt.Get(hashBytes) == nil { return fmt.Errorf("ticket does not exist with hash %s", ticket.Hash) } ticketBytes, err := json.Marshal(ticket) if err != nil { return fmt.Errorf("could not marshal ticket: %w", err) } return ticketBkt.Put(hashBytes, ticketBytes) }) } 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) ticketBytes := ticketBkt.Get([]byte(ticketHash)) if ticketBytes == nil { return nil } err := json.Unmarshal(ticketBytes, &ticket) if err != nil { return fmt.Errorf("could not unmarshal ticket: %w", err) } found = true return nil }) return ticket, found, err } func (vdb *VspDatabase) CountTickets() (int64, int64, int64, error) { vdb.ticketsMtx.RLock() defer vdb.ticketsMtx.RUnlock() var voting, voted, revoked int64 err := vdb.db.View(func(tx *bolt.Tx) error { ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK) return ticketBkt.ForEach(func(k, v []byte) error { var ticket Ticket err := json.Unmarshal(v, &ticket) if err != nil { return fmt.Errorf("could not unmarshal ticket: %w", err) } if ticket.FeeTxStatus == FeeConfirmed { switch ticket.Outcome { case Voted: voted++ case Revoked: revoked++ default: voting++ } } return nil }) }) return voting, voted, revoked, err } // GetUnconfirmedTickets returns tickets which are not yet confirmed. func (vdb *VspDatabase) GetUnconfirmedTickets() ([]Ticket, error) { vdb.ticketsMtx.RLock() defer vdb.ticketsMtx.RUnlock() return vdb.filterTickets(func(t Ticket) bool { return !t.Confirmed }) } // GetPendingFees returns tickets which are confirmed and have a fee tx which is // not yet broadcast. func (vdb *VspDatabase) GetPendingFees() ([]Ticket, error) { vdb.ticketsMtx.RLock() defer vdb.ticketsMtx.RUnlock() return vdb.filterTickets(func(t Ticket) bool { return t.Confirmed && t.FeeTxStatus == FeeReceieved }) } // GetUnconfirmedFees returns tickets with a fee tx that is broadcast but not // confirmed yet. func (vdb *VspDatabase) GetUnconfirmedFees() ([]Ticket, error) { vdb.ticketsMtx.RLock() defer vdb.ticketsMtx.RUnlock() return vdb.filterTickets(func(t Ticket) bool { return t.FeeTxStatus == FeeBroadcast }) } // GetVotableTickets returns tickets with a confirmed fee tx and no outcome (ie. // not expired/voted/missed). func (vdb *VspDatabase) GetVotableTickets() ([]Ticket, error) { return vdb.filterTickets(func(t Ticket) bool { return t.FeeTxStatus == FeeConfirmed && t.Outcome == "" }) } // 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(Ticket) bool) ([]Ticket, error) { var tickets []Ticket err := vdb.db.View(func(tx *bolt.Tx) error { ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK) return ticketBkt.ForEach(func(k, v []byte) error { var ticket Ticket err := json.Unmarshal(v, &ticket) if err != nil { return fmt.Errorf("could not unmarshal ticket: %w", err) } if filter(ticket) { tickets = append(tickets, ticket) } return nil }) }) return tickets, err }