To allow signing with addresses other than the commitment address, add an alternate signature address. In order to continue to prove that the address was chosen by the user, add an alternate signature history. Only allow one record per ticket to be saved to cap needed db space.
193 lines
5.0 KiB
Go
193 lines
5.0 KiB
Go
// 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 (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"io"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
testDb = "test.db"
|
|
backupDb = "test.db-backup"
|
|
feeXPub = "feexpub"
|
|
maxVoteChangeRecords = 3
|
|
|
|
addrCharset = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
hexCharset = "1234567890abcdef"
|
|
sigCharset = "0123456789ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/="
|
|
)
|
|
|
|
var (
|
|
db *VspDatabase
|
|
seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
)
|
|
|
|
// randBytes returns a byte slice of size n filled with random bytes.
|
|
func randBytes(n int) []byte {
|
|
slice := make([]byte, n)
|
|
if _, err := seededRand.Read(slice); err != nil {
|
|
panic(err)
|
|
}
|
|
return slice
|
|
}
|
|
|
|
// randString randomly generates a string of the requested length, using only
|
|
// characters from the provided charset.
|
|
func randString(length int, charset string) string {
|
|
b := make([]byte, length)
|
|
for i := range b {
|
|
b[i] = charset[seededRand.Intn(len(charset))]
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// TestDatabase runs all database tests.
|
|
func TestDatabase(t *testing.T) {
|
|
// Ensure we are starting with a clean environment.
|
|
os.Remove(testDb)
|
|
os.Remove(backupDb)
|
|
|
|
// All sub-tests to run.
|
|
tests := map[string]func(*testing.T){
|
|
"testCreateNew": testCreateNew,
|
|
"testInsertNewTicket": testInsertNewTicket,
|
|
"testGetTicketByHash": testGetTicketByHash,
|
|
"testUpdateTicket": testUpdateTicket,
|
|
"testTicketFeeExpired": testTicketFeeExpired,
|
|
"testFilterTickets": testFilterTickets,
|
|
"testCountTickets": testCountTickets,
|
|
"testAddressIndex": testAddressIndex,
|
|
"testDeleteTicket": testDeleteTicket,
|
|
"testVoteChangeRecords": testVoteChangeRecords,
|
|
"testHTTPBackup": testHTTPBackup,
|
|
"testAltSigData": testAltSigData,
|
|
"testInsertAltSig": testInsertAltSig,
|
|
"testDeleteAltSig": testDeleteAltSig,
|
|
}
|
|
|
|
for testName, test := range tests {
|
|
// Create a new blank database for each sub-test.
|
|
var err error
|
|
var wg sync.WaitGroup
|
|
ctx, cancel := context.WithCancel(context.TODO())
|
|
err = CreateNew(testDb, feeXPub)
|
|
if err != nil {
|
|
t.Fatalf("error creating test database: %v", err)
|
|
}
|
|
db, err = Open(ctx, &wg, testDb, time.Hour, maxVoteChangeRecords)
|
|
if err != nil {
|
|
t.Fatalf("error opening test database: %v", err)
|
|
}
|
|
|
|
// Run the sub-test.
|
|
t.Run(testName, test)
|
|
|
|
// Request database shutdown and wait for it to complete.
|
|
cancel()
|
|
wg.Wait()
|
|
|
|
db.Close()
|
|
|
|
os.Remove(testDb)
|
|
os.Remove(backupDb)
|
|
}
|
|
}
|
|
|
|
func testCreateNew(t *testing.T) {
|
|
// A newly created DB should contain a signing keypair.
|
|
priv, pub, err := db.KeyPair()
|
|
if err != nil {
|
|
t.Fatalf("error getting keypair: %v", err)
|
|
}
|
|
|
|
// Ensure keypair can be used for signing/verifying messages.
|
|
msg := []byte("msg")
|
|
sig := ed25519.Sign(priv, msg)
|
|
if !ed25519.Verify(pub, msg, sig) {
|
|
t.Fatalf("keypair from database could not be used to sign/verify a message")
|
|
}
|
|
|
|
// A newly created DB should have a cookie secret.
|
|
secret, err := db.CookieSecret()
|
|
if err != nil {
|
|
t.Fatalf("error getting cookie secret: %v", err)
|
|
}
|
|
|
|
if len(secret) != 32 {
|
|
t.Fatalf("expected a 32 byte cookie secret, got %d bytes", len(secret))
|
|
}
|
|
|
|
// A newly created DB should store the fee xpub it was initialized with.
|
|
retrievedXPub, err := db.FeeXPub()
|
|
if err != nil {
|
|
t.Fatalf("error getting fee xpub: %v", err)
|
|
}
|
|
|
|
if retrievedXPub != feeXPub {
|
|
t.Fatalf("expected fee xpub %v, got %v", feeXPub, retrievedXPub)
|
|
}
|
|
}
|
|
|
|
func testHTTPBackup(t *testing.T) {
|
|
// Capture the HTTP response written by the backup func.
|
|
rr := httptest.NewRecorder()
|
|
err := db.BackupDB(rr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Check the HTTP status is OK.
|
|
if status := rr.Code; status != http.StatusOK {
|
|
t.Fatalf("wrong HTTP status code: expected %v, got %v",
|
|
http.StatusOK, status)
|
|
}
|
|
|
|
// Check HTTP headers.
|
|
header := "Content-Type"
|
|
expected := "application/octet-stream"
|
|
if actual := rr.Header().Get(header); actual != expected {
|
|
t.Errorf("wrong %s header: expected %s, got %s",
|
|
header, expected, actual)
|
|
}
|
|
|
|
header = "Content-Disposition"
|
|
expected = `attachment; filename="vspd.db"`
|
|
if actual := rr.Header().Get(header); actual != expected {
|
|
t.Errorf("wrong %s header: expected %s, got %s",
|
|
header, expected, actual)
|
|
}
|
|
|
|
header = "Content-Length"
|
|
cLength, err := strconv.Atoi(rr.Header().Get(header))
|
|
if err != nil {
|
|
t.Fatalf("could not convert %s to integer: %v", header, err)
|
|
}
|
|
|
|
if cLength <= 0 {
|
|
t.Fatalf("expected a %s greater than zero, got %d", header, cLength)
|
|
}
|
|
|
|
// Check reported length matches actual.
|
|
body, err := io.ReadAll(rr.Result().Body)
|
|
if err != nil {
|
|
t.Fatalf("could not read http response body: %v", err)
|
|
}
|
|
|
|
if len(body) != cLength {
|
|
t.Fatalf("expected reported content-length to match actual body length. %v != %v",
|
|
cLength, len(body))
|
|
}
|
|
}
|