Allow keystore as source of validator.

This commit is contained in:
Jim McDonald
2023-02-26 22:43:45 +00:00
parent e15b22dc3c
commit 793a8d6d79
4 changed files with 214 additions and 86 deletions

View File

@@ -1,3 +1,6 @@
dev:
- allow validator exit to use a keystore as its validator parameter
1.28.2:
- fix bix stopping validator exit creation by direct validator specification

View File

@@ -1,4 +1,3 @@
// Copyright © 2020, 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -16,11 +15,17 @@ package util
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/pkg/errors"
"github.com/wealdtech/go-ecodec"
util "github.com/wealdtech/go-eth2-util"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
@@ -33,81 +38,206 @@ func ParseAccount(ctx context.Context,
e2wtypes.Account,
error,
) {
if accountStr == "" {
switch {
case accountStr == "":
return nil, errors.New("no account specified")
case strings.HasPrefix(accountStr, "0x"):
// A key.
return parseAccountFromKey(ctx, accountStr, unlock)
case strings.HasPrefix(accountStr, "{"):
// This could be a keystore.
return parseAccountFromKeystore(ctx, accountStr, supplementary, unlock)
case strings.Contains(accountStr, "/"):
// An account specifier.
account, err := parseAccountFromSpecifier(ctx, accountStr, supplementary, unlock)
if err != nil {
// It is possible that this is actually a path to a keystore, so try that instead.
if _, err = os.Stat(accountStr); err == nil {
account, err = parseAccountFromKeystorePath(ctx, accountStr, supplementary, unlock)
}
}
if err != nil {
return nil, err
}
return account, nil
case strings.Contains(accountStr, " "):
// A mnemonic.
return parseAccountFromMnemonic(ctx, accountStr, supplementary, unlock)
default:
// This could be the path to a keystore.
return nil, fmt.Errorf("unknown account specifier %s", accountStr)
}
}
func parseAccountFromKey(ctx context.Context,
accountStr string,
unlock bool,
) (
e2wtypes.Account,
error,
) {
var account e2wtypes.Account
var err error
switch {
case strings.HasPrefix(accountStr, "0x"):
// A key. Could be public or private.
data, err := hex.DecodeString(strings.TrimPrefix(accountStr, "0x"))
// A key. Could be public or private.
data, err := hex.DecodeString(strings.TrimPrefix(accountStr, "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to parse account key")
}
switch len(data) {
case 48:
// Public key.
account, err = newScratchAccountFromPubKey(data)
if err != nil {
return nil, errors.Wrap(err, "failed to parse account key")
}
switch len(data) {
case 48:
// Public key.
account, err = newScratchAccountFromPubKey(data)
if err != nil {
return nil, errors.Wrap(err, "failed to create account from public key")
}
if unlock {
return nil, errors.New("cannot unlock an account specified by its public key")
}
case 32:
// Private key.
account, err = newScratchAccountFromPrivKey(data)
if err != nil {
return nil, errors.Wrap(err, "failed to create account from private key")
}
if unlock {
_, err = UnlockAccount(ctx, account, nil)
if err != nil {
return nil, err
}
}
default:
return nil, fmt.Errorf("key of length %d neither public nor private key", len(data))
}
case strings.Contains(accountStr, "/"):
// An account.
_, account, err = WalletAndAccountFromPath(ctx, accountStr)
if err != nil {
return nil, errors.Wrap(err, "unable to obtain account")
return nil, errors.Wrap(err, "failed to create account from public key")
}
if unlock {
// Supplementary will be the unlock passphrase(s).
_, err = UnlockAccount(ctx, account, supplementary)
return nil, errors.New("cannot unlock an account specified by its public key")
}
case 32:
// Private key.
account, err = newScratchAccountFromPrivKey(data)
if err != nil {
return nil, errors.Wrap(err, "failed to create account from private key")
}
if unlock {
_, err = UnlockAccount(ctx, account, nil)
if err != nil {
return nil, err
}
}
case strings.Contains(accountStr, " "):
// A mnemonic.
// Supplementary will be the path.
if len(supplementary) == 0 {
return nil, errors.New("missing derivation path")
}
account, err = accountFromMnemonicAndPath(accountStr, supplementary[0])
if err != nil {
return nil, err
}
if unlock {
err = account.(e2wtypes.AccountLocker).Unlock(ctx, nil)
if err != nil {
return nil, errors.Wrap(err, "failed to unlock account")
}
}
default:
return nil, fmt.Errorf("unknown account specifier %s", accountStr)
return nil, fmt.Errorf("key of length %d neither public nor private key", len(data))
}
return account, nil
}
func parseAccountFromSpecifier(ctx context.Context,
accountStr string,
supplementary []string,
unlock bool,
) (
e2wtypes.Account,
error,
) {
var account e2wtypes.Account
var err error
_, account, err = WalletAndAccountFromPath(ctx, accountStr)
if err != nil {
return nil, errors.Wrap(err, "unable to obtain account")
}
if unlock {
// Supplementary will be the unlock passphrase(s).
_, err = UnlockAccount(ctx, account, supplementary)
if err != nil {
return nil, err
}
}
return account, nil
}
func parseAccountFromMnemonic(ctx context.Context,
accountStr string,
supplementary []string,
unlock bool,
) (
e2wtypes.Account,
error,
) {
var account e2wtypes.Account
var err error
if len(supplementary) == 0 {
return nil, errors.New("missing derivation path")
}
account, err = accountFromMnemonicAndPath(accountStr, supplementary[0])
if err != nil {
return nil, err
}
if unlock {
err = account.(e2wtypes.AccountLocker).Unlock(ctx, nil)
if err != nil {
return nil, errors.Wrap(err, "failed to unlock account")
}
}
return account, nil
}
func parseAccountFromKeystore(ctx context.Context,
accountStr string,
supplementary []string,
unlock bool,
) (
e2wtypes.Account,
error,
) {
var account e2wtypes.Account
var err error
// Need to import the keystore in to a temporary wallet to fetch the private key.
store := scratch.New()
encryptor := keystorev4.New()
// Need to add a couple of fields to the keystore to make it compliant.
var keystore map[string]any
if err := json.Unmarshal([]byte(accountStr), &keystore); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal keystore")
}
keystore["name"] = "Import"
keystore["encryptor"] = "keystore"
keystoreData, err := json.Marshal(keystore)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal keystore")
}
walletData := fmt.Sprintf(`{"wallet":{"name":"Import","type":"non-deterministic","uuid":"e1526407-1dc7-4f3f-9d05-ab696f40707c","version":1},"accounts":[%s]}`, keystoreData)
encryptedData, err := ecodec.Encrypt([]byte(walletData), []byte(`password`))
if err != nil {
return nil, err
}
wallet, err := nd.Import(ctx, encryptedData, []byte(`password`), store, encryptor)
if err != nil {
return nil, errors.Wrap(err, "failed to import account")
}
account = <-wallet.Accounts(ctx)
if unlock {
if locker, isLocker := account.(e2wtypes.AccountLocker); isLocker {
unlocked := false
for _, passphrase := range supplementary {
if err = locker.Unlock(ctx, []byte(passphrase)); err == nil {
unlocked = true
break
}
}
if !unlocked {
return nil, errors.New("failed to unlock account")
}
}
}
return account, nil
}
func parseAccountFromKeystorePath(ctx context.Context,
accountStr string,
supplementary []string,
unlock bool,
) (
e2wtypes.Account,
error,
) {
data, err := os.ReadFile(accountStr)
if err != nil {
return nil, errors.Wrap(err, "failed to read keystore file")
}
return parseAccountFromKeystore(ctx, string(data), supplementary, unlock)
}
func accountFromMnemonicAndPath(mnemonic string, path string) (e2wtypes.Account, error) {
seed, err := SeedFromMnemonic(mnemonic)
if err != nil {

View File

@@ -83,6 +83,19 @@ func TestParseAccount(t *testing.T) {
unlock: true,
expectedUnlocked: true,
},
{
name: "Keystore",
accountStr: `{"crypto": {"kdf": {"function": "scrypt", "params": {"dklen": 32, "n": 262144, "r": 8, "p": 1, "salt": "d27e392342918fa1912dadb171d90683c81146ba7ad36c0c22936d7fe3528300"}, "message": ""}, "checksum": {"function": "sha256", "params": {}, "message": "6f60216a8eda37426d3103f9fa608fe474944c4e287e09f416aad6bfe3983283"}, "cipher": {"function": "aes-128-ctr", "params": {"iv": "8b542e5a71fbde321407ba3d1ae098f6"}, "message": "a6bb744433adf9b7474b3793a09b71b451be1d595d031dba39adaaf6b9d6a67a"}}, "description": "", "pubkey": "91a4e10c877569f930e8800b745d4cb8fd03fd52dc17e87b49a55b548813275145e77ae01d56423becb5572f2632be5a", "path": "m/12381/3600/0/0/0", "uuid": "7858f402-cb53-4898-9193-b38bbf8fec12", "version": 4}`,
expectedPubkey: "0x91a4e10c877569f930e8800b745d4cb8fd03fd52dc17e87b49a55b548813275145e77ae01d56423becb5572f2632be5a",
},
{
name: "KeystoreUnlocked",
accountStr: `{"crypto": {"kdf": {"function": "scrypt", "params": {"dklen": 32, "n": 262144, "r": 8, "p": 1, "salt": "d27e392342918fa1912dadb171d90683c81146ba7ad36c0c22936d7fe3528300"}, "message": ""}, "checksum": {"function": "sha256", "params": {}, "message": "6f60216a8eda37426d3103f9fa608fe474944c4e287e09f416aad6bfe3983283"}, "cipher": {"function": "aes-128-ctr", "params": {"iv": "8b542e5a71fbde321407ba3d1ae098f6"}, "message": "a6bb744433adf9b7474b3793a09b71b451be1d595d031dba39adaaf6b9d6a67a"}}, "description": "", "pubkey": "91a4e10c877569f930e8800b745d4cb8fd03fd52dc17e87b49a55b548813275145e77ae01d56423becb5572f2632be5a", "path": "m/12381/3600/0/0/0", "uuid": "7858f402-cb53-4898-9193-b38bbf8fec12", "version": 4}`,
supplementary: []string{"testtest"},
expectedPubkey: "0x91a4e10c877569f930e8800b745d4cb8fd03fd52dc17e87b49a55b548813275145e77ae01d56423becb5572f2632be5a",
unlock: true,
expectedUnlocked: true,
},
}
for _, test := range tests {

View File

@@ -15,7 +15,6 @@ package util
import (
"context"
"encoding/hex"
"fmt"
"strconv"
"strings"
@@ -77,28 +76,20 @@ func ParseValidator(ctx context.Context,
) {
var validators map[phase0.ValidatorIndex]*apiv1.Validator
switch {
case strings.HasPrefix(validatorStr, "0x"):
// A public key.
data, err := hex.DecodeString(strings.TrimPrefix(validatorStr, "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to parse validator public key")
}
pubKey := phase0.BLSPubKey{}
copy(pubKey[:], data)
validators, err = validatorsProvider.ValidatorsByPubKey(ctx,
stateID,
[]phase0.BLSPubKey{pubKey},
)
// Could be a simple index.
index, err := strconv.ParseUint(validatorStr, 10, 64)
if err == nil {
validators, err = validatorsProvider.Validators(ctx, stateID, []phase0.ValidatorIndex{phase0.ValidatorIndex(index)})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validator information")
}
case strings.Contains(validatorStr, "/"):
// An account.
_, account, err := WalletAndAccountFromPath(ctx, validatorStr)
} else {
// Some sort of specifier.
account, err := ParseAccount(ctx, validatorStr, nil, false)
if err != nil {
return nil, errors.Wrap(err, "unable to obtain account")
return nil, err
}
accPubKey, err := BestPublicKey(account)
if err != nil {
return nil, errors.Wrap(err, "unable to obtain public key for account")
@@ -112,18 +103,9 @@ func ParseValidator(ctx context.Context,
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validator information")
}
default:
// An index.
index, err := strconv.ParseUint(validatorStr, 10, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse validator index")
}
validators, err = validatorsProvider.Validators(ctx, stateID, []phase0.ValidatorIndex{phase0.ValidatorIndex(index)})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validator information")
}
}
// Validator is first entry in the map.
// Validator is first and only entry in the map.
for _, validator := range validators {
return validator, nil
}