Skip to content

Commit

Permalink
vspadmin: Add retirexpub command.
Browse files Browse the repository at this point in the history
The new command opens an existing vspd database and replaces the
currently used xpub with a new one.
  • Loading branch information
jholdstock committed Jun 26, 2024
1 parent 16c9e0c commit c5415f7
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 7 deletions.
14 changes: 14 additions & 0 deletions cmd/vspadmin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,17 @@ Example:
```no-highlight
$ go run ./cmd/vspadmin writeconfig
```

### `retirexpub`

Replaces the currently used xpub with a new one. Once an xpub key has been
retired it can not be used by the VSP again.

**Note:** vspd must be stopped before this command can be used because it
modifies values in the vspd database.

Example:

```no-highlight
$ go run ./cmd/vspadmin retirexpub <xpub>
```
64 changes: 57 additions & 7 deletions cmd/vspadmin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/decred/dcrd/dcrutil/v4"
"github.com/decred/dcrd/hdkeychain/v3"
"github.com/decred/slog"
"github.com/decred/vspd/database"
"github.com/decred/vspd/internal/config"
"github.com/decred/vspd/internal/vspd"
Expand Down Expand Up @@ -45,6 +46,21 @@ func fileExists(name string) bool {
return true
}

// validatePubkey returns an error if the provided key is invalid, not for the
// expected network, or it is public instead of private.
func validatePubkey(key string, network *config.Network) error {
parsedKey, err := hdkeychain.NewKeyFromString(key, network.Params)
if err != nil {
return fmt.Errorf("failed to parse feexpub: %w", err)
}

if parsedKey.IsPrivate() {
return errors.New("feexpub is a private key, should be public")
}

return nil
}

func createDatabase(homeDir string, feeXPub string, network *config.Network) error {
dataDir := filepath.Join(homeDir, "data", network.Name)
dbFile := filepath.Join(dataDir, dbFilename)
Expand All @@ -55,14 +71,9 @@ func createDatabase(homeDir string, feeXPub string, network *config.Network) err
}

// Ensure provided xpub is a valid key for the selected network.
feeXpub, err := hdkeychain.NewKeyFromString(feeXPub, network.Params)
err := validatePubkey(feeXPub, network)
if err != nil {
return fmt.Errorf("failed to parse feexpub: %w", err)
}

// Ensure key is public.
if feeXpub.IsPrivate() {
return errors.New("feexpub is a private key, should be public")
return err
}

// Ensure the data directory exists.
Expand Down Expand Up @@ -106,6 +117,29 @@ func writeConfig(homeDir string) error {
return nil
}

func retireXPub(homeDir string, feeXPub string, network *config.Network) error {
dataDir := filepath.Join(homeDir, "data", network.Name)
dbFile := filepath.Join(dataDir, dbFilename)

// Ensure provided xpub is a valid key for the selected network.
err := validatePubkey(feeXPub, network)
if err != nil {
return err
}

db, err := database.Open(dbFile, slog.Disabled, 999)
if err != nil {
return fmt.Errorf("error opening db file %s: %w", dbFile, err)
}

err = db.RetireXPub(feeXPub)
if err != nil {
return fmt.Errorf("db.RetireXPub failed: %w", err)
}

return nil
}

// run is the real main function for vspadmin. It is necessary to work around
// the fact that deferred functions do not run when os.Exit() is called.
func run() int {
Expand Down Expand Up @@ -161,6 +195,22 @@ func run() int {
log("Config file with default values written to %s", cfg.HomeDir)
log("Edit the file and fill in values specific to your vspd deployment")

case "retirexpub":
if len(remainingArgs) != 2 {
log("retirexpub has one required argument, fee xpub")
return 1
}

feeXPub := remainingArgs[1]

err = retireXPub(cfg.HomeDir, feeXPub, network)
if err != nil {
log("retirexpub failed: %v", err)
return 1
}

log("Xpub successfully retired, all future tickets will use the new xpub")

default:
log("%q is not a valid command", remainingArgs[0])
return 1
Expand Down
1 change: 1 addition & 0 deletions database/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func TestDatabase(t *testing.T) {
"testFilterTickets": testFilterTickets,
"testCountTickets": testCountTickets,
"testFeeXPub": testFeeXPub,
"testRetireFeeXPub": testRetireFeeXPub,
"testDeleteTicket": testDeleteTicket,
"testVoteChangeRecords": testVoteChangeRecords,
"testHTTPBackup": testHTTPBackup,
Expand Down
45 changes: 45 additions & 0 deletions database/feexpub.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ package database

import (
"encoding/json"
"errors"
"fmt"
"time"

bolt "go.etcd.io/bbolt"
)
Expand Down Expand Up @@ -63,6 +65,49 @@ func (vdb *VspDatabase) FeeXPub() (FeeXPub, error) {
return xpubs[highest], nil
}

// RetireXPub will mark the currently active xpub key as retired and insert the
// provided pubkey as the currently active one.
func (vdb *VspDatabase) RetireXPub(xpub string) error {
// Ensure the new xpub has never been used before.
xpubs, err := vdb.AllXPubs()
if err != nil {
return err
}
for _, x := range xpubs {
if x.Key == xpub {
return errors.New("provided xpub has already been used")
}
}

current, err := vdb.FeeXPub()
if err != nil {
return err
}
current.Retired = time.Now().Unix()

return vdb.db.Update(func(tx *bolt.Tx) error {
// Store the retired xpub.
err := insertFeeXPub(tx, current)
if err != nil {
return err
}

// Insert new xpub.
newKey := FeeXPub{
ID: current.ID + 1,
Key: xpub,
LastUsedIdx: 0,
Retired: 0,
}
err = insertFeeXPub(tx, newKey)
if err != nil {
return err
}

return nil
})
}

// AllXPubs retrieves the current and any retired extended pubkeys from the
// database.
func (vdb *VspDatabase) AllXPubs() (map[uint32]FeeXPub, error) {
Expand Down
58 changes: 58 additions & 0 deletions database/feexpub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,61 @@ func testFeeXPub(t *testing.T) {
t.Fatalf("expected xpub retirement 0, got %d", retrievedXPub.Retired)
}
}

func testRetireFeeXPub(t *testing.T) {
// Increment the last used index to simulate some usage.
idx := uint32(99)
err := db.SetLastAddressIndex(idx)
if err != nil {
t.Fatalf("error setting address index: %v", err)
}

// Ensure a previously used xpub is rejected.
err = db.RetireXPub(feeXPub)
if err == nil {
t.Fatalf("previous xpub was not rejected")
}

const expectedErr = "provided xpub has already been used"
if err == nil || err.Error() != expectedErr {
t.Fatalf("incorrect error, expected %q, got %q",
expectedErr, err.Error())
}

// An unused xpub should be accepted.
const feeXPub2 = "feexpub2"
err = db.RetireXPub(feeXPub2)
if err != nil {
t.Fatalf("retiring xpub failed: %v", err)
}

// Retrieve the new xpub. Index should be incremented, last addr should be
// reset to 0, key should not be retired.
retrievedXPub, err := db.FeeXPub()
if err != nil {
t.Fatalf("error getting fee xpub: %v", err)
}

if retrievedXPub.Key != feeXPub2 {
t.Fatalf("expected fee xpub %q, got %q", feeXPub2, retrievedXPub.Key)
}
if retrievedXPub.ID != 1 {
t.Fatalf("expected xpub ID 1, got %d", retrievedXPub.ID)
}
if retrievedXPub.LastUsedIdx != 0 {
t.Fatalf("expected xpub last used 0, got %d", retrievedXPub.LastUsedIdx)
}
if retrievedXPub.Retired != 0 {
t.Fatalf("expected xpub retirement 0, got %d", retrievedXPub.Retired)
}

// Old xpub should have retired field set.
xpubs, err := db.AllXPubs()
if err != nil {
t.Fatalf("error getting all fee xpubs: %v", err)
}

if xpubs[0].Retired == 0 {
t.Fatalf("old xpub retired field not set")
}
}

0 comments on commit c5415f7

Please sign in to comment.