From 591a1b0b9d28cbbe30dad56a4405d1bbd53dfaaf Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 16 Jul 2023 21:58:41 -0500 Subject: [PATCH 01/17] Entropy-to-mnemonic CLI utility * Makes our existing mnemonic_generation.oy accessible as an interactive CLI utility for converting dice rolls or coin flips into 12- or 24-word mnemonics as well as the final word calculation. * Reduces python dependencies in mnemonic_generation.py * Removes need to always specify `wordlist_language_code`. * Adds constants for number of dice rolls for each mnemonic phrase length (since 99 may become 100 soon). * Generates test dice rolls, coin flips, or word selections via its "rand12" and "rand24" option. * Adds support for coin flips, even though the SeedSigner UI does not support coin flips yet (and might not ever). * Updates dice_verification.md accordingly. --- docs/dice_verification.md | 54 ++++ src/seedsigner/helpers/mnemonic_generation.py | 284 +++++++++++++++++- src/seedsigner/views/tools_views.py | 8 +- tests/test_mnemonic_generation.py | 57 ++-- 4 files changed, 370 insertions(+), 33 deletions(-) diff --git a/docs/dice_verification.md b/docs/dice_verification.md index 361bf14de..d5a23cb77 100644 --- a/docs/dice_verification.md +++ b/docs/dice_verification.md @@ -205,3 +205,57 @@ We double-checked in two different web tools implementing different methods for So congratulations if the fingerprints, zpubs and addresses all match up in your example so you can be much more confident that nothing is wrong with your generated seed. +### SeedSigner verification script +_(for more advanced/python-savvy users)_ + +Install the `embit` dependency: +``` +pip3 install embit +``` + +Then run the utility script with `-h` to view the usage instructions: +``` +cd src/seedsigner/helpers +python3 mnemonic_generation.py -h +``` + +``` + Verify SeedSigner's dice rolls and coin flip entropy-to-mnemonic conversion via this tool. + + Compare its results against iancoleman.io/bip39 and bitcoiner.guide/seed + + Usage: + # 50 dice rolls / 12-word mnemonic + python3 mnemonic_generation.py dice 5624433434... + + # 99 dice rolls / 24-word mnemonic + python3 mnemonic_generation.py dice 6151463561... + + # 50 dice rolls, entered as 0-5 / 12-word mnemonic + python3 mnemonic_generation.py --zero-indexed-dice dice 5135535514... + + # 128 coin flips / 12-word mnemonic + python3 mnemonic_generation.py coins 1111100111... + + # 256 coin flips / 24-word mnemonic + python mnemonic_generation.py coins 0010111010... + + # GENERATE 50 random dice rolls / 12-word mnemonic + python3 mnemonic_generation.py dice rand12 + + # GENERATE 99 random dice rolls / 24-word mnemonic + python3 mnemonic_generation.py dice rand24 + + # GENERATE 99 random dice rolls, entered as 0-5 / 24-word mnemonic + python3 mnemonic_generation.py --zero-indexed-dice dice rand24 + + # GENERATE 128 random coin flips / 12-word mnemonic + python3 mnemonic_generation.py coins rand12 + + # GENERATE 256 random coin flips / 24-word mnemonic + python3 mnemonic_generation.py coins rand24 +``` + + +### Epilogue +You can use these methods to do dry run time to time to verify that no one has changed the micro sdcard. But do not use the generated 24 words as a valid wallet, they need to be generated alone, only on the seedsigner! diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index a021a8346..9a7caa639 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -1,22 +1,76 @@ import hashlib +import random import unicodedata from embit import bip39 from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes -from typing import List +from embit.wordlists.bip39 import WORDLIST as WORDLIST__ENGLISH +""" + This is SeedSigner's internal mnemonic generation utility but is also meant to + function as an independently-executable CLI to facilitate external verification of + SeedSigner's mnemonic generation for a given input entropy. + Therefore its module imports should be kept to the bare minimum. -def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str]: + ## Running as a standalone script + Install the `embit` library: + ``` + pip3 install embit + ``` + + And then run: + ``` + python3 mnemonic_generation.py -h + ``` +""" + + +# Hard-coded value from SettingsConstants to avoid dependencies +WORDLIST_LANGUAGE__ENGLISH = 'en' + +DICE__NUM_ROLLS__12WORD = 50 +DICE__NUM_ROLLS__24WORD = 99 + + +def _get_wordlist(wordlist_language_code) -> list[str]: + """ + Convenience method to fetch the wordlist for the given language code without + requiring any SeedSigner module dependencies for when this is run as a + standalone CLI. + """ + if wordlist_language_code == WORDLIST_LANGUAGE__ENGLISH: + return WORDLIST__ENGLISH + else: + # Nested import to avoid dependency on Seed model when running this script standalone + from seedsigner.models import Seed + return Seed.get_wordlist(wordlist_language_code) + + + +def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> list[str]: """ Provide 12- or 24-word mnemonic, returns complete mnemonic w/checksum as a list. + Mnemonic may be a list of words or a string of words separated by spaces or commas. + If 11- or 23-words are provided, append word `0000` to end of list as temp final word. """ - from seedsigner.models.seed import Seed + if type(mnemonic) == str: + import re + # split on commas or spaces + mnemonic = re.findall(r'[^,\s]+', mnemonic) + if len(mnemonic) in [11, 23]: - mnemonic.append(Seed.get_wordlist(wordlist_language_code)[0]) + if wordlist_language_code == WORDLIST_LANGUAGE__ENGLISH: + temp_final_word = "abandon" + else: + # Nested import to avoid dependency on Seed model when running this script standalone + from seedsigner.models import Seed + temp_final_word = Seed.get_wordlist(wordlist_language_code)[0] + + mnemonic.append(temp_final_word) if len(mnemonic) not in [12, 24]: raise Exception("Pass in a 12- or 24-word mnemonic") @@ -27,7 +81,7 @@ def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str] # Convert the resulting mnemonic to bytes, but we `ignore_checksum` validation # because we assume it's incorrect since we either let the user select their own # final word OR we injected the 0000 word from the wordlist. - mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=Seed.get_wordlist(wordlist_language_code)) + mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=_get_wordlist(wordlist_language_code)) # This function will convert the bytes back into a mnemonic, but it will also # calculate the proper checksum bits while doing so. For a 12-word seed it will just @@ -37,28 +91,234 @@ def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str] -def generate_mnemonic_from_bytes(entropy_bytes) -> List[str]: - return bip39.mnemonic_from_bytes(entropy_bytes).split() +def generate_mnemonic_from_bytes(entropy_bytes, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> list[str]: + return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=_get_wordlist(wordlist_language_code)).split() -def generate_mnemonic_from_dice(roll_data: str) -> List[str]: +def generate_mnemonic_from_dice(roll_data: str, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> list[str]: + """ + Takes a string of 50 or 99 dice rolls and returns a 12- or 24-word mnemonic. + + Uses the iancoleman.io/bip39 and bitcoiner.guide/seed "Base 10" or "Hex" mode approach: + * dice rolls are treated as string data. + * hashed via SHA256. + + Important note: This method is NOT compatible with iancoleman's "Dice" mode. + """ entropy_bytes = hashlib.sha256(roll_data.encode()).digest() - if len(roll_data) == 50: + if len(roll_data) == DICE__NUM_ROLLS__12WORD: # 12-word mnemonic; only use 128bits / 16 bytes entropy_bytes = entropy_bytes[:16] # Return as a list - return bip39.mnemonic_from_bytes(entropy_bytes).split() + return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=_get_wordlist(wordlist_language_code)).split() + + + +def generate_mnemonic_from_coin_flips(coin_flips: str, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> list[str]: + """ + Takes a string of 128 or 256 0s and 1s and returns a 12- or 24-word mnemonic. + + Uses the iancoleman.io/bip39 and bitcoiner.guide/seed "Binary" mode approach: + * binary digit stream is treated as string data. + * hashed via SHA256. + """ + entropy_bytes = hashlib.sha256(coin_flips.encode()).digest() + + if len(coin_flips) == 128: + # 12-word mnemonic; only use 128bits / 16 bytes + entropy_bytes = entropy_bytes[:16] + + # Return as a list + return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=_get_wordlist(wordlist_language_code)).split() + + + +def get_partial_final_word(coin_flips: str, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> str: + """ Look up the partial final word for the given coin flips. + 7 coin flips: 0101010 + 0000 where the final 4 bits will be replaced with the checksum + 3 coin flips: 0101 + 0000000 where the final 8 bits will be replaced with the checksum + """ + binary_string = coin_flips + "0" * (11 - len(coin_flips)) + wordlist_index = int(binary_string, 2) + + if wordlist_language_code == WORDLIST_LANGUAGE__ENGLISH: + wordlist = WORDLIST__ENGLISH + else: + from seedsigner.models import Seed, SettingsConstants + wordlist = Seed.get_wordlist(wordlist_language_code) + + return wordlist[wordlist_index] + # Note: This currently isn't being used since we're now chaining hashed bytes for the # image-based entropy and aren't just ingesting a single image. -def generate_mnemonic_from_image(image) -> List[str]: +def generate_mnemonic_from_image(image, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> list[str]: import hashlib hash = hashlib.sha256(image.tobytes()) # Return as a list - return bip39.mnemonic_from_bytes(hash.digest()).split() + return bip39.mnemonic_from_bytes(hash.digest(), wordlist=_get_wordlist(wordlist_language_code)).split() + + + +if __name__ == "__main__": + import argparse + + usage = f""" + Verify SeedSigner's dice rolls and coin flip entropy-to-mnemonic conversion via this tool. + + Compare its results against iancoleman.io/bip39 and bitcoiner.guide/seed + + Usage: + # {DICE__NUM_ROLLS__12WORD} dice rolls / 12-word mnemonic + python3 mnemonic_generation.py dice 5624433434... + + # {DICE__NUM_ROLLS__24WORD} dice rolls / 24-word mnemonic + python3 mnemonic_generation.py dice 6151463561... + + # {DICE__NUM_ROLLS__12WORD} dice rolls, entered as 0-5 / 12-word mnemonic + python3 mnemonic_generation.py --zero-indexed-dice dice 5135535514... + + # 128 coin flips / 12-word mnemonic + python3 mnemonic_generation.py coins 1111100111... + + # 256 coin flips / 24-word mnemonic + python mnemonic_generation.py coins 0010111010... + + # GENERATE {DICE__NUM_ROLLS__12WORD} random dice rolls / 12-word mnemonic + python3 mnemonic_generation.py dice rand12 + + # GENERATE {DICE__NUM_ROLLS__24WORD} random dice rolls / 24-word mnemonic + python3 mnemonic_generation.py dice rand24 + + # GENERATE {DICE__NUM_ROLLS__24WORD} random dice rolls, entered as 0-5 / 24-word mnemonic + python3 mnemonic_generation.py --zero-indexed-dice dice rand24 + + # GENERATE 128 random coin flips / 12-word mnemonic + python3 mnemonic_generation.py coins rand12 + + # GENERATE 256 random coin flips / 24-word mnemonic + python3 mnemonic_generation.py coins rand24 + """ + RAND_12 = "rand12" + RAND_24 = "rand24" + parser = argparse.ArgumentParser(description=f'SeedSigner entropy-to-mnemonic tool\n\n{usage}', formatter_class=argparse.RawTextHelpFormatter) + + # Required positional arguments + parser.add_argument('method', type=str, choices=['dice', 'coins', 'final_word'], help="Input entropy method") + parser.add_argument('entropy', type=str, help=f"""Entropy data. Enter "{ RAND_12 }" or "{ RAND_24 }" to create a random (not-secure) example seed.""") + + # Optional arguments + parser.add_argument('-z', '--zero-indexed-dice', + action="store_true", + default=False, + dest="zero_indexed_dice", + help="Enables dice entry as [0-5] instead of default [1-6]") + + args = parser.parse_args() + + method = args.method + entropy = args.entropy + zero_indexed_dice = args.zero_indexed_dice + + is_rand_seed = 'rand' in entropy + if is_rand_seed: + # Generate random data as our entropy + if entropy not in [RAND_12, RAND_24]: + print(f"""Invalid random entropy value: Must be either "{RAND_12}" or "{RAND_24}".""") + exit(1) + mnemonic_length = 12 if entropy == RAND_12 else 24 + + if method == 'dice': + if zero_indexed_dice: + entropy = ''.join([str(random.randint(0, 5)) for i in range(DICE__NUM_ROLLS__12WORD if mnemonic_length == 12 else DICE__NUM_ROLLS__24WORD)]) + else: + entropy = ''.join([str(random.randint(1, 6)) for i in range(DICE__NUM_ROLLS__12WORD if mnemonic_length == 12 else DICE__NUM_ROLLS__24WORD)]) + + elif method == 'coins': + entropy = ''.join([str(random.randint(0, 1)) for i in range(128 if mnemonic_length == 12 else 256)]) + + elif method == 'final_word': + random_dice_rolls = ''.join([str(random.randint(0, 1)) for i in range(128 if mnemonic_length == 12 else 256)]) + entropy = " ".join(generate_mnemonic_from_coin_flips(random_dice_rolls)[:-1]) + print(len(entropy.split()), entropy) + + if method == 'dice': + if not zero_indexed_dice and ('0' in entropy or '6' not in entropy): + print("Dice entry must be 1-6 unless --zero-indexed-dice is specified") + exit(1) + if len(entropy) not in [DICE__NUM_ROLLS__12WORD, DICE__NUM_ROLLS__24WORD]: + print(f"Dice entropy must be {DICE__NUM_ROLLS__12WORD} or {DICE__NUM_ROLLS__24WORD} rolls") + exit(1) + mnemonic = generate_mnemonic_from_dice(entropy) + + elif method == 'coins': + if len(entropy) not in [128, 256]: + print("Coin flip entropy must be 128 or 256 flips") + exit(1) + mnemonic = generate_mnemonic_from_coin_flips(entropy) + + elif method == 'final_word': + num_input_words = len(entropy.split()) + if num_input_words not in [11, 12, 23, 24]: + print(f"Final word entropy must be 11, 12, 23, or 24 words ({num_input_words} provided)") + exit(1) + + if num_input_words in [11, 23]: + # We need to fill the last bits of entropy + if num_input_words == 11: + # 7 final bits of entropy in the 12th word (7 + 4-bit checksum) + num_final_entropy_bits = 7 + else: + # 3 final bits of entropy in the 24th word (3 + 8-bit checksum) + num_final_entropy_bits = 3 + + final_entropy_method = None + while final_entropy_method not in ['1', '2', '3']: + final_entropy_method = input(f""" + How would you like to fill the final {num_final_entropy_bits} bits of entropy? + + 1.) {num_final_entropy_bits} coin flips + 2.) Select an additional word from the wordlist + 3.) Fill with zeros + + Type 1, 2, or 3: """) + + if final_entropy_method == '1': + coin_flips = input(f""" Enter {num_final_entropy_bits} coin flips as 0 or 1 (e.g. { "".join(str(random.randint(0,1)) for f in range(0, num_final_entropy_bits)) }): """) + if len(coin_flips) != num_final_entropy_bits: + print(f"Invalid number of coin flips: needed {num_final_entropy_bits}, got {len(coin_flips)}") + final_word = get_partial_final_word(coin_flips) + entropy += f" {final_word}" + + elif final_entropy_method == '2': + final_word = input(f""" Enter the final word: """) + if final_word not in WORDLIST__ENGLISH: + print(f"Invalid word: {final_word}") + exit(1) + entropy += f" {final_word}" + + elif final_entropy_method == '3': + # Nothing to do; just pass the 11 or 23 words straight to calculate_checksum + pass + + mnemonic = calculate_checksum(entropy) + + print("\n") + if is_rand_seed: + print("\t***** This is a demo seed. Do not use it to store funds!!! *****") + + print(f"""\t{" ".join(mnemonic)}\n""") + + if is_rand_seed: + print(f"\tEntropy: {entropy}\n") + + if method == "dice": + print(f"""\tVerify at iancoleman.io/bip39 or bitcoiner.guide/seed using "Base 10" or "Hex" mode.\n""") + elif method == "coins": + print(f"""\tVerify at iancoleman.io/bip39 or bitcoiner.guide/seed using "Binary" mode.\n""") \ No newline at end of file diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 12a4493b3..b94c9b46d 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -188,8 +188,8 @@ def run(self): ****************************************************************************""" class ToolsDiceEntropyMnemonicLengthView(View): def run(self): - TWELVE = "12 words (50 rolls)" - TWENTY_FOUR = "24 words (99 rolls)" + TWELVE = f"12 words ({mnemonic_generation.DICE__NUM_ROLLS__12WORD} rolls)" + TWENTY_FOUR = f"24 words ({mnemonic_generation.DICE__NUM_ROLLS__24WORD} rolls)" button_data = [TWELVE, TWENTY_FOUR] selected_menu_num = ButtonListScreen( @@ -203,10 +203,10 @@ def run(self): return Destination(BackStackView) elif button_data[selected_menu_num] == TWELVE: - return Destination(ToolsDiceEntropyEntryView, view_args=dict(total_rolls=50)) + return Destination(ToolsDiceEntropyEntryView, view_args=dict(total_rolls=mnemonic_generation.DICE__NUM_ROLLS__12WORD)) elif button_data[selected_menu_num] == TWENTY_FOUR: - return Destination(ToolsDiceEntropyEntryView, view_args=dict(total_rolls=99)) + return Destination(ToolsDiceEntropyEntryView, view_args=dict(total_rolls=mnemonic_generation.DICE__NUM_ROLLS__24WORD)) diff --git a/tests/test_mnemonic_generation.py b/tests/test_mnemonic_generation.py index 76298d34f..000ce4d7e 100644 --- a/tests/test_mnemonic_generation.py +++ b/tests/test_mnemonic_generation.py @@ -12,16 +12,17 @@ def test_dice_rolls(): dice_rolls = "" for i in range(0, 99): # Do not need truly rigorous random for this test - dice_rolls += str(random.randint(0, 5)) + dice_rolls += str(random.randint(1, 6)) mnemonic = mnemonic_generation.generate_mnemonic_from_dice(dice_rolls) + assert len(mnemonic) == 24 assert bip39.mnemonic_is_valid(" ".join(mnemonic)) dice_rolls = "" - for i in range(0, 50): + for i in range(0, mnemonic_generation.DICE__NUM_ROLLS__12WORD): # Do not need truly rigorous random for this test - dice_rolls += str(random.randint(0, 5)) + dice_rolls += str(random.randint(1, 6)) mnemonic = mnemonic_generation.generate_mnemonic_from_dice(dice_rolls) assert len(mnemonic) == 12 @@ -29,18 +30,40 @@ def test_dice_rolls(): -def test_calculate_checksum(): - """ Given an 11-word or 23-word mnemonic, the calculated checksum should yield a +def test_calculate_checksum_input_type(): + """ + Given an 11-word or 23-word mnemonic, the calculated checksum should yield a valid complete mnemonic. + + calculate_checksum should accept the mnemonic as: + * a list of strings + * string: "A B C", "A, B, C", "A,B,C" """ # Test mnemonics from https://iancoleman.io/bip39/ + def _try_all_input_formats(partial_mnemonic: str): + # List of strings + mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) + assert bip39.mnemonic_is_valid(" ".join(mnemonic)) + + # Comma-separated string + mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.replace(" ", ",")) + assert bip39.mnemonic_is_valid(" ".join(mnemonic)) + + # Comma-separated string w/space + mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.replace(" ", ", ")) + assert bip39.mnemonic_is_valid(" ".join(mnemonic)) + + # Space-separated string + mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic) + assert bip39.mnemonic_is_valid(" ".join(mnemonic)) + partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day" - mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) - assert bip39.mnemonic_is_valid(" ".join(mnemonic)) + _try_all_input_formats(partial_mnemonic) partial_mnemonic = "bubble father debate ankle injury fence mesh evolve section wet coyote violin pyramid flower rent arrow round clutch myth safe base skin mobile" - mnemonic = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) - assert bip39.mnemonic_is_valid(" ".join(mnemonic)) + _try_all_input_formats(partial_mnemonic) + + def test_calculate_checksum_invalid_mnemonics(): @@ -50,25 +73,25 @@ def test_calculate_checksum_invalid_mnemonics(): with pytest.raises(Exception) as e: # Mnemonic is too short: 10 words instead of 11 partial_mnemonic = "abandon " * 9 + "about" - mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) assert "12- or 24-word" in str(e) with pytest.raises(Exception) as e: # Valid mnemonic but unsupported length mnemonic = "devote myth base logic dust horse nut collect buddy element eyebrow visit empty dress jungle" - mnemonic_generation.calculate_checksum(mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + mnemonic_generation.calculate_checksum(mnemonic.split(" ")) assert "12- or 24-word" in str(e) with pytest.raises(Exception) as e: # Mnemonic is too short: 22 words instead of 23 partial_mnemonic = "abandon " * 21 + "about" - mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) assert "12- or 24-word" in str(e) with pytest.raises(ValueError) as e: # Invalid BIP-39 word partial_mnemonic = "foobar " * 11 + "about" - mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) assert "not in the dictionary" in str(e) @@ -78,17 +101,17 @@ def test_calculate_checksum_with_default_final_word(): the mnemonic. """ partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day" - mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) partial_mnemonic += " abandon" - mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) assert mnemonic1 == mnemonic2 partial_mnemonic = "bubble father debate ankle injury fence mesh evolve section wet coyote violin pyramid flower rent arrow round clutch myth safe base skin mobile" - mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) partial_mnemonic += " abandon" - mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" "), wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) assert mnemonic1 == mnemonic2 From 752d5c404bf8c14ab44da6321cbe934ac40be149 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 16 Jul 2023 22:00:14 -0500 Subject: [PATCH 02/17] Update dice_verification.md --- docs/dice_verification.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dice_verification.md b/docs/dice_verification.md index d5a23cb77..de699feb3 100644 --- a/docs/dice_verification.md +++ b/docs/dice_verification.md @@ -205,8 +205,9 @@ We double-checked in two different web tools implementing different methods for So congratulations if the fingerprints, zpubs and addresses all match up in your example so you can be much more confident that nothing is wrong with your generated seed. -### SeedSigner verification script +### Command Line Tool _(for more advanced/python-savvy users)_ +Run the exact same SeedSigner mnemonic generation code from the command line to quickly test and externally verify the results. Install the `embit` dependency: ``` From 1d399b577d9b053b503ec578e6f325e401960adc Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 16 Jul 2023 22:05:14 -0500 Subject: [PATCH 03/17] cleanup --- src/seedsigner/helpers/mnemonic_generation.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index 9a7caa639..9a96be2bd 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -63,13 +63,7 @@ def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = WORDL mnemonic = re.findall(r'[^,\s]+', mnemonic) if len(mnemonic) in [11, 23]: - if wordlist_language_code == WORDLIST_LANGUAGE__ENGLISH: - temp_final_word = "abandon" - else: - # Nested import to avoid dependency on Seed model when running this script standalone - from seedsigner.models import Seed - temp_final_word = Seed.get_wordlist(wordlist_language_code)[0] - + temp_final_word = _get_wordlist(wordlist_language_code)[0] mnemonic.append(temp_final_word) if len(mnemonic) not in [12, 24]: From b04ef6dc33f66288f4b0c77d2f38a09eb41b2166 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 16 Jul 2023 22:07:59 -0500 Subject: [PATCH 04/17] cleanup --- src/seedsigner/helpers/mnemonic_generation.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index 9a96be2bd..c35ea6843 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -138,14 +138,7 @@ def get_partial_final_word(coin_flips: str, wordlist_language_code: str = WORDLI binary_string = coin_flips + "0" * (11 - len(coin_flips)) wordlist_index = int(binary_string, 2) - if wordlist_language_code == WORDLIST_LANGUAGE__ENGLISH: - wordlist = WORDLIST__ENGLISH - else: - from seedsigner.models import Seed, SettingsConstants - wordlist = Seed.get_wordlist(wordlist_language_code) - - return wordlist[wordlist_index] - + return _get_wordlist(wordlist_language_code)[wordlist_index] From 9b640ade2d509a773bde3594b0eb72595857d355 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 16 Jul 2023 22:12:23 -0500 Subject: [PATCH 05/17] cleanup --- tests/test_mnemonic_generation.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_mnemonic_generation.py b/tests/test_mnemonic_generation.py index 000ce4d7e..a1f46e85d 100644 --- a/tests/test_mnemonic_generation.py +++ b/tests/test_mnemonic_generation.py @@ -73,25 +73,25 @@ def test_calculate_checksum_invalid_mnemonics(): with pytest.raises(Exception) as e: # Mnemonic is too short: 10 words instead of 11 partial_mnemonic = "abandon " * 9 + "about" - mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) + mnemonic_generation.calculate_checksum(partial_mnemonic) assert "12- or 24-word" in str(e) with pytest.raises(Exception) as e: # Valid mnemonic but unsupported length mnemonic = "devote myth base logic dust horse nut collect buddy element eyebrow visit empty dress jungle" - mnemonic_generation.calculate_checksum(mnemonic.split(" ")) + mnemonic_generation.calculate_checksum(mnemonic) assert "12- or 24-word" in str(e) with pytest.raises(Exception) as e: # Mnemonic is too short: 22 words instead of 23 partial_mnemonic = "abandon " * 21 + "about" - mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) + mnemonic_generation.calculate_checksum(partial_mnemonic) assert "12- or 24-word" in str(e) with pytest.raises(ValueError) as e: # Invalid BIP-39 word partial_mnemonic = "foobar " * 11 + "about" - mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) + mnemonic_generation.calculate_checksum(partial_mnemonic) assert "not in the dictionary" in str(e) @@ -101,17 +101,17 @@ def test_calculate_checksum_with_default_final_word(): the mnemonic. """ partial_mnemonic = "crawl focus rescue cable view pledge rather dinner cousin unfair day" - mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) + mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic) partial_mnemonic += " abandon" - mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) + mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic) assert mnemonic1 == mnemonic2 partial_mnemonic = "bubble father debate ankle injury fence mesh evolve section wet coyote violin pyramid flower rent arrow round clutch myth safe base skin mobile" - mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) + mnemonic1 = mnemonic_generation.calculate_checksum(partial_mnemonic) partial_mnemonic += " abandon" - mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic.split(" ")) + mnemonic2 = mnemonic_generation.calculate_checksum(partial_mnemonic) assert mnemonic1 == mnemonic2 From d94ffb58c8511e388801c08ba2e35177c692c816 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 7 Aug 2023 10:08:08 -0500 Subject: [PATCH 06/17] Remove Epilogue that had already been edited out --- docs/dice_verification.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/dice_verification.md b/docs/dice_verification.md index de699feb3..93a14be00 100644 --- a/docs/dice_verification.md +++ b/docs/dice_verification.md @@ -256,7 +256,3 @@ python3 mnemonic_generation.py -h # GENERATE 256 random coin flips / 24-word mnemonic python3 mnemonic_generation.py coins rand24 ``` - - -### Epilogue -You can use these methods to do dry run time to time to verify that no one has changed the micro sdcard. But do not use the generated 24 words as a valid wallet, they need to be generated alone, only on the seedsigner! From f696ec2fd85bbe93d0fe04800f02fc5ece22774e Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 7 Aug 2023 10:29:47 -0500 Subject: [PATCH 07/17] Update dice_verification.md --- docs/dice_verification.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/dice_verification.md b/docs/dice_verification.md index 93a14be00..e36e4c1d6 100644 --- a/docs/dice_verification.md +++ b/docs/dice_verification.md @@ -205,8 +205,11 @@ We double-checked in two different web tools implementing different methods for So congratulations if the fingerprints, zpubs and addresses all match up in your example so you can be much more confident that nothing is wrong with your generated seed. -### Command Line Tool +--- + +# Command Line Tool _(for more advanced/python-savvy users)_ + Run the exact same SeedSigner mnemonic generation code from the command line to quickly test and externally verify the results. Install the `embit` dependency: From 2a15925aea4481f533c7a1552da0233f3f979af3 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Mon, 7 Aug 2023 19:58:14 -0500 Subject: [PATCH 08/17] How to verify in iancoleman.io --- docs/dice_verification.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/dice_verification.md b/docs/dice_verification.md index e36e4c1d6..79ca82cbc 100644 --- a/docs/dice_verification.md +++ b/docs/dice_verification.md @@ -259,3 +259,12 @@ python3 mnemonic_generation.py -h # GENERATE 256 random coin flips / 24-word mnemonic python3 mnemonic_generation.py coins rand24 ``` + +### How to get the same results in iancoleman.io +Always specify your expected length in the "Mnemonic Length" droplist (defaults to "Use Raw Entropy (3 words per 32 bits)"). + +Dice Rolls: Do NOT use the "Dice [1-6]" option; select "Base 10 [0-9]" or "Hex [0-9A-F]" + +Zero-indexed dice rolls: Select "Base 6 [0-5]", "Base 10 [0-9]", or "Hex [0-9A-F]" + +Coin Flips: Select "Binary [0-1]", "Base 6 [0-5]", "Base 10 [0-9]", or "Hex [0-9A-F]" From 6fb8a351c421927680b07fd16cd1532e20e4a3cb Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 11 Aug 2023 08:43:57 -0500 Subject: [PATCH 09/17] comment correction --- src/seedsigner/helpers/mnemonic_generation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index c35ea6843..04b5d598c 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -132,8 +132,8 @@ def generate_mnemonic_from_coin_flips(coin_flips: str, wordlist_language_code: s def get_partial_final_word(coin_flips: str, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> str: """ Look up the partial final word for the given coin flips. - 7 coin flips: 0101010 + 0000 where the final 4 bits will be replaced with the checksum - 3 coin flips: 0101 + 0000000 where the final 8 bits will be replaced with the checksum + 7 coin flips: 0101010 + **** where the final 4 bits will be replaced with the checksum + 3 coin flips: 010 + ******** where the final 8 bits will be replaced with the checksum """ binary_string = coin_flips + "0" * (11 - len(coin_flips)) wordlist_index = int(binary_string, 2) From 9535f21a7ed744ae891bac69c55de591acf9e2d8 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 28 Jan 2024 10:54:03 -0600 Subject: [PATCH 10/17] refactor script into tools/ --- docs/dice_verification.md | 14 +- src/seedsigner/helpers/mnemonic_generation.py | 159 ---------------- tools/mnemonic.py | 169 ++++++++++++++++++ 3 files changed, 178 insertions(+), 164 deletions(-) create mode 100644 tools/mnemonic.py diff --git a/docs/dice_verification.md b/docs/dice_verification.md index 79ca82cbc..bc26c6eba 100644 --- a/docs/dice_verification.md +++ b/docs/dice_verification.md @@ -212,15 +212,19 @@ _(for more advanced/python-savvy users)_ Run the exact same SeedSigner mnemonic generation code from the command line to quickly test and externally verify the results. -Install the `embit` dependency: -``` +Create a python virtualenv (out of the scope of this doc) and install dependencies: +```bash pip3 install embit + +# Install the main project code to make it importable +pip3 install -e . ``` + Then run the utility script with `-h` to view the usage instructions: -``` -cd src/seedsigner/helpers -python3 mnemonic_generation.py -h +```bash +cd tools +python3 mnemonic.py -h ``` ``` diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index 04b5d598c..2cc959509 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -1,9 +1,7 @@ import hashlib -import random import unicodedata from embit import bip39 -from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes from embit.wordlists.bip39 import WORDLIST as WORDLIST__ENGLISH """ @@ -152,160 +150,3 @@ def generate_mnemonic_from_image(image, wordlist_language_code: str = WORDLIST_L return bip39.mnemonic_from_bytes(hash.digest(), wordlist=_get_wordlist(wordlist_language_code)).split() - -if __name__ == "__main__": - import argparse - - usage = f""" - Verify SeedSigner's dice rolls and coin flip entropy-to-mnemonic conversion via this tool. - - Compare its results against iancoleman.io/bip39 and bitcoiner.guide/seed - - Usage: - # {DICE__NUM_ROLLS__12WORD} dice rolls / 12-word mnemonic - python3 mnemonic_generation.py dice 5624433434... - - # {DICE__NUM_ROLLS__24WORD} dice rolls / 24-word mnemonic - python3 mnemonic_generation.py dice 6151463561... - - # {DICE__NUM_ROLLS__12WORD} dice rolls, entered as 0-5 / 12-word mnemonic - python3 mnemonic_generation.py --zero-indexed-dice dice 5135535514... - - # 128 coin flips / 12-word mnemonic - python3 mnemonic_generation.py coins 1111100111... - - # 256 coin flips / 24-word mnemonic - python mnemonic_generation.py coins 0010111010... - - # GENERATE {DICE__NUM_ROLLS__12WORD} random dice rolls / 12-word mnemonic - python3 mnemonic_generation.py dice rand12 - - # GENERATE {DICE__NUM_ROLLS__24WORD} random dice rolls / 24-word mnemonic - python3 mnemonic_generation.py dice rand24 - - # GENERATE {DICE__NUM_ROLLS__24WORD} random dice rolls, entered as 0-5 / 24-word mnemonic - python3 mnemonic_generation.py --zero-indexed-dice dice rand24 - - # GENERATE 128 random coin flips / 12-word mnemonic - python3 mnemonic_generation.py coins rand12 - - # GENERATE 256 random coin flips / 24-word mnemonic - python3 mnemonic_generation.py coins rand24 - """ - RAND_12 = "rand12" - RAND_24 = "rand24" - parser = argparse.ArgumentParser(description=f'SeedSigner entropy-to-mnemonic tool\n\n{usage}', formatter_class=argparse.RawTextHelpFormatter) - - # Required positional arguments - parser.add_argument('method', type=str, choices=['dice', 'coins', 'final_word'], help="Input entropy method") - parser.add_argument('entropy', type=str, help=f"""Entropy data. Enter "{ RAND_12 }" or "{ RAND_24 }" to create a random (not-secure) example seed.""") - - # Optional arguments - parser.add_argument('-z', '--zero-indexed-dice', - action="store_true", - default=False, - dest="zero_indexed_dice", - help="Enables dice entry as [0-5] instead of default [1-6]") - - args = parser.parse_args() - - method = args.method - entropy = args.entropy - zero_indexed_dice = args.zero_indexed_dice - - is_rand_seed = 'rand' in entropy - if is_rand_seed: - # Generate random data as our entropy - if entropy not in [RAND_12, RAND_24]: - print(f"""Invalid random entropy value: Must be either "{RAND_12}" or "{RAND_24}".""") - exit(1) - mnemonic_length = 12 if entropy == RAND_12 else 24 - - if method == 'dice': - if zero_indexed_dice: - entropy = ''.join([str(random.randint(0, 5)) for i in range(DICE__NUM_ROLLS__12WORD if mnemonic_length == 12 else DICE__NUM_ROLLS__24WORD)]) - else: - entropy = ''.join([str(random.randint(1, 6)) for i in range(DICE__NUM_ROLLS__12WORD if mnemonic_length == 12 else DICE__NUM_ROLLS__24WORD)]) - - elif method == 'coins': - entropy = ''.join([str(random.randint(0, 1)) for i in range(128 if mnemonic_length == 12 else 256)]) - - elif method == 'final_word': - random_dice_rolls = ''.join([str(random.randint(0, 1)) for i in range(128 if mnemonic_length == 12 else 256)]) - entropy = " ".join(generate_mnemonic_from_coin_flips(random_dice_rolls)[:-1]) - print(len(entropy.split()), entropy) - - if method == 'dice': - if not zero_indexed_dice and ('0' in entropy or '6' not in entropy): - print("Dice entry must be 1-6 unless --zero-indexed-dice is specified") - exit(1) - if len(entropy) not in [DICE__NUM_ROLLS__12WORD, DICE__NUM_ROLLS__24WORD]: - print(f"Dice entropy must be {DICE__NUM_ROLLS__12WORD} or {DICE__NUM_ROLLS__24WORD} rolls") - exit(1) - mnemonic = generate_mnemonic_from_dice(entropy) - - elif method == 'coins': - if len(entropy) not in [128, 256]: - print("Coin flip entropy must be 128 or 256 flips") - exit(1) - mnemonic = generate_mnemonic_from_coin_flips(entropy) - - elif method == 'final_word': - num_input_words = len(entropy.split()) - if num_input_words not in [11, 12, 23, 24]: - print(f"Final word entropy must be 11, 12, 23, or 24 words ({num_input_words} provided)") - exit(1) - - if num_input_words in [11, 23]: - # We need to fill the last bits of entropy - if num_input_words == 11: - # 7 final bits of entropy in the 12th word (7 + 4-bit checksum) - num_final_entropy_bits = 7 - else: - # 3 final bits of entropy in the 24th word (3 + 8-bit checksum) - num_final_entropy_bits = 3 - - final_entropy_method = None - while final_entropy_method not in ['1', '2', '3']: - final_entropy_method = input(f""" - How would you like to fill the final {num_final_entropy_bits} bits of entropy? - - 1.) {num_final_entropy_bits} coin flips - 2.) Select an additional word from the wordlist - 3.) Fill with zeros - - Type 1, 2, or 3: """) - - if final_entropy_method == '1': - coin_flips = input(f""" Enter {num_final_entropy_bits} coin flips as 0 or 1 (e.g. { "".join(str(random.randint(0,1)) for f in range(0, num_final_entropy_bits)) }): """) - if len(coin_flips) != num_final_entropy_bits: - print(f"Invalid number of coin flips: needed {num_final_entropy_bits}, got {len(coin_flips)}") - final_word = get_partial_final_word(coin_flips) - entropy += f" {final_word}" - - elif final_entropy_method == '2': - final_word = input(f""" Enter the final word: """) - if final_word not in WORDLIST__ENGLISH: - print(f"Invalid word: {final_word}") - exit(1) - entropy += f" {final_word}" - - elif final_entropy_method == '3': - # Nothing to do; just pass the 11 or 23 words straight to calculate_checksum - pass - - mnemonic = calculate_checksum(entropy) - - print("\n") - if is_rand_seed: - print("\t***** This is a demo seed. Do not use it to store funds!!! *****") - - print(f"""\t{" ".join(mnemonic)}\n""") - - if is_rand_seed: - print(f"\tEntropy: {entropy}\n") - - if method == "dice": - print(f"""\tVerify at iancoleman.io/bip39 or bitcoiner.guide/seed using "Base 10" or "Hex" mode.\n""") - elif method == "coins": - print(f"""\tVerify at iancoleman.io/bip39 or bitcoiner.guide/seed using "Binary" mode.\n""") \ No newline at end of file diff --git a/tools/mnemonic.py b/tools/mnemonic.py new file mode 100644 index 000000000..3e46f792f --- /dev/null +++ b/tools/mnemonic.py @@ -0,0 +1,169 @@ +import argparse +import random +from seedsigner.helpers import mnemonic_generation +from embit.wordlists.bip39 import WORDLIST as WORDLIST__ENGLISH + +""" +see: docs/dice_verification.md (the "Command Line Tool" section) for full instructions. + +tldr: + pip3 install embit + pip3 install -e . + cd tools + python3 mnemonic.py -h +""" + + +usage = f""" +Verify SeedSigner's dice rolls and coin flip entropy-to-mnemonic conversion via this tool. + +Compare its results against iancoleman.io/bip39 and bitcoiner.guide/seed + +Usage: + # {mnemonic_generation.DICE__NUM_ROLLS__12WORD} dice rolls / 12-word mnemonic + python3 mnemonic_generation.py dice 5624433434... + + # {mnemonic_generation.DICE__NUM_ROLLS__24WORD} dice rolls / 24-word mnemonic + python3 mnemonic_generation.py dice 6151463561... + + # {mnemonic_generation.DICE__NUM_ROLLS__12WORD} dice rolls, entered as 0-5 / 12-word mnemonic + python3 mnemonic_generation.py --zero-indexed-dice dice 5135535514... + + # 128 coin flips / 12-word mnemonic + python3 mnemonic_generation.py coins 1111100111... + + # 256 coin flips / 24-word mnemonic + python mnemonic_generation.py coins 0010111010... + + # GENERATE {mnemonic_generation.DICE__NUM_ROLLS__12WORD} random dice rolls / 12-word mnemonic + python3 mnemonic_generation.py dice rand12 + + # GENERATE {mnemonic_generation.DICE__NUM_ROLLS__24WORD} random dice rolls / 24-word mnemonic + python3 mnemonic_generation.py dice rand24 + + # GENERATE {mnemonic_generation.DICE__NUM_ROLLS__24WORD} random dice rolls, entered as 0-5 / 24-word mnemonic + python3 mnemonic_generation.py --zero-indexed-dice dice rand24 + + # GENERATE 128 random coin flips / 12-word mnemonic + python3 mnemonic_generation.py coins rand12 + + # GENERATE 256 random coin flips / 24-word mnemonic + python3 mnemonic_generation.py coins rand24 +""" +RAND_12 = "rand12" +RAND_24 = "rand24" +parser = argparse.ArgumentParser(description=f'SeedSigner entropy-to-mnemonic tool\n\n{usage}', formatter_class=argparse.RawTextHelpFormatter) + +# Required positional arguments +parser.add_argument('method', type=str, choices=['dice', 'coins', 'final_word'], help="Input entropy method") +parser.add_argument('entropy', type=str, help=f"""Entropy data. Enter "{ RAND_12 }" or "{ RAND_24 }" to create a random (not-secure) example seed.""") + +# Optional arguments +parser.add_argument('-z', '--zero-indexed-dice', + action="store_true", + default=False, + dest="zero_indexed_dice", + help="Enables dice entry as [0-5] instead of default [1-6]") + +args = parser.parse_args() + +method = args.method +entropy = args.entropy +zero_indexed_dice = args.zero_indexed_dice + +is_rand_seed = 'rand' in entropy +if is_rand_seed: + # Generate random data as our entropy + if entropy not in [RAND_12, RAND_24]: + print(f"""Invalid random entropy value: Must be either "{RAND_12}" or "{RAND_24}".""") + exit(1) + mnemonic_length = 12 if entropy == RAND_12 else 24 + + if method == 'dice': + if zero_indexed_dice: + entropy = ''.join([str(random.randint(0, 5)) for i in range(mnemonic_generation.DICE__NUM_ROLLS__12WORD if mnemonic_length == 12 else mnemonic_generation.DICE__NUM_ROLLS__24WORD)]) + else: + entropy = ''.join([str(random.randint(1, 6)) for i in range(mnemonic_generation.DICE__NUM_ROLLS__12WORD if mnemonic_length == 12 else mnemonic_generation.DICE__NUM_ROLLS__24WORD)]) + + elif method == 'coins': + entropy = ''.join([str(random.randint(0, 1)) for i in range(128 if mnemonic_length == 12 else 256)]) + + elif method == 'final_word': + random_dice_rolls = ''.join([str(random.randint(0, 1)) for i in range(128 if mnemonic_length == 12 else 256)]) + entropy = " ".join(mnemonic_generation.generate_mnemonic_from_coin_flips(random_dice_rolls)[:-1]) + print(len(entropy.split()), entropy) + +if method == 'dice': + if not zero_indexed_dice and ('0' in entropy or '6' not in entropy): + print("Dice entry must be 1-6 unless --zero-indexed-dice is specified") + exit(1) + if len(entropy) not in [mnemonic_generation.DICE__NUM_ROLLS__12WORD, mnemonic_generation.DICE__NUM_ROLLS__24WORD]: + print(f"Dice entropy must be {mnemonic_generation.DICE__NUM_ROLLS__12WORD} or {mnemonic_generation.DICE__NUM_ROLLS__24WORD} rolls") + exit(1) + mnemonic = mnemonic_generation.generate_mnemonic_from_dice(entropy) + +elif method == 'coins': + if len(entropy) not in [128, 256]: + print("Coin flip entropy must be 128 or 256 flips") + exit(1) + mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(entropy) + +elif method == 'final_word': + num_input_words = len(entropy.split()) + if num_input_words not in [11, 12, 23, 24]: + print(f"Final word entropy must be 11, 12, 23, or 24 words ({num_input_words} provided)") + exit(1) + + if num_input_words in [11, 23]: + # We need to fill the last bits of entropy + if num_input_words == 11: + # 7 final bits of entropy in the 12th word (7 + 4-bit checksum) + num_final_entropy_bits = 7 + else: + # 3 final bits of entropy in the 24th word (3 + 8-bit checksum) + num_final_entropy_bits = 3 + + final_entropy_method = None + while final_entropy_method not in ['1', '2', '3']: + final_entropy_method = input(f""" +How would you like to fill the final {num_final_entropy_bits} bits of entropy? + +1.) {num_final_entropy_bits} coin flips +2.) Select an additional word from the wordlist +3.) Fill with zeros + +Type 1, 2, or 3: """) + + if final_entropy_method == '1': + coin_flips = input(f""" Enter {num_final_entropy_bits} coin flips as 0 or 1 (e.g. { "".join(str(random.randint(0,1)) for f in range(0, num_final_entropy_bits)) }): """) + if len(coin_flips) != num_final_entropy_bits: + print(f"Invalid number of coin flips: needed {num_final_entropy_bits}, got {len(coin_flips)}") + final_word = mnemonic_generation.get_partial_final_word(coin_flips) + entropy += f" {final_word}" + + elif final_entropy_method == '2': + final_word = input(f""" Enter the final word: """) + if final_word not in WORDLIST__ENGLISH: + print(f"Invalid word: {final_word}") + exit(1) + entropy += f" {final_word}" + + elif final_entropy_method == '3': + # Nothing to do; just pass the 11 or 23 words straight to calculate_checksum + pass + + mnemonic = mnemonic_generation.calculate_checksum(entropy) + +print("\n") +if is_rand_seed: + print("\t***** This is a demo seed. Do not use it to store funds!!! *****") + +print(f"""\t{" ".join(mnemonic)}\n""") + +if is_rand_seed: + print(f"\tEntropy: {entropy}\n") + +if method == "dice": + print(f"""\tVerify at iancoleman.io/bip39 or bitcoiner.guide/seed using "Base 10" or "Hex" mode.\n""") +elif method == "coins": + print(f"""\tVerify at iancoleman.io/bip39 or bitcoiner.guide/seed using "Binary" mode.\n""") From 1ed703b8bc3d44948b4bc39200628bb92a97b7c8 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sun, 28 Jan 2024 11:00:37 -0600 Subject: [PATCH 11/17] cleanup --- src/seedsigner/helpers/mnemonic_generation.py | 44 +++++++------------ 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index 2cc959509..aa124a33b 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -3,41 +3,29 @@ from embit import bip39 from embit.wordlists.bip39 import WORDLIST as WORDLIST__ENGLISH +from seedsigner.models.settings_definition import SettingsConstants """ - This is SeedSigner's internal mnemonic generation utility but is also meant to - function as an independently-executable CLI to facilitate external verification of - SeedSigner's mnemonic generation for a given input entropy. - - Therefore its module imports should be kept to the bare minimum. - - ## Running as a standalone script - Install the `embit` library: - ``` - pip3 install embit - ``` - - And then run: - ``` - python3 mnemonic_generation.py -h - ``` -""" - + This is SeedSigner's internal mnemonic generation utility. + + It can also be run as an independently-executable CLI to facilitate external + verification of SeedSigner's results for a given input entropy. -# Hard-coded value from SettingsConstants to avoid dependencies -WORDLIST_LANGUAGE__ENGLISH = 'en' + see: docs/dice_verification.md (the "Command Line Tool" section). +""" DICE__NUM_ROLLS__12WORD = 50 DICE__NUM_ROLLS__24WORD = 99 + def _get_wordlist(wordlist_language_code) -> list[str]: """ Convenience method to fetch the wordlist for the given language code without requiring any SeedSigner module dependencies for when this is run as a standalone CLI. """ - if wordlist_language_code == WORDLIST_LANGUAGE__ENGLISH: + if wordlist_language_code == SettingsConstants.WORDLIST_LANGUAGE__ENGLISH: return WORDLIST__ENGLISH else: # Nested import to avoid dependency on Seed model when running this script standalone @@ -46,7 +34,7 @@ def _get_wordlist(wordlist_language_code) -> list[str]: -def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> list[str]: +def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]: """ Provide 12- or 24-word mnemonic, returns complete mnemonic w/checksum as a list. @@ -83,12 +71,12 @@ def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = WORDL -def generate_mnemonic_from_bytes(entropy_bytes, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> list[str]: +def generate_mnemonic_from_bytes(entropy_bytes, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]: return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=_get_wordlist(wordlist_language_code)).split() -def generate_mnemonic_from_dice(roll_data: str, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> list[str]: +def generate_mnemonic_from_dice(roll_data: str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]: """ Takes a string of 50 or 99 dice rolls and returns a 12- or 24-word mnemonic. @@ -109,7 +97,7 @@ def generate_mnemonic_from_dice(roll_data: str, wordlist_language_code: str = WO -def generate_mnemonic_from_coin_flips(coin_flips: str, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> list[str]: +def generate_mnemonic_from_coin_flips(coin_flips: str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]: """ Takes a string of 128 or 256 0s and 1s and returns a 12- or 24-word mnemonic. @@ -128,7 +116,7 @@ def generate_mnemonic_from_coin_flips(coin_flips: str, wordlist_language_code: s -def get_partial_final_word(coin_flips: str, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> str: +def get_partial_final_word(coin_flips: str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> str: """ Look up the partial final word for the given coin flips. 7 coin flips: 0101010 + **** where the final 4 bits will be replaced with the checksum 3 coin flips: 010 + ******** where the final 8 bits will be replaced with the checksum @@ -142,11 +130,9 @@ def get_partial_final_word(coin_flips: str, wordlist_language_code: str = WORDLI # Note: This currently isn't being used since we're now chaining hashed bytes for the # image-based entropy and aren't just ingesting a single image. -def generate_mnemonic_from_image(image, wordlist_language_code: str = WORDLIST_LANGUAGE__ENGLISH) -> list[str]: +def generate_mnemonic_from_image(image, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]: import hashlib hash = hashlib.sha256(image.tobytes()) # Return as a list return bip39.mnemonic_from_bytes(hash.digest(), wordlist=_get_wordlist(wordlist_language_code)).split() - - From b29e0bcf50f4d4a94a17ab30b29a493d73cabb8c Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 2 Mar 2024 07:56:49 -0600 Subject: [PATCH 12/17] Simplification; no longer need to reduce imports --- src/seedsigner/helpers/mnemonic_generation.py | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index aa124a33b..8a5600491 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -4,6 +4,7 @@ from embit import bip39 from embit.wordlists.bip39 import WORDLIST as WORDLIST__ENGLISH from seedsigner.models.settings_definition import SettingsConstants +from seedsigner.models.seed import Seed """ This is SeedSigner's internal mnemonic generation utility. @@ -19,21 +20,6 @@ -def _get_wordlist(wordlist_language_code) -> list[str]: - """ - Convenience method to fetch the wordlist for the given language code without - requiring any SeedSigner module dependencies for when this is run as a - standalone CLI. - """ - if wordlist_language_code == SettingsConstants.WORDLIST_LANGUAGE__ENGLISH: - return WORDLIST__ENGLISH - else: - # Nested import to avoid dependency on Seed model when running this script standalone - from seedsigner.models import Seed - return Seed.get_wordlist(wordlist_language_code) - - - def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]: """ Provide 12- or 24-word mnemonic, returns complete mnemonic w/checksum as a list. @@ -49,7 +35,7 @@ def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = Setti mnemonic = re.findall(r'[^,\s]+', mnemonic) if len(mnemonic) in [11, 23]: - temp_final_word = _get_wordlist(wordlist_language_code)[0] + temp_final_word = Seed.get_wordlist(wordlist_language_code)[0] mnemonic.append(temp_final_word) if len(mnemonic) not in [12, 24]: @@ -61,7 +47,7 @@ def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = Setti # Convert the resulting mnemonic to bytes, but we `ignore_checksum` validation # because we assume it's incorrect since we either let the user select their own # final word OR we injected the 0000 word from the wordlist. - mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=_get_wordlist(wordlist_language_code)) + mnemonic_bytes = bip39.mnemonic_to_bytes(unicodedata.normalize("NFKD", " ".join(mnemonic_copy)), ignore_checksum=True, wordlist=Seed.get_wordlist(wordlist_language_code)) # This function will convert the bytes back into a mnemonic, but it will also # calculate the proper checksum bits while doing so. For a 12-word seed it will just @@ -72,7 +58,7 @@ def calculate_checksum(mnemonic: list | str, wordlist_language_code: str = Setti def generate_mnemonic_from_bytes(entropy_bytes, wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]: - return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=_get_wordlist(wordlist_language_code)).split() + return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=Seed.get_wordlist(wordlist_language_code)).split() @@ -93,7 +79,7 @@ def generate_mnemonic_from_dice(roll_data: str, wordlist_language_code: str = Se entropy_bytes = entropy_bytes[:16] # Return as a list - return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=_get_wordlist(wordlist_language_code)).split() + return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=Seed.get_wordlist(wordlist_language_code)).split() @@ -112,7 +98,7 @@ def generate_mnemonic_from_coin_flips(coin_flips: str, wordlist_language_code: s entropy_bytes = entropy_bytes[:16] # Return as a list - return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=_get_wordlist(wordlist_language_code)).split() + return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=Seed.get_wordlist(wordlist_language_code)).split() @@ -124,7 +110,7 @@ def get_partial_final_word(coin_flips: str, wordlist_language_code: str = Settin binary_string = coin_flips + "0" * (11 - len(coin_flips)) wordlist_index = int(binary_string, 2) - return _get_wordlist(wordlist_language_code)[wordlist_index] + return Seed.get_wordlist(wordlist_language_code)[wordlist_index] @@ -135,4 +121,4 @@ def generate_mnemonic_from_image(image, wordlist_language_code: str = SettingsCo hash = hashlib.sha256(image.tobytes()) # Return as a list - return bip39.mnemonic_from_bytes(hash.digest(), wordlist=_get_wordlist(wordlist_language_code)).split() + return bip39.mnemonic_from_bytes(hash.digest(), wordlist=Seed.get_wordlist(wordlist_language_code)).split() From 5160418449ed1a7ce8b9143406431aa97d59bf70 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 2 Mar 2024 07:57:39 -0600 Subject: [PATCH 13/17] Docs fixup after moving CLI to its own file --- docs/dice_verification.md | 20 ++++++++++---------- tools/mnemonic.py | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/dice_verification.md b/docs/dice_verification.md index bc26c6eba..7e3833b46 100644 --- a/docs/dice_verification.md +++ b/docs/dice_verification.md @@ -234,34 +234,34 @@ python3 mnemonic.py -h Usage: # 50 dice rolls / 12-word mnemonic - python3 mnemonic_generation.py dice 5624433434... + python3 mnemonic.py dice 5624433434... # 99 dice rolls / 24-word mnemonic - python3 mnemonic_generation.py dice 6151463561... + python3 mnemonic.py dice 6151463561... # 50 dice rolls, entered as 0-5 / 12-word mnemonic - python3 mnemonic_generation.py --zero-indexed-dice dice 5135535514... + python3 mnemonic.py --zero-indexed-dice dice 5135535514... # 128 coin flips / 12-word mnemonic - python3 mnemonic_generation.py coins 1111100111... + python3 mnemonic.py coins 1111100111... # 256 coin flips / 24-word mnemonic - python mnemonic_generation.py coins 0010111010... + python mnemonic.py coins 0010111010... # GENERATE 50 random dice rolls / 12-word mnemonic - python3 mnemonic_generation.py dice rand12 + python3 mnemonic.py dice rand12 # GENERATE 99 random dice rolls / 24-word mnemonic - python3 mnemonic_generation.py dice rand24 + python3 mnemonic.py dice rand24 # GENERATE 99 random dice rolls, entered as 0-5 / 24-word mnemonic - python3 mnemonic_generation.py --zero-indexed-dice dice rand24 + python3 mnemonic.py --zero-indexed-dice dice rand24 # GENERATE 128 random coin flips / 12-word mnemonic - python3 mnemonic_generation.py coins rand12 + python3 mnemonic.py coins rand12 # GENERATE 256 random coin flips / 24-word mnemonic - python3 mnemonic_generation.py coins rand24 + python3 mnemonic.py coins rand24 ``` ### How to get the same results in iancoleman.io diff --git a/tools/mnemonic.py b/tools/mnemonic.py index 3e46f792f..28490e943 100644 --- a/tools/mnemonic.py +++ b/tools/mnemonic.py @@ -21,34 +21,34 @@ Usage: # {mnemonic_generation.DICE__NUM_ROLLS__12WORD} dice rolls / 12-word mnemonic - python3 mnemonic_generation.py dice 5624433434... + python3 mnemonic.py dice 5624433434... # {mnemonic_generation.DICE__NUM_ROLLS__24WORD} dice rolls / 24-word mnemonic - python3 mnemonic_generation.py dice 6151463561... + python3 mnemonic.py dice 6151463561... # {mnemonic_generation.DICE__NUM_ROLLS__12WORD} dice rolls, entered as 0-5 / 12-word mnemonic - python3 mnemonic_generation.py --zero-indexed-dice dice 5135535514... + python3 mnemonic.py --zero-indexed-dice dice 5135535514... # 128 coin flips / 12-word mnemonic - python3 mnemonic_generation.py coins 1111100111... + python3 mnemonic.py coins 1111100111... # 256 coin flips / 24-word mnemonic - python mnemonic_generation.py coins 0010111010... + python mnemonic.py coins 0010111010... # GENERATE {mnemonic_generation.DICE__NUM_ROLLS__12WORD} random dice rolls / 12-word mnemonic - python3 mnemonic_generation.py dice rand12 + python3 mnemonic.py dice rand12 # GENERATE {mnemonic_generation.DICE__NUM_ROLLS__24WORD} random dice rolls / 24-word mnemonic - python3 mnemonic_generation.py dice rand24 + python3 mnemonic.py dice rand24 # GENERATE {mnemonic_generation.DICE__NUM_ROLLS__24WORD} random dice rolls, entered as 0-5 / 24-word mnemonic - python3 mnemonic_generation.py --zero-indexed-dice dice rand24 + python3 mnemonic.py --zero-indexed-dice dice rand24 # GENERATE 128 random coin flips / 12-word mnemonic - python3 mnemonic_generation.py coins rand12 + python3 mnemonic.py coins rand12 # GENERATE 256 random coin flips / 24-word mnemonic - python3 mnemonic_generation.py coins rand24 + python3 mnemonic.py coins rand24 """ RAND_12 = "rand12" RAND_24 = "rand24" From c4aafd0bf32bf245d636f18efdebaeafd1fce5ce Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 2 Mar 2024 07:59:16 -0600 Subject: [PATCH 14/17] minor further import cleanup --- src/seedsigner/helpers/mnemonic_generation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index 8a5600491..b3e90680e 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -2,7 +2,6 @@ import unicodedata from embit import bip39 -from embit.wordlists.bip39 import WORDLIST as WORDLIST__ENGLISH from seedsigner.models.settings_definition import SettingsConstants from seedsigner.models.seed import Seed From ba89fd8e8f10798a0ecab5de497436bf6e00c23d Mon Sep 17 00:00:00 2001 From: The Don <135184930+thedon702@users.noreply.github.com> Date: Tue, 5 Mar 2024 22:44:51 +0000 Subject: [PATCH 15/17] add coin icon --- src/seedsigner/gui/components.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index e505635ae..e82d28b05 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -77,6 +77,7 @@ class FontAwesomeIconConstants: CHEVRON_DOWN = "\uf078" CIRCLE = "\uf111" CIRCLE_CHEVRON_RIGHT = "\uf138" + COIN = "\uf51e" DICE = "\uf522" DICE_ONE = "\uf525" DICE_TWO = "\uf528" From d07f21f3696d5e7a446451ec0321fb030d72fb5e Mon Sep 17 00:00:00 2001 From: The Don <135184930+thedon702@users.noreply.github.com> Date: Tue, 5 Mar 2024 22:45:22 +0000 Subject: [PATCH 16/17] add coin flip num variables 128 & 256 --- src/seedsigner/helpers/mnemonic_generation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index b3e90680e..b5f99d456 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -16,6 +16,8 @@ DICE__NUM_ROLLS__12WORD = 50 DICE__NUM_ROLLS__24WORD = 99 +COIN__NUM_FLIPS__12WORD = 128 +COIN__NUM_FLIPS__24WORD = 256 From ccd6cdd63d8d7201033b4909233c36de181dd1ce Mon Sep 17 00:00:00 2001 From: The Don <135184930+thedon702@users.noreply.github.com> Date: Tue, 5 Mar 2024 22:45:41 +0000 Subject: [PATCH 17/17] add coin logic code --- src/seedsigner/views/tools_views.py | 60 ++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index b94c9b46d..53530dbb5 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -27,12 +27,13 @@ class ToolsMenuView(View): IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA) DICE = ("New seed", FontAwesomeIconConstants.DICE) + COIN = ("New seed", FontAwesomeIconConstants.COIN) KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD) EXPLORER = "Address Explorer" ADDRESS = "Verify address" def run(self): - button_data = [self.IMAGE, self.DICE, self.KEYBOARD, self.EXPLORER, self.ADDRESS] + button_data = [self.IMAGE, self.DICE, self.COIN, self.KEYBOARD, self.EXPLORER, self.ADDRESS] selected_menu_num = self.run_screen( ButtonListScreen, @@ -50,6 +51,9 @@ def run(self): elif button_data[selected_menu_num] == self.DICE: return Destination(ToolsDiceEntropyMnemonicLengthView) + elif button_data[selected_menu_num] == self.COIN: + return Destination(ToolsCoinEntropyMnemonicLengthView) + elif button_data[selected_menu_num] == self.KEYBOARD: return Destination(ToolsCalcFinalWordNumWordsView) @@ -234,6 +238,60 @@ def run(self): return Destination(SeedWordsWarningView, view_args={"seed_num": None}, clear_history=True) +"""**************************************************************************** + Coin Flip Views +****************************************************************************""" +class ToolsCoinEntropyMnemonicLengthView(View): + def run(self): + TWELVE = f"12 words ({mnemonic_generation.COIN__NUM_FLIPS__12WORD} flips)" + TWENTY_FOUR = f"24 words ({mnemonic_generation.COIN__NUM_FLIPS__24WORD} flips)" + + button_data = [TWELVE, TWENTY_FOUR] + selected_menu_num = ButtonListScreen( + title="Mnemonic Length", + is_bottom_list=True, + is_button_text_centered=True, + button_data=button_data, + ).display() + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + elif button_data[selected_menu_num] == TWELVE: + return Destination(ToolsCoinEntropyEntryView, view_args=dict(total_flips=mnemonic_generation.COIN__NUM_FLIPS__12WORD)) + + elif button_data[selected_menu_num] == TWENTY_FOUR: + return Destination(ToolsCoinEntropyEntryView, view_args=dict(total_flips=mnemonic_generation.COIN__NUM_FLIPS__24WORD)) + + + +class ToolsCoinEntropyEntryView(View): + def __init__(self, total_flips: int): + super().__init__() + self.total_flips = total_flips + + + def run(self): + ret = ToolsCoinFlipEntryScreen( + return_after_n_chars=self.total_flips, + ).display() + + if ret == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + print(f"Coin Flips: {ret}") + coin_seed_phrase = mnemonic_generation.generate_mnemonic_from_coin_flips(ret) + print(f"""Mnemonic: "{coin_seed_phrase}" """) + + # Add the mnemonic as an in-memory Seed + seed = Seed(coin_seed_phrase, wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)) + self.controller.storage.set_pending_seed(seed) + + # Cannot return BACK to this View + return Destination(SeedWordsWarningView, view_args={"seed_num": None}, clear_history=True) + + + """**************************************************************************** Calc final word Views