vspadmin: Add retirexpub command.
The new command opens an existing vspd database and replaces the currently used xpub with a new one.
This commit is contained in:
parent
c23095444f
commit
cab4058710
@ -38,3 +38,17 @@ Example:
|
|||||||
```no-highlight
|
```no-highlight
|
||||||
$ go run ./cmd/vspadmin writeconfig
|
$ 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>
|
||||||
|
```
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/decred/dcrd/dcrutil/v4"
|
"github.com/decred/dcrd/dcrutil/v4"
|
||||||
"github.com/decred/dcrd/hdkeychain/v3"
|
"github.com/decred/dcrd/hdkeychain/v3"
|
||||||
|
"github.com/decred/slog"
|
||||||
"github.com/decred/vspd/database"
|
"github.com/decred/vspd/database"
|
||||||
"github.com/decred/vspd/internal/config"
|
"github.com/decred/vspd/internal/config"
|
||||||
"github.com/decred/vspd/internal/vspd"
|
"github.com/decred/vspd/internal/vspd"
|
||||||
@ -45,6 +46,21 @@ func fileExists(name string) bool {
|
|||||||
return true
|
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 {
|
func createDatabase(homeDir string, feeXPub string, network *config.Network) error {
|
||||||
dataDir := filepath.Join(homeDir, "data", network.Name)
|
dataDir := filepath.Join(homeDir, "data", network.Name)
|
||||||
dbFile := filepath.Join(dataDir, dbFilename)
|
dbFile := filepath.Join(dataDir, dbFilename)
|
||||||
@ -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.
|
// 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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse feexpub: %w", err)
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure key is public.
|
|
||||||
if feeXpub.IsPrivate() {
|
|
||||||
return errors.New("feexpub is a private key, should be public")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the data directory exists.
|
// Ensure the data directory exists.
|
||||||
@ -106,6 +117,29 @@ func writeConfig(homeDir string) error {
|
|||||||
return nil
|
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
|
// 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.
|
// the fact that deferred functions do not run when os.Exit() is called.
|
||||||
func run() int {
|
func run() int {
|
||||||
@ -161,6 +195,22 @@ func run() int {
|
|||||||
log("Config file with default values written to %s", cfg.HomeDir)
|
log("Config file with default values written to %s", cfg.HomeDir)
|
||||||
log("Edit the file and fill in values specific to your vspd deployment")
|
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:
|
default:
|
||||||
log("%q is not a valid command", remainingArgs[0])
|
log("%q is not a valid command", remainingArgs[0])
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@ -78,6 +78,7 @@ func TestDatabase(t *testing.T) {
|
|||||||
"testFilterTickets": testFilterTickets,
|
"testFilterTickets": testFilterTickets,
|
||||||
"testCountTickets": testCountTickets,
|
"testCountTickets": testCountTickets,
|
||||||
"testFeeXPub": testFeeXPub,
|
"testFeeXPub": testFeeXPub,
|
||||||
|
"testRetireFeeXPub": testRetireFeeXPub,
|
||||||
"testDeleteTicket": testDeleteTicket,
|
"testDeleteTicket": testDeleteTicket,
|
||||||
"testVoteChangeRecords": testVoteChangeRecords,
|
"testVoteChangeRecords": testVoteChangeRecords,
|
||||||
"testHTTPBackup": testHTTPBackup,
|
"testHTTPBackup": testHTTPBackup,
|
||||||
|
|||||||
@ -6,7 +6,9 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
@ -63,6 +65,49 @@ func (vdb *VspDatabase) FeeXPub() (FeeXPub, error) {
|
|||||||
return xpubs[highest], nil
|
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
|
// AllXPubs retrieves the current and any retired extended pubkeys from the
|
||||||
// database.
|
// database.
|
||||||
func (vdb *VspDatabase) AllXPubs() (map[uint32]FeeXPub, error) {
|
func (vdb *VspDatabase) AllXPubs() (map[uint32]FeeXPub, error) {
|
||||||
|
|||||||
@ -57,3 +57,61 @@ func testFeeXPub(t *testing.T) {
|
|||||||
t.Fatalf("expected xpub retirement 0, got %d", retrievedXPub.Retired)
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user