github.com/prysmaticlabs/prysm@v1.4.4/validator/rpc/wallet.go (about) 1 package rpc 2 3 import ( 4 "context" 5 "encoding/hex" 6 "encoding/json" 7 "fmt" 8 "path/filepath" 9 "strings" 10 11 "github.com/golang/protobuf/ptypes/empty" 12 "github.com/pkg/errors" 13 pb "github.com/prysmaticlabs/prysm/proto/validator/accounts/v2" 14 "github.com/prysmaticlabs/prysm/shared/featureconfig" 15 "github.com/prysmaticlabs/prysm/shared/fileutil" 16 "github.com/prysmaticlabs/prysm/shared/promptutil" 17 "github.com/prysmaticlabs/prysm/shared/rand" 18 "github.com/prysmaticlabs/prysm/validator/accounts" 19 "github.com/prysmaticlabs/prysm/validator/accounts/iface" 20 "github.com/prysmaticlabs/prysm/validator/accounts/wallet" 21 "github.com/prysmaticlabs/prysm/validator/keymanager" 22 "github.com/prysmaticlabs/prysm/validator/keymanager/imported" 23 "github.com/tyler-smith/go-bip39" 24 "github.com/tyler-smith/go-bip39/wordlists" 25 "google.golang.org/grpc/codes" 26 "google.golang.org/grpc/status" 27 ) 28 29 const ( 30 checkExistsErrMsg = "Could not check if wallet exists" 31 checkValidityErrMsg = "Could not check if wallet is valid" 32 invalidWalletMsg = "Directory does not contain a valid wallet" 33 ) 34 35 // CreateWallet via an API request, allowing a user to save a new 36 // imported wallet via RPC. 37 func (s *Server) CreateWallet(ctx context.Context, req *pb.CreateWalletRequest) (*pb.CreateWalletResponse, error) { 38 walletDir := s.walletDir 39 exists, err := wallet.Exists(walletDir) 40 if err != nil { 41 return nil, status.Errorf(codes.Internal, "Could not check for existing wallet: %v", err) 42 } 43 if exists { 44 if err := s.initializeWallet(ctx, &wallet.Config{ 45 WalletDir: walletDir, 46 WalletPassword: req.WalletPassword, 47 }); err != nil { 48 return nil, err 49 } 50 keymanagerKind := pb.KeymanagerKind_IMPORTED 51 switch s.wallet.KeymanagerKind() { 52 case keymanager.Derived: 53 keymanagerKind = pb.KeymanagerKind_DERIVED 54 case keymanager.Remote: 55 keymanagerKind = pb.KeymanagerKind_REMOTE 56 } 57 return &pb.CreateWalletResponse{ 58 Wallet: &pb.WalletResponse{ 59 WalletPath: walletDir, 60 KeymanagerKind: keymanagerKind, 61 }, 62 }, nil 63 } 64 if req.Keymanager == pb.KeymanagerKind_IMPORTED { 65 _, err := accounts.CreateWalletWithKeymanager(ctx, &accounts.CreateWalletConfig{ 66 WalletCfg: &wallet.Config{ 67 WalletDir: walletDir, 68 KeymanagerKind: keymanager.Imported, 69 WalletPassword: req.WalletPassword, 70 }, 71 SkipMnemonicConfirm: true, 72 }) 73 if err != nil { 74 return nil, err 75 } 76 if err := s.initializeWallet(ctx, &wallet.Config{ 77 WalletDir: walletDir, 78 KeymanagerKind: keymanager.Imported, 79 WalletPassword: req.WalletPassword, 80 }); err != nil { 81 return nil, err 82 } 83 if err := writeWalletPasswordToDisk(walletDir, req.WalletPassword); err != nil { 84 return nil, status.Error(codes.Internal, "Could not write wallet password to disk") 85 } 86 return &pb.CreateWalletResponse{ 87 Wallet: &pb.WalletResponse{ 88 WalletPath: walletDir, 89 KeymanagerKind: pb.KeymanagerKind_IMPORTED, 90 }, 91 }, nil 92 } 93 return nil, status.Errorf(codes.InvalidArgument, "Keymanager type %T create wallet not supported through web", req.Keymanager) 94 } 95 96 // WalletConfig returns the wallet's configuration. If no wallet exists, we return an empty response. 97 func (s *Server) WalletConfig(ctx context.Context, _ *empty.Empty) (*pb.WalletResponse, error) { 98 exists, err := wallet.Exists(s.walletDir) 99 if err != nil { 100 return nil, status.Errorf(codes.Internal, checkExistsErrMsg) 101 } 102 if !exists { 103 // If no wallet is found, we simply return an empty response. 104 return &pb.WalletResponse{}, nil 105 } 106 valid, err := wallet.IsValid(s.walletDir) 107 if errors.Is(err, wallet.ErrNoWalletFound) { 108 return &pb.WalletResponse{}, nil 109 } 110 if err != nil { 111 return nil, status.Errorf(codes.Internal, checkValidityErrMsg) 112 } 113 if !valid { 114 return nil, status.Errorf(codes.FailedPrecondition, invalidWalletMsg) 115 } 116 117 if s.wallet == nil || s.keymanager == nil { 118 // If no wallet is found, we simply return an empty response. 119 return &pb.WalletResponse{}, nil 120 } 121 var keymanagerKind pb.KeymanagerKind 122 switch s.wallet.KeymanagerKind() { 123 case keymanager.Derived: 124 keymanagerKind = pb.KeymanagerKind_DERIVED 125 case keymanager.Imported: 126 keymanagerKind = pb.KeymanagerKind_IMPORTED 127 case keymanager.Remote: 128 keymanagerKind = pb.KeymanagerKind_REMOTE 129 } 130 return &pb.WalletResponse{ 131 WalletPath: s.walletDir, 132 KeymanagerKind: keymanagerKind, 133 }, nil 134 } 135 136 // RecoverWallet via an API request, allowing a user to recover a derived. 137 // Generate the seed from the mnemonic + language + 25th passphrase(optional). 138 // Create N validator keystores from the seed specified by req.NumAccounts. 139 // Set the wallet password to req.WalletPassword, then create the wallet from 140 // the provided Mnemonic and return CreateWalletResponse. 141 func (s *Server) RecoverWallet(ctx context.Context, req *pb.RecoverWalletRequest) (*pb.CreateWalletResponse, error) { 142 numAccounts := int(req.NumAccounts) 143 if numAccounts == 0 { 144 return nil, status.Error(codes.InvalidArgument, "Must create at least 1 validator account") 145 } 146 147 // Check validate mnemonic with chosen language 148 language := strings.ToLower(req.Language) 149 allowedLanguages := map[string][]string{ 150 "chinese_simplified": wordlists.ChineseSimplified, 151 "chinese_traditional": wordlists.ChineseTraditional, 152 "czech": wordlists.Czech, 153 "english": wordlists.English, 154 "french": wordlists.French, 155 "japanese": wordlists.Japanese, 156 "korean": wordlists.Korean, 157 "italian": wordlists.Italian, 158 "spanish": wordlists.Spanish, 159 } 160 if _, ok := allowedLanguages[language]; !ok { 161 return nil, status.Error(codes.InvalidArgument, "input not in the list of supported languages") 162 } 163 bip39.SetWordList(allowedLanguages[language]) 164 mnemonic := req.Mnemonic 165 if err := accounts.ValidateMnemonic(mnemonic); err != nil { 166 return nil, status.Error(codes.InvalidArgument, "invalid mnemonic in request") 167 } 168 169 // Check it is not null and not an empty string. 170 if req.Mnemonic25ThWord != "" && strings.TrimSpace(req.Mnemonic25ThWord) == "" { 171 return nil, status.Error(codes.InvalidArgument, "mnemonic 25th word cannot be empty") 172 } 173 174 // Web UI is structured to only write to the default wallet directory 175 // accounts.Recoverwallet checks if wallet already exists. 176 walletDir := s.walletDir 177 178 // Web UI should check the new and confirmed password are equal. 179 walletPassword := req.WalletPassword 180 if err := promptutil.ValidatePasswordInput(walletPassword); err != nil { 181 return nil, status.Error(codes.InvalidArgument, "password did not pass validation") 182 } 183 184 if _, err := accounts.RecoverWallet(ctx, &accounts.RecoverWalletConfig{ 185 WalletDir: walletDir, 186 WalletPassword: walletPassword, 187 Mnemonic: mnemonic, 188 NumAccounts: numAccounts, 189 Mnemonic25thWord: req.Mnemonic25ThWord, 190 }); err != nil { 191 return nil, err 192 } 193 if err := s.initializeWallet(ctx, &wallet.Config{ 194 WalletDir: walletDir, 195 KeymanagerKind: keymanager.Derived, 196 WalletPassword: walletPassword, 197 }); err != nil { 198 return nil, err 199 } 200 if err := writeWalletPasswordToDisk(walletDir, walletPassword); err != nil { 201 return nil, status.Error(codes.Internal, "Could not write wallet password to disk") 202 } 203 return &pb.CreateWalletResponse{ 204 Wallet: &pb.WalletResponse{ 205 WalletPath: walletDir, 206 KeymanagerKind: pb.KeymanagerKind_DERIVED, 207 }, 208 }, nil 209 } 210 211 // GenerateMnemonic creates a new, random bip39 mnemonic phrase. 212 func (s *Server) GenerateMnemonic(_ context.Context, _ *empty.Empty) (*pb.GenerateMnemonicResponse, error) { 213 mnemonicRandomness := make([]byte, 32) 214 if _, err := rand.NewGenerator().Read(mnemonicRandomness); err != nil { 215 return nil, status.Errorf( 216 codes.FailedPrecondition, 217 "Could not initialize mnemonic source of randomness: %v", 218 err, 219 ) 220 } 221 mnemonic, err := bip39.NewMnemonic(mnemonicRandomness) 222 if err != nil { 223 return nil, status.Errorf(codes.Internal, "Could not generate wallet seed: %v", err) 224 } 225 return &pb.GenerateMnemonicResponse{ 226 Mnemonic: mnemonic, 227 }, nil 228 } 229 230 // ImportKeystores allows importing new keystores via RPC into the wallet 231 // which will be decrypted using the specified password . 232 func (s *Server) ImportKeystores( 233 ctx context.Context, req *pb.ImportKeystoresRequest, 234 ) (*pb.ImportKeystoresResponse, error) { 235 if s.wallet == nil { 236 return nil, status.Error(codes.FailedPrecondition, "No wallet initialized") 237 } 238 km, ok := s.keymanager.(*imported.Keymanager) 239 if !ok { 240 return nil, status.Error(codes.FailedPrecondition, "Only imported wallets can import more keystores") 241 } 242 if req.KeystoresPassword == "" { 243 return nil, status.Error(codes.InvalidArgument, "Password required for keystores") 244 } 245 // Needs to unmarshal the keystores from the requests. 246 if req.KeystoresImported == nil || len(req.KeystoresImported) < 1 { 247 return nil, status.Error(codes.InvalidArgument, "No keystores included for import") 248 } 249 keystores := make([]*keymanager.Keystore, len(req.KeystoresImported)) 250 importedPubKeys := make([][]byte, len(req.KeystoresImported)) 251 for i := 0; i < len(req.KeystoresImported); i++ { 252 encoded := req.KeystoresImported[i] 253 keystore := &keymanager.Keystore{} 254 if err := json.Unmarshal([]byte(encoded), &keystore); err != nil { 255 return nil, status.Errorf(codes.InvalidArgument, "Not a valid EIP-2335 keystore JSON file: %v", err) 256 } 257 keystores[i] = keystore 258 pubKey, err := hex.DecodeString(keystore.Pubkey) 259 if err != nil { 260 return nil, status.Errorf(codes.InvalidArgument, "Not a valid BLS public key in keystore file: %v", err) 261 } 262 importedPubKeys[i] = pubKey 263 } 264 // Import the uploaded accounts. 265 if err := accounts.ImportAccounts(ctx, &accounts.ImportAccountsConfig{ 266 Keymanager: km, 267 Keystores: keystores, 268 AccountPassword: req.KeystoresPassword, 269 }); err != nil { 270 return nil, err 271 } 272 s.walletInitializedFeed.Send(s.wallet) 273 return &pb.ImportKeystoresResponse{ 274 ImportedPublicKeys: importedPubKeys, 275 }, nil 276 } 277 278 // Initialize a wallet and send it over a global feed. 279 func (s *Server) initializeWallet(ctx context.Context, cfg *wallet.Config) error { 280 // We first ensure the user has a wallet. 281 exists, err := wallet.Exists(cfg.WalletDir) 282 if err != nil { 283 return errors.Wrap(err, wallet.CheckExistsErrMsg) 284 } 285 if !exists { 286 return wallet.ErrNoWalletFound 287 } 288 valid, err := wallet.IsValid(cfg.WalletDir) 289 if errors.Is(err, wallet.ErrNoWalletFound) { 290 return wallet.ErrNoWalletFound 291 } 292 if err != nil { 293 return errors.Wrap(err, wallet.CheckValidityErrMsg) 294 } 295 if !valid { 296 return errors.New(wallet.InvalidWalletErrMsg) 297 } 298 299 // We fire an event with the opened wallet over 300 // a global feed signifying wallet initialization. 301 w, err := wallet.OpenWallet(ctx, &wallet.Config{ 302 WalletDir: cfg.WalletDir, 303 WalletPassword: cfg.WalletPassword, 304 }) 305 if err != nil { 306 return errors.Wrap(err, "could not open wallet") 307 } 308 309 s.walletInitialized = true 310 km, err := w.InitializeKeymanager(ctx, iface.InitKeymanagerConfig{ListenForChanges: true}) 311 if err != nil { 312 return errors.Wrap(err, accounts.ErrCouldNotInitializeKeymanager) 313 } 314 s.keymanager = km 315 s.wallet = w 316 s.walletDir = cfg.WalletDir 317 318 // Only send over feed if we have validating keys. 319 validatingPublicKeys, err := km.FetchValidatingPublicKeys(ctx) 320 if err != nil { 321 return errors.Wrap(err, "could not check for validating public keys") 322 } 323 if len(validatingPublicKeys) > 0 { 324 s.walletInitializedFeed.Send(w) 325 } 326 return nil 327 } 328 329 func writeWalletPasswordToDisk(walletDir, password string) error { 330 if !featureconfig.Get().WriteWalletPasswordOnWebOnboarding { 331 return nil 332 } 333 passwordFilePath := filepath.Join(walletDir, wallet.DefaultWalletPasswordFile) 334 if fileutil.FileExists(passwordFilePath) { 335 return fmt.Errorf("cannot write wallet password file as it already exists %s", passwordFilePath) 336 } 337 return fileutil.WriteFile(passwordFilePath, []byte(password)) 338 }