diff --git a/database/altsig.go b/database/altsig.go new file mode 100644 index 0000000..287a9c6 --- /dev/null +++ b/database/altsig.go @@ -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 + }) +} diff --git a/database/altsig_test.go b/database/altsig_test.go new file mode 100644 index 0000000..53b1c08 --- /dev/null +++ b/database/altsig_test.go @@ -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) +} diff --git a/database/database.go b/database/database.go index 5e52bd0..0c24d98 100644 --- a/database/database.go +++ b/database/database.go @@ -50,6 +50,8 @@ var ( privateKeyK = []byte("privatekey") // lastaddressindex is the index of the last address used for fees. lastAddressIndexK = []byte("lastaddressindex") + // altSigBktK stores alternate signatures. + altSigBktK = []byte("altsigbkt") ) const ( diff --git a/database/database_test.go b/database/database_test.go index 3122136..1eaf343 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -8,6 +8,7 @@ import ( "context" "crypto/ed25519" "io" + "math/rand" "net/http" "net/http/httptest" "os" @@ -22,12 +23,36 @@ const ( backupDb = "test.db-backup" feeXPub = "feexpub" maxVoteChangeRecords = 3 + + addrCharset = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + hexCharset = "1234567890abcdef" + sigCharset = "0123456789ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/=" ) 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. func TestDatabase(t *testing.T) { // Ensure we are starting with a clean environment. @@ -47,6 +72,9 @@ func TestDatabase(t *testing.T) { "testDeleteTicket": testDeleteTicket, "testVoteChangeRecords": testVoteChangeRecords, "testHTTPBackup": testHTTPBackup, + "testAltSigData": testAltSigData, + "testInsertAltSig": testInsertAltSig, + "testDeleteAltSig": testDeleteAltSig, } for testName, test := range tests { diff --git a/database/ticket_test.go b/database/ticket_test.go index daa5e3f..fe78f62 100644 --- a/database/ticket_test.go +++ b/database/ticket_test.go @@ -5,7 +5,6 @@ package database import ( - "math/rand" "reflect" "testing" "time" @@ -13,22 +12,7 @@ import ( 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 { - const hexCharset = "1234567890abcdef" - const addrCharset = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" - return Ticket{ Hash: randString(64, hexCharset), CommitmentAddress: randString(35, addrCharset), diff --git a/database/upgrade_v4.go b/database/upgrade_v4.go new file mode 100644 index 0000000..de12b24 --- /dev/null +++ b/database/upgrade_v4.go @@ -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 +} diff --git a/database/upgrades.go b/database/upgrades.go index 29a4666..64a30be 100644 --- a/database/upgrades.go +++ b/database/upgrades.go @@ -25,10 +25,14 @@ const ( // moves each ticket into its own bucket and does away with JSON encoding. 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 // vspd. Databases with recorded versions higher than this will fail to open // (meaning any upgrades prevent reverting to older software). - latestVersion = ticketBucketVersion + latestVersion = altSigVersion ) // upgrades maps between old database versions and the upgrade function to @@ -36,6 +40,7 @@ const ( var upgrades = []func(tx *bolt.DB) error{ initialVersion: removeOldFeeTxUpgrade, removeOldFeeTxVersion: ticketBucketUpgrade, + ticketBucketVersion: altSigUpgrade, } // v1Ticket has the json tags required to unmarshal tickets stored in the