From fb5ffe64e6cb00a68ad5bc9c8f813b6bdf72e0a7 Mon Sep 17 00:00:00 2001 From: Thorsten Behrens Date: Sat, 5 Feb 2022 14:21:26 +0000 Subject: [PATCH 1/7] Allow 4-character abbreviations of mnemonic words --- staking_deposit/cli/existing_mnemonic.py | 3 +- .../key_handling/key_derivation/mnemonic.py | 28 +++++++++++-------- .../test_key_derivation/test_mnemonic.py | 8 +++--- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/staking_deposit/cli/existing_mnemonic.py b/staking_deposit/cli/existing_mnemonic.py index 98e8bcd4..6ded63ae 100644 --- a/staking_deposit/cli/existing_mnemonic.py +++ b/staking_deposit/cli/existing_mnemonic.py @@ -23,7 +23,8 @@ def validate_mnemonic(ctx: click.Context, param: Any, mnemonic: str) -> str: - if verify_mnemonic(mnemonic, WORD_LISTS_PATH): + mnemonic = verify_mnemonic(mnemonic, WORD_LISTS_PATH) + if mnemonic is not None: return mnemonic else: raise ValidationError(load_text(['err_invalid_mnemonic'])) diff --git a/staking_deposit/key_handling/key_derivation/mnemonic.py b/staking_deposit/key_handling/key_derivation/mnemonic.py index c93c37f4..3c81a701 100644 --- a/staking_deposit/key_handling/key_derivation/mnemonic.py +++ b/staking_deposit/key_handling/key_derivation/mnemonic.py @@ -68,9 +68,10 @@ def determine_mnemonic_language(mnemonic: str, words_path: str) -> Sequence[str] languages = MNEMONIC_LANG_OPTIONS.keys() word_language_map = {word: lang for lang in languages for word in _get_word_list(lang, words_path)} try: - mnemonic_list = mnemonic.split(' ') - word_languages = [word_language_map[word] for word in mnemonic_list] - return list(set(word_languages)) + mnemonic_list = [normalize('NFKC', word)[:4] for word in mnemonic.lower().split(' ')] + word_languages = [[lang for word, lang in word_language_map.items() if normalize('NFKC', word)[:4] == abbrev] + for abbrev in mnemonic_list] + return list(set(sum(word_languages, []))) except KeyError: raise ValueError('Word not found in mnemonic word lists for any language.') @@ -90,30 +91,35 @@ def _get_checksum(entropy: bytes) -> int: return int.from_bytes(SHA256(entropy), 'big') >> (256 - checksum_length) -def verify_mnemonic(mnemonic: str, words_path: str) -> bool: +def verify_mnemonic(mnemonic: str, words_path: str) -> str: """ - Given a mnemonic, verify it against its own checksum." + Given a mnemonic, verify it against its own checksum and return + a reconstructed full version - useful in case it was abbreviated. """ try: languages = determine_mnemonic_language(mnemonic, words_path) except ValueError: - return False + return None for language in languages: try: - word_list = _get_word_list(language, words_path) - mnemonic_list = mnemonic.split(' ') + word_list = [normalize('NFKC', word)[:4] for word in _get_word_list(language, words_path)] + mnemonic_list = [normalize('NFKC', word)[:4] for word in mnemonic.lower().split(' ')] if len(mnemonic_list) not in range(12, 25, 3): - return False + return None word_indices = [_word_to_index(word_list, word) for word in mnemonic_list] mnemonic_int = _uint11_array_to_uint(word_indices) checksum_length = len(mnemonic_list) // 3 checksum = mnemonic_int & 2**checksum_length - 1 entropy = (mnemonic_int - checksum) >> checksum_length entropy_bits = entropy.to_bytes(checksum_length * 4, 'big') - return _get_checksum(entropy_bits) == checksum + full_word_list = _get_word_list(language, words_path) + if _get_checksum(entropy_bits) == checksum: + return ' '.join([_index_to_word(full_word_list, index) for index in word_indices]) + else: + pass except ValueError: pass - return False + return None def get_mnemonic(*, language: str, words_path: str, entropy: Optional[bytes]=None) -> str: diff --git a/tests/test_key_handling/test_key_derivation/test_mnemonic.py b/tests/test_key_handling/test_key_derivation/test_mnemonic.py index a913e391..ab543c2f 100644 --- a/tests/test_key_handling/test_key_derivation/test_mnemonic.py +++ b/tests/test_key_handling/test_key_derivation/test_mnemonic.py @@ -40,13 +40,13 @@ def test_bip39(language: str, test: Sequence[str]) -> None: @pytest.mark.parametrize( - 'test_mnemonic,is_valid', - [(test_mnemonic[1], True) + 'test_mnemonic', + [(test_mnemonic[1]) for _, language_test_vectors in test_vectors.items() for test_mnemonic in language_test_vectors] ) -def test_verify_mnemonic(test_mnemonic: str, is_valid: bool) -> None: - assert verify_mnemonic(test_mnemonic, WORD_LISTS_PATH) == is_valid +def test_verify_mnemonic(test_mnemonic: str) -> None: + assert verify_mnemonic(test_mnemonic, WORD_LISTS_PATH) is not None @pytest.mark.parametrize( From 5953f30cd6ffb687c4dc2d147da61d2a34221ae9 Mon Sep 17 00:00:00 2001 From: Carl Beekhuizen Date: Thu, 24 Mar 2022 12:46:32 +0100 Subject: [PATCH 2/7] abbreviation tweaks --- staking_deposit/cli/existing_mnemonic.py | 4 ++-- staking_deposit/intl/en/cli/existing_mnemonic.json | 2 +- staking_deposit/intl/en/cli/new_mnemonic.json | 2 +- staking_deposit/key_handling/key_derivation/mnemonic.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/staking_deposit/cli/existing_mnemonic.py b/staking_deposit/cli/existing_mnemonic.py index 6ded63ae..97e090f6 100644 --- a/staking_deposit/cli/existing_mnemonic.py +++ b/staking_deposit/cli/existing_mnemonic.py @@ -5,7 +5,7 @@ from staking_deposit.exceptions import ValidationError from staking_deposit.key_handling.key_derivation.mnemonic import ( - verify_mnemonic, + reconstruct_mnemonic, ) from staking_deposit.utils.constants import ( WORD_LISTS_PATH, @@ -23,7 +23,7 @@ def validate_mnemonic(ctx: click.Context, param: Any, mnemonic: str) -> str: - mnemonic = verify_mnemonic(mnemonic, WORD_LISTS_PATH) + mnemonic = reconstruct_mnemonic(mnemonic, WORD_LISTS_PATH) if mnemonic is not None: return mnemonic else: diff --git a/staking_deposit/intl/en/cli/existing_mnemonic.json b/staking_deposit/intl/en/cli/existing_mnemonic.json index 9b70c276..4e5b3fbd 100644 --- a/staking_deposit/intl/en/cli/existing_mnemonic.json +++ b/staking_deposit/intl/en/cli/existing_mnemonic.json @@ -8,7 +8,7 @@ }, "arg_mnemonic": { "help": "The mnemonic that you used to generate your keys. (It is recommended not to use this argument, and wait for the CLI to ask you for your mnemonic as otherwise it will appear in your shell history.)", - "prompt": "Please enter your mnemonic separated by spaces (\" \")" + "prompt": "Please enter your mnemonic separated by spaces (\" \"). Note: you only need to enter the first 4 letters of each word." }, "arg_mnemonic_password": { "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 initially, 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.", diff --git a/staking_deposit/intl/en/cli/new_mnemonic.json b/staking_deposit/intl/en/cli/new_mnemonic.json index 2c6d54c3..94d3c126 100644 --- a/staking_deposit/intl/en/cli/new_mnemonic.json +++ b/staking_deposit/intl/en/cli/new_mnemonic.json @@ -10,6 +10,6 @@ }, "msg_mnemonic_presentation": "This is your mnemonic (seed phrase). Write it down and store it safely. It is the ONLY way to retrieve your deposit.", "msg_press_any_key": "Press any key when you have written down your mnemonic.", - "msg_mnemonic_retype_prompt": "Please type your mnemonic (separated by spaces) to confirm you have written it down" + "msg_mnemonic_retype_prompt": "Please type your mnemonic (separated by spaces) to confirm you have written it down. Note: you only need to enter the first 4 letters of each word." } } diff --git a/staking_deposit/key_handling/key_derivation/mnemonic.py b/staking_deposit/key_handling/key_derivation/mnemonic.py index 3c81a701..82ef4bce 100644 --- a/staking_deposit/key_handling/key_derivation/mnemonic.py +++ b/staking_deposit/key_handling/key_derivation/mnemonic.py @@ -91,10 +91,10 @@ def _get_checksum(entropy: bytes) -> int: return int.from_bytes(SHA256(entropy), 'big') >> (256 - checksum_length) -def verify_mnemonic(mnemonic: str, words_path: str) -> str: +def reconstruct_mnemonic(mnemonic: str, words_path: str) -> Optional[str]: """ - Given a mnemonic, verify it against its own checksum and return - a reconstructed full version - useful in case it was abbreviated. + Given a mnemonic, a reconstructed the full version (incase the abbreviated words were used) + then verify it against its own checksum """ try: languages = determine_mnemonic_language(mnemonic, words_path) From 567ae3629515fce0b086f4a681a3f1220ef2bbdf Mon Sep 17 00:00:00 2001 From: Carl Beekhuizen Date: Thu, 24 Mar 2022 12:47:11 +0100 Subject: [PATCH 3/7] Adds abbreviation tests --- .../test_key_derivation/test_mnemonic.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/test_key_handling/test_key_derivation/test_mnemonic.py b/tests/test_key_handling/test_key_derivation/test_mnemonic.py index ab543c2f..04427926 100644 --- a/tests/test_key_handling/test_key_derivation/test_mnemonic.py +++ b/tests/test_key_handling/test_key_derivation/test_mnemonic.py @@ -4,6 +4,7 @@ from typing import ( Sequence, ) +from unicodedata import normalize from staking_deposit.utils.constants import ( MNEMONIC_LANG_OPTIONS, @@ -13,7 +14,7 @@ _get_word_list, get_seed, get_mnemonic, - verify_mnemonic, + reconstruct_mnemonic, ) @@ -45,8 +46,22 @@ def test_bip39(language: str, test: Sequence[str]) -> None: for _, language_test_vectors in test_vectors.items() for test_mnemonic in language_test_vectors] ) -def test_verify_mnemonic(test_mnemonic: str) -> None: - assert verify_mnemonic(test_mnemonic, WORD_LISTS_PATH) is not None +def test_reconstruct_mnemonic(test_mnemonic: str) -> None: + assert reconstruct_mnemonic(test_mnemonic, WORD_LISTS_PATH) is not None + +def abbreviate_mnemonic(mnemonic: str) -> str: + words = str.split(mnemonic) + words = [normalize('NFKC', word) for word in words] + return str.join(' ', words) + +@pytest.mark.parametrize( + 'test_mnemonic', + [abbreviate_mnemonic(test_mnemonic[1]) + for _, language_test_vectors in test_vectors.items() + for test_mnemonic in language_test_vectors] +) +def test_reconstruct_abbreviated_mnemonic(test_mnemonic: str) -> None: + assert reconstruct_mnemonic(test_mnemonic, WORD_LISTS_PATH) is not None @pytest.mark.parametrize( From bbeeb1f439bb21b070f9bf88c1789f4bd73f896c Mon Sep 17 00:00:00 2001 From: Thorsten Behrens Date: Thu, 24 Mar 2022 14:03:32 +0000 Subject: [PATCH 4/7] Fix lint; revert new-mnemonic text change --- staking_deposit/intl/en/cli/new_mnemonic.json | 2 +- tests/test_key_handling/test_key_derivation/test_mnemonic.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/staking_deposit/intl/en/cli/new_mnemonic.json b/staking_deposit/intl/en/cli/new_mnemonic.json index 94d3c126..4e6042c5 100644 --- a/staking_deposit/intl/en/cli/new_mnemonic.json +++ b/staking_deposit/intl/en/cli/new_mnemonic.json @@ -10,6 +10,6 @@ }, "msg_mnemonic_presentation": "This is your mnemonic (seed phrase). Write it down and store it safely. It is the ONLY way to retrieve your deposit.", "msg_press_any_key": "Press any key when you have written down your mnemonic.", - "msg_mnemonic_retype_prompt": "Please type your mnemonic (separated by spaces) to confirm you have written it down. Note: you only need to enter the first 4 letters of each word." + "msg_mnemonic_retype_prompt": "Please type your mnemonic (separated by spaces) to confirm you have written it down." } } diff --git a/tests/test_key_handling/test_key_derivation/test_mnemonic.py b/tests/test_key_handling/test_key_derivation/test_mnemonic.py index 04427926..03aaf8b0 100644 --- a/tests/test_key_handling/test_key_derivation/test_mnemonic.py +++ b/tests/test_key_handling/test_key_derivation/test_mnemonic.py @@ -49,11 +49,13 @@ def test_bip39(language: str, test: Sequence[str]) -> None: def test_reconstruct_mnemonic(test_mnemonic: str) -> None: assert reconstruct_mnemonic(test_mnemonic, WORD_LISTS_PATH) is not None + def abbreviate_mnemonic(mnemonic: str) -> str: words = str.split(mnemonic) words = [normalize('NFKC', word) for word in words] return str.join(' ', words) + @pytest.mark.parametrize( 'test_mnemonic', [abbreviate_mnemonic(test_mnemonic[1]) From 202a620a625ee395801e19f8bed57cae6d91ecc9 Mon Sep 17 00:00:00 2001 From: Carl Beekhuizen Date: Mon, 28 Mar 2022 10:54:57 +0200 Subject: [PATCH 5/7] Adds tests for existing mnemonic abbreviated words --- .../intl/en/cli/existing_mnemonic.json | 2 +- .../key_handling/key_derivation/mnemonic.py | 12 ++++- tests/test_cli/test_existing_menmonic.py | 47 +++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/staking_deposit/intl/en/cli/existing_mnemonic.json b/staking_deposit/intl/en/cli/existing_mnemonic.json index 4e5b3fbd..af57b5b8 100644 --- a/staking_deposit/intl/en/cli/existing_mnemonic.json +++ b/staking_deposit/intl/en/cli/existing_mnemonic.json @@ -8,7 +8,7 @@ }, "arg_mnemonic": { "help": "The mnemonic that you used to generate your keys. (It is recommended not to use this argument, and wait for the CLI to ask you for your mnemonic as otherwise it will appear in your shell history.)", - "prompt": "Please enter your mnemonic separated by spaces (\" \"). Note: you only need to enter the first 4 letters of each word." + "prompt": "Please enter your mnemonic separated by spaces (\" \"). Note: you only need to enter the first 4 letters of each word if you'd prefer." }, "arg_mnemonic_password": { "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 initially, 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.", diff --git a/staking_deposit/key_handling/key_derivation/mnemonic.py b/staking_deposit/key_handling/key_derivation/mnemonic.py index 82ef4bce..49f5ada1 100644 --- a/staking_deposit/key_handling/key_derivation/mnemonic.py +++ b/staking_deposit/key_handling/key_derivation/mnemonic.py @@ -2,6 +2,7 @@ from unicodedata import normalize from secrets import randbits from typing import ( + List, Optional, Sequence, ) @@ -91,6 +92,13 @@ def _get_checksum(entropy: bytes) -> int: return int.from_bytes(SHA256(entropy), 'big') >> (256 - checksum_length) +def abbreviate_words(words: Sequence[str]) -> List[str]: + """ + Given a series of word strings, return the 4-letter version of each word (which is unique according to BIP39) + """ + return [normalize('NFKC', word)[:4] for word in words] + + def reconstruct_mnemonic(mnemonic: str, words_path: str) -> Optional[str]: """ Given a mnemonic, a reconstructed the full version (incase the abbreviated words were used) @@ -102,8 +110,8 @@ def reconstruct_mnemonic(mnemonic: str, words_path: str) -> Optional[str]: return None for language in languages: try: - word_list = [normalize('NFKC', word)[:4] for word in _get_word_list(language, words_path)] - mnemonic_list = [normalize('NFKC', word)[:4] for word in mnemonic.lower().split(' ')] + word_list = abbreviate_words(_get_word_list(language, words_path)) + mnemonic_list = abbreviate_words(mnemonic.lower().split(' ')) if len(mnemonic_list) not in range(12, 25, 3): return None word_indices = [_word_to_index(word_list, word) for word in mnemonic_list] diff --git a/tests/test_cli/test_existing_menmonic.py b/tests/test_cli/test_existing_menmonic.py index 25e0556d..bcd5a8a1 100644 --- a/tests/test_cli/test_existing_menmonic.py +++ b/tests/test_cli/test_existing_menmonic.py @@ -152,3 +152,50 @@ async def test_script() -> None: # Clean up clean_key_folder(my_folder_path) + + +@pytest.mark.asyncio +async def test_script_abbreviated_mnemonic() -> None: + my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER') + if not os.path.exists(my_folder_path): + os.mkdir(my_folder_path) + + if os.name == 'nt': # Windows + run_script_cmd = 'sh deposit.sh' + else: # Mac or Linux + run_script_cmd = './deposit.sh' + + install_cmd = run_script_cmd + ' install' + proc = await asyncio.create_subprocess_shell( + install_cmd, + ) + await proc.wait() + + cmd_args = [ + run_script_cmd, + '--language', 'english', + '--non_interactive', + 'existing-mnemonic', + '--num_validators', '1', + '--mnemonic="aban aban aban aban aban aban aban aban aban aban aban abou"', + '--mnemonic-password', 'TREZOR', + '--validator_start_index', '1', + '--chain', 'mainnet', + '--keystore_password', 'MyPassword', + '--folder', my_folder_path, + ] + proc = await asyncio.create_subprocess_shell( + ' '.join(cmd_args), + ) + await proc.wait() + # Check files + validator_keys_folder_path = os.path.join(my_folder_path, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME) + _, _, key_files = next(os.walk(validator_keys_folder_path)) + + # Verify file permissions + if os.name == 'posix': + for file_name in key_files: + assert get_permissions(validator_keys_folder_path, file_name) == '0o440' + + # Clean up + clean_key_folder(my_folder_path) From 885826c5262545c28d84c7e21f8a34d95fae25ed Mon Sep 17 00:00:00 2001 From: Carl Beekhuizen Date: Mon, 28 Mar 2022 10:57:04 +0200 Subject: [PATCH 6/7] Adds abbriavted words to new-mnemonics --- staking_deposit/cli/new_mnemonic.py | 4 +- staking_deposit/intl/en/cli/new_mnemonic.json | 2 +- tests/test_cli/test_new_mnemonic.py | 84 ++++++++++++++++++- 3 files changed, 83 insertions(+), 7 deletions(-) diff --git a/staking_deposit/cli/new_mnemonic.py b/staking_deposit/cli/new_mnemonic.py index da2e7134..f288aeac 100644 --- a/staking_deposit/cli/new_mnemonic.py +++ b/staking_deposit/cli/new_mnemonic.py @@ -5,6 +5,7 @@ from staking_deposit.key_handling.key_derivation.mnemonic import ( get_mnemonic, + reconstruct_mnemonic, ) from staking_deposit.utils.click import ( captive_prompt_callback, @@ -47,7 +48,7 @@ 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: + while mnemonic != reconstruct_mnemonic(test_mnemonic, WORD_LISTS_PATH): click.clear() click.echo(load_text(['msg_mnemonic_presentation'])) click.echo('\n\n%s\n\n' % mnemonic) @@ -55,7 +56,6 @@ def new_mnemonic(ctx: click.Context, mnemonic_language: str, **kwargs: Any) -> N click.clear() test_mnemonic = click.prompt(load_text(['msg_mnemonic_retype_prompt']) + '\n\n') - test_mnemonic = test_mnemonic.lower() click.clear() # Do NOT use mnemonic_password. ctx.obj = {'mnemonic': mnemonic, 'mnemonic_password': ''} diff --git a/staking_deposit/intl/en/cli/new_mnemonic.json b/staking_deposit/intl/en/cli/new_mnemonic.json index 4e6042c5..feee3ac2 100644 --- a/staking_deposit/intl/en/cli/new_mnemonic.json +++ b/staking_deposit/intl/en/cli/new_mnemonic.json @@ -10,6 +10,6 @@ }, "msg_mnemonic_presentation": "This is your mnemonic (seed phrase). Write it down and store it safely. It is the ONLY way to retrieve your deposit.", "msg_press_any_key": "Press any key when you have written down your mnemonic.", - "msg_mnemonic_retype_prompt": "Please type your mnemonic (separated by spaces) to confirm you have written it down." + "msg_mnemonic_retype_prompt": "Please type your mnemonic (separated by spaces) to confirm you have written it down. Note: you only need to enter the first 4 letters of each word if you'd prefer." } } diff --git a/tests/test_cli/test_new_mnemonic.py b/tests/test_cli/test_new_mnemonic.py index 098bc6a8..fb70a255 100644 --- a/tests/test_cli/test_new_mnemonic.py +++ b/tests/test_cli/test_new_mnemonic.py @@ -9,6 +9,7 @@ from staking_deposit.cli import new_mnemonic from staking_deposit.deposit import cli +from staking_deposit.key_handling.key_derivation.mnemonic import abbreviate_words from staking_deposit.utils.constants import DEFAULT_VALIDATOR_KEYS_FOLDER_NAME, ETH1_ADDRESS_WITHDRAWAL_PREFIX from staking_deposit.utils.intl import load_text from .helpers import clean_key_folder, get_permissions, get_uuid @@ -17,7 +18,7 @@ def test_new_mnemonic_bls_withdrawal(monkeypatch) -> None: # monkeypatch get_mnemonic def mock_get_mnemonic(language, words_path, entropy=None) -> str: - return "fakephrase" + return "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" monkeypatch.setattr(new_mnemonic, "get_mnemonic", mock_get_mnemonic) @@ -28,7 +29,8 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str: os.mkdir(my_folder_path) runner = CliRunner() - inputs = ['english', 'english', '1', 'mainnet', 'MyPassword', 'MyPassword', 'fakephrase'] + inputs = ['english', 'english', '1', 'mainnet', 'MyPassword', 'MyPassword', + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'] data = '\n'.join(inputs) result = runner.invoke(cli, ['new-mnemonic', '--folder', my_folder_path], input=data) assert result.exit_code == 0 @@ -56,7 +58,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str: def test_new_mnemonic_eth1_address_withdrawal(monkeypatch) -> None: # monkeypatch get_mnemonic def mock_get_mnemonic(language, words_path, entropy=None) -> str: - return "fakephrase" + return "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" monkeypatch.setattr(new_mnemonic, "get_mnemonic", mock_get_mnemonic) @@ -67,7 +69,8 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str: os.mkdir(my_folder_path) runner = CliRunner() - inputs = ['english', '1', 'mainnet', 'MyPassword', 'MyPassword', 'fakephrase'] + inputs = ['english', '1', 'mainnet', 'MyPassword', 'MyPassword', + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'] data = '\n'.join(inputs) eth1_withdrawal_address = '0x00000000219ab540356cbb839cbe05303d7705fa' arguments = [ @@ -178,3 +181,76 @@ async def test_script() -> None: # Clean up clean_key_folder(my_folder_path) + + +@pytest.mark.asyncio +async def test_script_abbreviated_mnemonic() -> None: + my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER') + if not os.path.exists(my_folder_path): + os.mkdir(my_folder_path) + + if os.name == 'nt': # Windows + run_script_cmd = 'sh deposit.sh' + else: # Mac or Linux + run_script_cmd = './deposit.sh' + + install_cmd = run_script_cmd + ' install' + proc = await asyncio.create_subprocess_shell( + install_cmd, + ) + await proc.wait() + + cmd_args = [ + run_script_cmd, + '--language', 'english', + '--non_interactive', + 'new-mnemonic', + '--num_validators', '5', + '--mnemonic_language', 'english', + '--chain', 'mainnet', + '--keystore_password', 'MyPassword', + '--folder', my_folder_path, + ] + proc = await asyncio.create_subprocess_shell( + ' '.join(cmd_args), + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + ) + + seed_phrase = '' + parsing = False + mnemonic_json_file = os.path.join(os.getcwd(), 'staking_deposit/../staking_deposit/cli/', 'new_mnemonic.json') + async for out in proc.stdout: + output = out.decode('utf-8').rstrip() + if output.startswith(load_text(['msg_mnemonic_presentation'], mnemonic_json_file, 'new_mnemonic')): + parsing = True + elif output.startswith(load_text(['msg_mnemonic_retype_prompt'], mnemonic_json_file, 'new_mnemonic')): + parsing = False + elif parsing: + seed_phrase += output + if len(seed_phrase) > 0: + abbreviated_mnemonic = ' '.join(abbreviate_words(seed_phrase.split(' '))) + encoded_phrase = abbreviated_mnemonic.encode() + proc.stdin.write(encoded_phrase) + proc.stdin.write(b'\n') + + assert len(seed_phrase) > 0 + + # Check files + validator_keys_folder_path = os.path.join(my_folder_path, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME) + _, _, key_files = next(os.walk(validator_keys_folder_path)) + + all_uuid = [ + get_uuid(validator_keys_folder_path + '/' + key_file) + for key_file in key_files + if key_file.startswith('keystore') + ] + assert len(set(all_uuid)) == 5 + + # Verify file permissions + if os.name == 'posix': + for file_name in key_files: + assert get_permissions(validator_keys_folder_path, file_name) == '0o440' + + # Clean up + clean_key_folder(my_folder_path) From 0d3440e7ba2081530155967ea4a8dfe772c7dc10 Mon Sep 17 00:00:00 2001 From: Carl Beekhuizen Date: Mon, 28 Mar 2022 11:24:52 +0200 Subject: [PATCH 7/7] Adds check that multiple mnemonic languages aren't detected --- .../key_handling/key_derivation/mnemonic.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/staking_deposit/key_handling/key_derivation/mnemonic.py b/staking_deposit/key_handling/key_derivation/mnemonic.py index 49f5ada1..378bafdf 100644 --- a/staking_deposit/key_handling/key_derivation/mnemonic.py +++ b/staking_deposit/key_handling/key_derivation/mnemonic.py @@ -108,6 +108,7 @@ def reconstruct_mnemonic(mnemonic: str, words_path: str) -> Optional[str]: languages = determine_mnemonic_language(mnemonic, words_path) except ValueError: return None + reconstructed_mnemonic = None for language in languages: try: word_list = abbreviate_words(_get_word_list(language, words_path)) @@ -122,12 +123,17 @@ def reconstruct_mnemonic(mnemonic: str, words_path: str) -> Optional[str]: entropy_bits = entropy.to_bytes(checksum_length * 4, 'big') full_word_list = _get_word_list(language, words_path) if _get_checksum(entropy_bits) == checksum: - return ' '.join([_index_to_word(full_word_list, index) for index in word_indices]) + """ + This check guarantees that only one language has a valid mnemonic. + It is needed to ensure abbrivated words aren't valid in multiple languages + """ + assert reconstructed_mnemonic is None + reconstructed_mnemonic = ' '.join([_index_to_word(full_word_list, index) for index in word_indices]) else: pass except ValueError: pass - return None + return reconstructed_mnemonic def get_mnemonic(*, language: str, words_path: str, entropy: Optional[bytes]=None) -> str: