Skip to content
This repository was archived by the owner on Oct 6, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c10eff1
Update links to the proper releases page
marioevz Mar 11, 2022
6adfc6f
Merge pull request #248 from marioevz/patch-1
CarlBeek Mar 16, 2022
a76f187
bump the versions on all the packages!
CarlBeek Mar 16, 2022
e84e942
resolves version conflicts
CarlBeek Mar 16, 2022
925960f
unify function sigs to make click happy
CarlBeek Mar 16, 2022
fd74dfd
revert click to 7.1.2 again
CarlBeek Mar 21, 2022
78ae263
try upgrade circleCI to >python3.8 and probably break everything
CarlBeek Mar 21, 2022
7ce367f
revert zipp version becuase i over-used find-and-replace 🤦
CarlBeek Mar 21, 2022
e82b3e3
Updates windows server version to edge
CarlBeek Mar 21, 2022
e829e94
update pyenv python versions for arm
CarlBeek Mar 21, 2022
c10e0e4
update sundry python version references
CarlBeek Mar 21, 2022
d2ed768
upgrade windows machince reasources
CarlBeek Mar 21, 2022
c59c518
Add in missing python version stuffies
CarlBeek Mar 21, 2022
712002c
revert to old windows version
CarlBeek Mar 21, 2022
ec1b641
installes python 3.10 for windows
CarlBeek Mar 21, 2022
513d281
use python 3.10.2 for linux amd64
CarlBeek Mar 21, 2022
76b7b3c
update to current ubuntu images
CarlBeek Mar 21, 2022
07ed5a2
allow pyenv to skip version warning
CarlBeek Mar 21, 2022
72c8a0f
lock ubuntu version
CarlBeek Mar 21, 2022
d5cd39a
force install using pyenv
CarlBeek Mar 21, 2022
3f97a0e
try 3.10.3 for ubuntu again
CarlBeek Mar 21, 2022
e91a27d
revert to 3.10.2 for ubuntu again
CarlBeek Mar 21, 2022
cd52e9d
Merge pull request #249 from ethereum/version_bump
CarlBeek Mar 25, 2022
fb5ffe6
Allow 4-character abbreviations of mnemonic words
yorickdowne Feb 5, 2022
5953f30
abbreviation tweaks
CarlBeek Mar 24, 2022
567ae36
Adds abbreviation tests
CarlBeek Mar 24, 2022
bbeeb1f
Fix lint; revert new-mnemonic text change
yorickdowne Mar 24, 2022
202a620
Adds tests for existing mnemonic abbreviated words
CarlBeek Mar 28, 2022
885826c
Adds abbriavted words to new-mnemonics
CarlBeek Mar 28, 2022
0d3440e
Adds check that multiple mnemonic languages aren't detected
CarlBeek Mar 28, 2022
c97cfc3
Merge pull request #242 from yorickdowne/abbreviations
CarlBeek Mar 28, 2022
5e343d4
version bump -> v2.1
CarlBeek Mar 28, 2022
9edea63
Kick venv cache version
hwwhww Mar 29, 2022
a55514b
Merge pull request #253 from ethereum/kick-ci-cache
CarlBeek Mar 29, 2022
1bc54e0
Merge pull request #251 from ethereum/version_bump
CarlBeek Mar 29, 2022
3be3723
Actually shortens words for abbrviated mnemonic test vectors
CarlBeek Mar 29, 2022
fce407c
Merge pull request #254 from ethereum/fix_abbreviated_test
hwwhww Mar 29, 2022
e35ed2c
Re-implement Clarifies when abbreviated words are used
CarlBeek Mar 29, 2022
dc6fa0c
Merge pull request #256 from ethereum/clarify_abbreviations
hwwhww Mar 29, 2022
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
5 changes: 3 additions & 2 deletions staking_deposit/cli/existing_mnemonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,7 +23,8 @@


def validate_mnemonic(ctx: click.Context, param: Any, mnemonic: str) -> str:
if verify_mnemonic(mnemonic, WORD_LISTS_PATH):
mnemonic = reconstruct_mnemonic(mnemonic, WORD_LISTS_PATH)
if mnemonic is not None:
return mnemonic
else:
raise ValidationError(load_text(['err_invalid_mnemonic']))
Expand Down
4 changes: 2 additions & 2 deletions staking_deposit/cli/new_mnemonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -47,15 +48,14 @@
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)
click.pause(load_text(['msg_press_any_key']))

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': ''}
Expand Down
2 changes: 1 addition & 1 deletion staking_deposit/intl/en/cli/existing_mnemonic.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 if you'd prefer."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • What do you think about translation in the future release?
  • 4-letters abbreviations are not available in Chinese and Korean so we may want to avoid adding translation in these languages.

Copy link
Collaborator Author

@CarlBeek CarlBeek Mar 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you say, Chinese, Japanese & Korean don't have abbreviated versions. And I'm a little concerned about collisions between 4-letter mnemonics in the romance languages. I've done some testing here, but I'm by no means convinced it can't happen (and trigger the sanity assert) especially considering all the alternate forms of letters (eg moving accents etc).

As such, I think we should avoid translating this bit at all. ie. if you use not-English (tm) you will have to type out the full mnemonic. At a later point if we implement interactive predictive text for words, we can address this.

},
"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.",
Expand Down
2 changes: 1 addition & 1 deletion staking_deposit/intl/en/cli/new_mnemonic.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
42 changes: 31 additions & 11 deletions staking_deposit/key_handling/key_derivation/mnemonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from unicodedata import normalize
from secrets import randbits
from typing import (
List,
Optional,
Sequence,
)
Expand Down Expand Up @@ -68,9 +69,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.')

Expand All @@ -90,30 +92,48 @@ 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 abbreviate_words(words: Sequence[str]) -> List[str]:
"""
Given a mnemonic, verify it against its own checksum."
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)
then verify it against its own checksum
"""
try:
languages = determine_mnemonic_language(mnemonic, words_path)
except ValueError:
return False
return None
reconstructed_mnemonic = None
for language in languages:
try:
word_list = _get_word_list(language, words_path)
mnemonic_list = mnemonic.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 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:
"""
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 False
return reconstructed_mnemonic


def get_mnemonic(*, language: str, words_path: str, entropy: Optional[bytes]=None) -> str:
Expand Down
47 changes: 47 additions & 0 deletions tests/test_cli/test_existing_menmonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
84 changes: 80 additions & 4 deletions tests/test_cli/test_new_mnemonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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 = [
Expand Down Expand Up @@ -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)
27 changes: 22 additions & 5 deletions tests/test_key_handling/test_key_derivation/test_mnemonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import (
Sequence,
)
from unicodedata import normalize

from staking_deposit.utils.constants import (
MNEMONIC_LANG_OPTIONS,
Expand All @@ -13,7 +14,7 @@
_get_word_list,
get_seed,
get_mnemonic,
verify_mnemonic,
reconstruct_mnemonic,
)


Expand All @@ -40,13 +41,29 @@ 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_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(
Expand Down