From 28e4a3b7e818bfa61ad7e4a8a8169d23d1dfed49 Mon Sep 17 00:00:00 2001 From: Preston Van Loon Date: Tue, 30 Mar 2021 15:38:40 -0500 Subject: [PATCH] Add a Tool to Split a Series of Mnemonic Keys into Distinct Wallets (#8651) * Add a tool to split a series of mnemonic keys into distinct wallets * split func * keysplit tool * gaz/viz Co-authored-by: Raul Jordan --- tools/interop/split-keys/BUILD.bazel | 36 ++++++ tools/interop/split-keys/main.go | 143 ++++++++++++++++++++++ tools/interop/split-keys/main_test.go | 101 +++++++++++++++ validator/accounts/wallet/BUILD.bazel | 5 +- validator/keymanager/BUILD.bazel | 2 +- validator/keymanager/derived/BUILD.bazel | 5 +- validator/keymanager/imported/BUILD.bazel | 1 + 7 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 tools/interop/split-keys/BUILD.bazel create mode 100644 tools/interop/split-keys/main.go create mode 100644 tools/interop/split-keys/main_test.go diff --git a/tools/interop/split-keys/BUILD.bazel b/tools/interop/split-keys/BUILD.bazel new file mode 100644 index 0000000000..bb70f6031e --- /dev/null +++ b/tools/interop/split-keys/BUILD.bazel @@ -0,0 +1,36 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_test") +load("@prysm//tools/go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["main.go"], + importpath = "github.com/prysmaticlabs/prysm/tools/interop/split-keys", + visibility = ["//visibility:private"], + deps = [ + "//shared/fileutil:go_default_library", + "//validator/accounts/wallet:go_default_library", + "//validator/keymanager:go_default_library", + "//validator/keymanager/derived:go_default_library", + "//validator/keymanager/imported:go_default_library", + "@com_github_tyler_smith_go_bip39//:go_default_library", + "@com_github_wealdtech_go_eth2_util//:go_default_library", + ], +) + +go_binary( + name = "split-keys", + embed = [":go_default_library"], + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = ["main_test.go"], + embed = [":go_default_library"], + deps = [ + "//shared/testutil/require:go_default_library", + "//validator/accounts/wallet:go_default_library", + "//validator/keymanager:go_default_library", + "//validator/keymanager/imported:go_default_library", + ], +) diff --git a/tools/interop/split-keys/main.go b/tools/interop/split-keys/main.go new file mode 100644 index 0000000000..78766956ac --- /dev/null +++ b/tools/interop/split-keys/main.go @@ -0,0 +1,143 @@ +// Package main provides a tool named split-keys which allows for generating any number of eth2 validator keys +// from a list of BIP39 mnemonics and spreading them across any number of Prysm wallets. This is useful for creating +// custom allocations of keys across containers running in a cloud environment, such as for public testnets. +// An example of why you would use this tool is as follows. Let's say we have 1 mnemonic contained inside of a file. +// Then, we want to generate 10 keys from the mnemonic, and we want to spread them across 5 different wallets, each +// containing two keys. Then, you would run the tool as follows: +// +// ./main -mnemonics-file=/path/to/file.txt -keys-per-mnemonic=10 -num-wallets=5 +// +// You can also specify the output directory for the wallet files using -out-dir and also the password +// used to encrypt the wallets in a text file using -wallet-password-file. +package main + +import ( + "bufio" + "context" + "flag" + "fmt" + "log" + "os" + "path" + + "github.com/prysmaticlabs/prysm/shared/fileutil" + "github.com/prysmaticlabs/prysm/validator/accounts/wallet" + "github.com/prysmaticlabs/prysm/validator/keymanager" + "github.com/prysmaticlabs/prysm/validator/keymanager/derived" + "github.com/prysmaticlabs/prysm/validator/keymanager/imported" + "github.com/tyler-smith/go-bip39" + util "github.com/wealdtech/go-eth2-util" +) + +var ( + mnemonicsFileFlag = flag.String("mnemonics-file", "", "File containing mnemonics, one mnemonic per line") + keysPerMnemonicFlag = flag.Int("keys-per-mnemonic", 0, "The number of keys per mnemonic to generate") + numberOfWalletsFlag = flag.Int("num-wallets", 0, "Number of wallets to generate") + walletOutDirFlag = flag.String("out-dir", "", "Output directory for wallet files") + walletPasswordFileFlag = flag.String("wallet-password-file", "", "File containing the password to encrypt all generated wallets") +) + +// This application is run to generate keystores for testnets. +func main() { + flag.Parse() + f, err := os.Open(*mnemonicsFileFlag) + if err != nil { + log.Fatal(err) + } + defer func() { + if err = f.Close(); err != nil { + log.Fatal(err) + } + }() + + pubKeys, privKeys, err := generateKeysFromMnemonicList(bufio.NewScanner(f), *keysPerMnemonicFlag) + if err != nil { + log.Fatal(err) + } + + log.Printf("Splitting %d keys across %d wallets\n", len(privKeys), *numberOfWalletsFlag) + wPass, err := fileutil.ReadFileAsBytes(*walletPasswordFileFlag) + if err != nil { + log.Fatal(err) + } + + keysPerWallet := len(privKeys) / *numberOfWalletsFlag + if err := spreadKeysAcrossImportedWallets( + pubKeys, + privKeys, + *numberOfWalletsFlag, + keysPerWallet, + *walletOutDirFlag, + string(wPass), + ); err != nil { + log.Fatal(err) + } + log.Println("Done") +} + +// Uses the provided mnemonic seed phrase to generate the +// appropriate seed file for recovering a derived wallets. +func seedFromMnemonic(mnemonic, mnemonicPassphrase string) ([]byte, error) { + if ok := bip39.IsMnemonicValid(mnemonic); !ok { + return nil, bip39.ErrInvalidMnemonic + } + return bip39.NewSeed(mnemonic, mnemonicPassphrase), nil +} + +func generateKeysFromMnemonicList(mnemonicListFile *bufio.Scanner, keysPerMnemonic int) (pubKeys, privKeys [][]byte, err error) { + pubKeys = make([][]byte, 0) + privKeys = make([][]byte, 0) + var seed []byte + for mnemonicListFile.Scan() { + log.Printf("Generating %d keys from mnemonic\n", keysPerMnemonic) + mnemonic := mnemonicListFile.Text() + seed, err = seedFromMnemonic(mnemonic, "" /* 25th word*/) + if err != nil { + return + } + for i := 0; i < keysPerMnemonic; i++ { + if i%250 == 0 && i > 0 { + log.Printf("%d/%d keys generated\n", i, keysPerMnemonic) + } + privKey, seedErr := util.PrivateKeyFromSeedAndPath( + seed, fmt.Sprintf(derived.ValidatingKeyDerivationPathTemplate, i), + ) + if seedErr != nil { + err = seedErr + return + } + privKeys = append(privKeys, privKey.Marshal()) + pubKeys = append(pubKeys, privKey.PublicKey().Marshal()) + } + } + return +} + +func spreadKeysAcrossImportedWallets( + pubKeys, + privKeys [][]byte, + numWallets, + keysPerWallet int, + walletOutputDir string, + walletPassword string, +) error { + ctx := context.Background() + for i := 0; i < numWallets; i++ { + w := wallet.New(&wallet.Config{ + WalletDir: path.Join(walletOutputDir, fmt.Sprintf("wallet_%d", i)), + KeymanagerKind: keymanager.Imported, + WalletPassword: walletPassword, + }) + km, err := imported.NewKeymanager(ctx, &imported.SetupConfig{ + Wallet: w, + }) + if err != nil { + return err + } + log.Printf("Importing %d keys into wallet %d\n", keysPerWallet, i) + if err := km.ImportKeypairs(ctx, privKeys[i*keysPerWallet:(i+1)*keysPerWallet], pubKeys[i*keysPerWallet:(i+1)*keysPerWallet]); err != nil { + return err + } + } + return nil +} diff --git a/tools/interop/split-keys/main_test.go b/tools/interop/split-keys/main_test.go new file mode 100644 index 0000000000..e794e0d5f5 --- /dev/null +++ b/tools/interop/split-keys/main_test.go @@ -0,0 +1,101 @@ +package main + +import ( + "bufio" + "context" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/prysmaticlabs/prysm/shared/testutil/require" + "github.com/prysmaticlabs/prysm/validator/accounts/wallet" + "github.com/prysmaticlabs/prysm/validator/keymanager" + "github.com/prysmaticlabs/prysm/validator/keymanager/imported" +) + +const testMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + +var ( + testPrivKeys = [][]byte{ + hexDecodeOrDie("3ec45abb2792f1f287ab1434acfde9d7aac879eb74c45cf7b59d25f15ba7a650"), + hexDecodeOrDie("3b6e255c01a33ccce39927196c7f96ee512e29b9aefcfe98132c2df2e2f04043"), + hexDecodeOrDie("39f52a9ac0a2eb05b9633ff2e125bdb1313776f40418bb7b2d82b22ab4ca534a"), + hexDecodeOrDie("04ef1acec58a2d7ee1c0f12d4083df2990f83a798a84ad4393b2ce6322d377d5"), + hexDecodeOrDie("66e05a4dea6ee3292d35f281a542c4931070b064cbe0f4436461db29208426b7"), + } + testPubKeys = [][]byte{ + hexDecodeOrDie("b3e445d43871965d890a398f719348a1405ac72e35b92727cc570026f54471af7ea7b2040622a8fd0b5bfb2a209b5911"), + hexDecodeOrDie("aeb399bf5648b0e9980c1731824c269631a41320c3d7f730c40587e1a37a5e1c8b5755fd90080a7b3fb90d3fd419c0a7"), + hexDecodeOrDie("92f46b0dcc7db24f4946b5773b5525efa0bbb0810088588323d9de84f0e42f22df96cbb97065b49a2006c653ec8060f4"), + hexDecodeOrDie("9948ea3862b8889636c3caeaa1b9877a12cffca9bf6a1ef2264fa7e69604d55c56b4f519062e6785d21d3c9593c2adcd"), + hexDecodeOrDie("849b4bcd8670f81909baad27c4d9c8d9b956b19192f12af8fe57d30731fa11a55a97a1ab72bb1cfd76c1461dcaba714a"), + } +) + +func Test_generateKeysFromMnemonicList(t *testing.T) { + rdr := strings.NewReader(testMnemonic) + scanner := bufio.NewScanner(rdr) + keysPerMnemonic := 5 + pubKeys, privKeys, err := generateKeysFromMnemonicList(scanner, keysPerMnemonic) + require.NoError(t, err) + require.Equal(t, keysPerMnemonic, len(pubKeys)) + require.Equal(t, keysPerMnemonic, len(privKeys)) + + // Text the generated keys match some predetermined ones for the test. + for i, key := range privKeys { + require.DeepEqual(t, testPrivKeys[i], key) + } + for i, key := range pubKeys { + require.DeepEqual(t, testPubKeys[i], key) + } +} + +func Test_spreadKeysAcrossImportedWallets(t *testing.T) { + walletPassword := "Sr0ngPass0q0z929301" + tmpDir := filepath.Join(os.TempDir(), "testwallets") + t.Cleanup(func() { + require.NoError(t, os.RemoveAll(tmpDir)) + }) + + // Spread 5 keys across 5 wallets, meaning there is 1 + // key per wallet stored on disk. + numWallets := 5 + keysPerWallet := 1 + err := spreadKeysAcrossImportedWallets( + testPubKeys, + testPrivKeys, + numWallets, + keysPerWallet, + tmpDir, + walletPassword, + ) + require.NoError(t, err) + ctx := context.Background() + for i := 0; i < numWallets; i++ { + w, err := wallet.OpenWallet(ctx, &wallet.Config{ + WalletDir: filepath.Join(tmpDir, fmt.Sprintf("wallet_%d", i)), + KeymanagerKind: keymanager.Imported, + WalletPassword: walletPassword, + }) + require.NoError(t, err) + km, err := imported.NewKeymanager(ctx, &imported.SetupConfig{ + Wallet: w, + }) + require.NoError(t, err) + pubKeys, err := km.FetchValidatingPublicKeys(ctx) + require.NoError(t, err) + require.Equal(t, 1, len(pubKeys)) + require.DeepEqual(t, testPubKeys[i], pubKeys[0][:]) + } +} + +func hexDecodeOrDie(str string) []byte { + decoded, err := hex.DecodeString(str) + if err != nil { + panic(err) + } + return decoded +} diff --git a/validator/accounts/wallet/BUILD.bazel b/validator/accounts/wallet/BUILD.bazel index 8ee110a9cd..23845754a0 100644 --- a/validator/accounts/wallet/BUILD.bazel +++ b/validator/accounts/wallet/BUILD.bazel @@ -8,7 +8,10 @@ go_library( "wallet.go", ], importpath = "github.com/prysmaticlabs/prysm/validator/accounts/wallet", - visibility = ["//validator:__subpackages__"], + visibility = [ + "//tools:__subpackages__", + "//validator:__subpackages__", + ], deps = [ "//cmd/validator/flags:go_default_library", "//shared/fileutil:go_default_library", diff --git a/validator/keymanager/BUILD.bazel b/validator/keymanager/BUILD.bazel index bc7b169c3e..6ae27f5b60 100644 --- a/validator/keymanager/BUILD.bazel +++ b/validator/keymanager/BUILD.bazel @@ -9,7 +9,7 @@ go_library( ], importpath = "github.com/prysmaticlabs/prysm/validator/keymanager", visibility = [ - "//tools/keystores:__pkg__", + "//tools:__subpackages__", "//validator:__pkg__", "//validator:__subpackages__", ], diff --git a/validator/keymanager/derived/BUILD.bazel b/validator/keymanager/derived/BUILD.bazel index ad02f15610..91132cdab6 100644 --- a/validator/keymanager/derived/BUILD.bazel +++ b/validator/keymanager/derived/BUILD.bazel @@ -9,7 +9,10 @@ go_library( "mnemonic.go", ], importpath = "github.com/prysmaticlabs/prysm/validator/keymanager/derived", - visibility = ["//validator:__subpackages__"], + visibility = [ + "//tools:__subpackages__", + "//validator:__subpackages__", + ], deps = [ "//proto/validator/accounts/v2:go_default_library", "//shared/bls:go_default_library", diff --git a/validator/keymanager/imported/BUILD.bazel b/validator/keymanager/imported/BUILD.bazel index 10e85c0a97..2e6451fa93 100644 --- a/validator/keymanager/imported/BUILD.bazel +++ b/validator/keymanager/imported/BUILD.bazel @@ -13,6 +13,7 @@ go_library( ], importpath = "github.com/prysmaticlabs/prysm/validator/keymanager/imported", visibility = [ + "//tools:__subpackages__", "//validator:__pkg__", "//validator:__subpackages__", ],