diff --git a/docs/dice_verification.md b/docs/dice_verification.md index 361bf14de..7e3833b46 100644 --- a/docs/dice_verification.md +++ b/docs/dice_verification.md @@ -205,3 +205,70 @@ 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 +_(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. + +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: +```bash +cd tools +python3 mnemonic.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.py dice 5624433434... + + # 99 dice rolls / 24-word mnemonic + python3 mnemonic.py dice 6151463561... + + # 50 dice rolls, entered as 0-5 / 12-word mnemonic + python3 mnemonic.py --zero-indexed-dice dice 5135535514... + + # 128 coin flips / 12-word mnemonic + python3 mnemonic.py coins 1111100111... + + # 256 coin flips / 24-word mnemonic + python mnemonic.py coins 0010111010... + + # GENERATE 50 random dice rolls / 12-word mnemonic + python3 mnemonic.py dice rand12 + + # GENERATE 99 random dice rolls / 24-word mnemonic + python3 mnemonic.py dice rand24 + + # GENERATE 99 random dice rolls, entered as 0-5 / 24-word mnemonic + python3 mnemonic.py --zero-indexed-dice dice rand24 + + # GENERATE 128 random coin flips / 12-word mnemonic + python3 mnemonic.py coins rand12 + + # GENERATE 256 random coin flips / 24-word mnemonic + python3 mnemonic.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]" 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" diff --git a/src/seedsigner/helpers/mnemonic_generation.py b/src/seedsigner/helpers/mnemonic_generation.py index a021a8346..b5f99d456 100644 --- a/src/seedsigner/helpers/mnemonic_generation.py +++ b/src/seedsigner/helpers/mnemonic_generation.py @@ -2,21 +2,42 @@ import unicodedata from embit import bip39 -from embit.bip39 import mnemonic_to_bytes, mnemonic_from_bytes -from typing import List +from seedsigner.models.settings_definition import SettingsConstants +from seedsigner.models.seed import Seed +""" + 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. + see: docs/dice_verification.md (the "Command Line Tool" section). +""" -def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str]: +DICE__NUM_ROLLS__12WORD = 50 +DICE__NUM_ROLLS__24WORD = 99 +COIN__NUM_FLIPS__12WORD = 128 +COIN__NUM_FLIPS__24WORD = 256 + + + +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. + 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]) + 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") @@ -37,28 +58,68 @@ 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 = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) -> list[str]: + return bip39.mnemonic_from_bytes(entropy_bytes, wordlist=Seed.get_wordlist(wordlist_language_code)).split() + +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. -def generate_mnemonic_from_dice(roll_data: str) -> List[str]: + 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=Seed.get_wordlist(wordlist_language_code)).split() + + + +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. + + 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=Seed.get_wordlist(wordlist_language_code)).split() + + + +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 + """ + binary_string = coin_flips + "0" * (11 - len(coin_flips)) + wordlist_index = int(binary_string, 2) + + return Seed.get_wordlist(wordlist_language_code)[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 = SettingsConstants.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=Seed.get_wordlist(wordlist_language_code)).split() diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 12a4493b3..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) @@ -188,8 +192,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 +207,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)) @@ -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 diff --git a/tests/test_mnemonic_generation.py b/tests/test_mnemonic_generation.py index 76298d34f..a1f46e85d 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) 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) 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) 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) 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) 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) 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) 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) assert mnemonic1 == mnemonic2 diff --git a/tools/mnemonic.py b/tools/mnemonic.py new file mode 100644 index 000000000..28490e943 --- /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.py dice 5624433434... + + # {mnemonic_generation.DICE__NUM_ROLLS__24WORD} dice rolls / 24-word mnemonic + python3 mnemonic.py dice 6151463561... + + # {mnemonic_generation.DICE__NUM_ROLLS__12WORD} dice rolls, entered as 0-5 / 12-word mnemonic + python3 mnemonic.py --zero-indexed-dice dice 5135535514... + + # 128 coin flips / 12-word mnemonic + python3 mnemonic.py coins 1111100111... + + # 256 coin flips / 24-word mnemonic + python mnemonic.py coins 0010111010... + + # GENERATE {mnemonic_generation.DICE__NUM_ROLLS__12WORD} random dice rolls / 12-word mnemonic + python3 mnemonic.py dice rand12 + + # GENERATE {mnemonic_generation.DICE__NUM_ROLLS__24WORD} random dice rolls / 24-word mnemonic + python3 mnemonic.py dice rand24 + + # GENERATE {mnemonic_generation.DICE__NUM_ROLLS__24WORD} random dice rolls, entered as 0-5 / 24-word mnemonic + python3 mnemonic.py --zero-indexed-dice dice rand24 + + # GENERATE 128 random coin flips / 12-word mnemonic + python3 mnemonic.py coins rand12 + + # GENERATE 256 random coin flips / 24-word mnemonic + python3 mnemonic.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""")