prysm-pulse/validator/keymanager/v2/derived/derived.go
RoBiK75 b4c0a89d49
Fixes incorrect output produced by the ListAccounts function (#6976) (#7095)
Co-authored-by: Ivan Martinez <ivanthegreatdev@gmail.com>
Co-authored-by: Raul Jordan <raul@prysmaticlabs.com>
2020-09-01 13:13:44 -05:00

509 lines
19 KiB
Go

package derived
import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"path"
"path/filepath"
"sync"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/beacon-chain/core/helpers"
validatorpb "github.com/prysmaticlabs/prysm/proto/validator/accounts/v2"
"github.com/prysmaticlabs/prysm/shared/bls"
"github.com/prysmaticlabs/prysm/shared/bytesutil"
"github.com/prysmaticlabs/prysm/shared/depositutil"
"github.com/prysmaticlabs/prysm/shared/fileutil"
"github.com/prysmaticlabs/prysm/shared/params"
"github.com/prysmaticlabs/prysm/shared/petnames"
"github.com/prysmaticlabs/prysm/shared/rand"
"github.com/prysmaticlabs/prysm/validator/accounts/v2/iface"
"github.com/sirupsen/logrus"
"github.com/tyler-smith/go-bip39"
util "github.com/wealdtech/go-eth2-util"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
)
var log = logrus.WithField("prefix", "derived-keymanager-v2")
const (
// EIPVersion used by this derived keymanager implementation.
EIPVersion = "EIP-2334"
// WithdrawalKeyDerivationPathTemplate defining the hierarchical path for withdrawal
// keys for Prysm eth2 validators. According to EIP-2334, the format is as follows:
// m / purpose / coin_type / account_index / withdrawal_key
WithdrawalKeyDerivationPathTemplate = "m/12381/3600/%d/0"
// ValidatingKeyDerivationPathTemplate defining the hierarchical path for validating
// keys for Prysm eth2 validators. According to EIP-2334, the format is as follows:
// m / purpose / coin_type / account_index / withdrawal_key / validating_key
ValidatingKeyDerivationPathTemplate = "m/12381/3600/%d/0/0"
// EncryptedSeedFileName for persisting a wallet's seed when using a derived keymanager.
EncryptedSeedFileName = "seed.encrypted.json"
)
// SeedConfig json file representation as a Go struct.
type SeedConfig struct {
Crypto map[string]interface{} `json:"crypto"`
ID string `json:"uuid"`
NextAccount uint64 `json:"next_account"`
Version uint `json:"version"`
Name string `json:"name"`
}
// KeymanagerOpts defines options for the keymanager that
// are stored to disk in the wallet.
type KeymanagerOpts struct {
DerivedPathStructure string `json:"derived_path_structure"`
DerivedEIPNumber string `json:"derived_eip_number"`
}
// SetupConfig includes configuration values for initializing
// a keymanager, such as passwords, the wallet, and more.
type SetupConfig struct {
Opts *KeymanagerOpts
Wallet iface.Wallet
SkipMnemonicConfirm bool
WalletPassword string
Mnemonic string
}
// Keymanager implementation for derived, HD keymanager using EIP-2333 and EIP-2334.
type Keymanager struct {
wallet iface.Wallet
opts *KeymanagerOpts
mnemonicGenerator SeedPhraseFactory
publicKeysCache [][48]byte
secretKeysCache map[[48]byte]bls.SecretKey
lock sync.RWMutex
seedCfg *SeedConfig
seed []byte
}
// DefaultKeymanagerOpts for a derived keymanager implementation.
func DefaultKeymanagerOpts() *KeymanagerOpts {
return &KeymanagerOpts{
DerivedPathStructure: "m / purpose / coin_type / account_index / withdrawal_key / validating_key",
DerivedEIPNumber: EIPVersion,
}
}
// NewKeymanager instantiates a new derived keymanager from configuration options.
func NewKeymanager(
ctx context.Context,
cfg *SetupConfig,
) (*Keymanager, error) {
// Check if the wallet seed file exists. If it does not, we initialize one
// by creating a new mnemonic and writing the encrypted file to disk.
var encodedSeedFile []byte
if !fileutil.FileExists(filepath.Join(cfg.Wallet.AccountsDir(), EncryptedSeedFileName)) {
seedConfig, err := initializeWalletSeedFile(cfg.WalletPassword, cfg.SkipMnemonicConfirm)
if err != nil {
return nil, errors.Wrap(err, "could not initialize new wallet seed file")
}
encodedSeedFile, err = marshalEncryptedSeedFile(seedConfig)
if err != nil {
return nil, errors.Wrap(err, "could not marshal encrypted wallet seed file")
}
if err = cfg.Wallet.WriteEncryptedSeedToDisk(ctx, encodedSeedFile); err != nil {
return nil, errors.Wrap(err, "could not write encrypted wallet seed config to disk")
}
} else {
seedConfigFile, err := cfg.Wallet.ReadEncryptedSeedFromDisk(ctx)
if err != nil {
return nil, errors.Wrap(err, "could not read encrypted seed file from disk")
}
encodedSeedFile, err = ioutil.ReadAll(seedConfigFile)
if err != nil {
return nil, errors.Wrap(err, "could not read seed configuration file contents")
}
defer func() {
if err := seedConfigFile.Close(); err != nil {
log.Errorf("Could not close keymanager config file: %v", err)
}
}()
}
seedConfig := &SeedConfig{}
if err := json.Unmarshal(encodedSeedFile, seedConfig); err != nil {
return nil, errors.Wrap(err, "could not unmarshal seed configuration")
}
decryptor := keystorev4.New()
seed, err := decryptor.Decrypt(seedConfig.Crypto, cfg.WalletPassword)
if err != nil {
return nil, errors.Wrap(err, "could not decrypt seed configuration with password")
}
k := &Keymanager{
wallet: cfg.Wallet,
opts: cfg.Opts,
mnemonicGenerator: &EnglishMnemonicGenerator{
skipMnemonicConfirm: cfg.SkipMnemonicConfirm,
},
seedCfg: seedConfig,
seed: seed,
}
// Initialize public and secret key caches that are used to speed up the functions
// FetchValidatingPublicKeys and Sign
err = k.initializeKeysCachesFromSeed()
if err != nil {
return nil, errors.Wrap(err, "failed to initialize keys caches from seed")
}
return k, nil
}
// KeymanagerForPhrase instantiates a new derived keymanager from configuration and an existing mnemonic phrase provided.
func KeymanagerForPhrase(
ctx context.Context,
cfg *SetupConfig,
) (*Keymanager, error) {
// Check if the wallet seed file exists. If it does not, we initialize one
// by creating a new mnemonic and writing the encrypted file to disk.
var encodedSeedFile []byte
seedConfig, err := seedFileFromMnemonic(cfg.Mnemonic, cfg.WalletPassword)
if err != nil {
return nil, errors.Wrap(err, "could not initialize new wallet seed file")
}
encodedSeedFile, err = marshalEncryptedSeedFile(seedConfig)
if err != nil {
return nil, errors.Wrap(err, "could not marshal encrypted wallet seed file")
}
if err = cfg.Wallet.WriteEncryptedSeedToDisk(ctx, encodedSeedFile); err != nil {
return nil, errors.Wrap(err, "could not write encrypted wallet seed config to disk")
}
decryptor := keystorev4.New()
seed, err := decryptor.Decrypt(seedConfig.Crypto, cfg.WalletPassword)
if err != nil {
return nil, errors.Wrap(err, "could not decrypt seed configuration with password")
}
k := &Keymanager{
wallet: cfg.Wallet,
opts: cfg.Opts,
mnemonicGenerator: &EnglishMnemonicGenerator{
skipMnemonicConfirm: true,
},
seedCfg: seedConfig,
seed: seed,
}
// Initialize public and secret key caches that are used to speed up the functions
// FetchValidatingPublicKeys and Sign
err = k.initializeKeysCachesFromSeed()
if err != nil {
return nil, errors.Wrap(err, "failed to initialize keys caches from seed")
}
return k, nil
}
// UnmarshalOptionsFile attempts to JSON unmarshal a derived keymanager
// options file into the *Config{} struct.
func UnmarshalOptionsFile(r io.ReadCloser) (*KeymanagerOpts, error) {
enc, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
defer func() {
if err := r.Close(); err != nil {
log.Errorf("Could not close keymanager config file: %v", err)
}
}()
opts := &KeymanagerOpts{}
if err := json.Unmarshal(enc, opts); err != nil {
return nil, err
}
return opts, nil
}
// MarshalOptionsFile returns a marshaled options file for a keymanager.
func MarshalOptionsFile(ctx context.Context, opts *KeymanagerOpts) ([]byte, error) {
return json.MarshalIndent(opts, "", "\t")
}
// KeymanagerOpts returns the derived keymanager options.
func (dr *Keymanager) KeymanagerOpts() *KeymanagerOpts {
return dr.opts
}
// NextAccountNumber managed by the derived keymanager.
func (dr *Keymanager) NextAccountNumber(ctx context.Context) uint64 {
return dr.seedCfg.NextAccount
}
// WriteEncryptedSeedToWallet given a mnemonic phrase, is able to regenerate a wallet seed
// encrypt it, and write it to the wallet's path.
func (dr *Keymanager) WriteEncryptedSeedToWallet(ctx context.Context, mnemonic string) error {
seedConfig, err := seedFileFromMnemonic(mnemonic, dr.wallet.Password())
if err != nil {
return errors.Wrap(err, "could not initialize new wallet seed file")
}
seedConfigFile, err := marshalEncryptedSeedFile(seedConfig)
if err != nil {
return errors.Wrap(err, "could not marshal encrypted wallet seed file")
}
if err := dr.wallet.WriteEncryptedSeedToDisk(ctx, seedConfigFile); err != nil {
return errors.Wrap(err, "could not write encrypted wallet seed config to disk")
}
return nil
}
// ValidatingAccountNames for the derived keymanager.
func (dr *Keymanager) ValidatingAccountNames(ctx context.Context) ([]string, error) {
dr.lock.RLock()
names := make([]string, len(dr.publicKeysCache))
for i, pubKey := range dr.publicKeysCache {
names[i] = petnames.DeterministicName(bytesutil.FromBytes48(pubKey), "-")
}
dr.lock.RUnlock()
return names, nil
}
// CreateAccount for a derived keymanager implementation. This utilizes
// the EIP-2335 keystore standard for BLS12-381 keystores. It uses the EIP-2333 and EIP-2334
// for hierarchical derivation of BLS secret keys and a common derivation path structure for
// persisting accounts to disk. Each account stores the generated keystore.json file.
// The entire derived wallet seed phrase can be recovered from a BIP-39 english mnemonic.
func (dr *Keymanager) CreateAccount(ctx context.Context, logAccountInfo bool) (string, error) {
withdrawalKeyPath := fmt.Sprintf(WithdrawalKeyDerivationPathTemplate, dr.seedCfg.NextAccount)
validatingKeyPath := fmt.Sprintf(ValidatingKeyDerivationPathTemplate, dr.seedCfg.NextAccount)
withdrawalKey, err := util.PrivateKeyFromSeedAndPath(dr.seed, withdrawalKeyPath)
if err != nil {
return "", errors.Wrapf(err, "failed to create withdrawal key for account %d", dr.seedCfg.NextAccount)
}
validatingKey, err := util.PrivateKeyFromSeedAndPath(dr.seed, validatingKeyPath)
if err != nil {
return "", errors.Wrapf(err, "failed to create validating key for account %d", dr.seedCfg.NextAccount)
}
// Upon confirmation of the withdrawal key, proceed to display
// and write associated deposit data to disk.
blsValidatingKey, err := bls.SecretKeyFromBytes(validatingKey.Marshal())
if err != nil {
return "", err
}
blsWithdrawalKey, err := bls.SecretKeyFromBytes(withdrawalKey.Marshal())
if err != nil {
return "", err
}
// Upon confirmation of the withdrawal key, proceed to display
// and write associated deposit data to disk.
tx, data, err := depositutil.GenerateDepositTransaction(blsValidatingKey, blsWithdrawalKey)
if err != nil {
return "", errors.Wrap(err, "could not generate deposit transaction data")
}
domain, err := helpers.ComputeDomain(
params.BeaconConfig().DomainDeposit,
nil, /*forkVersion*/
nil, /*genesisValidatorsRoot*/
)
if err := depositutil.VerifyDepositSignature(data, domain); err != nil {
return "", errors.Wrap(err, "failed to verify deposit signature, please make sure your account was created properly")
}
// Log the deposit transaction data to the user.
fmt.Printf(`
==================Eth1 Deposit Transaction Data=================
%#x
================Verified for the %s network================`, tx.Data(), params.BeaconConfig().NetworkName)
fmt.Println("")
// Finally, write the account creation timestamps as a files.
newAccountNumber := dr.seedCfg.NextAccount
if logAccountInfo {
log.WithFields(logrus.Fields{
"accountNumber": newAccountNumber,
"withdrawalPublicKey": fmt.Sprintf("%#x", withdrawalKey.PublicKey().Marshal()),
"validatingPublicKey": fmt.Sprintf("%#x", validatingKey.PublicKey().Marshal()),
"withdrawalKeyPath": path.Join(dr.wallet.AccountsDir(), withdrawalKeyPath),
"validatingKeyPath": path.Join(dr.wallet.AccountsDir(), validatingKeyPath),
}).Info("Successfully created new validator account")
}
dr.lock.Lock()
dr.seedCfg.NextAccount++
// Append the new account keys to the account keys caches
publicKey := bytesutil.ToBytes48(blsValidatingKey.PublicKey().Marshal())
dr.publicKeysCache = append(dr.publicKeysCache, publicKey)
dr.secretKeysCache[publicKey] = blsValidatingKey
dr.lock.Unlock()
encodedCfg, err := marshalEncryptedSeedFile(dr.seedCfg)
if err != nil {
return "", errors.Wrap(err, "could not marshal encrypted seed file")
}
if err := dr.wallet.WriteEncryptedSeedToDisk(ctx, encodedCfg); err != nil {
return "", errors.Wrap(err, "could not write encrypted seed file to disk")
}
return fmt.Sprintf("%d", newAccountNumber), nil
}
// Sign signs a message using a validator key.
func (dr *Keymanager) Sign(ctx context.Context, req *validatorpb.SignRequest) (bls.Signature, error) {
rawPubKey := req.PublicKey
if rawPubKey == nil {
return nil, errors.New("nil public key in request")
}
dr.lock.RLock()
secretKey, ok := dr.secretKeysCache[bytesutil.ToBytes48(rawPubKey)]
dr.lock.RUnlock()
if !ok {
return nil, errors.New("no signing key found in keys cache")
}
return secretKey.Sign(req.SigningRoot), nil
}
// FetchValidatingPublicKeys fetches the list of validating public keys from the keymanager.
func (dr *Keymanager) FetchValidatingPublicKeys(ctx context.Context) ([][48]byte, error) {
dr.lock.RLock()
keys := dr.publicKeysCache
result := make([][48]byte, len(keys))
copy(result, keys)
dr.lock.RUnlock()
return result, nil
}
// FetchWithdrawalPublicKeys fetches the list of withdrawal public keys from keymanager
func (dr *Keymanager) FetchWithdrawalPublicKeys(ctx context.Context) ([][48]byte, error) {
publicKeys := make([][48]byte, 0)
for i := uint64(0); i < dr.seedCfg.NextAccount; i++ {
withdrawalKeyPath := fmt.Sprintf(WithdrawalKeyDerivationPathTemplate, i)
withdrawalKey, err := util.PrivateKeyFromSeedAndPath(dr.seed, withdrawalKeyPath)
if err != nil {
return nil, errors.Wrapf(err, "failed to create validating key for account %d", i)
}
publicKeys = append(publicKeys, bytesutil.ToBytes48(withdrawalKey.PublicKey().Marshal()))
}
return publicKeys, nil
}
// DepositDataForAccount with a given index returns the RLP encoded eth1 deposit transaction data.
func (dr *Keymanager) DepositDataForAccount(accountIndex uint64) ([]byte, error) {
withdrawalKeyPath := fmt.Sprintf(WithdrawalKeyDerivationPathTemplate, accountIndex)
validatingKeyPath := fmt.Sprintf(ValidatingKeyDerivationPathTemplate, accountIndex)
withdrawalKey, err := util.PrivateKeyFromSeedAndPath(dr.seed, withdrawalKeyPath)
if err != nil {
return nil, errors.Wrapf(err, "failed to create withdrawal key for account %d", accountIndex)
}
validatingKey, err := util.PrivateKeyFromSeedAndPath(dr.seed, validatingKeyPath)
if err != nil {
return nil, errors.Wrapf(err, "failed to create validating key for account %d", accountIndex)
}
// Upon confirmation of the withdrawal key, proceed to display
// and write associated deposit data to disk.
blsValidatingKey, err := bls.SecretKeyFromBytes(validatingKey.Marshal())
if err != nil {
return nil, err
}
blsWithdrawalKey, err := bls.SecretKeyFromBytes(withdrawalKey.Marshal())
if err != nil {
return nil, err
}
tx, _, err := depositutil.GenerateDepositTransaction(blsValidatingKey, blsWithdrawalKey)
if err != nil {
return nil, errors.Wrap(err, "could not generate deposit transaction data")
}
return tx.Data(), nil
}
// Append the public and the secret key for the provided secret key to their respective caches
func (dr *Keymanager) appendKeysToCaches(secretKey bls.SecretKey) error {
publicKey := bytesutil.ToBytes48(secretKey.PublicKey().Marshal())
dr.lock.Lock()
dr.publicKeysCache = append(dr.publicKeysCache, publicKey)
dr.secretKeysCache[publicKey] = secretKey
dr.lock.Unlock()
return nil
}
// Initialize public and secret key caches used to speed up the functions
// FetchValidatingPublicKeys and Sign as part of the Keymanager instance initialization
func (dr *Keymanager) initializeKeysCachesFromSeed() error {
dr.lock.Lock()
defer dr.lock.Unlock()
count := dr.seedCfg.NextAccount
dr.publicKeysCache = make([][48]byte, count)
dr.secretKeysCache = make(map[[48]byte]bls.SecretKey, count)
for i := uint64(0); i < count; i++ {
validatingKeyPath := fmt.Sprintf(ValidatingKeyDerivationPathTemplate, i)
derivedKey, err := util.PrivateKeyFromSeedAndPath(dr.seed, validatingKeyPath)
if err != nil {
return errors.Wrapf(err, "failed to derive validating key for account %s", validatingKeyPath)
}
secretKey, err := bls.SecretKeyFromBytes(derivedKey.Marshal())
if err != nil {
return errors.Wrapf(
err,
"could not instantiate bls secret key from bytes for account: %s",
validatingKeyPath,
)
}
publicKey := bytesutil.ToBytes48(secretKey.PublicKey().Marshal())
dr.publicKeysCache[i] = publicKey
dr.secretKeysCache[publicKey] = secretKey
}
return nil
}
// Creates a new, encrypted seed using a password input
// and persists its encrypted file metadata to disk under the wallet path.
func initializeWalletSeedFile(password string, skipMnemonicConfirm bool) (*SeedConfig, error) {
mnemonicRandomness := make([]byte, 32)
if _, err := rand.NewGenerator().Read(mnemonicRandomness); err != nil {
return nil, errors.Wrap(err, "could not initialize mnemonic source of randomness")
}
m := &EnglishMnemonicGenerator{
skipMnemonicConfirm: skipMnemonicConfirm,
}
phrase, err := m.Generate(mnemonicRandomness)
if err != nil {
return nil, errors.Wrap(err, "could not generate wallet seed")
}
if err := m.ConfirmAcknowledgement(phrase); err != nil {
return nil, errors.Wrap(err, "could not confirm mnemonic acknowledgement")
}
walletSeed := bip39.NewSeed(phrase, "")
encryptor := keystorev4.New()
cryptoFields, err := encryptor.Encrypt(walletSeed, password)
if err != nil {
return nil, errors.Wrap(err, "could not encrypt seed phrase into keystore")
}
id, err := uuid.NewRandom()
if err != nil {
return nil, errors.Wrap(err, "could not generate unique UUID")
}
return &SeedConfig{
Crypto: cryptoFields,
ID: id.String(),
NextAccount: 0,
Version: encryptor.Version(),
Name: encryptor.Name(),
}, nil
}
// Uses the provided mnemonic seed phrase to generate the
// appropriate seed file for recovering a derived wallets.
func seedFileFromMnemonic(mnemonic string, password string) (*SeedConfig, error) {
if ok := bip39.IsMnemonicValid(mnemonic); !ok {
return nil, bip39.ErrInvalidMnemonic
}
walletSeed := bip39.NewSeed(mnemonic, "")
encryptor := keystorev4.New()
cryptoFields, err := encryptor.Encrypt(walletSeed, password)
if err != nil {
return nil, errors.Wrap(err, "could not encrypt seed phrase into keystore")
}
id, err := uuid.NewRandom()
if err != nil {
return nil, errors.Wrap(err, "could not generate unique UUID")
}
return &SeedConfig{
Crypto: cryptoFields,
ID: id.String(),
NextAccount: 0,
Version: encryptor.Version(),
Name: encryptor.Name(),
}, nil
}
// marshalEncryptedSeedFile json encodes the seed configuration for a derived keymanager.
func marshalEncryptedSeedFile(seedCfg *SeedConfig) ([]byte, error) {
return json.MarshalIndent(seedCfg, "", "\t")
}