Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Create seed with coin flips #619

Open
wants to merge 11 commits into
base: dev
Choose a base branch
from
3 changes: 3 additions & 0 deletions src/seedsigner/gui/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ class FontAwesomeIconConstants:
DICE_FOUR = "\uf524"
DICE_FIVE = "\uf523"
DICE_SIX = "\uf526"
COINS = "\uf51e"
COIN_H = "\u0048" # H for heads
COIN_T = "\u0054" # T for tails
KEYBOARD = "\uf11c"
LOCK = "\uf023"
MAP = "\uf279"
Expand Down
32 changes: 32 additions & 0 deletions src/seedsigner/gui/screens/tools_screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,38 @@ def update_title(self) -> bool:



@dataclass
class ToolsCoinEntropyEntryScreen(KeyboardScreen):
def __post_init__(self):
# Override values set by the parent class
self.title = f"Coin Flip 1/{self.return_after_n_chars}"

# Specify the keys in the keyboard
self.rows = 3
self.cols = 3
self.keyboard_font_name = GUIConstants.ICON_FONT_NAME__FONT_AWESOME
self.keyboard_font_size = None # Force auto-scaling to Key height
self.keys_charset = "".join([
FontAwesomeIconConstants.COIN_H,
FontAwesomeIconConstants.COIN_T,
])

# Map Key display chars to actual output values
self.keys_to_values = {
FontAwesomeIconConstants.COIN_H: "1",
FontAwesomeIconConstants.COIN_T: "0",
}

# Now initialize the parent class
super().__post_init__()


def update_title(self) -> bool:
self.title = f"Coin Flip {self.cursor_position + 1}/{self.return_after_n_chars}"
return True



@dataclass
class ToolsCalcFinalWordFinalizePromptScreen(ButtonListScreen):
mnemonic_length: int = None
Expand Down
2 changes: 2 additions & 0 deletions src/seedsigner/helpers/mnemonic_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

DICE__NUM_ROLLS__12WORD = 50
DICE__NUM_ROLLS__24WORD = 99
COIN__NUM_FLIPS__12WORD = 128
COIN__NUM_FLIPS__24WORD = 256



Expand Down
60 changes: 58 additions & 2 deletions src/seedsigner/views/tools_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerIconConstants
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen)
from seedsigner.gui.screens.tools_screens import (ToolsCalcFinalWordDoneScreen, ToolsCalcFinalWordFinalizePromptScreen,
ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen,
ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsCoinEntropyEntryScreen, ToolsImageEntropyFinalImageScreen,
ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen)
from seedsigner.helpers import embit_utils, mnemonic_generation
from seedsigner.models.encode_qr import GenericStaticQrEncoder
Expand All @@ -28,12 +28,13 @@
class ToolsMenuView(View):
IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA)
DICE = ("New seed", FontAwesomeIconConstants.DICE)
COIN = (" New seed", FontAwesomeIconConstants.COINS)
KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD)
ADDRESS_EXPLORER = "Address Explorer"
VERIFY_ADDRESS = "Verify address"

def run(self):
button_data = [self.IMAGE, self.DICE, self.KEYBOARD, self.ADDRESS_EXPLORER, self.VERIFY_ADDRESS]
button_data = [self.IMAGE, self.DICE, self.COIN, self.KEYBOARD, self.ADDRESS_EXPLORER, self.VERIFY_ADDRESS]

selected_menu_num = self.run_screen(
ButtonListScreen,
Expand All @@ -51,6 +52,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)

Expand Down Expand Up @@ -236,6 +240,58 @@ def run(self):



"""****************************************************************************
Coin flips 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)"
Copy link
Contributor

Choose a reason for hiding this comment

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

Moving these constants to be class-level attrs will allow us to specify 12 vs 24 in various FlowTest scenarios.


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 = ToolsCoinEntropyEntryScreen(
return_after_n_chars=self.total_flips,
).display()

if ret == RET_CODE__BACK_BUTTON:
return Destination(BackStackView)

coin_seed_phrase = mnemonic_generation.generate_mnemonic_from_coin_flips(ret)

# 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
****************************************************************************"""
Expand Down
79 changes: 79 additions & 0 deletions tests/test_mnemonic_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,29 @@ def test_dice_rolls():



def test_coin_flips():
""" Given random coin flips, the resulting mnemonic should be valid. """
coin_flips = ""
for i in range(0, mnemonic_generation.COIN__NUM_FLIPS__24WORD):
# Do not need truly rigorous random for this test
coin_flips += str(random.randint(0, 1))

mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(coin_flips)

assert len(mnemonic) == 24
assert bip39.mnemonic_is_valid(" ".join(mnemonic))

coin_flips = ""
for i in range(0, mnemonic_generation.COIN__NUM_FLIPS__12WORD):
# Do not need truly rigorous random for this test
coin_flips += str(random.randint(0, 1))

mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(coin_flips)
assert len(mnemonic) == 12
assert bip39.mnemonic_is_valid(" ".join(mnemonic))



def test_calculate_checksum_input_type():
"""
Given an 11-word or 23-word mnemonic, the calculated checksum should yield a
Expand Down Expand Up @@ -195,3 +218,59 @@ def test_50_dice_rolls():
actual = " ".join(mnemonic)
assert bip39.mnemonic_is_valid(actual)
assert actual == expected



def test_256_coin_flips():
""" 256 coin flips input should yield the same 24-word mnemonic as iancoleman.io/bip39 """
# Check "Show entropy details", paste in coin flip sequence, click "Binary", select "Mnemonic Length" as "24 Words"
coin_flips = "1010101010101110110001000000001100100011000000000011000000001001110000000000000010000000000110000001110010110010000100110011001010101010101011101100010000000011001000110000000000110000000010011100000000000000100000000001100000011100101100100001001100110010"
expected = "grocery secret mountain turkey moment elbow size castle eagle water nominee general usual awesome attack daring cruise marriage debate tortoise document capital avocado engine"

mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(coin_flips)
actual = " ".join(mnemonic)
assert bip39.mnemonic_is_valid(actual)
assert actual == expected

coin_flips = "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
expected = "gun library main saddle doctor meat pizza bone brave output matter chef merry flag abuse puppy first rotate era tent news arrest pepper finger"

mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(coin_flips)
actual = " ".join(mnemonic)
assert bip39.mnemonic_is_valid(actual)
assert actual == expected

coin_flips = "1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"
expected = "rural oval civil ignore moon glide any pony perfect gain stable flag fortune require roast stereo mad guitar page flat reduce give borrow leisure"

mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(coin_flips)
actual = " ".join(mnemonic)
assert bip39.mnemonic_is_valid(actual)
assert actual == expected



def test_128_coin_flips():
""" 128 coin flips input should yield the same 12-word mnemonic as iancoleman.io/bip39 """
# Check "Show entropy details", paste in coin flip sequence, click "Binary", select "Mnemonic Length" as "12 Words"
coin_flips = "10101010101011101100010000000011001000110000000000110000000010011100000000000000100000000001100000011100101100100001001100110010"
expected = "prevent style echo subway next museum palace lobster toward office shoe unfair"
mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(coin_flips)
actual = " ".join(mnemonic)
assert bip39.mnemonic_is_valid(actual)
assert actual == expected

coin_flips = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
expected = "earth naive tongue material rebel cotton credit quarter market peanut memory other"
mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(coin_flips)
actual = " ".join(mnemonic)
assert bip39.mnemonic_is_valid(actual)
assert actual == expected

coin_flips = "11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"
expected = "exit pulp believe feature horror vehicle home more patrol hair drink resist"
mnemonic = mnemonic_generation.generate_mnemonic_from_coin_flips(coin_flips)
actual = " ".join(mnemonic)
assert bip39.mnemonic_is_valid(actual)
assert actual == expected

Loading