Merge pull request #571 from kdmukai/electrum_seed_cleanup

Light refactor of Electrum seed support
This commit is contained in:
Nick Klockenga
2024-07-14 23:00:31 -04:00
committed by GitHub
14 changed files with 329 additions and 100 deletions
+1
View File
@@ -19,6 +19,7 @@ from seedsigner.models.settings_definition import SettingsConstants
# TODO: PR these directly into `embit`? Or replace with new/existing methods already in `embit`?
# TODO: Refactor `wallet_type` to conform to our `sig_type` naming convention
def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG, script_type: str = SettingsConstants.NATIVE_SEGWIT) -> str:
if network == SettingsConstants.MAINNET:
network_path = "0'"
+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]
+34 -18
View File
@@ -42,7 +42,7 @@ class Seed:
raise Exception(f"Unrecognized wordlist_language_code {wordlist_language_code}")
def _generate_seed(self) -> bool:
def _generate_seed(self):
try:
self.seed_bytes = bip39.mnemonic_to_seed(self.mnemonic_str, password=self._passphrase, wordlist=self.wordlist)
except Exception as e:
@@ -64,6 +64,7 @@ class Seed:
def wordlist_language_code(self) -> str:
return self._wordlist_language_code
@property
def mnemonic_display_str(self) -> str:
return unicodedata.normalize("NFC", " ".join(self._mnemonic))
@@ -106,29 +107,36 @@ class Seed:
# TODO: Support other BIP-39 wordlist languages!
raise Exception("Not yet implemented!")
@property
def script_override(self) -> list:
return None
def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
def script_override(self) -> str:
return None
def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
def derivation_override(self, sig_type: str = SettingsConstants.SINGLE_SIG) -> str:
return None
def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, sig_type: str = SettingsConstants.SINGLE_SIG) -> str:
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)]
return bip32.detect_version(derivation_path, default="xpub", network=embit_network)
@property
def passphrase_label(self) -> str:
return SettingsConstants.LABEL__BIP39_PASSPHRASE
@property
def seedqr_supported(self) -> bool:
return True
@property
def bip85_supported(self) -> bool:
return True
def get_fingerprint(self, network: str = SettingsConstants.MAINNET) -> str:
root = bip32.HDKey.from_seed(self.seed_bytes, version=NETWORKS[SettingsConstants.map_network_to_embit(network)]["xprv"])
return hexlify(root.child(0).fingerprint).decode('utf-8')
@@ -158,19 +166,20 @@ class Seed:
class ElectrumSeed(Seed):
def _generate_seed(self) -> bool:
def _generate_seed(self):
if len(self._mnemonic) != 12:
return False
raise InvalidSeedException(f"Unsupported Electrum seed length: {len(self._mnemonic)}")
s = hmac.digest(b"Seed version", self.mnemonic_str.encode('utf8'), hashlib.sha512).hex()
prefix = s[0:3]
# only support Electrum Segwit version for now
if SettingsConstants.ELECTRUM_SEED_SEGWIT == prefix:
self.seed_bytes=hashlib.pbkdf2_hmac('sha512', self.mnemonic_str.encode('utf-8'), b'electrum' + self._passphrase.encode('utf-8'), iterations = SettingsConstants.ELECTRUM_PBKDF2_ROUNDS)
return True
else:
raise InvalidSeedException("Unsupported electrum seed input")
return False
raise InvalidSeedException(f"Unsupported Electrum seed format: {prefix}")
def set_passphrase(self, passphrase: str, regenerate_seed: bool = True):
if passphrase:
@@ -184,6 +193,7 @@ class ElectrumSeed(Seed):
# Regenerate the internal seed since passphrase changes the result
self._generate_seed()
@staticmethod
def normalize_electrum_passphrase(passphrase : str) -> str:
passphrase = unicodedata.normalize('NFKD', passphrase)
@@ -193,25 +203,31 @@ class ElectrumSeed(Seed):
passphrase = u' '.join(passphrase.split())
return passphrase
@property
def script_override(self) -> list:
return [SettingsConstants.NATIVE_SEGWIT]
def script_override(self) -> str:
return SettingsConstants.NATIVE_SEGWIT
def derivation_override(self, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
return "m/0h" if SettingsConstants.SINGLE_SIG == wallet_type else "m/1h"
def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, wallet_type: str = SettingsConstants.SINGLE_SIG) -> str:
def derivation_override(self, sig_type: str = SettingsConstants.SINGLE_SIG) -> str:
return "m/0h" if sig_type == SettingsConstants.SINGLE_SIG else "m/1h"
def detect_version(self, derivation_path: str, network: str = SettingsConstants.MAINNET, sig_type: str = SettingsConstants.SINGLE_SIG) -> str:
embit_network = NETWORKS[SettingsConstants.map_network_to_embit(network)]
return embit_network["zpub"] if SettingsConstants.SINGLE_SIG == wallet_type else embit_network["Zpub"]
return embit_network["zpub"] if sig_type == SettingsConstants.SINGLE_SIG else embit_network["Zpub"]
@property
def passphrase_label(self) -> str:
return SettingsConstants.LABEL__CUSTOM_EXTENSION
@property
def seedqr_supported(self) -> bool:
return False
@property
def bip85_supported(self) -> bool:
return False
+6 -3
View File
@@ -207,7 +207,9 @@ class SettingsConstants:
# Label strings
LABEL__BIP39_PASSPHRASE = "BIP-39 Passphrase"
LABEL__CUSTOM_EXTENSION = "Custom Extension"
LABEL__CUSTOM_EXTENSION = "Custom Extension" # Terminology used by Electrum seeds
@dataclass
class SettingsEntry:
@@ -470,8 +472,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),
+2 -10
View File
@@ -79,16 +79,8 @@ 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)
from seedsigner.views.seed_views import SeedElectrumMnemonicStartView
return Destination(SeedElectrumMnemonicStartView)
+43 -26
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:
****************************************************************************"""
@@ -614,6 +620,7 @@ class SeedBackupView(View):
def run(self):
button_data = [self.VIEW_WORDS]
if self.seed.seedqr_supported:
button_data.append(self.EXPORT_SEEDQR)
@@ -681,8 +688,16 @@ class SeedExportXpubScriptTypeView(View):
def run(self):
from .tools_views import ToolsAddressExplorerAddressTypeView
args = {"seed_num": self.seed_num, "sig_type": self.sig_type}
script_types = self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)
seed = self.controller.storage.seeds[self.seed_num]
script_types = seed.script_override if seed.script_override else self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES)
if seed.script_override:
# This seed only allows one script type
# TODO: Does it matter if the Settings don't have the override script type
# enabled?
script_types = [seed.script_override]
if len(script_types) == 1:
# Nothing to select; skip this screen
args["script_type"] = script_types[0]
@@ -1456,7 +1471,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 +1508,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:
@@ -1720,7 +1737,7 @@ class SeedAddressVerificationView(View):
raise Exception("Can't validate a single sig addr without specifying a seed")
self.seed_num = seed_num
self.seed = self.controller.get_seed(seed_num)
self.seed_derivation_override = self.seed.derivation_override(wallet_type=SettingsConstants.SINGLE_SIG)
self.seed_derivation_override = self.seed.derivation_override(sig_type=SettingsConstants.SINGLE_SIG)
else:
self.seed = None
self.address = self.controller.unverified_address["address"]
+3 -11
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 SeedElectrumMnemonicStartView
return Destination(SeedElectrumMnemonicStartView)
@@ -540,7 +532,7 @@ class ToolsAddressExplorerAddressTypeView(View):
if self.seed_num is not None:
self.seed = self.controller.storage.seeds[seed_num]
data["seed_num"] = self.seed
seed_derivation_override = self.seed.derivation_override(wallet_type=SettingsConstants.SINGLE_SIG)
seed_derivation_override = self.seed.derivation_override(sig_type=SettingsConstants.SINGLE_SIG)
if self.script_type == SettingsConstants.CUSTOM_DERIVATION:
derivation_path = self.custom_derivation
+12
View File
@@ -134,6 +134,16 @@ def test_generate_screenshots(target_locale):
# Automatically populate all Settings options Views
settings_views_list = []
settings_views_list.append(settings_views.SettingsMenuView)
settings_views_list.append((
settings_views.SettingsMenuView,
dict(
visibility=SettingsConstants.VISIBILITY__ADVANCED,
selected_attr=SettingsConstants.SETTING__ELECTRUM_SEEDS,
initial_scroll=240, # Just guessing how many pixels to scroll down
),
"SettingsMenuView__Advanced"
))
# so we get a choice for transcribe seed qr format
controller.settings.set_value(
attr_name=SettingsConstants.SETTING__COMPACT_SEEDQR,
@@ -215,6 +225,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'
+35 -1
View File
@@ -1,5 +1,6 @@
from base import FlowTest, FlowStep
from seedsigner.controller import Controller
from seedsigner.views.view import MainMenuView
from seedsigner.views import scan_views, seed_views, psbt_views
from seedsigner.models.settings import SettingsConstants
@@ -57,7 +58,40 @@ class TestPSBTFlows(FlowTest):
FlowStep(MainMenuView)
])
def test_scan_psbt_first_then_load_electrum_seed(self):
"""
Should be able to load an Electrum mnemonic after first loading in a psbt.
"""
def load_psbt_into_decoder(view: scan_views.ScanView):
# Single sig psbt for the below Electrum mnemonic
view.decoder.add_data("cHNidP8BAHECAAAAAX9/d6VyI7nvVTyhLBfqu05za2AJ2Z0dKMC0cUX+S2U7AQAAAAD9////AgeHAAAAAAAAFgAUOnNPuZMD1sQudt3+7LvHBUvGhyd//gAAAAAAABYAFGO9QLvu4V9/hz6ZjbIGMrqsEiIYAjQTAAABAR+ghgEAAAAAABYAFKawrgcT62jmIVQwyHPCV0thmJWbAQDBAQAAAAABAYeHL9UQlz/jEKUuNNY3LTeQRjudjBinsP2L0ppvgRt0AAAAAAD/////AnbP3rsPAAAAIlEgtgmCioGjfKwp6f8rOoI4OPb+ZV8db581J9IizZPskl2ghgEAAAAAABYAFKawrgcT62jmIVQwyHPCV0thmJWbAUDCBlMh9VjZN2NdU9Wabi0o3Ct1q9YHTsJRLAkLfUuIHB+BE+ucR4bdGAJG5nBhCWOmCXbpRwKP1INRYvkuQ2fHAAAAACIGA2+PEYHyVy6nhYwAx5SJKBIWXjsWgjhhf/2FEWqXgxnoEKNOC3gAAACAAAAAAAAAAAAAACICA0SBeeHxfHdny6rUnQJuteAnQ7shSydexjJCkSJarn3mEKNOC3gAAACAAQAAAAEAAAAA")
self.settings.set_value(SettingsConstants.SETTING__ELECTRUM_SEEDS, SettingsConstants.OPTION__ENABLED)
sequence = [
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
FlowStep(scan_views.ScanView, before_run=load_psbt_into_decoder), # simulate read PSBT; ret val is ignored
FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.TYPE_ELECTRUM),
FlowStep(seed_views.SeedElectrumMnemonicStartView),
]
# Load the associated Electrum mnemonic during the flow
for word in "apple drip silly junior language resource unaware whale snake copy gravity tank".split():
sequence += [
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word),
]
sequence += [
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
FlowStep(psbt_views.PSBTOverviewView),
FlowStep(psbt_views.PSBTMathView),
]
self.run_sequence(sequence)
def test_scan_multisig_psbt_seed_already_signed_flow(self):
def load_psbt_into_decoder(view: scan_views.ScanView):
+102 -11
View File
@@ -3,11 +3,11 @@ import pytest
# Must import test base before the Controller
from base import BaseTest, FlowTest, FlowStep
from base import FlowTestRunScreenNotExecutedException, FlowTestInvalidButtonDataSelectionException
from base import FlowTestInvalidButtonDataSelectionException
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
from seedsigner.models.settings import Settings, SettingsConstants
from seedsigner.models.seed import Seed
from seedsigner.models.seed import ElectrumSeed, Seed
from seedsigner.views.view import ErrorView, MainMenuView, OptionDisabledView, RemoveMicroSDWarningView, View, NetworkMismatchErrorView, NotYetImplementedView
from seedsigner.views import seed_views, scan_views, settings_views, tools_views
@@ -119,6 +119,66 @@ 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], custom_extension: str = None, 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
if custom_extension:
sequence += [
FlowStep(seed_views.SeedFinalizeView, screen_return_value=1), # The passphrase / custom extension button is dynamic so there's no constant to refer to here
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase=custom_extension)), # This is a one-off oddity where the Screen returns dict instead of int | str
FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE),
FlowStep(seed_views.SeedOptionsView),
]
else:
sequence += [
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
FlowStep(seed_views.SeedOptionsView),
]
else:
# Or we bomb out if the mnemonic is invalid for Electrum
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())
test_with_mnemonic("basket print toy noodle betray weird filter ticket insect copy force machine".split(), custom_extension="test")
test_with_mnemonic("basket print toy noodle betray weird filter ticket insect copy force machine".split(), custom_extension="monkey fling orange coin good")
# Most BIP-39 seeds should fail; test seed 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)
def test_export_xpub_standard_flow(self):
"""
Selecting "Export XPUB" from the SeedOptionsView should enter the Export XPUB flow and end at the MainMenuView
@@ -193,15 +253,15 @@ class TestSeedFlows(FlowTest):
self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in script_types if x!=disabled_script])
self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in coordinators if x!=disabled_coord])
# test that multisig is not an option via exception raised when redirected to next step instead of having a choice
with pytest.raises(FlowTestRunScreenNotExecutedException) as e:
self.run_sequence(
initial_destination_view_args=dict(seed_num=0),
sequence=[
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=disabled_sig),
]
)
# If multisig isn't an option, then the sig type selection is skipped altogether
self.run_sequence(
initial_destination_view_args=dict(seed_num=0),
sequence=[
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
FlowStep(seed_views.SeedExportXpubSigTypeView, is_redirect=True),
FlowStep(seed_views.SeedExportXpubScriptTypeView),
]
)
# test that taproot is not an option via exception raised when choice is taproot
with pytest.raises(FlowTestInvalidButtonDataSelectionException) as e:
@@ -295,6 +355,37 @@ class TestSeedFlows(FlowTest):
)
def test_export_xpub_electrum_seed_flow(self):
"""
Electrum seeds should skip script type selection
"""
# Load a finalized Seed into the Controller
self.controller.storage.init_pending_mnemonic(num_words=12, is_electrum=True)
self.controller.storage.set_pending_seed(ElectrumSeed("regular reject rare profit once math fringe chase until ketchup century escape".split()))
self.controller.storage.finalize_pending_seed()
# Make sure all options are enabled
self.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [x for x,y in SettingsConstants.ALL_SIG_TYPES])
self.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [x for x,y in SettingsConstants.ALL_SCRIPT_TYPES])
self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x,y in SettingsConstants.ALL_COORDINATORS])
self.run_sequence(
initial_destination_view_args=dict(seed_num=0),
sequence=[
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=seed_views.SeedExportXpubSigTypeView.SINGLE_SIG),
# Skips past the script type options via redirect
FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True),
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[0]),
FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0),
FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0),
FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0),
FlowStep(MainMenuView),
]
)
def test_discard_seed_flow(self):
"""
Selecting "Discard Seed" from the SeedOptionsView should enter the Discard Seed flow and
+36 -5
View File
@@ -39,8 +39,6 @@ class TestToolsFlows(FlowTest):
Finalizing a seed during the Address Explorer flow should return to the next
Address Explorer step upon completion.
"""
controller = Controller.get_instance()
def load_seed_into_decoder(view: scan_views.ScanView):
view.decoder.add_data("0000" * 11 + "0003")
@@ -55,11 +53,11 @@ class TestToolsFlows(FlowTest):
FlowStep(seed_views.SeedExportXpubScriptTypeView),
])
assert controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER
assert self.controller.resume_main_flow == Controller.FLOW__ADDRESS_EXPLORER
# Reset
controller.storage.seeds.clear()
controller.storage.set_pending_seed(Seed(mnemonic=["abandon "* 11 + "about"]))
self.controller.storage.seeds.clear()
self.controller.storage.set_pending_seed(Seed(mnemonic=["abandon "* 11 + "about"]))
# Finalize the new seed w/passphrase
self.run_sequence(
@@ -73,6 +71,39 @@ class TestToolsFlows(FlowTest):
)
def test__address_explorer__load_electrum_seed__sideflow(self):
"""
Loading an Electrum seed during the Address Explorer flow should return to
the Address Explorer flow upon completion, skip the script type selection,
and successfully generate receive or change addresses.
"""
self.settings.set_value(SettingsConstants.SETTING__ELECTRUM_SEEDS, SettingsConstants.OPTION__ENABLED)
sequence = [
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.ADDRESS_EXPLORER),
FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, button_data_selection=tools_views.ToolsAddressExplorerSelectSourceView.TYPE_ELECTRUM),
FlowStep(seed_views.SeedElectrumMnemonicStartView),
]
# Load an Electrum mnemonic during the flow (same one used in test_seed.py)
for word in "regular reject rare profit once math fringe chase until ketchup century escape".split():
sequence += [
FlowStep(seed_views.SeedMnemonicEntryView, screen_return_value=word),
]
sequence += [
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True),
FlowStep(tools_views.ToolsAddressExplorerAddressTypeView, button_data_selection=tools_views.ToolsAddressExplorerAddressTypeView.RECEIVE),
FlowStep(tools_views.ToolsAddressExplorerAddressListView),
]
self.run_sequence(sequence)
def test__address_explorer__scan_wrong_qrtype__flow(self):
"""
Scanning the wrong type of QR code when a SeedQR is expected should route to ErrorView
+41 -1
View File
@@ -1,8 +1,10 @@
from seedsigner.models.seed import Seed, ElectrumSeed
import pytest
from seedsigner.models.seed import InvalidSeedException, Seed, ElectrumSeed
from seedsigner.models.settings import SettingsConstants
# TODO: Change TAB indents to SPACE
def test_seed():
seed = Seed(mnemonic="obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash".split())
@@ -37,9 +39,47 @@ def test_seed():
def test_electrum_seed():
"""
ElectrumSeed should correctly parse a modern Electrum mnemonic.
"""
seed = ElectrumSeed(mnemonic="regular reject rare profit once math fringe chase until ketchup century escape".split())
intended_seed = b'\xcan|\xf8\x8a\x8d\xf78=Pq\xc4_\xe6\x02\x91\xfcs\xb2[\xed*\xdc\xc7%\xb6[_-(~D\xe5\x1e\x85%N\x9c\x03\x9dh\xafX}\x16\xb1\x99,\xbe\xc4\x11\xfaW\x0f\xb0\x89yD\xf4\x0f\xd5?\x8eA'
assert seed.seed_bytes == intended_seed
def test_electrum_mnemonic_format():
"""
ElectrumSeed should reject mnemonics that are not 12 words long.
"""
with pytest.raises(InvalidSeedException):
ElectrumSeed(mnemonic=["regular"] * 11)
with pytest.raises(InvalidSeedException):
ElectrumSeed(mnemonic=["regular"] * 13)
with pytest.raises(InvalidSeedException):
ElectrumSeed(mnemonic=["regular"] * 24)
def test_electrum_seed_rejects_most_bip39_mnemonics():
"""
ElectrumSeed should throw an exception for most BIP-39 mnemonics.
There are 1/16 odds that a seed will be valid for both formats.
"""
# Most BIP-39 seeds should fail; test seeds generated by bitcoiner.guide
with pytest.raises(InvalidSeedException):
ElectrumSeed(mnemonic="pioneer divide volcano art victory family grow novel mandate bicycle senior adjust".split())
with pytest.raises(InvalidSeedException):
ElectrumSeed(mnemonic="gentle combine cool hamster ghost harvest gossip lend dismiss slam any toast".split())
with pytest.raises(InvalidSeedException):
ElectrumSeed(mnemonic="enough board blossom stamp fire buffalo digital solution sadness random number stone".split())
# This one is valid for both formats
mnemonic = "only gain spot output unknown craft simple cram absorb suggest ridge famous".split()
Seed(mnemonic)
ElectrumSeed(mnemonic)
+2 -3
View File
@@ -14,12 +14,11 @@ 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)
qr = QR()
image = qr.qrimage(