vspd/database/ticket_test.go
jholdstock 4e4121335a database: Store xpub keys in a bucket.
**Warning: This commit contains a database upgrade.**

In order to add future support for retiring xpub keys, the database is
upgraded such that the keys are now stored in a dedicated bucket which
can hold multiple values rather than storing a single key as individual
values in the root bucket.

A new ID field is added to distinguish between keys. This ID is added to
every ticket record in the database in order to track which pubkey was
used for each ticket.

A new field named "Retired" has also been added to pubkeys. It is a unix
timestamp representing the moment the key was retired, or zero for the
currently active key.
2024-06-27 09:20:32 +01:00

330 lines
8.5 KiB
Go

// Copyright (c) 2020-2024 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 (
"reflect"
"testing"
"time"
bolt "go.etcd.io/bbolt"
)
func exampleTicket() Ticket {
return Ticket{
Hash: randString(64, hexCharset),
CommitmentAddress: randString(35, addrCharset),
FeeAddressIndex: 12345,
FeeAddressXPubID: 10,
FeeAddress: randString(35, addrCharset),
FeeAmount: 10000000,
FeeExpiration: 4,
Confirmed: false,
VoteChoices: map[string]string{"AgendaID": "yes"},
TSpendPolicy: map[string]string{randString(64, hexCharset): "no"},
TreasuryPolicy: map[string]string{randString(66, hexCharset): "abstain"},
VotingWIF: randString(53, addrCharset),
FeeTxHex: randString(504, hexCharset),
FeeTxHash: randString(64, hexCharset),
FeeTxStatus: FeeBroadcast,
}
}
func testInsertNewTicket(t *testing.T) {
// Insert a ticket into the database.
ticket := exampleTicket()
err := db.InsertNewTicket(ticket)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
// Insert another ticket.
err = db.InsertNewTicket(exampleTicket())
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
// Inserting a ticket with the same hash should fail.
ticket2 := exampleTicket()
ticket2.Hash = ticket.Hash
err = db.InsertNewTicket(ticket2)
if err == nil {
t.Fatal("expected an error inserting ticket with duplicate hash")
}
// Inserting a ticket with empty hash should fail.
ticket.Hash = ""
err = db.InsertNewTicket(ticket)
if err == nil {
t.Fatal("expected an error inserting ticket with no hash")
}
}
func testDeleteTicket(t *testing.T) {
// Insert a ticket into the database.
ticket := exampleTicket()
err := db.InsertNewTicket(ticket)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
// Delete ticket.
err = db.DeleteTicket(ticket)
if err != nil {
t.Fatalf("error deleting ticket: %v", err)
}
// Nothing should be in the db.
_, found, err := db.GetTicketByHash(ticket.Hash)
if err != nil {
t.Fatalf("error retrieving ticket by ticket hash: %v", err)
}
if found {
t.Fatal("expected found==false")
}
}
func testGetTicketByHash(t *testing.T) {
// Insert a ticket into the database.
ticket := exampleTicket()
err := db.InsertNewTicket(ticket)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
// Retrieve ticket from database.
retrieved, found, err := db.GetTicketByHash(ticket.Hash)
if err != nil {
t.Fatalf("error retrieving ticket by ticket hash: %v", err)
}
if !found {
t.Fatal("expected found==true")
}
// Check ticket fields match expected.
if !reflect.DeepEqual(retrieved, ticket) {
t.Fatal("retrieved ticket value didnt match expected")
}
// Check found==false when requesting a non-existent ticket.
_, found, err = db.GetTicketByHash("Not a real ticket hash")
if err != nil {
t.Fatalf("error retrieving ticket by ticket hash: %v", err)
}
if found {
t.Fatal("expected found==false")
}
}
func testUpdateTicket(t *testing.T) {
ticket := exampleTicket()
// Insert a ticket into the database.
err := db.InsertNewTicket(ticket)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
// Update ticket with new values.
ticket.FeeAmount = ticket.FeeAmount + 1
ticket.FeeExpiration = ticket.FeeExpiration + 1
ticket.VoteChoices = map[string]string{"New agenda": "New value"}
ticket.FeeAddressXPubID = 20
err = db.UpdateTicket(ticket)
if err != nil {
t.Fatalf("error updating ticket: %v", err)
}
// Retrieve updated ticket from database.
retrieved, found, err := db.GetTicketByHash(ticket.Hash)
if err != nil {
t.Fatalf("error retrieving ticket by ticket hash: %v", err)
}
if !found {
t.Fatal("expected found==true")
}
if !reflect.DeepEqual(retrieved, ticket) {
t.Fatal("retrieved ticket value didnt match expected")
}
// Updating a non-existent ticket should fail.
ticket.Hash = "doesnt exist"
err = db.UpdateTicket(ticket)
if err == nil {
t.Fatal("expected an error updating a ticket with non-existent hash")
}
}
func testTicketFeeExpired(t *testing.T) {
ticket := exampleTicket()
now := time.Now()
hourBefore := now.Add(-time.Hour).Unix()
hourAfter := now.Add(time.Hour).Unix()
ticket.FeeExpiration = hourAfter
if ticket.FeeExpired() {
t.Fatal("expected ticket not to be expired")
}
ticket.FeeExpiration = hourBefore
if !ticket.FeeExpired() {
t.Fatal("expected ticket to be expired")
}
}
func testFilterTickets(t *testing.T) {
// Insert a ticket.
ticket := exampleTicket()
err := db.InsertNewTicket(ticket)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
// Insert another ticket.
ticket2 := exampleTicket()
ticket2.Confirmed = !ticket.Confirmed
err = db.InsertNewTicket(ticket2)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
// Expect all tickets returned.
retrieved, err := db.filterTickets(func(_ *bolt.Bucket) bool {
return true
})
if err != nil {
t.Fatalf("error filtering tickets: %v", err)
}
if len(retrieved) != 2 {
t.Fatalf("expected to find 2 tickets, found %d", len(retrieved))
}
// Only one ticket should be confirmed.
retrieved, err = db.filterTickets(func(t *bolt.Bucket) bool {
return bytesToBool(t.Get(confirmedK))
})
if err != nil {
t.Fatalf("error filtering tickets: %v", err)
}
if len(retrieved) != 1 {
t.Fatalf("expected to find 1 ticket, found %d", len(retrieved))
}
if retrieved[0].Confirmed != true {
t.Fatal("expected retrieved ticket to be confirmed")
}
// Expect no tickets with confirmed fee.
retrieved, err = db.filterTickets(func(t *bolt.Bucket) bool {
return FeeStatus(t.Get(feeTxStatusK)) == FeeConfirmed
})
if err != nil {
t.Fatalf("error filtering tickets: %v", err)
}
if len(retrieved) != 0 {
t.Fatalf("expected to find 0 tickets, found %d", len(retrieved))
}
}
func testCountTickets(t *testing.T) {
count := func(test string, expectedVoting, expectedVoted, expectedExpired, expectedMissed int64) {
voting, voted, expired, missed, err := db.CountTickets()
if err != nil {
t.Fatalf("error counting tickets: %v", err)
}
if voting != expectedVoting {
t.Fatalf("test %s: expected %d voting tickets, got %d",
test, expectedVoting, voting)
}
if voted != expectedVoted {
t.Fatalf("test %s: expected %d voted tickets, got %d",
test, expectedVoted, voted)
}
if expired != expectedExpired {
t.Fatalf("test %s: expected %d expired tickets, got %d",
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.
count("empty db", 0, 0, 0, 0)
// Insert a ticket with non-confirmed fee into the database.
// This should not be counted.
ticket := exampleTicket()
ticket.FeeTxStatus = FeeReceieved
err := db.InsertNewTicket(ticket)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
count("unconfirmed fee", 0, 0, 0, 0)
// Insert a ticket with confirmed fee into the database.
// This should be counted.
ticket2 := exampleTicket()
ticket2.FeeTxStatus = FeeConfirmed
err = db.InsertNewTicket(ticket2)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
count("confirmed fee", 1, 0, 0, 0)
// Insert a voted ticket into the database.
// This should be counted.
ticket3 := exampleTicket()
ticket3.FeeTxStatus = FeeConfirmed
ticket3.Outcome = Voted
err = db.InsertNewTicket(ticket3)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
count("voted", 1, 1, 0, 0)
// Insert an expired ticket into the database.
// This should be counted.
ticket4 := exampleTicket()
ticket4.FeeTxStatus = FeeConfirmed
ticket4.Outcome = Expired
err = db.InsertNewTicket(ticket4)
if err != nil {
t.Fatalf("error storing ticket in database: %v", err)
}
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)
}