db/ticket: Add AltSigAddress.

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.
This commit is contained in:
JoeGruff 2021-06-25 14:13:17 +09:00 committed by Jamie Holdstock
parent 623bb192d1
commit 6e13d23214
7 changed files with 319 additions and 18 deletions

124
database/altsig.go Normal file
View File

@ -0,0 +1,124 @@
// Copyright (c) 2020-2021 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 (
"errors"
"fmt"
bolt "go.etcd.io/bbolt"
)
// The keys used to store the altsig in the database.
var (
altSigAddrK = []byte("altsig")
reqK = []byte("req")
reqSigK = []byte("reqsig")
resK = []byte("res")
resSigK = []byte("ressig")
)
// AltSigData holds the information needed to prove that a client added an
// alternate signature address.
type AltSigData struct {
// AltSigAddr is the new alternate signature address. It is base 58
// encoded.
AltSigAddr string
// Req is the original request to set an alternate signature.
Req []byte
// ReqSig is the request's signature signed by the private key that
// corresponds to the address. It is base 64 encoded.
ReqSig string
// Res is the original response from the server to the alternate
// signature address.
Res []byte
// ResSig is the response's signature signed by the server. It is base
// 64 encoded.
ResSig string
}
// InsertAltSig will insert the provided ticket into the database. Returns an
// error if data for the ticket hash already exist.
//
// Passed data must have no empty fields.
func (vdb *VspDatabase) InsertAltSig(ticketHash string, data *AltSigData) error {
if data == nil {
return errors.New("alt sig data must not be nil for inserts")
}
if data.AltSigAddr == "" || len(data.Req) == 0 || data.ReqSig == "" ||
len(data.Res) == 0 || data.ResSig == "" {
return errors.New("alt sig data has empty parameters")
}
return vdb.db.Update(func(tx *bolt.Tx) error {
altSigBkt := tx.Bucket(vspBktK).Bucket(altSigBktK)
// Create a bucket for the new altsig. Returns an error if bucket
// already exists.
bkt, err := altSigBkt.CreateBucket([]byte(ticketHash))
if err != nil {
return fmt.Errorf("could not create bucket for altsig: %w", err)
}
if err := bkt.Put(altSigAddrK, []byte(data.AltSigAddr)); err != nil {
return err
}
if err := bkt.Put(reqK, data.Req); err != nil {
return err
}
if err := bkt.Put(reqSigK, []byte(data.ReqSig)); err != nil {
return err
}
if err := bkt.Put(resK, data.Res); err != nil {
return err
}
return bkt.Put(resSigK, []byte(data.ResSig))
})
}
// DeleteAltSig deletes an altsig from the database. Does not error if there is
// no altsig in the database.
func (vdb *VspDatabase) DeleteAltSig(ticketHash string) error {
return vdb.db.Update(func(tx *bolt.Tx) error {
altSigBkt := tx.Bucket(vspBktK).Bucket(altSigBktK)
// Don't attempt delete if doesn't exist.
bkt := altSigBkt.Bucket([]byte(ticketHash))
if bkt == nil {
return nil
}
err := altSigBkt.DeleteBucket([]byte(ticketHash))
if err != nil {
return fmt.Errorf("could not delete altsig: %w", err)
}
return nil
})
}
// AltSigData retrieves a ticket's alternate signature data. Existence of an
// alternate signature can be inferred by no error and nil data return.
func (vdb *VspDatabase) AltSigData(ticketHash string) (*AltSigData, error) {
var h *AltSigData
return h, vdb.db.View(func(tx *bolt.Tx) error {
bkt := tx.Bucket(vspBktK).Bucket(altSigBktK).Bucket([]byte(ticketHash))
if bkt == nil {
return nil
}
h = &AltSigData{
AltSigAddr: string(bkt.Get(altSigAddrK)),
Req: bkt.Get(reqK),
ReqSig: string(bkt.Get(reqSigK)),
Res: bkt.Get(resK),
ResSig: string(bkt.Get(resSigK)),
}
return nil
})
}

117
database/altsig_test.go Normal file
View File

@ -0,0 +1,117 @@
// Copyright (c) 2020-2021 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"
)
func exampleAltSigData() *AltSigData {
return &AltSigData{
AltSigAddr: randString(35, addrCharset),
Req: randBytes(1000),
ReqSig: randString(96, sigCharset),
Res: randBytes(1000),
ResSig: randString(96, sigCharset),
}
}
func ensureData(t *testing.T, ticketHash string, wantData *AltSigData) {
t.Helper()
data, err := db.AltSigData(ticketHash)
if err != nil {
t.Fatalf("unexpected error fetching alt signature data: %v", err)
}
if !reflect.DeepEqual(wantData, data) {
t.Fatal("want data different than actual")
}
}
func testAltSigData(t *testing.T) {
ticketHash := randString(64, hexCharset)
// Not added yet so no values should exist in the db.
h, err := db.AltSigData(ticketHash)
if err != nil {
t.Fatalf("unexpected error fetching alt signature data: %v", err)
}
if h != nil {
t.Fatal("expected no data")
}
// Insert an altsig.
data := exampleAltSigData()
if err := db.InsertAltSig(ticketHash, data); err != nil {
t.Fatalf("unexpected error storing altsig in database: %v", err)
}
ensureData(t, ticketHash, data)
}
func testInsertAltSig(t *testing.T) {
ticketHash := randString(64, hexCharset)
// Not added yet so no values should exist in the db.
ensureData(t, ticketHash, nil)
data := exampleAltSigData()
// Clear alt sig addr for test.
data.AltSigAddr = ""
if err := db.InsertAltSig(ticketHash, data); err == nil {
t.Fatalf("expected error for insert blank address")
}
if err := db.InsertAltSig(ticketHash, nil); err == nil {
t.Fatalf("expected error for nil data")
}
// Still no change on errors.
ensureData(t, ticketHash, nil)
// Re-add alt sig addr.
data.AltSigAddr = randString(35, addrCharset)
// Insert an altsig.
if err := db.InsertAltSig(ticketHash, data); err != nil {
t.Fatalf("unexpected error storing altsig in database: %v", err)
}
ensureData(t, ticketHash, data)
// Further additions should error and not change the data.
secondData := exampleAltSigData()
secondData.AltSigAddr = data.AltSigAddr
if err := db.InsertAltSig(ticketHash, secondData); err == nil {
t.Fatalf("expected error for second altsig addition")
}
ensureData(t, ticketHash, data)
}
func testDeleteAltSig(t *testing.T) {
ticketHash := randString(64, hexCharset)
// Nothing to delete.
if err := db.DeleteAltSig(ticketHash); err != nil {
t.Fatalf("unexpected error deleting nonexistant altsig")
}
// Insert an altsig.
data := exampleAltSigData()
if err := db.InsertAltSig(ticketHash, data); err != nil {
t.Fatalf("unexpected error storing altsig in database: %v", err)
}
ensureData(t, ticketHash, data)
if err := db.DeleteAltSig(ticketHash); err != nil {
t.Fatalf("unexpected error deleting altsig: %v", err)
}
ensureData(t, ticketHash, nil)
}

View File

@ -50,6 +50,8 @@ var (
privateKeyK = []byte("privatekey") privateKeyK = []byte("privatekey")
// lastaddressindex is the index of the last address used for fees. // lastaddressindex is the index of the last address used for fees.
lastAddressIndexK = []byte("lastaddressindex") lastAddressIndexK = []byte("lastaddressindex")
// altSigBktK stores alternate signatures.
altSigBktK = []byte("altsigbkt")
) )
const ( const (

View File

@ -8,6 +8,7 @@ import (
"context" "context"
"crypto/ed25519" "crypto/ed25519"
"io" "io"
"math/rand"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
@ -22,12 +23,36 @@ const (
backupDb = "test.db-backup" backupDb = "test.db-backup"
feeXPub = "feexpub" feeXPub = "feexpub"
maxVoteChangeRecords = 3 maxVoteChangeRecords = 3
addrCharset = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
hexCharset = "1234567890abcdef"
sigCharset = "0123456789ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/="
) )
var ( var (
db *VspDatabase 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. // TestDatabase runs all database tests.
func TestDatabase(t *testing.T) { func TestDatabase(t *testing.T) {
// Ensure we are starting with a clean environment. // Ensure we are starting with a clean environment.
@ -47,6 +72,9 @@ func TestDatabase(t *testing.T) {
"testDeleteTicket": testDeleteTicket, "testDeleteTicket": testDeleteTicket,
"testVoteChangeRecords": testVoteChangeRecords, "testVoteChangeRecords": testVoteChangeRecords,
"testHTTPBackup": testHTTPBackup, "testHTTPBackup": testHTTPBackup,
"testAltSigData": testAltSigData,
"testInsertAltSig": testInsertAltSig,
"testDeleteAltSig": testDeleteAltSig,
} }
for testName, test := range tests { for testName, test := range tests {

View File

@ -5,7 +5,6 @@
package database package database
import ( import (
"math/rand"
"reflect" "reflect"
"testing" "testing"
"time" "time"
@ -13,22 +12,7 @@ import (
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
) )
var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
// 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)
}
func exampleTicket() Ticket { func exampleTicket() Ticket {
const hexCharset = "1234567890abcdef"
const addrCharset = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
return Ticket{ return Ticket{
Hash: randString(64, hexCharset), Hash: randString(64, hexCharset),
CommitmentAddress: randString(35, addrCharset), CommitmentAddress: randString(35, addrCharset),

41
database/upgrade_v4.go Normal file
View File

@ -0,0 +1,41 @@
// Copyright (c) 2021 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"
bolt "go.etcd.io/bbolt"
)
func altSigUpgrade(db *bolt.DB) error {
log.Infof("Upgrading database to version %d", altSigVersion)
// Run the upgrade in a single database transaction so it can be safely
// rolled back if an error is encountered.
err := db.Update(func(tx *bolt.Tx) error {
vspBkt := tx.Bucket(vspBktK)
// Create altsig bucket.
_, err := vspBkt.CreateBucket(altSigBktK)
if err != nil {
return fmt.Errorf("failed to create %s bucket: %w", altSigBktK, err)
}
// Update database version.
err = vspBkt.Put(versionK, uint32ToBytes(altSigVersion))
if err != nil {
return fmt.Errorf("failed to update db version: %w", err)
}
return nil
})
if err != nil {
return err
}
log.Info("Upgrade completed")
return nil
}

View File

@ -25,10 +25,14 @@ const (
// moves each ticket into its own bucket and does away with JSON encoding. // moves each ticket into its own bucket and does away with JSON encoding.
ticketBucketVersion = 3 ticketBucketVersion = 3
// altSigVersion adds a bucket to store alternate signatures used to verify
// messages sent to the vspd.
altSigVersion = 4
// latestVersion is the latest version of the database that is understood by // latestVersion is the latest version of the database that is understood by
// vspd. Databases with recorded versions higher than this will fail to open // vspd. Databases with recorded versions higher than this will fail to open
// (meaning any upgrades prevent reverting to older software). // (meaning any upgrades prevent reverting to older software).
latestVersion = ticketBucketVersion latestVersion = altSigVersion
) )
// upgrades maps between old database versions and the upgrade function to // upgrades maps between old database versions and the upgrade function to
@ -36,6 +40,7 @@ const (
var upgrades = []func(tx *bolt.DB) error{ var upgrades = []func(tx *bolt.DB) error{
initialVersion: removeOldFeeTxUpgrade, initialVersion: removeOldFeeTxUpgrade,
removeOldFeeTxVersion: ticketBucketUpgrade, removeOldFeeTxVersion: ticketBucketUpgrade,
ticketBucketVersion: altSigUpgrade,
} }
// v1Ticket has the json tags required to unmarshal tickets stored in the // v1Ticket has the json tags required to unmarshal tickets stored in the