Light refactor of Electrum seed support

This commit is contained in:
kdmukai
2024-07-14 13:21:23 -05:00
parent 0f26772918
commit fcd4242012
9 changed files with 104 additions and 56 deletions
+8 -6
View File
@@ -85,15 +85,17 @@ class BaseStaticQrEncoder(BaseQrEncoder):
@dataclass
class SeedQrEncoder(BaseStaticQrEncoder):
seed : Seed = None
mnemonic: List[str] = None
wordlist_language_code: str = SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
def __post_init__(self):
self.wordlist = Seed.get_wordlist(self.seed.wordlist_language_code)
self.wordlist = Seed.get_wordlist(self.wordlist_language_code)
super().__post_init__()
self.data = ""
# Output as Numeric data format
for word in self.seed.mnemonic_list:
for word in self.mnemonic:
index = self.wordlist.index(word)
self.data += str("%04d" % index)
@@ -108,18 +110,18 @@ class CompactSeedQrEncoder(SeedQrEncoder):
def next_part(self):
# Output as binary data format
binary_str = ""
for word in self.seed.mnemonic_list:
for word in self.mnemonic:
index = self.wordlist.index(word)
# Convert index to binary, strip out '0b' prefix; zero-pad to 11 bits
binary_str += bin(index).split('b')[1].zfill(11)
# We can exclude the checksum bits at the end
if len(self.seed.mnemonic_list) == 24:
if len(self.mnemonic) == 24:
# 8 checksum bits in a 24-word seed
binary_str = binary_str[:-8]
elif len(self.seed.mnemonic_list) == 12:
elif len(self.mnemonic) == 12:
# 4 checksum bits in a 12-word seed
binary_str = binary_str[:-4]
+3 -2
View File
@@ -470,8 +470,9 @@ class SettingsDefinition:
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
attr_name=SettingsConstants.SETTING__ELECTRUM_SEEDS,
abbreviated_name="Electrum",
display_name="Electrum seed support (Native Segwit only)",
abbreviated_name="electrum",
display_name="Electrum seeds",
help_text="Native Segwit only",
visibility=SettingsConstants.VISIBILITY__ADVANCED,
default_value=SettingsConstants.OPTION__DISABLED),
-7
View File
@@ -79,13 +79,6 @@ class PSBTSelectSeedView(View):
return Destination(SeedMnemonicEntryView)
elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
self.run_screen(
WarningScreen,
title="Electrum warning",
status_headline=None,
text=f"Some features disabled for Electrum seeds",
show_back_button=False,
)
from seedsigner.views.seed_views import SeedMnemonicEntryView
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
return Destination(SeedMnemonicEntryView)
+32 -24
View File
@@ -119,6 +119,7 @@ class SeedSelectSeedView(View):
button_data.append(self.SCAN_SEED)
button_data.append(self.TYPE_12WORD)
button_data.append(self.TYPE_24WORD)
if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED:
button_data.append(self.TYPE_ELECTRUM)
@@ -158,17 +159,7 @@ class SeedSelectSeedView(View):
return Destination(SeedMnemonicEntryView)
elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
self.run_screen(
WarningScreen,
title="Electrum warning",
status_headline=None,
text=f"Some features disabled for Electrum seeds",
show_back_button=False,
)
from seedsigner.views.seed_views import SeedMnemonicEntryView
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
return Destination(SeedMnemonicEntryView)
return Destination(SeedElectrumMnemonicStartView)
@@ -187,10 +178,12 @@ class LoadSeedView(View):
self.SEED_QR,
self.TYPE_12WORD,
self.TYPE_24WORD,
self.CREATE,
]
if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED:
button_data.insert(len(button_data)-1, self.TYPE_ELECTRUM)
button_data.append(self.TYPE_ELECTRUM)
button_data.append(self.CREATE)
selected_menu_num = self.run_screen(
ButtonListScreen,
@@ -215,15 +208,7 @@ class LoadSeedView(View):
return Destination(SeedMnemonicEntryView)
elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
self.run_screen(
WarningScreen,
title="Electrum warning",
status_headline=None,
text=f"Some features disabled for Electrum seeds",
show_back_button=False,
)
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
return Destination(SeedMnemonicEntryView)
return Destination(SeedElectrumMnemonicStartView)
elif button_data[selected_menu_num] == self.CREATE:
from .tools_views import ToolsMenuView
@@ -489,6 +474,27 @@ class SeedDiscardView(View):
class SeedElectrumMnemonicStartView(View):
"""
Currently just a warning display before entering an Electrum seed.
Could be expanded with a follow-up View to specify Electrum seed type.
"""
def run(self):
self.run_screen(
WarningScreen,
title="Electrum warning",
status_headline=None,
text=f"Some features are disabled for Electrum seeds.",
show_back_button=False,
)
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
return Destination(SeedMnemonicEntryView)
"""****************************************************************************
Views for actions on individual seeds:
****************************************************************************"""
@@ -1456,7 +1462,8 @@ class SeedTranscribeSeedQRWholeQRView(View):
def run(self):
encoder_args = dict(seed=self.seed)
encoder_args = dict(mnemonic=self.seed.mnemonic_list,
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
if self.seedqr_format == QRType.SEED__SEEDQR:
e = SeedQrEncoder(**encoder_args)
elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
@@ -1492,7 +1499,8 @@ class SeedTranscribeSeedQRZoomedInView(View):
def run(self):
encoder_args = dict(seed=self.seed)
encoder_args = dict(mnemonic=self.seed.mnemonic_list,
wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE))
if self.seedqr_format == QRType.SEED__SEEDQR:
e = SeedQrEncoder(**encoder_args)
elif self.seedqr_format == QRType.SEED__COMPACTSEEDQR:
+2 -10
View File
@@ -496,16 +496,8 @@ class ToolsAddressExplorerSelectSourceView(View):
return Destination(SeedMnemonicEntryView)
elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
self.run_screen(
WarningScreen,
title="Electrum warning",
status_headline=None,
text=f"Some features disabled for Electrum seeds",
show_back_button=False,
)
from seedsigner.views.seed_views import SeedMnemonicEntryView
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
return Destination(SeedMnemonicEntryView)
from seedsigner.views.seed_views import SeedElectrumWarningView
return Destination(SeedElectrumWarningView)
+2
View File
@@ -215,6 +215,8 @@ def test_generate_screenshots(target_locale):
seed_views.SeedSignMessageConfirmMessageView,
seed_views.SeedSignMessageConfirmAddressView,
seed_views.SeedElectrumMnemonicStartView,
],
"PSBT Views": [
psbt_views.PSBTSelectSeedView, # this will fail, be rerun below
+4 -5
View File
@@ -25,19 +25,18 @@ def test_ur_psbt_qr_encode():
def test_seedsigner_qr():
# test vector 1 from the SeedQR docs
mnemonic = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split()
e = SeedQrEncoder(seed=Seed(mnemonic))
e = SeedQrEncoder(mnemonic=mnemonic)
assert e.next_part() == "011513251154012711900771041507421289190620080870026613431420201617920614089619290300152408010643"
e = CompactSeedQrEncoder(seed=Seed(mnemonic))
e = CompactSeedQrEncoder(mnemonic=mnemonic)
assert e.next_part() == b'\x0et\xb6A\x07\xf9L\xc0\xcc\xfa\xe6\xa1=\xcb\xec6b\x15O\xecg\xe0\xe0\t\x99\xc0x\x92Y}\x19\n'
# test vector 4 from the SeedQR docs
mnemonic="forum undo fragile fade shy sign arrest garment culture tube off merit".split()
seed = Seed(mnemonic)
e = SeedQrEncoder(seed=seed)
e = SeedQrEncoder(mnemonic=mnemonic)
assert e.next_part() == "073318950739065415961602009907670428187212261116"
e = CompactSeedQrEncoder(seed=seed)
e = CompactSeedQrEncoder(mnemonic=mnemonic)
assert e.next_part() == b'[\xbd\x9dq\xa8\xecy\x90\x83\x1a\xff5\x9dBeE'
+51
View File
@@ -119,6 +119,57 @@ class TestSeedFlows(FlowTest):
self.run_sequence(sequence)
def test_electrum_mnemonic_entry_flow(self):
"""
Manually entering an Electrum mnemonic should land at the Finalize Seed flow and end at
the SeedOptionsView.
Most BIP-39 mnemonics should generate an error if entered as Electrum seeds.
"""
def test_with_mnemonic(mnemonic: list[str], expects_electrum_seed_is_valid: bool = True):
Settings.HOSTNAME = "not seedsigner-os"
settings = Settings.get_instance()
settings.set_value(SettingsConstants.SETTING__ELECTRUM_SEEDS, SettingsConstants.OPTION__ENABLED)
sequence = [
FlowStep(MainMenuView, button_data_selection=MainMenuView.SEEDS),
FlowStep(seed_views.SeedsMenuView, is_redirect=True), # When no seeds are loaded it auto-redirects to LoadSeedView
FlowStep(seed_views.LoadSeedView, button_data_selection=seed_views.LoadSeedView.TYPE_ELECTRUM),
FlowStep(seed_views.SeedElectrumMnemonicStartView), # Warning screen; no relevant button data selection.
]
# Now add each manual word entry step
for word in mnemonic:
sequence.append(
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word)
)
if expects_electrum_seed_is_valid:
# With the mnemonic completely entered, we land on the SeedFinalizeView
sequence += [
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
FlowStep(seed_views.SeedOptionsView),
]
else:
sequence.append(FlowStep(seed_views.SeedMnemonicInvalidView, button_data_selection=seed_views.SeedMnemonicInvalidView.DISCARD))
self.run_sequence(sequence)
BaseTest.reset_controller()
# Test seeds generated by Electrum v4.5.5
test_with_mnemonic("bomb congress scorpion mutual word stamp tongue valid permit salmon yellow spy".split())
test_with_mnemonic("morning pretty hobby click extend color wait joke define sausage boost salmon".split())
test_with_mnemonic("basket print toy noodle betray weird filter ticket insect copy force machine".split())
# Most BIP-39 seeds should fail; test seeds generated by bitcoiner.guide
test_with_mnemonic("pioneer divide volcano art victory family grow novel mandate bicycle senior adjust".split(), expects_electrum_seed_is_valid=False)
test_with_mnemonic("gentle combine cool hamster ghost harvest gossip lend dismiss slam any toast".split(), expects_electrum_seed_is_valid=False)
test_with_mnemonic("enough board blossom stamp fire buffalo digital solution sadness random number stone".split(), expects_electrum_seed_is_valid=False)
def test_export_xpub_standard_flow(self):
"""
Selecting "Export XPUB" from the SeedOptionsView should enter the Export XPUB flow and end at the MainMenuView
+2 -2
View File
@@ -14,9 +14,9 @@ def run_encode_decode_test(entropy: bytes, mnemonic_length, qr_type):
assert len(mnemonic) == mnemonic_length
if qr_type == QRType.SEED__SEEDQR:
e = SeedQrEncoder(seed=Seed(mnemonic))
e = SeedQrEncoder(mnemonic=mnemonic)
elif qr_type == QRType.SEED__COMPACTSEEDQR:
e = CompactSeedQrEncoder(seed=Seed(mnemonic))
e = CompactSeedQrEncoder(mnemonic=mnemonic)
data = e.next_part()
print(data)