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

Message signing network mismatch bugfix; OptionDisabledView bugfix #443

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions src/seedsigner/views/scan_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,19 +136,16 @@ def run(self):
)

elif self.decoder.is_sign_message:
if self.settings.get_value(SettingsConstants.SETTING__MESSAGE_SIGNING) == SettingsConstants.OPTION__ENABLED:
from seedsigner.views.seed_views import SeedSignMessageStartView
qr_data = self.decoder.get_qr_data()

return Destination(
SeedSignMessageStartView,
view_args=dict(
derivation_path=qr_data["derivation_path"],
message=qr_data["message"],
)
from seedsigner.views.seed_views import SeedSignMessageStartView
qr_data = self.decoder.get_qr_data()

return Destination(
SeedSignMessageStartView,
view_args=dict(
derivation_path=qr_data["derivation_path"],
message=qr_data["message"],
)
else:
return Destination(OptionDisabledView, view_args=dict(error_msg="Message signing is currently disabled in Settings"))
)

else:
return Destination(NotYetImplementedView)
Expand Down
60 changes: 29 additions & 31 deletions src/seedsigner/views/seed_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from seedsigner.models.settings import Settings, SettingsConstants
from seedsigner.models.settings_definition import SettingsDefinition
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
from seedsigner.views.view import NotYetImplementedView, View, Destination, BackStackView, MainMenuView
from seedsigner.views.view import NotYetImplementedView, OptionDisabledView, View, Destination, BackStackView, MainMenuView



Expand Down Expand Up @@ -1921,23 +1921,41 @@ def __init__(self, derivation_path: str, message: str):
self.derivation_path = derivation_path
self.message = message

if self.settings.get_value(SettingsConstants.SETTING__MESSAGE_SIGNING) == SettingsConstants.OPTION__DISABLED:
self.set_redirect(Destination(OptionDisabledView, view_args=dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)))
return

# calculate the actual receive address
addr_format = embit_utils.parse_derivation_path(derivation_path)
if not addr_format["clean_match"]:
raise NotYetImplementedView("Signing messages for custom derivation paths not supported")

# Note: addr_format["network"] can be MAINNET or [TESTNET, REGTEST]
if self.settings.get_value(SettingsConstants.SETTING__NETWORK) not in addr_format["network"]:
from seedsigner.views.view import NetworkMismatchErrorView
self.set_redirect(Destination(NetworkMismatchErrorView, view_args=dict(text=f"Current network setting ({self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK)}) doesn't match {self.derivation_path}")))

# cleanup. Note: We could leave this in place so the user can resume the
# flow, but for now we avoid complications and keep things simple.
self.controller.resume_main_flow = None
return

data = self.controller.sign_message_data
if not data:
data = {}
self.controller.sign_message_data = data
data["derivation_path"] = derivation_path
data["message"] = message
data["addr_format"] = addr_format

# May be None
self.seed_num = data.get("seed_num")


def run(self):
if self.seed_num is not None:
# We already know which seed we're signing with
return Destination(SeedSignMessageConfirmMessageView, skip_current_view=True)
self.set_redirect(Destination(SeedSignMessageConfirmMessageView, skip_current_view=True))
else:
return Destination(SeedSelectSeedView, view_args=dict(flow=Controller.FLOW__SIGN_MESSAGE), skip_current_view=True)
self.set_redirect(Destination(SeedSelectSeedView, view_args=dict(flow=Controller.FLOW__SIGN_MESSAGE), skip_current_view=True))



Expand Down Expand Up @@ -1979,34 +1997,14 @@ class SeedSignMessageConfirmAddressView(View):
def __init__(self):
super().__init__()
data = self.controller.sign_message_data
self.seed_num = data.get("seed_num")
seed = self.controller.storage.seeds[data.get("seed_num")]
self.derivation_path = data.get("derivation_path")
addr_format = data.get("addr_format")

if self.seed_num is None or not self.derivation_path:
raise Exception("Routing error: sign_message_data hasn't been set")

# calculate the actual receive address
seed = self.controller.storage.seeds[self.seed_num]
addr_format = embit_utils.parse_derivation_path(self.derivation_path)
if not addr_format["clean_match"]:
raise Exception("Signing messages for custom derivation paths not supported")

if addr_format["network"] != SettingsConstants.MAINNET:
# We're in either Testnet or Regtest or...?
if self.settings.get_value(SettingsConstants.SETTING__NETWORK) in [SettingsConstants.TESTNET, SettingsConstants.REGTEST]:
addr_format["network"] = self.settings.get_value(SettingsConstants.SETTING__NETWORK)
else:
from seedsigner.views.view import NetworkMismatchErrorView
self.set_redirect(Destination(NetworkMismatchErrorView, view_args=dict(text=f"Current network setting ({self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK)}) doesn't match {self.derivation_path}")))

# cleanup. Note: We could leave this in place so the user can resume the
# flow, but for now we avoid complications and keep things simple.
self.controller.resume_main_flow = None
self.controller.sign_message_data = None
return

xpub = seed.get_xpub(wallet_path=self.derivation_path, network=addr_format["network"])
embit_network = embit_utils.get_embit_network_name(addr_format["network"])
# Current settings will differentiate TESTNET and REGTEST (since derivation path
# alone doesn't specify which one we're using).
xpub = seed.get_xpub(wallet_path=self.derivation_path, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK))
embit_network = embit_utils.get_embit_network_name(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
self.address = embit_utils.get_single_sig_address(xpub=xpub, script_type=addr_format["script_type"], index=addr_format["index"], is_change=addr_format["is_change"], embit_network=embit_network)


Expand Down
26 changes: 21 additions & 5 deletions src/seedsigner/views/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from seedsigner.gui.screens import RET_CODE__POWER_BUTTON, RET_CODE__BACK_BUTTON
from seedsigner.gui.screens.screen import BaseScreen, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen
from seedsigner.models.settings import Settings, SettingsConstants
from seedsigner.models.settings_definition import SettingsDefinition
from seedsigner.models.threads import BaseThread


Expand Down Expand Up @@ -331,7 +332,7 @@ def run(self):
class NetworkMismatchErrorView(ErrorView):
title: str = "Network Mismatch"
show_back_button: bool = False
button_text: str = "Change Settings"
button_text: str = "Change Setting"
next_destination: Destination = None


Expand Down Expand Up @@ -368,18 +369,33 @@ def run(self):

@dataclass
class OptionDisabledView(View):
error_msg: str
UPDATE_SETTING = "Update Setting"
DONE = "Done"
settings_attr: str

def __post_init__(self):
super().__post_init__()
self.settings_entry = SettingsDefinition.get_settings_entry(self.settings_attr)
self.error_msg = f"\"{self.settings_entry.display_name}\" is currently disabled in Settings."


def run(self):
WarningScreen(
button_data = [self.UPDATE_SETTING, self.DONE]
selected_menu_num = self.run_screen(
WarningScreen,
title="Option Disabled",
status_headline=None,
text=self.error_msg,
button_data=["OK"],
button_data=button_data,
show_back_button=False,
allow_text_overflow=True, # Fit what we can, let the rest go off the edges
).display()
)

if button_data[selected_menu_num] == self.UPDATE_SETTING:
from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView
return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_attr), clear_history=True)
else:
return Destination(MainMenuView, clear_history=True)



Expand Down
13 changes: 10 additions & 3 deletions tests/screenshot_generator/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import sys
import time
from mock import Mock, patch, MagicMock
from seedsigner.helpers import embit_utils

from seedsigner.models.settings import Settings


# Prevent importing modules w/Raspi hardware dependencies.
Expand All @@ -28,7 +31,7 @@
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
from seedsigner.views import (MainMenuView, PowerOptionsView, RestartView, NotYetImplementedView, UnhandledExceptionView,
psbt_views, seed_views, settings_views, tools_views)
from seedsigner.views.view import NetworkMismatchErrorView, PowerOffView, View
from seedsigner.views.view import NetworkMismatchErrorView, OptionDisabledView, PowerOffView, View

from .utils import ScreenshotComplete, ScreenshotRenderer

Expand Down Expand Up @@ -80,10 +83,12 @@ def test_generate_screenshots(target_locale):
controller.multisig_wallet_descriptor = embit.descriptor.Descriptor.from_string(MULTISIG_WALLET_DESCRIPTOR)

# Message signing data
derivation_path = "m/84h/0h/0h/0/0"
controller.sign_message_data = {
"seed_num": 0,
"derivation_path": "m/84h/0h/0h/0/0",
"derivation_path": derivation_path,
"message": "I attest that I control this bitcoin address blah blah blah",
"addr_format": embit_utils.parse_derivation_path(derivation_path)
}

# Automatically populate all Settings options Views
Expand Down Expand Up @@ -116,6 +121,7 @@ def test_generate_screenshots(target_locale):
(UnhandledExceptionView, dict(error=UnhandledExceptionViewFood)),
(settings_views.SettingsIngestSettingsQRView, dict(data="settings::v1 name=factory_reset")),
NetworkMismatchErrorView,
(OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)),


],
Expand Down Expand Up @@ -231,7 +237,8 @@ def screencap_view(view_cls: View, view_name: str, view_args: dict={}, toast_thr
print(f"Completed {view_name}")
except Exception as e:
# Something else went wrong
print(repr(e))
from traceback import print_exc
print_exc()
raise e
finally:
if toast_thread:
Expand Down
67 changes: 54 additions & 13 deletions tests/test_flows_seed.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import Callable
import pytest

# Must import test base before the Controller
Expand All @@ -7,7 +8,7 @@
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.views.view import MainMenuView, RemoveMicroSDWarningView, View, NetworkMismatchErrorView
from seedsigner.views.view import MainMenuView, OptionDisabledView, RemoveMicroSDWarningView, View, NetworkMismatchErrorView
from seedsigner.views import seed_views, scan_views, settings_views, tools_views


Expand Down Expand Up @@ -420,19 +421,59 @@ def test_sign_message_network_mismatch_flow(self):
# Ensure message signing is enabled
self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__ENABLED)

# Ensure we're configured for mainnet
def expect_network_mismatch_error(load_message: Callable):
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
FlowStep(scan_views.ScanView, before_run=load_message), # simulate read message QR; ret val is ignored
FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True),
FlowStep(NetworkMismatchErrorView),
FlowStep(settings_views.SettingsEntryUpdateSelectionView),
])

# MAINNET settings vs TESTNET derivation path with the message
self.settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.MAINNET)
expect_network_mismatch_error(self.load_testnet_message_into_decoder)

self.run_sequence([
# TESTNET settings vs MAINNET derivation path with the message
self.settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.TESTNET)
expect_network_mismatch_error(self.load_short_message_into_decoder)

# REGTEST settings vs MAINNET derivation path with the message
self.settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.REGTEST)
expect_network_mismatch_error(self.load_short_message_into_decoder)




def test_sign_message_option_disabled(self):
"""
Should redirect to OptionDisabledView if a `signmessage` QR is scanned with
message signing disabled.

Should offer the option to route directly to enable that settings or return to
MainMenuView.
"""
# Ensure message signing is disabled
self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__DISABLED)

sequence = [
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
FlowStep(scan_views.ScanView, before_run=self.load_testnet_message_into_decoder), # simulate read message QR; ret val is ignored
FlowStep(scan_views.ScanView, before_run=self.load_short_message_into_decoder), # simulate read message QR; ret val is ignored
FlowStep(seed_views.SeedSignMessageStartView, is_redirect=True),
FlowStep(seed_views.SeedSelectSeedView, button_data_selection=seed_views.SeedSelectSeedView.SCAN_SEED),
FlowStep(scan_views.ScanView, before_run=self.load_seed_into_decoder), # simulate read SeedQR; ret val is ignored
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.FINALIZE),
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
FlowStep(seed_views.SeedSignMessageConfirmMessageView, before_run=self.inject_mesage_as_paged_message, screen_return_value=0),
FlowStep(seed_views.SeedSignMessageConfirmAddressView, is_redirect=True),
FlowStep(NetworkMismatchErrorView),
FlowStep(settings_views.SettingsEntryUpdateSelectionView),
])
]

# First test routing to update the setting
self.run_sequence(
sequence + [
FlowStep(OptionDisabledView, button_data_selection=OptionDisabledView.UPDATE_SETTING, is_redirect=True),
FlowStep(settings_views.SettingsEntryUpdateSelectionView),
]
)

# Now test exiting to Main Menu
self.run_sequence(
sequence + [
FlowStep(OptionDisabledView, button_data_selection=OptionDisabledView.DONE, is_redirect=True),
FlowStep(MainMenuView),
]
)