Skip to content

Commit

Permalink
Merge pull request SeedSigner#197 from kdmukai/final_word_v2
Browse files Browse the repository at this point in the history
Updated "Final Word" calculation flow
  • Loading branch information
newtonick authored Jun 16, 2022
2 parents 739ea82 + a42408a commit 53f3cda
Show file tree
Hide file tree
Showing 6 changed files with 381 additions and 37 deletions.
4 changes: 3 additions & 1 deletion src/seedsigner/gui/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,9 @@ def __post_init__(self):
# fits in its bounding rect (plus accounting for edge padding) using its given
# font.
# Measure from left baseline ("ls")
# getbbox() seems to ignore "\n" so won't affect height calcs
# TODO: getbbox() seems to ignore "\n" so isn't properly factored into height
# calcs and yields incorrect full_text_width. For now must specify self.height to
# render properly. Centering will be wrong.
(left, top, full_text_width, bottom) = self.font.getbbox(self.text, anchor="ls")
self.text_font_height = -1 * top
self.bbox_height = self.text_font_height + bottom
Expand Down
190 changes: 189 additions & 1 deletion src/seedsigner/gui/screens/tools_screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,195 @@ def update_title(self) -> bool:


@dataclass
class ToolsCalcFinalWordShowFinalWordScreen(ButtonListScreen):
class ToolsCalcFinalWordFinalizePromptScreen(ButtonListScreen):
mnemonic_length: int = None
num_entropy_bits: int = None

def __post_init__(self):
self.title = "Build Final Word"
self.is_bottom_list = True
self.is_button_text_centered = True
super().__post_init__()

self.components.append(TextArea(
text=f"The {self.mnemonic_length}th word is built from {self.num_entropy_bits} more entropy bits plus auto-calculated checksum.",
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
))



@dataclass
class ToolsCoinFlipEntryScreen(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 = 1
self.cols = 4
self.key_height = GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 2 + 2*GUIConstants.EDGE_PADDING
self.keys_charset = "10"

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

self.components.append(TextArea(
text="Heads = 1",
screen_y = self.keyboard.rect[3] + 4*GUIConstants.COMPONENT_PADDING,
))
self.components.append(TextArea(
text="Tails = 0",
screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING,
))


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



@dataclass
class ToolsCalcFinalWordScreen(ButtonListScreen):
selected_final_word: str = None
selected_final_bits: str = None
checksum_bits: str = None
actual_final_word: str = None

def __post_init__(self):
self.is_bottom_list = True
super().__post_init__()

# First what's the total bit display width and where do the checksum bits start?
bit_font_size = GUIConstants.BUTTON_FONT_SIZE + 2
font = Fonts.get_font(GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, bit_font_size)
(left, top, bit_display_width, bit_font_height) = font.getbbox("0" * 11, anchor="lt")
(left, top, checksum_x, bottom) = font.getbbox("0" * (11 - len(self.checksum_bits)), anchor="lt")
bit_display_x = int((self.canvas_width - bit_display_width)/2)
checksum_x += bit_display_x

# Display the user's additional entropy input
if self.selected_final_word:
selection_text = self.selected_final_word
keeper_selected_bits = self.selected_final_bits[:11 - len(self.checksum_bits)]

# The word's least significant bits will be rendered differently to convey
# the fact that they're being discarded.
discard_selected_bits = self.selected_final_bits[-1*len(self.checksum_bits):]
else:
# User entered coin flips or all zeros
selection_text = self.selected_final_bits
keeper_selected_bits = self.selected_final_bits

# We'll append spacer chars to preserve the vertical alignment (most
# significant n bits always rendered in same column)
discard_selected_bits = "_" * (len(self.checksum_bits))

self.components.append(TextArea(
text=f"""Your input: \"{selection_text}\"""",
screen_y=self.top_nav.height,
))

# ...and that entropy's associated 11 bits
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
self.components.append(TextArea(
text=keeper_selected_bits,
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
font_size=bit_font_size,
edge_padding=0,
screen_x=bit_display_x,
screen_y=screen_y,
height=bit_font_height,
is_text_centered=False,
))

# Render the least significant bits that will be replaced by the checksum in a
# de-emphasized font color.
self.components.append(TextArea(
text=discard_selected_bits,
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
font_color=GUIConstants.LABEL_FONT_COLOR,
font_size=bit_font_size,
edge_padding=0,
screen_x=checksum_x,
screen_y=screen_y,
height=bit_font_height,
is_text_centered=False,
))

# Show the checksum..
self.components.append(TextArea(
text="Checksum",
edge_padding=0,
screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
))

# ...and its actual bits. Prepend spacers to keep vertical alignment
checksum_spacer = "_" * (11 - len(self.checksum_bits))

screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING

# This time we de-emphasize the prepended spacers that are irrelevant
self.components.append(TextArea(
text=checksum_spacer,
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
font_color=GUIConstants.LABEL_FONT_COLOR,
font_size=bit_font_size,
edge_padding=0,
screen_x=bit_display_x,
screen_y=screen_y,
height=bit_font_height,
is_text_centered=False,
))

# And especially highlight (orange!) the actual checksum bits
self.components.append(TextArea(
text=self.checksum_bits,
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
font_size=bit_font_size,
font_color=GUIConstants.ACCENT_COLOR,
edge_padding=0,
screen_x=checksum_x,
screen_y=screen_y,
is_text_centered=False,
))

# And now the *actual* final word after merging the bit data
self.components.append(TextArea(
text=f"""Final Word: \"{self.actual_final_word}\"""",
screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
))

# Once again show the bits that came from the user's entropy...
num_checksum_bits = len(self.checksum_bits)
user_component = self.selected_final_bits[:11 - num_checksum_bits]
screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
self.components.append(TextArea(
text=user_component,
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
font_size=bit_font_size,
edge_padding=0,
screen_x=bit_display_x,
screen_y=screen_y,
is_text_centered=False,
))

# ...and append the checksum's bits, still highlighted in orange
self.components.append(TextArea(
text=self.checksum_bits,
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
font_color=GUIConstants.ACCENT_COLOR,
font_size=bit_font_size,
edge_padding=0,
screen_x=checksum_x,
screen_y=screen_y,
is_text_centered=False,
))



@dataclass
class ToolsCalcFinalWordDoneScreen(ButtonListScreen):
final_word: str = None
mnemonic_word_length: int = 12
fingerprint: str = None
Expand Down
30 changes: 16 additions & 14 deletions src/seedsigner/helpers/mnemonic_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,25 @@
from seedsigner.models.seed import Seed


def calculate_checksum(partial_mnemonic: list, wordlist_language_code: str) -> List[str]:
""" Provide 11- or 23-word mnemonic, returns complete mnemonic w/checksum as a list """
if len(partial_mnemonic) not in [11, 23]:
raise Exception("Pass in a 11- or 23-word mnemonic")

def calculate_checksum(mnemonic: list, wordlist_language_code: str) -> List[str]:
"""
Provide 12- or 24-word mnemonic, returns complete mnemonic w/checksum as a list.
If 11- or 23-words are provided, append word `0000` to end of list as temp final
word.
"""
if len(mnemonic) in [11, 23]:
mnemonic.append(Seed.get_wordlist(wordlist_language_code)[0])

if len(mnemonic) not in [12, 24]:
raise Exception("Pass in a 12- or 24-word mnemonic")

# Work on a copy of the input list
mnemonic_copy = partial_mnemonic.copy()

# 12-word seeds contribute 7 bits of entropy to the final word; 24-word seeds
# contribute 3 bits. But we don't have any partial entropy bits to use to help us
# create the final word. So just default to filling those missing values with zeroes
# ("abandon" is word 0000, so effectively inserts zeroes).
mnemonic_copy.append("abandon")
mnemonic_copy = mnemonic.copy()

# Convert the resulting mnemonic to bytes, but we `ignore_checksum` validation
# because we have to assume it's incorrect since we just hard-coded it above; we'll
# fix that next.
# 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))

# This function will convert the bytes back into a mnemonic, but it will also
Expand Down
33 changes: 16 additions & 17 deletions src/seedsigner/views/seed_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,14 @@ def run(self):

if ret == RET_CODE__BACK_BUTTON:
if self.cur_word_index > 0:
return Destination(
SeedMnemonicEntryView,
view_args={
"cur_word_index": self.cur_word_index - 1,
"is_calc_final_word": self.is_calc_final_word
}
)
return Destination(BackStackView)
# return Destination(
# SeedMnemonicEntryView,
# view_args={
# "cur_word_index": self.cur_word_index - 1,
# "is_calc_final_word": self.is_calc_final_word
# }
# )
else:
self.controller.storage.discard_pending_mnemonic()
return Destination(MainMenuView)
Expand All @@ -142,17 +143,15 @@ def run(self):
self.controller.storage.update_pending_mnemonic(ret, self.cur_word_index)

if self.is_calc_final_word and self.cur_word_index == self.controller.storage.pending_mnemonic_length - 2:
# Time to calculate the last word
# TODO: Option to add missing entropy for the last word:
# * 3 bits for a 24-word seed
# * 7 bits for a 12-word seed
from seedsigner.helpers import mnemonic_generation
# Time to calculate the last word. User must decide how they want to specify
# the last bits of entropy for the final word.
from seedsigner.views.tools_views import ToolsCalcFinalWordFinalizePromptView
return Destination(ToolsCalcFinalWordFinalizePromptView)

if self.is_calc_final_word and self.cur_word_index == self.controller.storage.pending_mnemonic_length - 1:
# Time to calculate the last word. User must either select a final word to
# contribute entropy to the checksum word OR we assume 0 ("abandon").
from seedsigner.views.tools_views import ToolsCalcFinalWordShowFinalWordView
full_mnemonic = mnemonic_generation.calculate_checksum(
self.controller.storage.pending_mnemonic[:-1], # Must omit the last word's empty value
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
)
self.controller.storage.update_pending_mnemonic(full_mnemonic[-1], self.cur_word_index+1)
return Destination(ToolsCalcFinalWordShowFinalWordView)

if self.cur_word_index < self.controller.storage.pending_mnemonic_length - 1:
Expand Down
Loading

0 comments on commit 53f3cda

Please sign in to comment.