github.com/prysmaticlabs/prysm@v1.4.4/validator/accounts/accounts_import.go (about) 1 package accounts 2 3 import ( 4 "context" 5 "encoding/hex" 6 "encoding/json" 7 "fmt" 8 "io/ioutil" 9 "path/filepath" 10 "regexp" 11 "sort" 12 "strconv" 13 "strings" 14 15 "github.com/google/uuid" 16 "github.com/pkg/errors" 17 "github.com/prysmaticlabs/prysm/cmd/validator/flags" 18 "github.com/prysmaticlabs/prysm/shared/bls" 19 "github.com/prysmaticlabs/prysm/shared/bytesutil" 20 "github.com/prysmaticlabs/prysm/shared/fileutil" 21 "github.com/prysmaticlabs/prysm/shared/promptutil" 22 "github.com/prysmaticlabs/prysm/validator/accounts/iface" 23 "github.com/prysmaticlabs/prysm/validator/accounts/prompt" 24 "github.com/prysmaticlabs/prysm/validator/accounts/wallet" 25 "github.com/prysmaticlabs/prysm/validator/keymanager" 26 "github.com/prysmaticlabs/prysm/validator/keymanager/imported" 27 "github.com/urfave/cli/v2" 28 keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4" 29 ) 30 31 var derivationPathRegex = regexp.MustCompile(`m_12381_3600_(\d+)_(\d+)_(\d+)`) 32 33 // byDerivationPath implements sort.Interface based on a 34 // derivation path present in a keystore filename, if any. This 35 // will allow us to sort filenames such as keystore-m_12381_3600_1_0_0.json 36 // in a directory and import them nicely in order of the derivation path. 37 type byDerivationPath []string 38 39 // Len is the number of elements in the collection. 40 func (fileNames byDerivationPath) Len() int { return len(fileNames) } 41 42 // Less reports whether the element with index i must sort before the element with index j. 43 func (fileNames byDerivationPath) Less(i, j int) bool { 44 // We check if file name at index i has a derivation path 45 // in the filename. If it does not, then it is not less than j, and 46 // we should swap it towards the end of the sorted list. 47 if !derivationPathRegex.MatchString(fileNames[i]) { 48 return false 49 } 50 derivationPathA := derivationPathRegex.FindString(fileNames[i]) 51 derivationPathB := derivationPathRegex.FindString(fileNames[j]) 52 if derivationPathA == "" { 53 return false 54 } 55 if derivationPathB == "" { 56 return true 57 } 58 a, err := strconv.Atoi(accountIndexFromFileName(derivationPathA)) 59 if err != nil { 60 return false 61 } 62 b, err := strconv.Atoi(accountIndexFromFileName(derivationPathB)) 63 if err != nil { 64 return false 65 } 66 return a < b 67 } 68 69 // Swap swaps the elements with indexes i and j. 70 func (fileNames byDerivationPath) Swap(i, j int) { 71 fileNames[i], fileNames[j] = fileNames[j], fileNames[i] 72 } 73 74 // ImportAccountsConfig defines values to run the import accounts function. 75 type ImportAccountsConfig struct { 76 Keystores []*keymanager.Keystore 77 Keymanager *imported.Keymanager 78 AccountPassword string 79 } 80 81 // ImportAccountsCli can import external, EIP-2335 compliant keystore.json files as 82 // new accounts into the Prysm validator wallet. This uses the CLI to extract 83 // values necessary to run the function. 84 func ImportAccountsCli(cliCtx *cli.Context) error { 85 w, err := wallet.OpenWalletOrElseCli(cliCtx, func(cliCtx *cli.Context) (*wallet.Wallet, error) { 86 walletDir, err := prompt.InputDirectory(cliCtx, prompt.WalletDirPromptText, flags.WalletDirFlag) 87 if err != nil { 88 return nil, err 89 } 90 exists, err := wallet.Exists(walletDir) 91 if err != nil { 92 return nil, errors.Wrap(err, wallet.CheckExistsErrMsg) 93 } 94 if exists { 95 isValid, err := wallet.IsValid(walletDir) 96 if err != nil { 97 return nil, errors.Wrap(err, wallet.CheckValidityErrMsg) 98 } 99 if !isValid { 100 return nil, errors.New(wallet.InvalidWalletErrMsg) 101 } 102 walletPassword, err := wallet.InputPassword( 103 cliCtx, 104 flags.WalletPasswordFileFlag, 105 wallet.PasswordPromptText, 106 false, /* Do not confirm password */ 107 wallet.ValidateExistingPass, 108 ) 109 if err != nil { 110 return nil, err 111 } 112 return wallet.OpenWallet(cliCtx.Context, &wallet.Config{ 113 WalletDir: walletDir, 114 WalletPassword: walletPassword, 115 }) 116 } 117 118 cfg, err := extractWalletCreationConfigFromCli(cliCtx, keymanager.Imported) 119 if err != nil { 120 return nil, err 121 } 122 w := wallet.New(&wallet.Config{ 123 KeymanagerKind: cfg.WalletCfg.KeymanagerKind, 124 WalletDir: cfg.WalletCfg.WalletDir, 125 WalletPassword: cfg.WalletCfg.WalletPassword, 126 }) 127 if err = createImportedKeymanagerWallet(cliCtx.Context, w); err != nil { 128 return nil, errors.Wrap(err, "could not create keymanager") 129 } 130 log.WithField("wallet-path", cfg.WalletCfg.WalletDir).Info( 131 "Successfully created new wallet", 132 ) 133 return w, nil 134 }) 135 if err != nil { 136 return errors.Wrap(err, "could not initialize wallet") 137 } 138 139 km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false}) 140 if err != nil { 141 return err 142 } 143 k, ok := km.(*imported.Keymanager) 144 if !ok { 145 return errors.New("only imported wallets can import more keystores") 146 } 147 148 // Check if the user wishes to import a one-off, private key directly 149 // as an account into the Prysm validator. 150 if cliCtx.IsSet(flags.ImportPrivateKeyFileFlag.Name) { 151 return importPrivateKeyAsAccount(cliCtx, w, k) 152 } 153 154 keysDir, err := prompt.InputDirectory(cliCtx, prompt.ImportKeysDirPromptText, flags.KeysDirFlag) 155 if err != nil { 156 return errors.Wrap(err, "could not parse keys directory") 157 } 158 // Consider that the keysDir might be a path to a specific file and handle accordingly. 159 isDir, err := fileutil.HasDir(keysDir) 160 if err != nil { 161 return errors.Wrap(err, "could not determine if path is a directory") 162 } 163 keystoresImported := make([]*keymanager.Keystore, 0) 164 if isDir { 165 files, err := ioutil.ReadDir(keysDir) 166 if err != nil { 167 return errors.Wrap(err, "could not read dir") 168 } 169 if len(files) == 0 { 170 return fmt.Errorf("directory %s has no files, cannot import from it", keysDir) 171 } 172 filesInDir := make([]string, 0) 173 for i := 0; i < len(files); i++ { 174 if files[i].IsDir() { 175 continue 176 } 177 filesInDir = append(filesInDir, files[i].Name()) 178 } 179 // Sort the imported keystores by derivation path if they 180 // specify this value in their filename. 181 sort.Sort(byDerivationPath(filesInDir)) 182 for _, name := range filesInDir { 183 keystore, err := readKeystoreFile(cliCtx.Context, filepath.Join(keysDir, name)) 184 if err != nil && strings.Contains(err.Error(), "could not decode keystore json") { 185 continue 186 } else if err != nil { 187 return errors.Wrapf(err, "could not import keystore at path: %s", name) 188 } 189 keystoresImported = append(keystoresImported, keystore) 190 } 191 } else { 192 keystore, err := readKeystoreFile(cliCtx.Context, keysDir) 193 if err != nil { 194 return errors.Wrap(err, "could not import keystore") 195 } 196 keystoresImported = append(keystoresImported, keystore) 197 } 198 199 var accountsPassword string 200 if cliCtx.IsSet(flags.AccountPasswordFileFlag.Name) { 201 passwordFilePath := cliCtx.String(flags.AccountPasswordFileFlag.Name) 202 data, err := ioutil.ReadFile(passwordFilePath) 203 if err != nil { 204 return err 205 } 206 accountsPassword = string(data) 207 } else { 208 accountsPassword, err = promptutil.PasswordPrompt( 209 "Enter the password for your imported accounts", promptutil.NotEmpty, 210 ) 211 if err != nil { 212 return fmt.Errorf("could not read account password: %w", err) 213 } 214 } 215 fmt.Println("Importing accounts, this may take a while...") 216 if err := ImportAccounts(cliCtx.Context, &ImportAccountsConfig{ 217 Keymanager: k, 218 Keystores: keystoresImported, 219 AccountPassword: accountsPassword, 220 }); err != nil { 221 return err 222 } 223 fmt.Printf( 224 "Successfully imported %s accounts, view all of them by running `accounts list`\n", 225 au.BrightMagenta(strconv.Itoa(len(keystoresImported))), 226 ) 227 return nil 228 } 229 230 // ImportAccounts can import external, EIP-2335 compliant keystore.json files as 231 // new accounts into the Prysm validator wallet. 232 func ImportAccounts(ctx context.Context, cfg *ImportAccountsConfig) error { 233 return cfg.Keymanager.ImportKeystores( 234 ctx, 235 cfg.Keystores, 236 cfg.AccountPassword, 237 ) 238 } 239 240 // Imports a one-off file containing a private key as a hex string into 241 // the Prysm validator's accounts. 242 func importPrivateKeyAsAccount(cliCtx *cli.Context, wallet *wallet.Wallet, km *imported.Keymanager) error { 243 privKeyFile := cliCtx.String(flags.ImportPrivateKeyFileFlag.Name) 244 fullPath, err := fileutil.ExpandPath(privKeyFile) 245 if err != nil { 246 return errors.Wrapf(err, "could not expand file path for %s", privKeyFile) 247 } 248 if !fileutil.FileExists(fullPath) { 249 return fmt.Errorf("file %s does not exist", fullPath) 250 } 251 privKeyHex, err := ioutil.ReadFile(fullPath) 252 if err != nil { 253 return errors.Wrapf(err, "could not read private key file at path %s", fullPath) 254 } 255 privKeyString := string(privKeyHex) 256 if len(privKeyString) > 2 && strings.Contains(privKeyString, "0x") { 257 privKeyString = privKeyString[2:] // Strip the 0x prefix, if any. 258 } 259 privKeyBytes, err := hex.DecodeString(strings.TrimRight(privKeyString, "\r\n")) 260 if err != nil { 261 return errors.Wrap( 262 err, "could not decode file as hex string, does the file contain a valid hex string?", 263 ) 264 } 265 privKey, err := bls.SecretKeyFromBytes(privKeyBytes) 266 if err != nil { 267 return errors.Wrap(err, "not a valid BLS private key") 268 } 269 keystore, err := createKeystoreFromPrivateKey(privKey, wallet.Password()) 270 if err != nil { 271 return errors.Wrap(err, "could not encrypt private key into a keystore file") 272 } 273 if err := ImportAccounts( 274 cliCtx.Context, 275 &ImportAccountsConfig{ 276 Keymanager: km, 277 AccountPassword: wallet.Password(), 278 Keystores: []*keymanager.Keystore{keystore}, 279 }, 280 ); err != nil { 281 return errors.Wrap(err, "could not import keystore into wallet") 282 } 283 fmt.Printf( 284 "Imported account with public key %#x, view all accounts by running `accounts list`\n", 285 au.BrightMagenta(bytesutil.Trunc(privKey.PublicKey().Marshal())), 286 ) 287 return nil 288 } 289 290 func readKeystoreFile(_ context.Context, keystoreFilePath string) (*keymanager.Keystore, error) { 291 keystoreBytes, err := ioutil.ReadFile(keystoreFilePath) 292 if err != nil { 293 return nil, errors.Wrap(err, "could not read keystore file") 294 } 295 keystoreFile := &keymanager.Keystore{} 296 if err := json.Unmarshal(keystoreBytes, keystoreFile); err != nil { 297 return nil, errors.Wrap(err, "could not decode keystore json") 298 } 299 if keystoreFile.Pubkey == "" { 300 return nil, errors.New("could not decode keystore json") 301 } 302 return keystoreFile, nil 303 } 304 305 func createKeystoreFromPrivateKey(privKey bls.SecretKey, walletPassword string) (*keymanager.Keystore, error) { 306 encryptor := keystorev4.New() 307 id, err := uuid.NewRandom() 308 if err != nil { 309 return nil, err 310 } 311 cryptoFields, err := encryptor.Encrypt(privKey.Marshal(), walletPassword) 312 if err != nil { 313 return nil, errors.Wrapf( 314 err, 315 "could not encrypt private key with public key %#x", 316 privKey.PublicKey().Marshal(), 317 ) 318 } 319 return &keymanager.Keystore{ 320 Crypto: cryptoFields, 321 ID: id.String(), 322 Version: encryptor.Version(), 323 Pubkey: fmt.Sprintf("%x", privKey.PublicKey().Marshal()), 324 Name: encryptor.Name(), 325 }, nil 326 } 327 328 // Extracts the account index, j, from a derivation path in a file name 329 // with the format m_12381_3600_j_0_0. 330 func accountIndexFromFileName(derivationPath string) string { 331 derivationPath = derivationPath[13:] 332 accIndexEnd := strings.Index(derivationPath, "_") 333 return derivationPath[:accIndexEnd] 334 }