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 <raul@prysmaticlabs.com>
This commit is contained in:
Preston Van Loon
2021-03-30 15:38:40 -05:00
committed by GitHub
parent d7103fdef3
commit 28e4a3b7e8
7 changed files with 290 additions and 3 deletions

View File

@@ -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",
],
)

View File

@@ -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
}

View File

@@ -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
}