mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 13:28:01 -05:00
Compare commits
2 Commits
d929e1dcaa
...
delete-der
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b9fb0cbac | ||
|
|
e4b392ddb2 |
@@ -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",
|
||||
],
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package derived
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
var log = logrus.WithField("prefix", "derived-keymanager")
|
||||
59
validator/keymanager/imported/delete.go
Normal file
59
validator/keymanager/imported/delete.go
Normal 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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package derived
|
||||
package imported
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
41
validator/keymanager/imported/fetch_keys.go
Normal file
41
validator/keymanager/imported/fetch_keys.go
Normal 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
|
||||
}
|
||||
71
validator/keymanager/imported/fetch_keys_test.go
Normal file
71
validator/keymanager/imported/fetch_keys_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package imported
|
||||
|
||||
import (
|
||||
"github.com/prysmaticlabs/prysm/crypto/bls"
|
||||
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])
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -120,13 +125,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()
|
||||
@@ -158,102 +156,6 @@ func (km *Keymanager) initializeKeysCachesFromKeystore() error {
|
||||
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") {
|
||||
|
||||
@@ -7,10 +7,7 @@ import (
|
||||
"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"
|
||||
@@ -67,152 +64,3 @@ func TestImportedKeymanager_RemoveAccounts(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package derived
|
||||
package imported
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package derived
|
||||
package imported
|
||||
|
||||
import (
|
||||
"testing"
|
||||
34
validator/keymanager/imported/recover.go
Normal file
34
validator/keymanager/imported/recover.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package imported
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
util "github.com/wealdtech/go-eth2-util"
|
||||
)
|
||||
|
||||
// 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 nil
|
||||
}
|
||||
64
validator/keymanager/imported/recover_test.go
Normal file
64
validator/keymanager/imported/recover_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package imported
|
||||
|
||||
import (
|
||||
"github.com/prysmaticlabs/prysm/crypto/rand"
|
||||
"github.com/prysmaticlabs/prysm/testing/assert"
|
||||
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 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
29
validator/keymanager/imported/sign.go
Normal file
29
validator/keymanager/imported/sign.go
Normal 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
|
||||
}
|
||||
100
validator/keymanager/imported/sign_test.go
Normal file
100
validator/keymanager/imported/sign_test.go
Normal 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)
|
||||
}
|
||||
@@ -9,13 +9,38 @@ import (
|
||||
validatorpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/validator-client"
|
||||
)
|
||||
|
||||
// IKeymanager defines a general keymanager interface for Prysm wallets.
|
||||
// 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.
|
||||
type IKeymanager interface {
|
||||
// FetchValidatingPublicKeys fetches the list of active public keys that should be used to validate with.
|
||||
KeysFetcher
|
||||
Signer
|
||||
Importer
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user