mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-08 21:48:05 -05:00
Allow keystore as source of validator.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
244
util/account.go
244
util/account.go
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user