From 793a8d6d7939e4f24d839d368b36cacc1ecfa926 Mon Sep 17 00:00:00 2001 From: Jim McDonald Date: Sun, 26 Feb 2023 22:43:45 +0000 Subject: [PATCH] Allow keystore as source of validator. --- CHANGELOG.md | 3 + util/account.go | 244 +++++++++++++++++++++++++++++++++---------- util/account_test.go | 13 +++ util/validators.go | 40 ++----- 4 files changed, 214 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c111484..c19ead8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/util/account.go b/util/account.go index 7d49d95..29cea23 100644 --- a/util/account.go +++ b/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 { diff --git a/util/account_test.go b/util/account_test.go index bb3c721..d74e2b5 100644 --- a/util/account_test.go +++ b/util/account_test.go @@ -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 { diff --git a/util/validators.go b/util/validators.go index 2607e6e..1f153ba 100644 --- a/util/validators.go +++ b/util/validators.go @@ -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 }