Compare commits

...

4 Commits

Author SHA1 Message Date
Raul Jordan
4d416de6d9 more progress on refactor 2021-11-09 14:09:59 -05:00
Raul Jordan
dfcad26ad2 modif test files 2021-11-09 13:53:21 -05:00
Raul Jordan
1b9fb0cbac separate out into specific files 2021-11-09 13:22:43 -05:00
Raul Jordan
e4b392ddb2 del 2021-11-09 10:36:37 -05:00
22 changed files with 611 additions and 826 deletions

View File

@@ -0,0 +1 @@
package wallet

View File

@@ -1,51 +0,0 @@
load("@prysm//tools/go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"keymanager.go",
"log.go",
"mnemonic.go",
],
importpath = "github.com/prysmaticlabs/prysm/validator/keymanager/derived",
visibility = [
"//tools:__subpackages__",
"//validator:__subpackages__",
],
deps = [
"//async/event:go_default_library",
"//crypto/bls:go_default_library",
"//crypto/rand:go_default_library",
"//io/prompt:go_default_library",
"//proto/prysm/v1alpha1/validator-client:go_default_library",
"//validator/accounts/iface:go_default_library",
"//validator/keymanager:go_default_library",
"//validator/keymanager/imported:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_tyler_smith_go_bip39//:go_default_library",
"@com_github_wealdtech_go_eth2_util//:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"eip_test.go",
"keymanager_test.go",
"mnemonic_test.go",
],
embed = [":go_default_library"],
deps = [
"//crypto/bls:go_default_library",
"//crypto/rand:go_default_library",
"//proto/prysm/v1alpha1/validator-client:go_default_library",
"//testing/assert:go_default_library",
"//testing/require:go_default_library",
"//validator/accounts/testing:go_default_library",
"//validator/testing:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_tyler_smith_go_bip39//:go_default_library",
"@com_github_wealdtech_go_eth2_util//:go_default_library",
],
)

View File

@@ -1,119 +0,0 @@
package derived
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/async/event"
"github.com/prysmaticlabs/prysm/crypto/bls"
validatorpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/validator-client"
"github.com/prysmaticlabs/prysm/validator/accounts/iface"
"github.com/prysmaticlabs/prysm/validator/keymanager"
"github.com/prysmaticlabs/prysm/validator/keymanager/imported"
util "github.com/wealdtech/go-eth2-util"
)
const (
// DerivationPathFormat describes the structure of how keys are derived from a master key.
DerivationPathFormat = "m / purpose / coin_type / account_index / withdrawal_key / validating_key"
// ValidatingKeyDerivationPathTemplate defining the hierarchical path for validating
// keys for Prysm Ethereum 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"
)
// SetupConfig includes configuration values for initializing
// a keymanager, such as passwords, the wallet, and more.
type SetupConfig struct {
Wallet iface.Wallet
ListenForChanges bool
}
// Keymanager implementation for derived, HD keymanager using EIP-2333 and EIP-2334.
type Keymanager struct {
importedKM *imported.Keymanager
}
// NewKeymanager instantiates a new derived keymanager from configuration options.
func NewKeymanager(
ctx context.Context,
cfg *SetupConfig,
) (*Keymanager, error) {
importedKM, err := imported.NewKeymanager(ctx, &imported.SetupConfig{
Wallet: cfg.Wallet,
ListenForChanges: cfg.ListenForChanges,
})
if err != nil {
return nil, err
}
return &Keymanager{
importedKM: importedKM,
}, nil
}
// RecoverAccountsFromMnemonic given a mnemonic phrase, is able to regenerate N accounts
// from a derived seed, encrypt them according to the EIP-2334 JSON standard, and write them
// to disk. Then, the mnemonic is never stored nor used by the validator.
func (km *Keymanager) RecoverAccountsFromMnemonic(
ctx context.Context, mnemonic, mnemonicPassphrase string, numAccounts int,
) error {
seed, err := seedFromMnemonic(mnemonic, mnemonicPassphrase)
if err != nil {
return errors.Wrap(err, "could not initialize new wallet seed file")
}
privKeys := make([][]byte, numAccounts)
pubKeys := make([][]byte, numAccounts)
for i := 0; i < numAccounts; i++ {
privKey, err := util.PrivateKeyFromSeedAndPath(
seed, fmt.Sprintf(ValidatingKeyDerivationPathTemplate, i),
)
if err != nil {
return err
}
privKeys[i] = privKey.Marshal()
pubKeys[i] = privKey.PublicKey().Marshal()
}
return km.importedKM.ImportKeypairs(ctx, privKeys, pubKeys)
}
// ExtractKeystores retrieves the secret keys for specified public keys
// in the function input, encrypts them using the specified password,
// and returns their respective EIP-2335 keystores.
func (km *Keymanager) ExtractKeystores(
ctx context.Context, publicKeys []bls.PublicKey, password string,
) ([]*keymanager.Keystore, error) {
return km.importedKM.ExtractKeystores(ctx, publicKeys, password)
}
// ValidatingAccountNames for the derived keymanager.
func (km *Keymanager) ValidatingAccountNames(_ context.Context) ([]string, error) {
return km.importedKM.ValidatingAccountNames()
}
// Sign signs a message using a validator key.
func (km *Keymanager) Sign(ctx context.Context, req *validatorpb.SignRequest) (bls.Signature, error) {
return km.importedKM.Sign(ctx, req)
}
// FetchValidatingPublicKeys fetches the list of validating public keys from the keymanager.
func (km *Keymanager) FetchValidatingPublicKeys(ctx context.Context) ([][48]byte, error) {
return km.importedKM.FetchValidatingPublicKeys(ctx)
}
// FetchValidatingPrivateKeys fetches the list of validating private keys from the keymanager.
func (km *Keymanager) FetchValidatingPrivateKeys(ctx context.Context) ([][32]byte, error) {
return km.importedKM.FetchValidatingPrivateKeys(ctx)
}
// DeleteAccounts for a derived keymanager.
func (km *Keymanager) DeleteAccounts(ctx context.Context, publicKeys [][]byte) error {
return km.importedKM.DeleteAccounts(ctx, publicKeys)
}
// SubscribeAccountChanges creates an event subscription for a channel
// to listen for public key changes at runtime, such as when new validator accounts
// are imported into the keymanager while the validator process is running.
func (km *Keymanager) SubscribeAccountChanges(pubKeysChan chan [][48]byte) event.Subscription {
return km.importedKM.SubscribeAccountChanges(pubKeysChan)
}

View File

@@ -1,209 +0,0 @@
package derived
import (
"context"
"fmt"
"testing"
"github.com/prysmaticlabs/prysm/crypto/bls"
"github.com/prysmaticlabs/prysm/crypto/rand"
validatorpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/validator-client"
"github.com/prysmaticlabs/prysm/testing/assert"
"github.com/prysmaticlabs/prysm/testing/require"
mock "github.com/prysmaticlabs/prysm/validator/accounts/testing"
constant "github.com/prysmaticlabs/prysm/validator/testing"
"github.com/tyler-smith/go-bip39"
util "github.com/wealdtech/go-eth2-util"
)
const (
password = "secretPassw0rd$1999"
)
// We test that using a '25th word' mnemonic passphrase leads to different
// public keys derived than not specifying the passphrase.
func TestDerivedKeymanager_MnemnonicPassphrase_DifferentResults(t *testing.T) {
ctx := context.Background()
wallet := &mock.Wallet{
Files: make(map[string]map[string][]byte),
AccountPasswords: make(map[string]string),
WalletPassword: password,
}
km, err := NewKeymanager(ctx, &SetupConfig{
Wallet: wallet,
ListenForChanges: false,
})
require.NoError(t, err)
numAccounts := 5
err = km.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "mnemonicpass", numAccounts)
require.NoError(t, err)
without25thWord, err := km.FetchValidatingPublicKeys(ctx)
require.NoError(t, err)
wallet = &mock.Wallet{
Files: make(map[string]map[string][]byte),
AccountPasswords: make(map[string]string),
WalletPassword: password,
}
km, err = NewKeymanager(ctx, &SetupConfig{
Wallet: wallet,
ListenForChanges: false,
})
require.NoError(t, err)
// No mnemonic passphrase this time.
err = km.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", numAccounts)
require.NoError(t, err)
with25thWord, err := km.FetchValidatingPublicKeys(ctx)
require.NoError(t, err)
for i, k := range with25thWord {
without := without25thWord[i]
assert.DeepNotEqual(t, k, without)
}
}
func TestDerivedKeymanager_RecoverSeedRoundTrip(t *testing.T) {
mnemonicEntropy := make([]byte, 32)
n, err := rand.NewGenerator().Read(mnemonicEntropy)
require.NoError(t, err)
require.Equal(t, n, len(mnemonicEntropy))
mnemonic, err := bip39.NewMnemonic(mnemonicEntropy)
require.NoError(t, err)
wanted := bip39.NewSeed(mnemonic, "")
got, err := seedFromMnemonic(mnemonic, "" /* no passphrase */)
require.NoError(t, err)
// Ensure the derived seed matches.
assert.DeepEqual(t, wanted, got)
}
func TestDerivedKeymanager_FetchValidatingPublicKeys(t *testing.T) {
derivedSeed, err := seedFromMnemonic(constant.TestMnemonic, "")
require.NoError(t, err)
wallet := &mock.Wallet{
Files: make(map[string]map[string][]byte),
AccountPasswords: make(map[string]string),
WalletPassword: password,
}
ctx := context.Background()
dr, err := NewKeymanager(ctx, &SetupConfig{
Wallet: wallet,
ListenForChanges: false,
})
require.NoError(t, err)
numAccounts := 5
err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", numAccounts)
require.NoError(t, err)
// Fetch the public keys.
publicKeys, err := dr.FetchValidatingPublicKeys(ctx)
require.NoError(t, err)
require.Equal(t, numAccounts, len(publicKeys))
wantedPubKeys := make([][48]byte, numAccounts)
for i := 0; i < numAccounts; i++ {
privKey, err := util.PrivateKeyFromSeedAndPath(derivedSeed, fmt.Sprintf(ValidatingKeyDerivationPathTemplate, i))
require.NoError(t, err)
pubKey := [48]byte{}
copy(pubKey[:], privKey.PublicKey().Marshal())
wantedPubKeys[i] = pubKey
}
// FetchValidatingPublicKeys is also used in generating the output of account list
// therefore the results must be in the same order as the order in which the accounts were derived
for i, key := range wantedPubKeys {
assert.Equal(t, key, publicKeys[i])
}
}
func TestDerivedKeymanager_FetchValidatingPrivateKeys(t *testing.T) {
derivedSeed, err := seedFromMnemonic(constant.TestMnemonic, "")
require.NoError(t, err)
wallet := &mock.Wallet{
Files: make(map[string]map[string][]byte),
AccountPasswords: make(map[string]string),
WalletPassword: password,
}
ctx := context.Background()
dr, err := NewKeymanager(ctx, &SetupConfig{
Wallet: wallet,
ListenForChanges: false,
})
require.NoError(t, err)
numAccounts := 5
err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", numAccounts)
require.NoError(t, err)
// Fetch the private keys.
privateKeys, err := dr.FetchValidatingPrivateKeys(ctx)
require.NoError(t, err)
require.Equal(t, numAccounts, len(privateKeys))
wantedPrivKeys := make([][32]byte, numAccounts)
for i := 0; i < numAccounts; i++ {
privKey, err := util.PrivateKeyFromSeedAndPath(derivedSeed, fmt.Sprintf(ValidatingKeyDerivationPathTemplate, i))
require.NoError(t, err)
privKeyBytes := [32]byte{}
copy(privKeyBytes[:], privKey.Marshal())
wantedPrivKeys[i] = privKeyBytes
}
// FetchValidatingPrivateKeys is also used in generating the output of account list
// therefore the results must be in the same order as the order in which the accounts were derived
for i, key := range wantedPrivKeys {
assert.Equal(t, key, privateKeys[i])
}
}
func TestDerivedKeymanager_Sign(t *testing.T) {
wallet := &mock.Wallet{
Files: make(map[string]map[string][]byte),
AccountPasswords: make(map[string]string),
WalletPassword: password,
}
ctx := context.Background()
dr, err := NewKeymanager(ctx, &SetupConfig{
Wallet: wallet,
ListenForChanges: false,
})
require.NoError(t, err)
numAccounts := 5
err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, "", numAccounts)
require.NoError(t, err)
pubKeys, err := dr.FetchValidatingPublicKeys(ctx)
require.NoError(t, err)
// We prepare naive data to sign.
data := []byte("eth2data")
signRequest := &validatorpb.SignRequest{
PublicKey: pubKeys[0][:],
SigningRoot: data,
}
sig, err := dr.Sign(ctx, signRequest)
require.NoError(t, err)
pubKey, err := bls.PublicKeyFromBytes(pubKeys[0][:])
require.NoError(t, err)
wrongPubKey, err := bls.PublicKeyFromBytes(pubKeys[1][:])
require.NoError(t, err)
// Check if the signature verifies.
assert.Equal(t, true, sig.Verify(pubKey, data))
// Check if the bad signature fails.
assert.Equal(t, false, sig.Verify(wrongPubKey, data))
}
func TestDerivedKeymanager_Sign_NoPublicKeySpecified(t *testing.T) {
req := &validatorpb.SignRequest{
PublicKey: nil,
}
dr := &Keymanager{}
_, err := dr.Sign(context.Background(), req)
assert.ErrorContains(t, "nil public key", err)
}
func TestDerivedKeymanager_Sign_NoPublicKeyInCache(t *testing.T) {
req := &validatorpb.SignRequest{
PublicKey: []byte("hello world"),
}
dr := &Keymanager{}
_, err := dr.Sign(context.Background(), req)
assert.ErrorContains(t, "no signing key found", err)
}

View File

@@ -1,5 +0,0 @@
package derived
import "github.com/sirupsen/logrus"
var log = logrus.WithField("prefix", "derived-keymanager")

View File

@@ -0,0 +1,59 @@
package imported
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
"github.com/prysmaticlabs/prysm/validator/accounts/petnames"
"github.com/sirupsen/logrus"
)
// DeleteAccounts takes in public keys and removes the accounts entirely. This includes their disk keystore and cached keystore.
func (km *Keymanager) DeleteAccounts(ctx context.Context, publicKeys [][]byte) error {
for _, publicKey := range publicKeys {
var index int
var found bool
for i, pubKey := range km.accountsStore.PublicKeys {
if bytes.Equal(pubKey, publicKey) {
index = i
found = true
break
}
}
if !found {
return fmt.Errorf("could not find public key %#x", publicKey)
}
deletedPublicKey := km.accountsStore.PublicKeys[index]
accountName := petnames.DeterministicName(deletedPublicKey, "-")
km.accountsStore.PrivateKeys = append(km.accountsStore.PrivateKeys[:index], km.accountsStore.PrivateKeys[index+1:]...)
km.accountsStore.PublicKeys = append(km.accountsStore.PublicKeys[:index], km.accountsStore.PublicKeys[index+1:]...)
newStore, err := km.CreateAccountsKeystore(ctx, km.accountsStore.PrivateKeys, km.accountsStore.PublicKeys)
if err != nil {
return errors.Wrap(err, "could not rewrite accounts keystore")
}
// Write the encoded keystore.
encoded, err := json.MarshalIndent(newStore, "", "\t")
if err != nil {
return err
}
if err := km.wallet.WriteFileAtPath(ctx, AccountsPath, AccountsKeystoreFileName, encoded); err != nil {
return errors.Wrap(err, "could not write keystore file for accounts")
}
log.WithFields(logrus.Fields{
"name": accountName,
"publicKey": fmt.Sprintf("%#x", bytesutil.Trunc(deletedPublicKey)),
}).Info("Successfully deleted validator account")
err = km.initializeKeysCachesFromKeystore()
if err != nil {
return errors.Wrap(err, "failed to initialize keys caches")
}
}
return nil
}

View File

@@ -0,0 +1,66 @@
package imported
import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
"github.com/prysmaticlabs/prysm/testing/require"
mock "github.com/prysmaticlabs/prysm/validator/accounts/testing"
"github.com/prysmaticlabs/prysm/validator/keymanager"
logTest "github.com/sirupsen/logrus/hooks/test"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
)
func TestImportedKeymanager_DeleteKeystores(t *testing.T) {
hook := logTest.NewGlobal()
wallet := &mock.Wallet{
Files: make(map[string]map[string][]byte),
WalletPassword: password,
}
dr := &Keymanager{
wallet: wallet,
accountsStore: &accountStore{},
}
numAccounts := 5
ctx := context.Background()
keystores := make([]*keymanager.Keystore, numAccounts)
for i := 0; i < numAccounts; i++ {
keystores[i] = createRandomKeystore(t, password)
}
require.NoError(t, dr.ImportKeystores(ctx, keystores, password))
accounts, err := dr.FetchValidatingPublicKeys(ctx)
require.NoError(t, err)
require.Equal(t, numAccounts, len(accounts))
accountToRemove := uint64(2)
accountPubKey := accounts[accountToRemove]
// Remove an account from the keystore.
require.NoError(t, dr.DeleteAccounts(ctx, [][]byte{accountPubKey[:]}))
// Ensure the keystore file was written to the wallet
// and ensure we can decrypt it using the EIP-2335 standard.
var encodedKeystore []byte
for k, v := range wallet.Files[AccountsPath] {
if strings.Contains(k, "keystore") {
encodedKeystore = v
}
}
require.NotNil(t, encodedKeystore, "could not find keystore file")
keystoreFile := &keymanager.Keystore{}
require.NoError(t, json.Unmarshal(encodedKeystore, keystoreFile))
// We extract the accounts from the keystore.
decryptor := keystorev4.New()
encodedAccounts, err := decryptor.Decrypt(keystoreFile.Crypto, password)
require.NoError(t, err, "Could not decrypt validator accounts")
store := &accountStore{}
require.NoError(t, json.Unmarshal(encodedAccounts, store))
require.Equal(t, numAccounts-1, len(store.PublicKeys))
require.Equal(t, numAccounts-1, len(store.PrivateKeys))
require.LogsContain(t, hook, fmt.Sprintf("%#x", bytesutil.Trunc(accountPubKey[:])))
require.LogsContain(t, hook, "Successfully deleted validator account")
}

View File

@@ -1,4 +1,4 @@
package derived
package imported
import (
"encoding/hex"

View File

@@ -0,0 +1,41 @@
package imported
import (
"context"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
"go.opencensus.io/trace"
)
// FetchValidatingPublicKeys fetches the list of active public keys from the imported account keystores.
func (km *Keymanager) FetchValidatingPublicKeys(ctx context.Context) ([][48]byte, error) {
ctx, span := trace.StartSpan(ctx, "keymanager.FetchValidatingPublicKeys")
defer span.End()
lock.RLock()
keys := orderedPublicKeys
result := make([][48]byte, len(keys))
copy(result, keys)
lock.RUnlock()
return result, nil
}
// FetchValidatingPrivateKeys fetches the list of private keys from the secret keys cache
func (km *Keymanager) FetchValidatingPrivateKeys(ctx context.Context) ([][32]byte, error) {
lock.RLock()
defer lock.RUnlock()
privKeys := make([][32]byte, len(secretKeysCache))
pubKeys, err := km.FetchValidatingPublicKeys(ctx)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve public keys")
}
for i, pk := range pubKeys {
seckey, ok := secretKeysCache[pk]
if !ok {
return nil, errors.New("Could not fetch private key")
}
privKeys[i] = bytesutil.ToBytes32(seckey.Marshal())
}
return privKeys, nil
}

View File

@@ -0,0 +1,77 @@
package imported
import (
"context"
"testing"
"github.com/prysmaticlabs/prysm/crypto/bls"
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
"github.com/prysmaticlabs/prysm/testing/assert"
"github.com/prysmaticlabs/prysm/testing/require"
mock "github.com/prysmaticlabs/prysm/validator/accounts/testing"
)
func TestImportedKeymanager_FetchValidatingPublicKeys(t *testing.T) {
wallet := &mock.Wallet{
Files: make(map[string]map[string][]byte),
WalletPassword: password,
}
dr := &Keymanager{
wallet: wallet,
accountsStore: &accountStore{},
}
// First, generate accounts and their keystore.json files.
ctx := context.Background()
numAccounts := 10
wantedPubKeys := make([][48]byte, 0)
for i := 0; i < numAccounts; i++ {
privKey, err := bls.RandKey()
require.NoError(t, err)
pubKey := bytesutil.ToBytes48(privKey.PublicKey().Marshal())
wantedPubKeys = append(wantedPubKeys, pubKey)
dr.accountsStore.PublicKeys = append(dr.accountsStore.PublicKeys, pubKey[:])
dr.accountsStore.PrivateKeys = append(dr.accountsStore.PrivateKeys, privKey.Marshal())
}
require.NoError(t, dr.initializeKeysCachesFromKeystore())
publicKeys, err := dr.FetchValidatingPublicKeys(ctx)
require.NoError(t, err)
assert.Equal(t, numAccounts, len(publicKeys))
// FetchValidatingPublicKeys is also used in generating the output of account list
// therefore the results must be in the same order as the order in which the accounts were derived
for i, key := range wantedPubKeys {
assert.Equal(t, key, publicKeys[i])
}
}
func TestImportedKeymanager_FetchValidatingPrivateKeys(t *testing.T) {
wallet := &mock.Wallet{
Files: make(map[string]map[string][]byte),
WalletPassword: password,
}
dr := &Keymanager{
wallet: wallet,
accountsStore: &accountStore{},
}
// First, generate accounts and their keystore.json files.
ctx := context.Background()
numAccounts := 10
wantedPrivateKeys := make([][32]byte, numAccounts)
for i := 0; i < numAccounts; i++ {
privKey, err := bls.RandKey()
require.NoError(t, err)
privKeyData := privKey.Marshal()
pubKey := bytesutil.ToBytes48(privKey.PublicKey().Marshal())
wantedPrivateKeys[i] = bytesutil.ToBytes32(privKeyData)
dr.accountsStore.PublicKeys = append(dr.accountsStore.PublicKeys, pubKey[:])
dr.accountsStore.PrivateKeys = append(dr.accountsStore.PrivateKeys, privKeyData)
}
require.NoError(t, dr.initializeKeysCachesFromKeystore())
privateKeys, err := dr.FetchValidatingPrivateKeys(ctx)
require.NoError(t, err)
assert.Equal(t, numAccounts, len(privateKeys))
// FetchValidatingPrivateKeys is also used in generating the output of account list
// therefore the results must be in the same order as the order in which the accounts were created
for i, key := range wantedPrivateKeys {
assert.Equal(t, key, privateKeys[i])
}
}

View File

@@ -1,7 +1,6 @@
package imported
import (
"bytes"
"context"
"encoding/json"
"fmt"
@@ -13,14 +12,11 @@ import (
"github.com/prysmaticlabs/prysm/async/event"
"github.com/prysmaticlabs/prysm/crypto/bls"
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
validatorpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/validator-client"
"github.com/prysmaticlabs/prysm/runtime/interop"
"github.com/prysmaticlabs/prysm/validator/accounts/iface"
"github.com/prysmaticlabs/prysm/validator/accounts/petnames"
"github.com/prysmaticlabs/prysm/validator/keymanager"
"github.com/sirupsen/logrus"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
"go.opencensus.io/trace"
)
var (
@@ -29,6 +25,15 @@ var (
secretKeysCache = make(map[[48]byte]bls.SecretKey)
)
const (
// DerivationPathFormat describes the structure of how keys are derived from a master key.
DerivationPathFormat = "m / purpose / coin_type / account_index / withdrawal_key / validating_key"
// ValidatingKeyDerivationPathTemplate defining the hierarchical path for validating
// keys for Prysm Ethereum 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"
)
const (
// KeystoreFileNameFormat exposes the filename the keystore should be formatted in.
KeystoreFileNameFormat = "keystore-%d.json"
@@ -83,6 +88,12 @@ func NewKeymanager(ctx context.Context, cfg *SetupConfig) (*Keymanager, error) {
accountsStore: &accountStore{},
accountsChangedFeed: new(event.Feed),
}
if strings.Contains(cfg.Wallet.AccountsDir(), "imported") ||
strings.Contains(cfg.Wallet.AccountsDir(), "derived") {
return nil, errors.New(
"keymanager kind found in wallet is not compatible as a local keymanager",
)
}
if err := k.initializeAccountKeystore(ctx); err != nil {
return nil, errors.Wrap(err, "failed to initialize account store")
@@ -96,8 +107,8 @@ func NewKeymanager(ctx context.Context, cfg *SetupConfig) (*Keymanager, error) {
return k, nil
}
// NewInteropKeymanager instantiates a new imported keymanager with the deterministically generated interop keys.
func NewInteropKeymanager(_ context.Context, offset, numValidatorKeys uint64) (*Keymanager, error) {
// NewDeterministicKeymanager instantiates a new imported keymanager with the deterministically generated interop keys.
func NewDeterministicKeymanager(_ context.Context, offset, numValidatorKeys uint64) (*Keymanager, error) {
k := &Keymanager{
accountsChangedFeed: new(event.Feed),
}
@@ -120,13 +131,6 @@ func NewInteropKeymanager(_ context.Context, offset, numValidatorKeys uint64) (*
return k, nil
}
// SubscribeAccountChanges creates an event subscription for a channel
// to listen for public key changes at runtime, such as when new validator accounts
// are imported into the keymanager while the validator process is running.
func (km *Keymanager) SubscribeAccountChanges(pubKeysChan chan [][48]byte) event.Subscription {
return km.accountsChangedFeed.Subscribe(pubKeysChan)
}
// ValidatingAccountNames for a imported keymanager.
func (km *Keymanager) ValidatingAccountNames() ([]string, error) {
lock.RLock()
@@ -138,164 +142,6 @@ func (km *Keymanager) ValidatingAccountNames() ([]string, error) {
return names, nil
}
// Initialize public and secret key caches that are used to speed up the functions
// FetchValidatingPublicKeys and Sign
func (km *Keymanager) initializeKeysCachesFromKeystore() error {
lock.Lock()
defer lock.Unlock()
count := len(km.accountsStore.PrivateKeys)
orderedPublicKeys = make([][48]byte, count)
secretKeysCache = make(map[[48]byte]bls.SecretKey, count)
for i, publicKey := range km.accountsStore.PublicKeys {
publicKey48 := bytesutil.ToBytes48(publicKey)
orderedPublicKeys[i] = publicKey48
secretKey, err := bls.SecretKeyFromBytes(km.accountsStore.PrivateKeys[i])
if err != nil {
return errors.Wrap(err, "failed to initialize keys caches from account keystore")
}
secretKeysCache[publicKey48] = secretKey
}
return nil
}
// DeleteAccounts takes in public keys and removes the accounts entirely. This includes their disk keystore and cached keystore.
func (km *Keymanager) DeleteAccounts(ctx context.Context, publicKeys [][]byte) error {
for _, publicKey := range publicKeys {
var index int
var found bool
for i, pubKey := range km.accountsStore.PublicKeys {
if bytes.Equal(pubKey, publicKey) {
index = i
found = true
break
}
}
if !found {
return fmt.Errorf("could not find public key %#x", publicKey)
}
deletedPublicKey := km.accountsStore.PublicKeys[index]
accountName := petnames.DeterministicName(deletedPublicKey, "-")
km.accountsStore.PrivateKeys = append(km.accountsStore.PrivateKeys[:index], km.accountsStore.PrivateKeys[index+1:]...)
km.accountsStore.PublicKeys = append(km.accountsStore.PublicKeys[:index], km.accountsStore.PublicKeys[index+1:]...)
newStore, err := km.CreateAccountsKeystore(ctx, km.accountsStore.PrivateKeys, km.accountsStore.PublicKeys)
if err != nil {
return errors.Wrap(err, "could not rewrite accounts keystore")
}
// Write the encoded keystore.
encoded, err := json.MarshalIndent(newStore, "", "\t")
if err != nil {
return err
}
if err := km.wallet.WriteFileAtPath(ctx, AccountsPath, AccountsKeystoreFileName, encoded); err != nil {
return errors.Wrap(err, "could not write keystore file for accounts")
}
log.WithFields(logrus.Fields{
"name": accountName,
"publicKey": fmt.Sprintf("%#x", bytesutil.Trunc(deletedPublicKey)),
}).Info("Successfully deleted validator account")
err = km.initializeKeysCachesFromKeystore()
if err != nil {
return errors.Wrap(err, "failed to initialize keys caches")
}
}
return nil
}
// FetchValidatingPublicKeys fetches the list of active public keys from the imported account keystores.
func (km *Keymanager) FetchValidatingPublicKeys(ctx context.Context) ([][48]byte, error) {
ctx, span := trace.StartSpan(ctx, "keymanager.FetchValidatingPublicKeys")
defer span.End()
lock.RLock()
keys := orderedPublicKeys
result := make([][48]byte, len(keys))
copy(result, keys)
lock.RUnlock()
return result, nil
}
// FetchValidatingPrivateKeys fetches the list of private keys from the secret keys cache
func (km *Keymanager) FetchValidatingPrivateKeys(ctx context.Context) ([][32]byte, error) {
lock.RLock()
defer lock.RUnlock()
privKeys := make([][32]byte, len(secretKeysCache))
pubKeys, err := km.FetchValidatingPublicKeys(ctx)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve public keys")
}
for i, pk := range pubKeys {
seckey, ok := secretKeysCache[pk]
if !ok {
return nil, errors.New("Could not fetch private key")
}
privKeys[i] = bytesutil.ToBytes32(seckey.Marshal())
}
return privKeys, nil
}
// Sign signs a message using a validator key.
func (km *Keymanager) Sign(ctx context.Context, req *validatorpb.SignRequest) (bls.Signature, error) {
ctx, span := trace.StartSpan(ctx, "keymanager.Sign")
defer span.End()
publicKey := req.PublicKey
if publicKey == nil {
return nil, errors.New("nil public key in request")
}
lock.RLock()
secretKey, ok := secretKeysCache[bytesutil.ToBytes48(publicKey)]
lock.RUnlock()
if !ok {
return nil, errors.New("no signing key found in keys cache")
}
return secretKey.Sign(req.SigningRoot), nil
}
func (km *Keymanager) initializeAccountKeystore(ctx context.Context) error {
encoded, err := km.wallet.ReadFileAtPath(ctx, AccountsPath, AccountsKeystoreFileName)
if err != nil && strings.Contains(err.Error(), "no files found") {
// If there are no keys to initialize at all, just exit.
return nil
} else if err != nil {
return errors.Wrapf(err, "could not read keystore file for accounts %s", AccountsKeystoreFileName)
}
keystoreFile := &AccountsKeystoreRepresentation{}
if err := json.Unmarshal(encoded, keystoreFile); err != nil {
return errors.Wrapf(err, "could not decode keystore file for accounts %s", AccountsKeystoreFileName)
}
// 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.
password := km.wallet.Password()
decryptor := keystorev4.New()
enc, err := decryptor.Decrypt(keystoreFile.Crypto, password)
if err != nil && strings.Contains(err.Error(), keymanager.IncorrectPasswordErrMsg) {
return errors.Wrap(err, "wrong password for wallet entered")
} else if err != nil {
return errors.Wrap(err, "could not decrypt keystore")
}
store := &accountStore{}
if err := json.Unmarshal(enc, store); err != nil {
return err
}
if len(store.PublicKeys) != len(store.PrivateKeys) {
return errors.New("unequal number of public keys and private keys")
}
if len(store.PublicKeys) == 0 {
return nil
}
km.accountsStore = store
err = km.initializeKeysCachesFromKeystore()
if err != nil {
return errors.Wrap(err, "failed to initialize keys caches")
}
return err
}
// CreateAccountsKeystore creates a new keystore holding the provided keys.
func (km *Keymanager) CreateAccountsKeystore(
_ context.Context,
@@ -356,3 +202,65 @@ func (km *Keymanager) CreateAccountsKeystore(
Name: encryptor.Name(),
}, nil
}
// Initialize public and secret key caches that are used to speed up the functions
// FetchValidatingPublicKeys and Sign
func (km *Keymanager) initializeKeysCachesFromKeystore() error {
lock.Lock()
defer lock.Unlock()
count := len(km.accountsStore.PrivateKeys)
orderedPublicKeys = make([][48]byte, count)
secretKeysCache = make(map[[48]byte]bls.SecretKey, count)
for i, publicKey := range km.accountsStore.PublicKeys {
publicKey48 := bytesutil.ToBytes48(publicKey)
orderedPublicKeys[i] = publicKey48
secretKey, err := bls.SecretKeyFromBytes(km.accountsStore.PrivateKeys[i])
if err != nil {
return errors.Wrap(err, "failed to initialize keys caches from account keystore")
}
secretKeysCache[publicKey48] = secretKey
}
return nil
}
func (km *Keymanager) initializeAccountKeystore(ctx context.Context) error {
encoded, err := km.wallet.ReadFileAtPath(ctx, AccountsPath, AccountsKeystoreFileName)
if err != nil && strings.Contains(err.Error(), "no files found") {
// If there are no keys to initialize at all, just exit.
return nil
} else if err != nil {
return errors.Wrapf(err, "could not read keystore file for accounts %s", AccountsKeystoreFileName)
}
keystoreFile := &AccountsKeystoreRepresentation{}
if err := json.Unmarshal(encoded, keystoreFile); err != nil {
return errors.Wrapf(err, "could not decode keystore file for accounts %s", AccountsKeystoreFileName)
}
// 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.
password := km.wallet.Password()
decryptor := keystorev4.New()
enc, err := decryptor.Decrypt(keystoreFile.Crypto, password)
if err != nil && strings.Contains(err.Error(), keymanager.IncorrectPasswordErrMsg) {
return errors.Wrap(err, "wrong password for wallet entered")
} else if err != nil {
return errors.Wrap(err, "could not decrypt keystore")
}
store := &accountStore{}
if err := json.Unmarshal(enc, store); err != nil {
return err
}
if len(store.PublicKeys) != len(store.PrivateKeys) {
return errors.New("unequal number of public keys and private keys")
}
if len(store.PublicKeys) == 0 {
return nil
}
km.accountsStore = store
err = km.initializeKeysCachesFromKeystore()
if err != nil {
return errors.Wrap(err, "failed to initialize keys caches")
}
return err
}

View File

@@ -1,218 +1 @@
package imported
import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/prysmaticlabs/prysm/crypto/bls"
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
validatorpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/validator-client"
"github.com/prysmaticlabs/prysm/testing/assert"
"github.com/prysmaticlabs/prysm/testing/require"
mock "github.com/prysmaticlabs/prysm/validator/accounts/testing"
"github.com/prysmaticlabs/prysm/validator/keymanager"
logTest "github.com/sirupsen/logrus/hooks/test"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
)
func TestImportedKeymanager_RemoveAccounts(t *testing.T) {
hook := logTest.NewGlobal()
wallet := &mock.Wallet{
Files: make(map[string]map[string][]byte),
WalletPassword: password,
}
dr := &Keymanager{
wallet: wallet,
accountsStore: &accountStore{},
}
numAccounts := 5
ctx := context.Background()
keystores := make([]*keymanager.Keystore, numAccounts)
for i := 0; i < numAccounts; i++ {
keystores[i] = createRandomKeystore(t, password)
}
require.NoError(t, dr.ImportKeystores(ctx, keystores, password))
accounts, err := dr.FetchValidatingPublicKeys(ctx)
require.NoError(t, err)
require.Equal(t, numAccounts, len(accounts))
accountToRemove := uint64(2)
accountPubKey := accounts[accountToRemove]
// Remove an account from the keystore.
require.NoError(t, dr.DeleteAccounts(ctx, [][]byte{accountPubKey[:]}))
// Ensure the keystore file was written to the wallet
// and ensure we can decrypt it using the EIP-2335 standard.
var encodedKeystore []byte
for k, v := range wallet.Files[AccountsPath] {
if strings.Contains(k, "keystore") {
encodedKeystore = v
}
}
require.NotNil(t, encodedKeystore, "could not find keystore file")
keystoreFile := &keymanager.Keystore{}
require.NoError(t, json.Unmarshal(encodedKeystore, keystoreFile))
// We extract the accounts from the keystore.
decryptor := keystorev4.New()
encodedAccounts, err := decryptor.Decrypt(keystoreFile.Crypto, password)
require.NoError(t, err, "Could not decrypt validator accounts")
store := &accountStore{}
require.NoError(t, json.Unmarshal(encodedAccounts, store))
require.Equal(t, numAccounts-1, len(store.PublicKeys))
require.Equal(t, numAccounts-1, len(store.PrivateKeys))
require.LogsContain(t, hook, fmt.Sprintf("%#x", bytesutil.Trunc(accountPubKey[:])))
require.LogsContain(t, hook, "Successfully deleted validator account")
}
func TestImportedKeymanager_FetchValidatingPublicKeys(t *testing.T) {
wallet := &mock.Wallet{
Files: make(map[string]map[string][]byte),
WalletPassword: password,
}
dr := &Keymanager{
wallet: wallet,
accountsStore: &accountStore{},
}
// First, generate accounts and their keystore.json files.
ctx := context.Background()
numAccounts := 10
wantedPubKeys := make([][48]byte, 0)
for i := 0; i < numAccounts; i++ {
privKey, err := bls.RandKey()
require.NoError(t, err)
pubKey := bytesutil.ToBytes48(privKey.PublicKey().Marshal())
wantedPubKeys = append(wantedPubKeys, pubKey)
dr.accountsStore.PublicKeys = append(dr.accountsStore.PublicKeys, pubKey[:])
dr.accountsStore.PrivateKeys = append(dr.accountsStore.PrivateKeys, privKey.Marshal())
}
require.NoError(t, dr.initializeKeysCachesFromKeystore())
publicKeys, err := dr.FetchValidatingPublicKeys(ctx)
require.NoError(t, err)
assert.Equal(t, numAccounts, len(publicKeys))
// FetchValidatingPublicKeys is also used in generating the output of account list
// therefore the results must be in the same order as the order in which the accounts were derived
for i, key := range wantedPubKeys {
assert.Equal(t, key, publicKeys[i])
}
}
func TestImportedKeymanager_FetchValidatingPrivateKeys(t *testing.T) {
wallet := &mock.Wallet{
Files: make(map[string]map[string][]byte),
WalletPassword: password,
}
dr := &Keymanager{
wallet: wallet,
accountsStore: &accountStore{},
}
// First, generate accounts and their keystore.json files.
ctx := context.Background()
numAccounts := 10
wantedPrivateKeys := make([][32]byte, numAccounts)
for i := 0; i < numAccounts; i++ {
privKey, err := bls.RandKey()
require.NoError(t, err)
privKeyData := privKey.Marshal()
pubKey := bytesutil.ToBytes48(privKey.PublicKey().Marshal())
wantedPrivateKeys[i] = bytesutil.ToBytes32(privKeyData)
dr.accountsStore.PublicKeys = append(dr.accountsStore.PublicKeys, pubKey[:])
dr.accountsStore.PrivateKeys = append(dr.accountsStore.PrivateKeys, privKeyData)
}
require.NoError(t, dr.initializeKeysCachesFromKeystore())
privateKeys, err := dr.FetchValidatingPrivateKeys(ctx)
require.NoError(t, err)
assert.Equal(t, numAccounts, len(privateKeys))
// FetchValidatingPrivateKeys is also used in generating the output of account list
// therefore the results must be in the same order as the order in which the accounts were created
for i, key := range wantedPrivateKeys {
assert.Equal(t, key, privateKeys[i])
}
}
func TestImportedKeymanager_Sign(t *testing.T) {
wallet := &mock.Wallet{
Files: make(map[string]map[string][]byte),
AccountPasswords: make(map[string]string),
WalletPassword: password,
}
dr := &Keymanager{
wallet: wallet,
accountsStore: &accountStore{},
}
// First, generate accounts and their keystore.json files.
ctx := context.Background()
numAccounts := 10
keystores := make([]*keymanager.Keystore, numAccounts)
for i := 0; i < numAccounts; i++ {
keystores[i] = createRandomKeystore(t, password)
}
require.NoError(t, dr.ImportKeystores(ctx, keystores, password))
var encodedKeystore []byte
for k, v := range wallet.Files[AccountsPath] {
if strings.Contains(k, "keystore") {
encodedKeystore = v
}
}
keystoreFile := &keymanager.Keystore{}
require.NoError(t, json.Unmarshal(encodedKeystore, keystoreFile))
// 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()
enc, err := decryptor.Decrypt(keystoreFile.Crypto, dr.wallet.Password())
require.NoError(t, err)
store := &accountStore{}
require.NoError(t, json.Unmarshal(enc, store))
require.Equal(t, len(store.PublicKeys), len(store.PrivateKeys))
require.NotEqual(t, 0, len(store.PublicKeys))
dr.accountsStore = store
require.NoError(t, dr.initializeKeysCachesFromKeystore())
publicKeys, err := dr.FetchValidatingPublicKeys(ctx)
require.NoError(t, err)
require.Equal(t, len(publicKeys), len(store.PublicKeys))
// We prepare naive data to sign.
data := []byte("hello world")
signRequest := &validatorpb.SignRequest{
PublicKey: publicKeys[0][:],
SigningRoot: data,
}
sig, err := dr.Sign(ctx, signRequest)
require.NoError(t, err)
pubKey, err := bls.PublicKeyFromBytes(publicKeys[0][:])
require.NoError(t, err)
wrongPubKey, err := bls.PublicKeyFromBytes(publicKeys[1][:])
require.NoError(t, err)
if !sig.Verify(pubKey, data) {
t.Fatalf("Expected sig to verify for pubkey %#x and data %v", pubKey.Marshal(), data)
}
if sig.Verify(wrongPubKey, data) {
t.Fatalf("Expected sig not to verify for pubkey %#x and data %v", wrongPubKey.Marshal(), data)
}
}
func TestImportedKeymanager_Sign_NoPublicKeySpecified(t *testing.T) {
req := &validatorpb.SignRequest{
PublicKey: nil,
}
dr := &Keymanager{}
_, err := dr.Sign(context.Background(), req)
assert.ErrorContains(t, "nil public key", err)
}
func TestImportedKeymanager_Sign_NoPublicKeyInCache(t *testing.T) {
req := &validatorpb.SignRequest{
PublicKey: []byte("hello world"),
}
secretKeysCache = make(map[[48]byte]bls.SecretKey)
dr := &Keymanager{}
_, err := dr.Sign(context.Background(), req)
assert.ErrorContains(t, "no signing key found in keys cache", err)
}

View File

@@ -1,4 +1,4 @@
package derived
package imported
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package derived
package imported
import (
"testing"

View File

@@ -0,0 +1,34 @@
package imported
import (
"context"
"fmt"
"github.com/pkg/errors"
util "github.com/wealdtech/go-eth2-util"
)
// RecoverKeystoresFromMnemonic given a mnemonic phrase, is able to regenerate N accounts
// from a derived seed, encrypt them according to the EIP-2334 JSON standard, and write them
// to disk. Then, the mnemonic is never stored nor used by the validator.
func (km *Keymanager) RecoverKeystoresFromMnemonic(
ctx context.Context, mnemonic, mnemonicPassphrase string, numAccounts int,
) error {
seed, err := seedFromMnemonic(mnemonic, mnemonicPassphrase)
if err != nil {
return errors.Wrap(err, "could not initialize new wallet seed file")
}
privKeys := make([][]byte, numAccounts)
pubKeys := make([][]byte, numAccounts)
for i := 0; i < numAccounts; i++ {
privKey, err := util.PrivateKeyFromSeedAndPath(
seed, fmt.Sprintf(ValidatingKeyDerivationPathTemplate, i),
)
if err != nil {
return err
}
privKeys[i] = privKey.Marshal()
pubKeys[i] = privKey.PublicKey().Marshal()
}
return nil
}

View File

@@ -0,0 +1,68 @@
package imported
import (
"context"
"testing"
"github.com/prysmaticlabs/prysm/crypto/rand"
"github.com/prysmaticlabs/prysm/testing/assert"
"github.com/prysmaticlabs/prysm/testing/require"
mock "github.com/prysmaticlabs/prysm/validator/accounts/testing"
constant "github.com/prysmaticlabs/prysm/validator/testing"
"github.com/tyler-smith/go-bip39"
)
// We test that using a '25th word' mnemonic passphrase leads to different
// public keys derived than not specifying the passphrase.
func TestImportedKeymanager_Recover_25Words(t *testing.T) {
ctx := context.Background()
wallet := &mock.Wallet{
Files: make(map[string]map[string][]byte),
AccountPasswords: make(map[string]string),
WalletPassword: password,
}
km, err := NewKeymanager(ctx, &SetupConfig{
Wallet: wallet,
ListenForChanges: false,
})
require.NoError(t, err)
numAccounts := 5
err = km.RecoverKeystoresFromMnemonic(ctx, constant.TestMnemonic, "mnemonicpass", numAccounts)
require.NoError(t, err)
without25thWord, err := km.FetchValidatingPublicKeys(ctx)
require.NoError(t, err)
wallet = &mock.Wallet{
Files: make(map[string]map[string][]byte),
AccountPasswords: make(map[string]string),
WalletPassword: password,
}
km, err = NewKeymanager(ctx, &SetupConfig{
Wallet: wallet,
ListenForChanges: false,
})
require.NoError(t, err)
// No mnemonic passphrase this time.
err = km.RecoverKeystoresFromMnemonic(ctx, constant.TestMnemonic, "", numAccounts)
require.NoError(t, err)
with25thWord, err := km.FetchValidatingPublicKeys(ctx)
require.NoError(t, err)
for i, k := range with25thWord {
without := without25thWord[i]
assert.DeepNotEqual(t, k, without)
}
}
func TestImportedKeymanager_Recover_RoundTrip(t *testing.T) {
mnemonicEntropy := make([]byte, 32)
n, err := rand.NewGenerator().Read(mnemonicEntropy)
require.NoError(t, err)
require.Equal(t, n, len(mnemonicEntropy))
mnemonic, err := bip39.NewMnemonic(mnemonicEntropy)
require.NoError(t, err)
wanted := bip39.NewSeed(mnemonic, "")
got, err := seedFromMnemonic(mnemonic, "" /* no passphrase */)
require.NoError(t, err)
// Ensure the derived seed matches.
assert.DeepEqual(t, wanted, got)
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/fsnotify/fsnotify"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/async"
"github.com/prysmaticlabs/prysm/async/event"
"github.com/prysmaticlabs/prysm/config/features"
"github.com/prysmaticlabs/prysm/crypto/bls"
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
@@ -17,6 +18,13 @@ import (
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
)
// SubscribeAccountChanges creates an event subscription for a channel
// to listen for public key changes at runtime, such as when new validator accounts
// are imported into the keymanager while the validator process is running.
func (km *Keymanager) SubscribeAccountChanges(pubKeysChan chan [][48]byte) event.Subscription {
return km.accountsChangedFeed.Subscribe(pubKeysChan)
}
// Listen for changes to the all-accounts.keystore.json file in our wallet
// to load in new keys we observe into our keymanager. This uses the fsnotify
// library to listen for file-system changes and debounces these events to

View File

@@ -0,0 +1,29 @@
package imported
import (
"context"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/crypto/bls"
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
validatorpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/validator-client"
"go.opencensus.io/trace"
)
// Sign signs a message using a validator key.
func (km *Keymanager) Sign(ctx context.Context, req *validatorpb.SignRequest) (bls.Signature, error) {
ctx, span := trace.StartSpan(ctx, "keymanager.Sign")
defer span.End()
publicKey := req.PublicKey
if publicKey == nil {
return nil, errors.New("nil public key in request")
}
lock.RLock()
secretKey, ok := secretKeysCache[bytesutil.ToBytes48(publicKey)]
lock.RUnlock()
if !ok {
return nil, errors.New("no signing key found in keys cache")
}
return secretKey.Sign(req.SigningRoot), nil
}

View File

@@ -0,0 +1,100 @@
package imported
import (
"context"
"encoding/json"
"strings"
"testing"
"github.com/prysmaticlabs/prysm/crypto/bls"
validatorpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/validator-client"
"github.com/prysmaticlabs/prysm/testing/assert"
"github.com/prysmaticlabs/prysm/testing/require"
mock "github.com/prysmaticlabs/prysm/validator/accounts/testing"
"github.com/prysmaticlabs/prysm/validator/keymanager"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
)
func TestImportedKeymanager_Sign(t *testing.T) {
wallet := &mock.Wallet{
Files: make(map[string]map[string][]byte),
AccountPasswords: make(map[string]string),
WalletPassword: password,
}
dr := &Keymanager{
wallet: wallet,
accountsStore: &accountStore{},
}
// First, generate accounts and their keystore.json files.
ctx := context.Background()
numAccounts := 10
keystores := make([]*keymanager.Keystore, numAccounts)
for i := 0; i < numAccounts; i++ {
keystores[i] = createRandomKeystore(t, password)
}
require.NoError(t, dr.ImportKeystores(ctx, keystores, password))
var encodedKeystore []byte
for k, v := range wallet.Files[AccountsPath] {
if strings.Contains(k, "keystore") {
encodedKeystore = v
}
}
keystoreFile := &keymanager.Keystore{}
require.NoError(t, json.Unmarshal(encodedKeystore, keystoreFile))
// 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()
enc, err := decryptor.Decrypt(keystoreFile.Crypto, dr.wallet.Password())
require.NoError(t, err)
store := &accountStore{}
require.NoError(t, json.Unmarshal(enc, store))
require.Equal(t, len(store.PublicKeys), len(store.PrivateKeys))
require.NotEqual(t, 0, len(store.PublicKeys))
dr.accountsStore = store
require.NoError(t, dr.initializeKeysCachesFromKeystore())
publicKeys, err := dr.FetchValidatingPublicKeys(ctx)
require.NoError(t, err)
require.Equal(t, len(publicKeys), len(store.PublicKeys))
// We prepare naive data to sign.
data := []byte("hello world")
signRequest := &validatorpb.SignRequest{
PublicKey: publicKeys[0][:],
SigningRoot: data,
}
sig, err := dr.Sign(ctx, signRequest)
require.NoError(t, err)
pubKey, err := bls.PublicKeyFromBytes(publicKeys[0][:])
require.NoError(t, err)
wrongPubKey, err := bls.PublicKeyFromBytes(publicKeys[1][:])
require.NoError(t, err)
if !sig.Verify(pubKey, data) {
t.Fatalf("Expected sig to verify for pubkey %#x and data %v", pubKey.Marshal(), data)
}
if sig.Verify(wrongPubKey, data) {
t.Fatalf("Expected sig not to verify for pubkey %#x and data %v", wrongPubKey.Marshal(), data)
}
}
func TestImportedKeymanager_Sign_NoPublicKeySpecified(t *testing.T) {
req := &validatorpb.SignRequest{
PublicKey: nil,
}
dr := &Keymanager{}
_, err := dr.Sign(context.Background(), req)
assert.ErrorContains(t, "nil public key", err)
}
func TestImportedKeymanager_Sign_NoPublicKeyInCache(t *testing.T) {
req := &validatorpb.SignRequest{
PublicKey: []byte("hello world"),
}
secretKeysCache = make(map[[48]byte]bls.SecretKey)
dr := &Keymanager{}
_, err := dr.Sign(context.Background(), req)
assert.ErrorContains(t, "no signing key found in keys cache", err)
}

View File

@@ -2,23 +2,62 @@ package keymanager
import (
"context"
"fmt"
"github.com/prysmaticlabs/prysm/async/event"
"github.com/prysmaticlabs/prysm/crypto/bls"
validatorpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/validator-client"
)
// IKeymanager defines a general keymanager interface for Prysm wallets.
const (
Local = "imported"
Derived = "derived"
Remote = "remote"
)
// IKeymanager defines a struct which can be used to manage keys with important
// actions such as listing public keys, signing data, and subscribing to key changes.
// It is defined by the simple composition of a few base features. We can assert whether
// or not a keymanager has "extra" functionality by casting it to other, useful key
// management interfaces defined in this package.
type IKeymanager interface {
// FetchValidatingPublicKeys fetches the list of active public keys that should be used to validate with.
PublicKeysFetcher
Signer
KeyChangeSubscriber
}
// KeysFetcher for validating private and public keys.
type KeysFetcher interface {
FetchValidatingPrivateKeys(ctx context.Context) ([][32]byte, error)
PublicKeysFetcher
}
// PublicKeysFetcher for validating public keys.
type PublicKeysFetcher interface {
FetchValidatingPublicKeys(ctx context.Context) ([][48]byte, error)
// Sign signs a message using a validator key.
}
// Signer allows signing messages using a validator private key.
type Signer interface {
Sign(context.Context, *validatorpb.SignRequest) (bls.Signature, error)
// SubscribeAccountChanges subscribes to changes made to the underlying keys.
}
// Importer can import new keystores into the keymanager.
type Importer interface {
ImportKeystores(ctx context.Context, keystores []*Keystore, importsPassword string) error
}
// KeyChangeSubscriber allows subscribing to changes made to the underlying keys.
type KeyChangeSubscriber interface {
SubscribeAccountChanges(pubKeysChan chan [][48]byte) event.Subscription
}
// KeyRecoverer allows recovering a keystore from a recovery phrase.
type KeyRecoverer interface {
RecoverKeystoresFromMnemonic(
ctx context.Context, mnemonic, mnemonicPassphrase string, numKeys int,
) error
}
// Keystore json file representation as a Go struct.
type Keystore struct {
Crypto map[string]interface{} `json:"crypto"`
@@ -28,47 +67,6 @@ type Keystore struct {
Name string `json:"name"`
}
// Kind defines an enum for either imported, derived, or remote-signing
// keystores for Prysm wallets.
type Kind int
const (
// Imported keymanager defines an on-disk, encrypted keystore-capable store.
Imported Kind = iota
// Derived keymanager using a hierarchical-deterministic algorithm.
Derived
// Remote keymanager capable of remote-signing data.
Remote
)
// IncorrectPasswordErrMsg defines a common error string representing an EIP-2335
// keystore password was incorrect.
const IncorrectPasswordErrMsg = "invalid checksum"
// String marshals a keymanager kind to a string value.
func (k Kind) String() string {
switch k {
case Derived:
return "derived"
case Imported:
return "direct"
case Remote:
return "remote"
default:
return fmt.Sprintf("%d", int(k))
}
}
// ParseKind from a raw string, returning a keymanager kind.
func ParseKind(k string) (Kind, error) {
switch k {
case "derived":
return Derived, nil
case "direct":
return Imported, nil
case "remote":
return Remote, nil
default:
return 0, fmt.Errorf("%s is not an allowed keymanager", k)
}
}

View File

@@ -2,13 +2,10 @@ package keymanager_test
import (
"github.com/prysmaticlabs/prysm/validator/keymanager"
"github.com/prysmaticlabs/prysm/validator/keymanager/derived"
"github.com/prysmaticlabs/prysm/validator/keymanager/imported"
"github.com/prysmaticlabs/prysm/validator/keymanager/remote"
)
var (
_ = keymanager.IKeymanager(&imported.Keymanager{})
_ = keymanager.IKeymanager(&derived.Keymanager{})
_ = keymanager.IKeymanager(&remote.Keymanager{})
//_ = keymanager.IKeymanager(&remote.Keymanager{})
)

View File

@@ -177,7 +177,7 @@ func (c *ValidatorClient) initializeFromCLI(cliCtx *cli.Context) error {
if cliCtx.IsSet(flags.InteropNumValidators.Name) {
numValidatorKeys := cliCtx.Uint64(flags.InteropNumValidators.Name)
offset := cliCtx.Uint64(flags.InteropStartIndex.Name)
keyManager, err = imported.NewInteropKeymanager(cliCtx.Context, offset, numValidatorKeys)
keyManager, err = imported.NewDeterministicKeymanager(cliCtx.Context, offset, numValidatorKeys)
if err != nil {
return errors.Wrap(err, "could not generate interop keys")
}