mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-08 23:18:15 -05:00
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:
36
tools/interop/split-keys/BUILD.bazel
Normal file
36
tools/interop/split-keys/BUILD.bazel
Normal 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",
|
||||
],
|
||||
)
|
||||
143
tools/interop/split-keys/main.go
Normal file
143
tools/interop/split-keys/main.go
Normal 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
|
||||
}
|
||||
101
tools/interop/split-keys/main_test.go
Normal file
101
tools/interop/split-keys/main_test.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user