Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Existing Mnemonic #123

Merged
merged 31 commits into from
Nov 3, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1e9f50f
Adds language detection and verification of mnemonics
CarlBeek Aug 23, 2020
0e17266
Creates new_mnemonic cli options
CarlBeek Aug 24, 2020
0fd3c0b
Break new and existing mnemonics into separate files with generate_ke…
CarlBeek Sep 28, 2020
c7ec603
minor cleanups
CarlBeek Sep 28, 2020
d3a6f7d
Pass cli tests
CarlBeek Sep 29, 2020
1e04ab2
Linting is a thing
CarlBeek Sep 29, 2020
016d2c9
merge in dev and resolve conflicts
CarlBeek Oct 14, 2020
5d92936
Merge branch 'dev' into existing_mnemonic
CarlBeek Oct 20, 2020
4ff9f2a
Clarifies the dangers of mnemonic-passwords
CarlBeek Oct 20, 2020
3891cba
Apply suggestions from code review
CarlBeek Oct 21, 2020
52b36bc
Fix indentation error
CarlBeek Oct 26, 2020
633409c
Implement @hwwhww's code review suggestions
CarlBeek Oct 26, 2020
f4d7341
Adds tests for both new and existing mnemonics via CLI
CarlBeek Oct 28, 2020
ffcbfbc
Adds new-mnemonic and existing-mnemonic commands to README
CarlBeek Oct 30, 2020
014e34c
Merge in dev
CarlBeek Nov 2, 2020
2d0813b
Fix Makefile and Dockerfile
hwwhww Nov 3, 2020
ad5b96e
Update README
hwwhww Nov 3, 2020
fe5b539
Merge pull request #139 from ethereum/existing_mnemonic-fix_dockerfile
CarlBeek Nov 3, 2020
741fa62
Fix CLI index bounds
CarlBeek Nov 3, 2020
29f0089
Re-adds password length checks from #138 which were accidentally remo…
CarlBeek Nov 3, 2020
07dce72
remove reference to "withdrawal keys"
CarlBeek Nov 3, 2020
d3149c6
Update help messages for all CLI arguments and commands
CarlBeek Nov 3, 2020
10bc333
Apply @hwwhww's README simplifications
CarlBeek Nov 3, 2020
6f54965
Add test_regeneration.py
hwwhww Nov 3, 2020
1208820
Rename `test_deposit` test case to `test_existing_mnemonic`
hwwhww Nov 3, 2020
28d8a36
Merge branch 'dev' into existing_mnemonic
hwwhww Nov 3, 2020
8c642d6
Fix typo
hwwhww Nov 3, 2020
fb49865
Update tests/test_cli/test_regeneration.py
hwwhww Nov 3, 2020
eb37e12
Apply suggestions from code review
hwwhww Nov 3, 2020
c853942
Merge pull request #143 from ethereum/existing_mnemonic-test_regenera…
CarlBeek Nov 3, 2020
45b2387
Merge in dev and resolve conflicts
CarlBeek Nov 3, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added eth2deposit/cli/__init__.py
Empty file.
64 changes: 64 additions & 0 deletions eth2deposit/cli/existing_mnemonic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import click
from typing import (
Any,
)

from eth2deposit.key_handling.key_derivation.mnemonic import (
verify_mnemonic,
)
from eth2deposit.utils.constants import (
WORD_LISTS_PATH,
)
from .generate_keys import (
generate_keys,
generate_keys_arguments_decorator,
)


def validate_mnemonic(cts: click.Context, param: Any, mnemonic: str) -> str:
if verify_mnemonic(mnemonic, WORD_LISTS_PATH):
return mnemonic
else:
raise click.BadParameter('That is not a valid mnemonic, please check for typos')
hwwhww marked this conversation as resolved.
Show resolved Hide resolved


@click.command()
@click.pass_context
@click.option(
'--mnemonic',
callback=validate_mnemonic,
prompt='Please enter your mnemonic separated by spaces (" ")',
required=True,
type=str,
)
@click.password_option(
'--mnemonic-password',
default='',
help=('This is almost certainly not the argument you are looking for: it is for mnemonic passwords, not keystore '
'passwords. Providing a password here when you didn\'t use one initially, can result in lost keys (and '
'therefore funds)! Also note that if you used this tool to generate your mnemonic intially, then you did not '
'use a mnemonic password. However, if you are certain you used a password to "increase" the security of your '
'mnemonic, this is where you enter it. '),
prompt=False,
)
@click.option(
'--validator_start_index',
confirmation_prompt=True,
default=0,
prompt=('Enter the index (key number) you wish to start generating more keys from. '
'For example, if you\'ve generated 4 keys in the past, you\'d enter 4 here,'),
type=click.IntRange(0, 2**32),
CarlBeek marked this conversation as resolved.
Show resolved Hide resolved
)
@generate_keys_arguments_decorator
def existing_mnemonic(ctx: click.Context, mnemonic: str, mnemonic_password: str, **kwargs: Any) -> None:
if mnemonic_password != '':
click.clear()
click.confirm(
('Are you absolutely certain that you used a mnemonic password? '
'(This is different from a keystore password!) '
'Using one when you are not supposed to can result in loss of funds!'),
abort=True)
click.confirm('Did you generate this mnemonic with this CLI tool initially?', abort=True)

ctx.obj = {'mnemonic': mnemonic, 'mnemonic_password': mnemonic_password}
ctx.forward(generate_keys)
88 changes: 88 additions & 0 deletions eth2deposit/cli/generate_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import os
import click
from typing import (
Any,
Callable,
)

from eth2deposit.credentials import (
CredentialList,
)
from eth2deposit.exceptions import ValidationError
from eth2deposit.utils.validation import verify_deposit_data_json
from eth2deposit.utils.constants import (
MAX_DEPOSIT_AMOUNT,
DEFAULT_VALIDATOR_KEYS_FOLDER_NAME,
)
from eth2deposit.utils.ascii_art import RHINO_0
from eth2deposit.settings import (
ALL_CHAINS,
MAINNET,
get_setting,
)


def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[..., Any]:
'''
This is a decorator that, when applied to a parent-command, implements the
to obtain the necessary arguments for the generate_keys() subcommand.
'''
decorators = [
click.option(
'--num_validators',
prompt='Please choose how many validators you wish to run',
required=True,
type=click.IntRange(0, 2**32),
CarlBeek marked this conversation as resolved.
Show resolved Hide resolved
),
click.option(
'--folder',
type=click.Path(exists=True, file_okay=False, dir_okay=True),
default=os.getcwd()
),
click.option(
'--chain',
prompt='Please choose the (mainnet or testnet) network/chain name',
type=click.Choice(ALL_CHAINS.keys(), case_sensitive=False),
default=MAINNET,
),
click.password_option('--keystore_password', prompt='Type the password that secures your validator keystore(s)')
CarlBeek marked this conversation as resolved.
Show resolved Hide resolved
]
for decorator in reversed(decorators):
function = decorator(function)
return function


@click.command()
@click.pass_context
def generate_keys(ctx: click.Context, validator_start_index: int,
num_validators: int, folder: str, chain: str, keystore_password: str, **kwargs: Any) -> None:
mnemonic = ctx.obj['mnemonic']
mnemonic_password = ctx.obj['mnemonic_password']
amounts = [MAX_DEPOSIT_AMOUNT] * num_validators
folder = os.path.join(folder, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME)
setting = get_setting(chain)
if not os.path.exists(folder):
os.mkdir(folder)
click.clear()
click.echo(RHINO_0)
click.echo('Creating your keys.')
credentials = CredentialList.from_mnemonic(
mnemonic=mnemonic,
mnemonic_password=mnemonic_password,
num_keys=num_validators,
amounts=amounts,
fork_version=setting.GENESIS_FORK_VERSION,
start_index=validator_start_index,
)
click.echo('Saving your keystore(s).')
keystore_filefolders = credentials.export_keystores(password=keystore_password, folder=folder)
click.echo('Creating your deposit(s).')
deposits_file = credentials.export_deposit_data_json(folder=folder)
click.echo('Verifying your keystore(s).')
if not credentials.verify_keystores(keystore_filefolders=keystore_filefolders, password=keystore_password):
raise ValidationError("Failed to verify the keystores.")
click.echo('Verifying your deposit(s).')
if not verify_deposit_data_json(deposits_file):
raise ValidationError("Failed to verify the deposit data JSON files.")
click.echo('\nSuccess!\nYour keys can be found at: %s' % folder)
click.pause('\n\nPress any key.')
44 changes: 44 additions & 0 deletions eth2deposit/cli/new_mnemonic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import click
from typing import (
Any,
)

from eth2deposit.key_handling.key_derivation.mnemonic import (
get_languages,
get_mnemonic,
)
from eth2deposit.utils.constants import WORD_LISTS_PATH

from .generate_keys import (
generate_keys,
generate_keys_arguments_decorator,
)

languages = get_languages(WORD_LISTS_PATH)


@click.command()
@click.pass_context
@click.option(
'--mnemonic_language',
prompt='Please choose your mnemonic language',
type=click.Choice(languages, case_sensitive=False),
default='english',
)
@generate_keys_arguments_decorator
def new_mnemonic(ctx: click.Context, mnemonic_language: str, **kwargs: Any) -> None:
mnemonic = get_mnemonic(language=mnemonic_language, words_path=WORD_LISTS_PATH)
test_mnemonic = ''
while mnemonic != test_mnemonic:
click.clear()
click.echo('This is your seed phrase. Write it down and store it safely, it is the ONLY way to retrieve your deposit.') # noqa: E501
click.echo('\n\n%s\n\n' % mnemonic)
click.pause('Press any key when you have written down your mnemonic.')

click.clear()
test_mnemonic = click.prompt('Please type your mnemonic (separated by spaces) to confirm you have written it down\n\n') # noqa: E501
test_mnemonic = test_mnemonic.lower()
click.clear()
ctx.obj = {'mnemonic': mnemonic, 'mnemonic_password': ''}
hwwhww marked this conversation as resolved.
Show resolved Hide resolved
ctx.params['validator_start_index'] = 0
ctx.forward(generate_keys)
14 changes: 9 additions & 5 deletions eth2deposit/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class Credential:
A Credential object contains all of the information for a single validator and the corresponding functionality.
Once created, it is the only object that should be required to perform any processing for a validator.
"""
def __init__(self, *, mnemonic: str, index: int, amount: int, fork_version: bytes):
def __init__(self, *, mnemonic: str, mnemonic_password: str, index: int, amount: int, fork_version: bytes):
# Set path as EIP-2334 format
# https://eips.ethereum.org/EIPS/eip-2334
purpose = '12381'
Expand All @@ -41,8 +41,10 @@ def __init__(self, *, mnemonic: str, index: int, amount: int, fork_version: byte
self.signing_key_path = f'{withdrawal_key_path}/0'

# Do NOT use password for seed generation.
CarlBeek marked this conversation as resolved.
Show resolved Hide resolved
self.withdrawal_sk = mnemonic_and_path_to_key(mnemonic=mnemonic, path=withdrawal_key_path, password='')
self.signing_sk = mnemonic_and_path_to_key(mnemonic=mnemonic, path=self.signing_key_path, password='')
self.withdrawal_sk = mnemonic_and_path_to_key(
mnemonic=mnemonic, path=withdrawal_key_path, password=mnemonic_password)
self.signing_sk = mnemonic_and_path_to_key(
mnemonic=mnemonic, path=self.signing_key_path, password=mnemonic_password)
self.amount = amount
self.fork_version = fork_version

Expand Down Expand Up @@ -121,16 +123,18 @@ def __init__(self, credentials: List[Credential]):
def from_mnemonic(cls,
*,
mnemonic: str,
mnemonic_password: str,
num_keys: int,
amounts: List[int],
fork_version: bytes,
start_index: int=0) -> 'CredentialList':
start_index: int) -> 'CredentialList':
if len(amounts) != num_keys:
raise ValueError(
f"The number of keys ({num_keys}) doesn't equal to the corresponding deposit amounts ({len(amounts)})."
)
key_indices = range(start_index, start_index + num_keys)
return cls([Credential(mnemonic=mnemonic, index=index, amount=amounts[index], fork_version=fork_version)
return cls([Credential(mnemonic=mnemonic, mnemonic_password=mnemonic_password,
index=index, amount=amounts[index - start_index], fork_version=fork_version)
for index in key_indices])

def export_keystores(self, password: str, folder: str) -> List[str]:
Expand Down
104 changes: 9 additions & 95 deletions eth2deposit/deposit.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,8 @@
import os
import sys
import click

from eth2deposit.credentials import (
CredentialList,
)
from eth2deposit.exceptions import ValidationError
from eth2deposit.key_handling.key_derivation.mnemonic import (
get_languages,
get_mnemonic,
)
from eth2deposit.utils.validation import verify_deposit_data_json
from eth2deposit.utils.constants import (
WORD_LISTS_PATH,
MAX_DEPOSIT_AMOUNT,
DEFAULT_VALIDATOR_KEYS_FOLDER_NAME,
)
from eth2deposit.utils.ascii_art import RHINO_0
from eth2deposit.settings import (
ALL_CHAINS,
MAINNET,
get_setting,
)

languages = get_languages(WORD_LISTS_PATH)


def generate_mnemonic(language: str, words_path: str) -> str:
mnemonic = get_mnemonic(language=language, words_path=words_path)
test_mnemonic = ''
while mnemonic != test_mnemonic:
click.clear()
click.echo('This is your seed phrase. Write it down and store it safely, it is the ONLY way to retrieve your deposit.') # noqa: E501
click.echo('\n\n%s\n\n' % mnemonic)
click.pause('Press any key when you have written down your mnemonic.')

click.clear()
test_mnemonic = click.prompt('Please type your mnemonic (separated by spaces) to confirm you have written it down\n\n') # noqa: E501
test_mnemonic = test_mnemonic.lower()
click.clear()
return mnemonic
from eth2deposit.cli.existing_mnemonic import existing_mnemonic
from eth2deposit.cli.new_mnemonic import new_mnemonic


def check_python_version() -> None:
Expand All @@ -51,64 +14,15 @@ def check_python_version() -> None:
sys.exit()


@click.command()
@click.option(
'--num_validators',
prompt='Please choose how many validators you wish to run',
required=True,
type=int,
)
@click.option(
'--mnemonic_language',
prompt='Please choose your mnemonic language',
type=click.Choice(languages, case_sensitive=False),
default='english',
)
@click.option(
'--folder',
type=click.Path(exists=True, file_okay=False, dir_okay=True),
default=os.getcwd()
)
@click.option(
'--chain',
prompt='Please choose the (mainnet or testnet) network/chain name',
type=click.Choice(ALL_CHAINS.keys(), case_sensitive=False),
default=MAINNET,
)
@click.password_option(prompt='Type the password that secures your validator keystore(s)')
def main(num_validators: int, mnemonic_language: str, folder: str, chain: str, password: str) -> None:
check_python_version()
mnemonic = generate_mnemonic(mnemonic_language, WORD_LISTS_PATH)
amounts = [MAX_DEPOSIT_AMOUNT] * num_validators
folder = os.path.join(folder, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME)
setting = get_setting(chain)
if not os.path.exists(folder):
os.mkdir(folder)
click.clear()
click.echo(RHINO_0)
click.echo('Creating your keys.')
credentials = CredentialList.from_mnemonic(
mnemonic=mnemonic,
num_keys=num_validators,
amounts=amounts,
fork_version=setting.GENESIS_FORK_VERSION,
)
click.echo('Saving your keystore(s).')
keystore_filefolders = credentials.export_keystores(password=password, folder=folder)
click.echo('Creating your deposit(s).')
deposits_file = credentials.export_deposit_data_json(folder=folder)

click.echo('Verifying your keystore(s).')
if not credentials.verify_keystores(keystore_filefolders=keystore_filefolders, password=password):
raise ValidationError("Failed to verify the keystores.")
@click.group()
def cli() -> None:
pass

click.echo('Verifying your deposit(s).')
if not verify_deposit_data_json(deposits_file):
raise ValidationError("Failed to verify the deposit data JSON files.")

click.echo('\nSuccess!\nYour keys can be found at: %s' % folder)
click.pause('\n\nPress any key.')
cli.add_command(existing_mnemonic)
cli.add_command(new_mnemonic)


if __name__ == '__main__':
main()
check_python_version()
cli()
Loading