Skip to content

Commit cab4058

Browse files
committed
vspadmin: Add retirexpub command.
The new command opens an existing vspd database and replaces the currently used xpub with a new one.
1 parent c230954 commit cab4058

File tree

5 files changed

+175
-7
lines changed

5 files changed

+175
-7
lines changed

cmd/vspadmin/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,17 @@ Example:
3838
```no-highlight
3939
$ go run ./cmd/vspadmin writeconfig
4040
```
41+
42+
### `retirexpub`
43+
44+
Replaces the currently used xpub with a new one. Once an xpub key has been
45+
retired it can not be used by the VSP again.
46+
47+
**Note:** vspd must be stopped before this command can be used because it
48+
modifies values in the vspd database.
49+
50+
Example:
51+
52+
```no-highlight
53+
$ go run ./cmd/vspadmin retirexpub <xpub>
54+
```

cmd/vspadmin/main.go

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/decred/dcrd/dcrutil/v4"
1414
"github.com/decred/dcrd/hdkeychain/v3"
15+
"github.com/decred/slog"
1516
"github.com/decred/vspd/database"
1617
"github.com/decred/vspd/internal/config"
1718
"github.com/decred/vspd/internal/vspd"
@@ -45,6 +46,21 @@ func fileExists(name string) bool {
4546
return true
4647
}
4748

49+
// validatePubkey returns an error if the provided key is invalid, not for the
50+
// expected network, or it is public instead of private.
51+
func validatePubkey(key string, network *config.Network) error {
52+
parsedKey, err := hdkeychain.NewKeyFromString(key, network.Params)
53+
if err != nil {
54+
return fmt.Errorf("failed to parse feexpub: %w", err)
55+
}
56+
57+
if parsedKey.IsPrivate() {
58+
return errors.New("feexpub is a private key, should be public")
59+
}
60+
61+
return nil
62+
}
63+
4864
func createDatabase(homeDir string, feeXPub string, network *config.Network) error {
4965
dataDir := filepath.Join(homeDir, "data", network.Name)
5066
dbFile := filepath.Join(dataDir, dbFilename)
@@ -55,14 +71,9 @@ func createDatabase(homeDir string, feeXPub string, network *config.Network) err
5571
}
5672

5773
// Ensure provided xpub is a valid key for the selected network.
58-
feeXpub, err := hdkeychain.NewKeyFromString(feeXPub, network.Params)
74+
err := validatePubkey(feeXPub, network)
5975
if err != nil {
60-
return fmt.Errorf("failed to parse feexpub: %w", err)
61-
}
62-
63-
// Ensure key is public.
64-
if feeXpub.IsPrivate() {
65-
return errors.New("feexpub is a private key, should be public")
76+
return err
6677
}
6778

6879
// Ensure the data directory exists.
@@ -106,6 +117,29 @@ func writeConfig(homeDir string) error {
106117
return nil
107118
}
108119

120+
func retireXPub(homeDir string, feeXPub string, network *config.Network) error {
121+
dataDir := filepath.Join(homeDir, "data", network.Name)
122+
dbFile := filepath.Join(dataDir, dbFilename)
123+
124+
// Ensure provided xpub is a valid key for the selected network.
125+
err := validatePubkey(feeXPub, network)
126+
if err != nil {
127+
return err
128+
}
129+
130+
db, err := database.Open(dbFile, slog.Disabled, 999)
131+
if err != nil {
132+
return fmt.Errorf("error opening db file %s: %w", dbFile, err)
133+
}
134+
135+
err = db.RetireXPub(feeXPub)
136+
if err != nil {
137+
return fmt.Errorf("db.RetireXPub failed: %w", err)
138+
}
139+
140+
return nil
141+
}
142+
109143
// run is the real main function for vspadmin. It is necessary to work around
110144
// the fact that deferred functions do not run when os.Exit() is called.
111145
func run() int {
@@ -161,6 +195,22 @@ func run() int {
161195
log("Config file with default values written to %s", cfg.HomeDir)
162196
log("Edit the file and fill in values specific to your vspd deployment")
163197

198+
case "retirexpub":
199+
if len(remainingArgs) != 2 {
200+
log("retirexpub has one required argument, fee xpub")
201+
return 1
202+
}
203+
204+
feeXPub := remainingArgs[1]
205+
206+
err = retireXPub(cfg.HomeDir, feeXPub, network)
207+
if err != nil {
208+
log("retirexpub failed: %v", err)
209+
return 1
210+
}
211+
212+
log("Xpub successfully retired, all future tickets will use the new xpub")
213+
164214
default:
165215
log("%q is not a valid command", remainingArgs[0])
166216
return 1

database/database_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func TestDatabase(t *testing.T) {
7878
"testFilterTickets": testFilterTickets,
7979
"testCountTickets": testCountTickets,
8080
"testFeeXPub": testFeeXPub,
81+
"testRetireFeeXPub": testRetireFeeXPub,
8182
"testDeleteTicket": testDeleteTicket,
8283
"testVoteChangeRecords": testVoteChangeRecords,
8384
"testHTTPBackup": testHTTPBackup,

database/feexpub.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ package database
66

77
import (
88
"encoding/json"
9+
"errors"
910
"fmt"
11+
"time"
1012

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

68+
// RetireXPub will mark the currently active xpub key as retired and insert the
69+
// provided pubkey as the currently active one.
70+
func (vdb *VspDatabase) RetireXPub(xpub string) error {
71+
// Ensure the new xpub has never been used before.
72+
xpubs, err := vdb.AllXPubs()
73+
if err != nil {
74+
return err
75+
}
76+
for _, x := range xpubs {
77+
if x.Key == xpub {
78+
return errors.New("provided xpub has already been used")
79+
}
80+
}
81+
82+
current, err := vdb.FeeXPub()
83+
if err != nil {
84+
return err
85+
}
86+
current.Retired = time.Now().Unix()
87+
88+
return vdb.db.Update(func(tx *bolt.Tx) error {
89+
// Store the retired xpub.
90+
err := insertFeeXPub(tx, current)
91+
if err != nil {
92+
return err
93+
}
94+
95+
// Insert new xpub.
96+
newKey := FeeXPub{
97+
ID: current.ID + 1,
98+
Key: xpub,
99+
LastUsedIdx: 0,
100+
Retired: 0,
101+
}
102+
err = insertFeeXPub(tx, newKey)
103+
if err != nil {
104+
return err
105+
}
106+
107+
return nil
108+
})
109+
}
110+
66111
// AllXPubs retrieves the current and any retired extended pubkeys from the
67112
// database.
68113
func (vdb *VspDatabase) AllXPubs() (map[uint32]FeeXPub, error) {

database/feexpub_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,61 @@ func testFeeXPub(t *testing.T) {
5757
t.Fatalf("expected xpub retirement 0, got %d", retrievedXPub.Retired)
5858
}
5959
}
60+
61+
func testRetireFeeXPub(t *testing.T) {
62+
// Increment the last used index to simulate some usage.
63+
idx := uint32(99)
64+
err := db.SetLastAddressIndex(idx)
65+
if err != nil {
66+
t.Fatalf("error setting address index: %v", err)
67+
}
68+
69+
// Ensure a previously used xpub is rejected.
70+
err = db.RetireXPub(feeXPub)
71+
if err == nil {
72+
t.Fatalf("previous xpub was not rejected")
73+
}
74+
75+
const expectedErr = "provided xpub has already been used"
76+
if err == nil || err.Error() != expectedErr {
77+
t.Fatalf("incorrect error, expected %q, got %q",
78+
expectedErr, err.Error())
79+
}
80+
81+
// An unused xpub should be accepted.
82+
const feeXPub2 = "feexpub2"
83+
err = db.RetireXPub(feeXPub2)
84+
if err != nil {
85+
t.Fatalf("retiring xpub failed: %v", err)
86+
}
87+
88+
// Retrieve the new xpub. Index should be incremented, last addr should be
89+
// reset to 0, key should not be retired.
90+
retrievedXPub, err := db.FeeXPub()
91+
if err != nil {
92+
t.Fatalf("error getting fee xpub: %v", err)
93+
}
94+
95+
if retrievedXPub.Key != feeXPub2 {
96+
t.Fatalf("expected fee xpub %q, got %q", feeXPub2, retrievedXPub.Key)
97+
}
98+
if retrievedXPub.ID != 1 {
99+
t.Fatalf("expected xpub ID 1, got %d", retrievedXPub.ID)
100+
}
101+
if retrievedXPub.LastUsedIdx != 0 {
102+
t.Fatalf("expected xpub last used 0, got %d", retrievedXPub.LastUsedIdx)
103+
}
104+
if retrievedXPub.Retired != 0 {
105+
t.Fatalf("expected xpub retirement 0, got %d", retrievedXPub.Retired)
106+
}
107+
108+
// Old xpub should have retired field set.
109+
xpubs, err := db.AllXPubs()
110+
if err != nil {
111+
t.Fatalf("error getting all fee xpubs: %v", err)
112+
}
113+
114+
if xpubs[0].Retired == 0 {
115+
t.Fatalf("old xpub retired field not set")
116+
}
117+
}

0 commit comments

Comments
 (0)