mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-31 08:08:18 -05:00
Compare commits
2 Commits
e2e-debugg
...
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 (
|
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
|
||||||
|
}
|
||||||
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
|
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"
|
||||||
@@ -120,13 +125,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()
|
||||||
@@ -158,102 +156,6 @@ func (km *Keymanager) initializeKeysCachesFromKeystore() error {
|
|||||||
return nil
|
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 {
|
func (km *Keymanager) initializeAccountKeystore(ctx context.Context) error {
|
||||||
encoded, err := km.wallet.ReadFileAtPath(ctx, AccountsPath, AccountsKeystoreFileName)
|
encoded, err := km.wallet.ReadFileAtPath(ctx, AccountsPath, AccountsKeystoreFileName)
|
||||||
if err != nil && strings.Contains(err.Error(), "no files found") {
|
if err != nil && strings.Contains(err.Error(), "no files found") {
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"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/testing/assert"
|
|
||||||
"github.com/prysmaticlabs/prysm/testing/require"
|
"github.com/prysmaticlabs/prysm/testing/require"
|
||||||
mock "github.com/prysmaticlabs/prysm/validator/accounts/testing"
|
mock "github.com/prysmaticlabs/prysm/validator/accounts/testing"
|
||||||
"github.com/prysmaticlabs/prysm/validator/keymanager"
|
"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, fmt.Sprintf("%#x", bytesutil.Trunc(accountPubKey[:])))
|
||||||
require.LogsContain(t, hook, "Successfully deleted validator account")
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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/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)
|
||||||
|
}
|
||||||
@@ -9,13 +9,38 @@ import (
|
|||||||
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.
|
// 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 {
|
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)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user