mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-31 08:08:18 -05:00
Compare commits
4 Commits
e2e-debugg
...
keymanager
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d416de6d9 | ||
|
|
dfcad26ad2 | ||
|
|
1b9fb0cbac | ||
|
|
e4b392ddb2 |
1
cmd/validator/wallet/keymanager.go
Normal file
1
cmd/validator/wallet/keymanager.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package wallet
|
||||||
@@ -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
|
||||||
|
}
|
||||||
66
validator/keymanager/imported/delete_test.go
Normal file
66
validator/keymanager/imported/delete_test.go
Normal 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")
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package derived
|
package imported
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"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
|
||||||
|
}
|
||||||
77
validator/keymanager/imported/fetch_keys_test.go
Normal file
77
validator/keymanager/imported/fetch_keys_test.go
Normal 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package imported
|
package imported
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -13,14 +12,11 @@ import (
|
|||||||
"github.com/prysmaticlabs/prysm/async/event"
|
"github.com/prysmaticlabs/prysm/async/event"
|
||||||
"github.com/prysmaticlabs/prysm/crypto/bls"
|
"github.com/prysmaticlabs/prysm/crypto/bls"
|
||||||
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
|
"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/runtime/interop"
|
||||||
"github.com/prysmaticlabs/prysm/validator/accounts/iface"
|
"github.com/prysmaticlabs/prysm/validator/accounts/iface"
|
||||||
"github.com/prysmaticlabs/prysm/validator/accounts/petnames"
|
"github.com/prysmaticlabs/prysm/validator/accounts/petnames"
|
||||||
"github.com/prysmaticlabs/prysm/validator/keymanager"
|
"github.com/prysmaticlabs/prysm/validator/keymanager"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||||
"go.opencensus.io/trace"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -29,6 +25,15 @@ var (
|
|||||||
secretKeysCache = make(map[[48]byte]bls.SecretKey)
|
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 (
|
const (
|
||||||
// KeystoreFileNameFormat exposes the filename the keystore should be formatted in.
|
// KeystoreFileNameFormat exposes the filename the keystore should be formatted in.
|
||||||
KeystoreFileNameFormat = "keystore-%d.json"
|
KeystoreFileNameFormat = "keystore-%d.json"
|
||||||
@@ -83,6 +88,12 @@ func NewKeymanager(ctx context.Context, cfg *SetupConfig) (*Keymanager, error) {
|
|||||||
accountsStore: &accountStore{},
|
accountsStore: &accountStore{},
|
||||||
accountsChangedFeed: new(event.Feed),
|
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 {
|
if err := k.initializeAccountKeystore(ctx); err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to initialize account store")
|
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
|
return k, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewInteropKeymanager instantiates a new imported keymanager with the deterministically generated interop keys.
|
// NewDeterministicKeymanager instantiates a new imported keymanager with the deterministically generated interop keys.
|
||||||
func NewInteropKeymanager(_ context.Context, offset, numValidatorKeys uint64) (*Keymanager, error) {
|
func NewDeterministicKeymanager(_ context.Context, offset, numValidatorKeys uint64) (*Keymanager, error) {
|
||||||
k := &Keymanager{
|
k := &Keymanager{
|
||||||
accountsChangedFeed: new(event.Feed),
|
accountsChangedFeed: new(event.Feed),
|
||||||
}
|
}
|
||||||
@@ -120,13 +131,6 @@ func NewInteropKeymanager(_ context.Context, offset, numValidatorKeys uint64) (*
|
|||||||
return k, nil
|
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.
|
// ValidatingAccountNames for a imported keymanager.
|
||||||
func (km *Keymanager) ValidatingAccountNames() ([]string, error) {
|
func (km *Keymanager) ValidatingAccountNames() ([]string, error) {
|
||||||
lock.RLock()
|
lock.RLock()
|
||||||
@@ -138,164 +142,6 @@ func (km *Keymanager) ValidatingAccountNames() ([]string, error) {
|
|||||||
return names, nil
|
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.
|
// CreateAccountsKeystore creates a new keystore holding the provided keys.
|
||||||
func (km *Keymanager) CreateAccountsKeystore(
|
func (km *Keymanager) CreateAccountsKeystore(
|
||||||
_ context.Context,
|
_ context.Context,
|
||||||
@@ -356,3 +202,65 @@ func (km *Keymanager) CreateAccountsKeystore(
|
|||||||
Name: encryptor.Name(),
|
Name: encryptor.Name(),
|
||||||
}, nil
|
}, 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,218 +1 @@
|
|||||||
package imported
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package derived
|
package imported
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package derived
|
package imported
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
68
validator/keymanager/imported/recover_test.go
Normal file
68
validator/keymanager/imported/recover_test.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prysmaticlabs/prysm/async"
|
"github.com/prysmaticlabs/prysm/async"
|
||||||
|
"github.com/prysmaticlabs/prysm/async/event"
|
||||||
"github.com/prysmaticlabs/prysm/config/features"
|
"github.com/prysmaticlabs/prysm/config/features"
|
||||||
"github.com/prysmaticlabs/prysm/crypto/bls"
|
"github.com/prysmaticlabs/prysm/crypto/bls"
|
||||||
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
|
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
|
||||||
@@ -17,6 +18,13 @@ import (
|
|||||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
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
|
// 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
|
// 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
|
// 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)
|
||||||
|
}
|
||||||
@@ -2,23 +2,62 @@ package keymanager
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/prysmaticlabs/prysm/async/event"
|
"github.com/prysmaticlabs/prysm/async/event"
|
||||||
"github.com/prysmaticlabs/prysm/crypto/bls"
|
"github.com/prysmaticlabs/prysm/crypto/bls"
|
||||||
validatorpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1/validator-client"
|
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 {
|
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)
|
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)
|
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
|
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.
|
// Keystore json file representation as a Go struct.
|
||||||
type Keystore struct {
|
type Keystore struct {
|
||||||
Crypto map[string]interface{} `json:"crypto"`
|
Crypto map[string]interface{} `json:"crypto"`
|
||||||
@@ -28,47 +67,6 @@ type Keystore struct {
|
|||||||
Name string `json:"name"`
|
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
|
// IncorrectPasswordErrMsg defines a common error string representing an EIP-2335
|
||||||
// keystore password was incorrect.
|
// keystore password was incorrect.
|
||||||
const IncorrectPasswordErrMsg = "invalid checksum"
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,13 +2,10 @@ package keymanager_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/prysmaticlabs/prysm/validator/keymanager"
|
"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/imported"
|
||||||
"github.com/prysmaticlabs/prysm/validator/keymanager/remote"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ = keymanager.IKeymanager(&imported.Keymanager{})
|
_ = keymanager.IKeymanager(&imported.Keymanager{})
|
||||||
_ = keymanager.IKeymanager(&derived.Keymanager{})
|
//_ = keymanager.IKeymanager(&remote.Keymanager{})
|
||||||
_ = keymanager.IKeymanager(&remote.Keymanager{})
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ func (c *ValidatorClient) initializeFromCLI(cliCtx *cli.Context) error {
|
|||||||
if cliCtx.IsSet(flags.InteropNumValidators.Name) {
|
if cliCtx.IsSet(flags.InteropNumValidators.Name) {
|
||||||
numValidatorKeys := cliCtx.Uint64(flags.InteropNumValidators.Name)
|
numValidatorKeys := cliCtx.Uint64(flags.InteropNumValidators.Name)
|
||||||
offset := cliCtx.Uint64(flags.InteropStartIndex.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 {
|
if err != nil {
|
||||||
return errors.Wrap(err, "could not generate interop keys")
|
return errors.Wrap(err, "could not generate interop keys")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user