/
accounts_backup.go
249 lines (235 loc) · 8.08 KB
/
accounts_backup.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
package accounts
import (
"archive/zip"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/logrusorgru/aurora"
"github.com/manifoldco/promptui"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/cmd/validator/flags"
"github.com/prysmaticlabs/prysm/shared/bls"
"github.com/prysmaticlabs/prysm/shared/bytesutil"
"github.com/prysmaticlabs/prysm/shared/fileutil"
"github.com/prysmaticlabs/prysm/shared/petnames"
"github.com/prysmaticlabs/prysm/shared/promptutil"
"github.com/prysmaticlabs/prysm/validator/accounts/iface"
"github.com/prysmaticlabs/prysm/validator/accounts/prompt"
"github.com/prysmaticlabs/prysm/validator/accounts/wallet"
"github.com/prysmaticlabs/prysm/validator/keymanager"
"github.com/prysmaticlabs/prysm/validator/keymanager/derived"
"github.com/prysmaticlabs/prysm/validator/keymanager/imported"
"github.com/urfave/cli/v2"
)
var (
au = aurora.NewAurora(true)
)
const (
allAccountsText = "All accounts"
archiveFilename = "backup.zip"
backupPromptText = "Enter the directory where your backup.zip file will be written to"
)
// BackupAccountsCli allows users to select validator accounts from their wallet
// and export them as a backup.zip file containing the keys as EIP-2335 compliant
// keystore.json files, which are compatible with importing in other Ethereum consensus clients.
func BackupAccountsCli(cliCtx *cli.Context) error {
w, err := wallet.OpenWalletOrElseCli(cliCtx, func(cliCtx *cli.Context) (*wallet.Wallet, error) {
return nil, wallet.ErrNoWalletFound
})
if err != nil {
return errors.Wrap(err, "could not initialize wallet")
}
if w.KeymanagerKind() == keymanager.Remote {
return errors.New(
"remote wallets cannot backup accounts",
)
}
km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false})
if err != nil {
return errors.Wrap(err, ErrCouldNotInitializeKeymanager)
}
pubKeys, err := km.FetchValidatingPublicKeys(cliCtx.Context)
if err != nil {
return errors.Wrap(err, "could not fetch validating public keys")
}
// Input the directory where they wish to backup their accounts.
backupDir, err := prompt.InputDirectory(cliCtx, backupPromptText, flags.BackupDirFlag)
if err != nil {
return errors.Wrap(err, "could not parse keys directory")
}
// Allow the user to interactively select the accounts to backup or optionally
// provide them via cli flags as a string of comma-separated, hex strings.
filteredPubKeys, err := filterPublicKeysFromUserInput(
cliCtx,
flags.BackupPublicKeysFlag,
pubKeys,
prompt.SelectAccountsBackupPromptText,
)
if err != nil {
return errors.Wrap(err, "could not filter public keys for backup")
}
// Ask the user for their desired password for their backed up accounts.
backupsPassword, err := promptutil.InputPassword(
cliCtx,
flags.BackupPasswordFile,
"Enter a new password for your backed up accounts",
"Confirm new password",
true,
promptutil.ValidatePasswordInput,
)
if err != nil {
return errors.Wrap(err, "could not determine password for backed up accounts")
}
var keystoresToBackup []*keymanager.Keystore
switch w.KeymanagerKind() {
case keymanager.Imported:
km, ok := km.(*imported.Keymanager)
if !ok {
return errors.New("could not assert keymanager interface to concrete type")
}
keystoresToBackup, err = km.ExtractKeystores(cliCtx.Context, filteredPubKeys, backupsPassword)
if err != nil {
return errors.Wrap(err, "could not backup accounts for imported keymanager")
}
case keymanager.Derived:
km, ok := km.(*derived.Keymanager)
if !ok {
return errors.New("could not assert keymanager interface to concrete type")
}
keystoresToBackup, err = km.ExtractKeystores(cliCtx.Context, filteredPubKeys, backupsPassword)
if err != nil {
return errors.Wrap(err, "could not backup accounts for derived keymanager")
}
case keymanager.Remote:
return errors.New("backing up keys is not supported for a remote keymanager")
default:
return fmt.Errorf(errKeymanagerNotSupported, w.KeymanagerKind())
}
return zipKeystoresToOutputDir(keystoresToBackup, backupDir)
}
// Ask user to select accounts via an interactive prompt.
func selectAccounts(selectionPrompt string, pubKeys [][48]byte) (filteredPubKeys []bls.PublicKey, err error) {
pubKeyStrings := make([]string, len(pubKeys))
for i, pk := range pubKeys {
name := petnames.DeterministicName(pk[:], "-")
pubKeyStrings[i] = fmt.Sprintf(
"%d | %s | %#x", i, au.BrightGreen(name), au.BrightMagenta(bytesutil.Trunc(pk[:])),
)
}
templates := &promptui.SelectTemplates{
Label: "{{ . }}",
Active: "\U0001F336 {{ .Name | cyan }}",
Inactive: " {{ .Name | cyan }}",
Selected: "\U0001F336 {{ .Name | red | cyan }}",
Details: `
--------- Account ----------
{{ "Name:" | faint }} {{ .Name }}`,
}
var result string
exit := "Done selecting"
results := make([]int, 0)
au := aurora.NewAurora(true)
for result != exit {
p := promptui.Select{
Label: selectionPrompt,
HideSelected: true,
Items: append([]string{exit, allAccountsText}, pubKeyStrings...),
Templates: templates,
}
_, result, err = p.Run()
if err != nil {
return nil, err
}
if result == exit {
fmt.Printf("%s\n", au.BrightRed("Done with selections").Bold())
break
}
if result == allAccountsText {
fmt.Printf("%s\n", au.BrightRed("[Selected all accounts]").Bold())
for i := 0; i < len(pubKeys); i++ {
results = append(results, i)
}
break
}
idx := strings.Index(result, " |")
accountIndexStr := result[:idx]
accountIndex, err := strconv.Atoi(accountIndexStr)
if err != nil {
return nil, err
}
results = append(results, accountIndex)
fmt.Printf("%s %s\n", au.BrightRed("[Selected account]").Bold(), result)
}
// Deduplicate the results.
seen := make(map[int]bool)
for i := 0; i < len(results); i++ {
if _, ok := seen[results[i]]; !ok {
seen[results[i]] = true
}
}
// Filter the public keys based on user input.
filteredPubKeys = make([]bls.PublicKey, 0)
for selectedIndex := range seen {
pk, err := bls.PublicKeyFromBytes(pubKeys[selectedIndex][:])
if err != nil {
return nil, err
}
filteredPubKeys = append(filteredPubKeys, pk)
}
return filteredPubKeys, nil
}
// Zips a list of keystore into respective EIP-2335 keystore.json files and
// writes their zipped format into the specified output directory.
func zipKeystoresToOutputDir(keystoresToBackup []*keymanager.Keystore, outputDir string) error {
if len(keystoresToBackup) == 0 {
return errors.New("nothing to backup")
}
if err := fileutil.MkdirAll(outputDir); err != nil {
return errors.Wrapf(err, "could not create directory at path: %s", outputDir)
}
// Marshal and zip all keystore files together and write the zip file
// to the specified output directory.
archivePath := filepath.Join(outputDir, archiveFilename)
if fileutil.FileExists(archivePath) {
return errors.Errorf("Zip file already exists in directory: %s", archivePath)
}
// We create a new file to store our backup.zip.
zipfile, err := os.Create(archivePath)
if err != nil {
return errors.Wrapf(err, "could not create zip file with path: %s", archivePath)
}
defer func() {
if err := zipfile.Close(); err != nil {
log.WithError(err).Error("Could not close zipfile")
}
}()
// Using this zip file, we create a new zip writer which we write
// files to directly from our marshaled keystores.
writer := zip.NewWriter(zipfile)
defer func() {
// We close the zip writer when done.
if err := writer.Close(); err != nil {
log.WithError(err).Error("Could not close zip file after writing")
}
}()
for i, k := range keystoresToBackup {
encodedFile, err := json.MarshalIndent(k, "", "\t")
if err != nil {
return errors.Wrap(err, "could not marshal keystore to JSON file")
}
f, err := writer.Create(fmt.Sprintf("keystore-%d.json", i))
if err != nil {
return errors.Wrap(err, "could not write keystore file to zip")
}
if _, err = f.Write(encodedFile); err != nil {
return errors.Wrap(err, "could not write keystore file contents")
}
}
log.WithField(
"backup-path", archivePath,
).Infof("Successfully backed up %d accounts", len(keystoresToBackup))
return nil
}