2020-07-01 21:30:01 +00:00
|
|
|
package direct
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2020-07-09 16:23:53 +00:00
|
|
|
"encoding/hex"
|
2020-07-03 18:49:16 +00:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2020-07-01 21:30:01 +00:00
|
|
|
"io"
|
2020-07-03 18:49:16 +00:00
|
|
|
"io/ioutil"
|
2020-07-08 19:21:54 +00:00
|
|
|
"strconv"
|
2020-07-03 23:00:02 +00:00
|
|
|
"sync"
|
2020-07-01 21:30:01 +00:00
|
|
|
|
2020-07-09 16:23:53 +00:00
|
|
|
"github.com/google/uuid"
|
2020-07-03 18:49:16 +00:00
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/prysmaticlabs/go-ssz"
|
2020-07-03 23:00:02 +00:00
|
|
|
validatorpb "github.com/prysmaticlabs/prysm/proto/validator/accounts/v2"
|
2020-07-01 21:30:01 +00:00
|
|
|
"github.com/prysmaticlabs/prysm/shared/bls"
|
2020-07-03 20:26:00 +00:00
|
|
|
"github.com/prysmaticlabs/prysm/shared/bytesutil"
|
2020-07-03 18:49:16 +00:00
|
|
|
"github.com/prysmaticlabs/prysm/shared/depositutil"
|
2020-07-08 19:21:54 +00:00
|
|
|
"github.com/prysmaticlabs/prysm/shared/roughtime"
|
2020-07-21 02:05:23 +00:00
|
|
|
"github.com/prysmaticlabs/prysm/validator/accounts/v2/iface"
|
|
|
|
v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2"
|
2020-07-01 21:30:01 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
2020-07-03 18:49:16 +00:00
|
|
|
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
2020-07-01 21:30:01 +00:00
|
|
|
)
|
|
|
|
|
2020-07-10 05:49:56 +00:00
|
|
|
var log = logrus.WithField("prefix", "direct-keymanager-v2")
|
2020-07-01 21:30:01 +00:00
|
|
|
|
2020-07-03 18:49:16 +00:00
|
|
|
const (
|
2020-07-08 19:21:54 +00:00
|
|
|
// DepositTransactionFileName for the encoded, eth1 raw deposit tx data
|
|
|
|
// for a validator account.
|
|
|
|
DepositTransactionFileName = "deposit_transaction.rlp"
|
|
|
|
// TimestampFileName stores a timestamp for account creation as a
|
|
|
|
// file for a direct keymanager account.
|
2020-07-13 21:37:18 +00:00
|
|
|
TimestampFileName = "created_at.txt"
|
|
|
|
// KeystoreFileName exposes the expected filename for the keystore file for an account.
|
|
|
|
KeystoreFileName = "keystore.json"
|
2020-07-08 19:21:54 +00:00
|
|
|
depositDataFileName = "deposit_data.ssz"
|
|
|
|
eipVersion = "EIP-2335"
|
2020-07-03 18:49:16 +00:00
|
|
|
)
|
|
|
|
|
2020-07-01 21:30:01 +00:00
|
|
|
// Config for a direct keymanager.
|
2020-07-03 18:49:16 +00:00
|
|
|
type Config struct {
|
|
|
|
EIPVersion string `json:"direct_eip_version"`
|
|
|
|
}
|
2020-07-01 21:30:01 +00:00
|
|
|
|
2020-07-03 18:49:16 +00:00
|
|
|
// Keymanager implementation for direct keystores utilizing EIP-2335.
|
2020-07-01 21:30:01 +00:00
|
|
|
type Keymanager struct {
|
2020-07-21 02:05:23 +00:00
|
|
|
wallet iface.Wallet
|
2020-07-03 18:49:16 +00:00
|
|
|
cfg *Config
|
|
|
|
mnemonicGenerator SeedPhraseFactory
|
2020-07-03 23:00:02 +00:00
|
|
|
keysCache map[[48]byte]bls.SecretKey
|
|
|
|
lock sync.RWMutex
|
2020-07-01 21:30:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// DefaultConfig for a direct keymanager implementation.
|
|
|
|
func DefaultConfig() *Config {
|
2020-07-03 18:49:16 +00:00
|
|
|
return &Config{
|
|
|
|
EIPVersion: eipVersion,
|
|
|
|
}
|
2020-07-01 21:30:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewKeymanager instantiates a new direct keymanager from configuration options.
|
2020-07-21 02:05:23 +00:00
|
|
|
func NewKeymanager(ctx context.Context, wallet iface.Wallet, cfg *Config, skipMnemonicConfirm bool) (*Keymanager, error) {
|
2020-07-09 16:23:53 +00:00
|
|
|
k := &Keymanager{
|
2020-07-14 23:00:58 +00:00
|
|
|
wallet: wallet,
|
|
|
|
cfg: cfg,
|
|
|
|
mnemonicGenerator: &EnglishMnemonicGenerator{
|
|
|
|
skipMnemonicConfirm: skipMnemonicConfirm,
|
|
|
|
},
|
|
|
|
keysCache: make(map[[48]byte]bls.SecretKey),
|
2020-07-09 16:23:53 +00:00
|
|
|
}
|
|
|
|
// If the wallet has the capability of unlocking accounts using
|
|
|
|
// passphrases, then we initialize a cache of public key -> secret keys
|
|
|
|
// used to retrieve secrets keys for the accounts via password unlock.
|
|
|
|
// This cache is needed to process Sign requests using a public key.
|
|
|
|
if wallet.CanUnlockAccounts() {
|
|
|
|
if err := k.initializeSecretKeysCache(); err != nil {
|
|
|
|
return nil, errors.Wrap(err, "could not initialize keys cache")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return k, nil
|
2020-07-01 21:30:01 +00:00
|
|
|
}
|
|
|
|
|
2020-07-03 18:49:16 +00:00
|
|
|
// UnmarshalConfigFile attempts to JSON unmarshal a direct keymanager
|
|
|
|
// configuration file into the *Config{} struct.
|
|
|
|
func UnmarshalConfigFile(r io.ReadCloser) (*Config, 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)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
cfg := &Config{}
|
|
|
|
if err := json.Unmarshal(enc, cfg); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return cfg, nil
|
2020-07-01 21:30:01 +00:00
|
|
|
}
|
|
|
|
|
2020-07-15 04:05:21 +00:00
|
|
|
// MarshalConfigFile returns a marshaled configuration file for a keymanager.
|
|
|
|
func MarshalConfigFile(ctx context.Context, cfg *Config) ([]byte, error) {
|
|
|
|
return json.MarshalIndent(cfg, "", "\t")
|
|
|
|
}
|
|
|
|
|
2020-07-03 18:49:16 +00:00
|
|
|
// CreateAccount for a direct keymanager implementation. This utilizes
|
|
|
|
// the EIP-2335 keystore standard for BLS12-381 keystores. It
|
|
|
|
// stores the generated keystore.json file in the wallet and additionally
|
|
|
|
// generates a mnemonic for withdrawal credentials. At the end, it logs
|
|
|
|
// the raw deposit data hex string for users to copy.
|
2020-07-03 20:26:00 +00:00
|
|
|
func (dr *Keymanager) CreateAccount(ctx context.Context, password string) (string, error) {
|
2020-07-03 18:49:16 +00:00
|
|
|
// Create a new, unique account name and write its password + directory to disk.
|
|
|
|
accountName, err := dr.wallet.WriteAccountToDisk(ctx, password)
|
|
|
|
if err != nil {
|
2020-07-03 20:26:00 +00:00
|
|
|
return "", errors.Wrap(err, "could not write account to disk")
|
2020-07-03 18:49:16 +00:00
|
|
|
}
|
|
|
|
// Generates a new EIP-2335 compliant keystore file
|
|
|
|
// from a BLS private key and marshals it as JSON.
|
|
|
|
validatingKey := bls.RandKey()
|
2020-07-09 16:23:53 +00:00
|
|
|
encoded, err := dr.generateKeystoreFile(validatingKey, password)
|
2020-07-03 18:49:16 +00:00
|
|
|
if err != nil {
|
2020-07-09 16:23:53 +00:00
|
|
|
return "", err
|
2020-07-03 18:49:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a withdrawal key and confirm user
|
|
|
|
// acknowledgement of a 256-bit entropy mnemonic phrase.
|
|
|
|
withdrawalKey := bls.RandKey()
|
|
|
|
rawWithdrawalKey := withdrawalKey.Marshal()[:]
|
|
|
|
seedPhrase, err := dr.mnemonicGenerator.Generate(rawWithdrawalKey)
|
|
|
|
if err != nil {
|
2020-07-03 20:26:00 +00:00
|
|
|
return "", errors.Wrap(err, "could not generate mnemonic for withdrawal key")
|
2020-07-03 18:49:16 +00:00
|
|
|
}
|
|
|
|
if err := dr.mnemonicGenerator.ConfirmAcknowledgement(seedPhrase); err != nil {
|
2020-07-03 20:26:00 +00:00
|
|
|
return "", errors.Wrap(err, "could not confirm acknowledgement of mnemonic")
|
2020-07-03 18:49:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Upon confirmation of the withdrawal key, proceed to display
|
|
|
|
// and write associated deposit data to disk.
|
2020-07-22 22:21:41 +00:00
|
|
|
tx, depositData, err := depositutil.GenerateDepositTransaction(validatingKey, withdrawalKey)
|
2020-07-03 18:49:16 +00:00
|
|
|
if err != nil {
|
2020-07-03 20:26:00 +00:00
|
|
|
return "", errors.Wrap(err, "could not generate deposit transaction data")
|
2020-07-03 18:49:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Log the deposit transaction data to the user.
|
2020-07-22 22:21:41 +00:00
|
|
|
depositutil.LogDepositTransaction(log, tx)
|
2020-07-03 18:49:16 +00:00
|
|
|
|
|
|
|
// We write the raw deposit transaction as an .rlp encoded file.
|
2020-07-08 19:21:54 +00:00
|
|
|
if err := dr.wallet.WriteFileForAccount(ctx, accountName, DepositTransactionFileName, tx.Data()); err != nil {
|
|
|
|
return "", errors.Wrapf(err, "could not write for account %s: %s", accountName, DepositTransactionFileName)
|
2020-07-03 18:49:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// We write the ssz-encoded deposit data to disk as a .ssz file.
|
|
|
|
encodedDepositData, err := ssz.Marshal(depositData)
|
|
|
|
if err != nil {
|
2020-07-03 20:26:00 +00:00
|
|
|
return "", errors.Wrap(err, "could not marshal deposit data")
|
2020-07-03 18:49:16 +00:00
|
|
|
}
|
|
|
|
if err := dr.wallet.WriteFileForAccount(ctx, accountName, depositDataFileName, encodedDepositData); err != nil {
|
2020-07-03 20:26:00 +00:00
|
|
|
return "", errors.Wrapf(err, "could not write for account %s: %s", accountName, encodedDepositData)
|
2020-07-03 18:49:16 +00:00
|
|
|
}
|
|
|
|
|
2020-07-08 19:21:54 +00:00
|
|
|
// Write the encoded keystore to disk.
|
2020-07-13 21:37:18 +00:00
|
|
|
if err := dr.wallet.WriteFileForAccount(ctx, accountName, KeystoreFileName, encoded); err != nil {
|
2020-07-03 20:26:00 +00:00
|
|
|
return "", errors.Wrapf(err, "could not write keystore file for account %s", accountName)
|
2020-07-03 18:49:16 +00:00
|
|
|
}
|
2020-07-08 19:21:54 +00:00
|
|
|
|
|
|
|
// Finally, write the account creation timestamp as a file.
|
|
|
|
createdAt := roughtime.Now().Unix()
|
|
|
|
createdAtStr := strconv.FormatInt(createdAt, 10)
|
|
|
|
if err := dr.wallet.WriteFileForAccount(ctx, accountName, TimestampFileName, []byte(createdAtStr)); err != nil {
|
|
|
|
return "", errors.Wrapf(err, "could not write timestamp file for account %s", accountName)
|
|
|
|
}
|
|
|
|
|
2020-07-03 18:49:16 +00:00
|
|
|
log.WithFields(logrus.Fields{
|
|
|
|
"name": accountName,
|
|
|
|
"path": dr.wallet.AccountsDir(),
|
|
|
|
}).Info("Successfully created new validator account")
|
2020-07-03 20:26:00 +00:00
|
|
|
return accountName, nil
|
2020-07-01 21:30:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// FetchValidatingPublicKeys fetches the list of public keys from the direct account keystores.
|
2020-07-03 23:00:02 +00:00
|
|
|
func (dr *Keymanager) FetchValidatingPublicKeys(ctx context.Context) ([][48]byte, error) {
|
2020-07-03 20:26:00 +00:00
|
|
|
accountNames, err := dr.wallet.AccountNames()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-07-03 23:00:02 +00:00
|
|
|
|
|
|
|
// Return the public keys from the cache if they match the
|
|
|
|
// number of accounts from the wallet.
|
2020-07-03 20:26:00 +00:00
|
|
|
publicKeys := make([][48]byte, len(accountNames))
|
2020-07-03 23:00:02 +00:00
|
|
|
dr.lock.Lock()
|
|
|
|
defer dr.lock.Unlock()
|
|
|
|
if dr.keysCache != nil && len(dr.keysCache) == len(accountNames) {
|
|
|
|
var i int
|
|
|
|
for k := range dr.keysCache {
|
|
|
|
publicKeys[i] = k
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
return publicKeys, nil
|
|
|
|
}
|
|
|
|
|
2020-07-03 20:26:00 +00:00
|
|
|
for i, name := range accountNames {
|
2020-07-13 21:37:18 +00:00
|
|
|
encoded, err := dr.wallet.ReadFileForAccount(name, KeystoreFileName)
|
2020-07-03 20:26:00 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "could not read keystore file for account %s", name)
|
|
|
|
}
|
2020-07-21 02:05:23 +00:00
|
|
|
keystoreFile := &v2keymanager.Keystore{}
|
2020-07-09 16:23:53 +00:00
|
|
|
if err := json.Unmarshal(encoded, keystoreFile); err != nil {
|
2020-07-03 20:26:00 +00:00
|
|
|
return nil, errors.Wrapf(err, "could not decode keystore json for account: %s", name)
|
|
|
|
}
|
2020-07-09 16:23:53 +00:00
|
|
|
pubKeyBytes, err := hex.DecodeString(keystoreFile.Pubkey)
|
2020-07-03 20:26:00 +00:00
|
|
|
if err != nil {
|
2020-07-09 16:23:53 +00:00
|
|
|
return nil, errors.Wrapf(err, "could not decode pubkey bytes: %#x", keystoreFile.Pubkey)
|
2020-07-03 20:26:00 +00:00
|
|
|
}
|
2020-07-09 16:23:53 +00:00
|
|
|
publicKeys[i] = bytesutil.ToBytes48(pubKeyBytes)
|
2020-07-03 20:26:00 +00:00
|
|
|
}
|
|
|
|
return publicKeys, nil
|
2020-07-01 21:30:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Sign signs a message using a validator key.
|
2020-07-03 23:00:02 +00:00
|
|
|
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()
|
|
|
|
defer dr.lock.RUnlock()
|
|
|
|
secretKey, ok := dr.keysCache[bytesutil.ToBytes48(rawPubKey)]
|
|
|
|
if !ok {
|
|
|
|
return nil, errors.New("no signing key found in keys cache")
|
|
|
|
}
|
2020-07-10 05:49:56 +00:00
|
|
|
return secretKey.Sign(req.SigningRoot), nil
|
2020-07-01 21:30:01 +00:00
|
|
|
}
|
2020-07-03 18:49:16 +00:00
|
|
|
|
2020-07-09 16:23:53 +00:00
|
|
|
func (dr *Keymanager) initializeSecretKeysCache() error {
|
|
|
|
accountNames, err := dr.wallet.AccountNames()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, name := range accountNames {
|
|
|
|
password, err := dr.wallet.ReadPasswordForAccount(name)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "could not read password for account %s", name)
|
|
|
|
}
|
2020-07-13 21:37:18 +00:00
|
|
|
encoded, err := dr.wallet.ReadFileForAccount(name, KeystoreFileName)
|
2020-07-09 16:23:53 +00:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "could not read keystore file for account %s", name)
|
|
|
|
}
|
2020-07-21 02:05:23 +00:00
|
|
|
keystoreFile := &v2keymanager.Keystore{}
|
2020-07-09 16:23:53 +00:00
|
|
|
if err := json.Unmarshal(encoded, keystoreFile); err != nil {
|
|
|
|
return errors.Wrapf(err, "could not decode keystore json for account: %s", name)
|
|
|
|
}
|
|
|
|
// We extract the validator signing private key from the keystore
|
|
|
|
// by utilizing the password and initialize a new BLS secret key from
|
|
|
|
// its raw bytes.
|
|
|
|
decryptor := keystorev4.New()
|
|
|
|
rawSigningKey, err := decryptor.Decrypt(keystoreFile.Crypto, []byte(password))
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "could not decrypt validator signing key for account: %s", name)
|
|
|
|
}
|
|
|
|
validatorSigningKey, err := bls.SecretKeyFromBytes(rawSigningKey)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "could not instantiate bls secret key from bytes for account: %s", name)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update a simple cache of public key -> secret key utilized
|
|
|
|
// for fast signing access in the direct keymanager.
|
|
|
|
dr.keysCache[bytesutil.ToBytes48(validatorSigningKey.PublicKey().Marshal())] = validatorSigningKey
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (dr *Keymanager) generateKeystoreFile(validatingKey bls.SecretKey, password string) ([]byte, error) {
|
|
|
|
encryptor := keystorev4.New()
|
|
|
|
cryptoFields, err := encryptor.Encrypt(validatingKey.Marshal(), []byte(password))
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "could not encrypt validating key into keystore")
|
|
|
|
}
|
|
|
|
id, err := uuid.NewRandom()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-07-21 02:05:23 +00:00
|
|
|
keystoreFile := &v2keymanager.Keystore{
|
|
|
|
Crypto: cryptoFields,
|
|
|
|
ID: id.String(),
|
|
|
|
Pubkey: fmt.Sprintf("%x", validatingKey.PublicKey().Marshal()),
|
|
|
|
Version: encryptor.Version(),
|
|
|
|
Name: encryptor.Name(),
|
|
|
|
}
|
2020-07-09 16:23:53 +00:00
|
|
|
return json.MarshalIndent(keystoreFile, "", "\t")
|
|
|
|
}
|