Skip to content
Open
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
28 changes: 21 additions & 7 deletions src/seedsigner/helpers/embit_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,6 @@ def parse_derivation_path(derivation_path: str) -> dict:

sections = derivation_path.split("/")

if sections[1] == "48h":
# So far this helper is only meant for single sig message signing
raise Exception("Not implemented")

lookups = {
"script_types": {
"44h": SettingsConstants.LEGACY_P2PKH,
Expand All @@ -146,9 +142,27 @@ def parse_derivation_path(derivation_path: str) -> dict:
}

details = dict()
details["script_type"] = lookups["script_types"].get(sections[1])
if not details["script_type"]:
details["script_type"] = SettingsConstants.CUSTOM_DERIVATION

if sections[1] == "48h":
# BIP48 multisig: m/48h/<network>/<account>/<script_type>/[change]/[index]
# The script type is encoded in sections[4]:
# 1h = p2sh-p2wsh (nested segwit)
# 2h = p2wsh (native segwit)
bip48_script_types = {
"1h": SettingsConstants.NESTED_SEGWIT,
"2h": SettingsConstants.NATIVE_SEGWIT,
}
if len(sections) > 4:
details["script_type"] = bip48_script_types.get(sections[4])
else:
details["script_type"] = None

if not details["script_type"]:
details["script_type"] = SettingsConstants.CUSTOM_DERIVATION
else:
details["script_type"] = lookups["script_types"].get(sections[1])
if not details["script_type"]:
details["script_type"] = SettingsConstants.CUSTOM_DERIVATION
details["network"] = lookups["networks"].get(sections[2])

# Check if there's a standard change path
Expand Down
44 changes: 44 additions & 0 deletions tests/test_embit_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,3 +426,47 @@ def test_parse_derivation_path():
assert actual_result["index"] == expected_result[3]
else:
assert actual_result["index"] == int(derivation_path.split("/")[-1])

# BIP48 multisig derivation paths (separate dict since script_type keys
# overlap with single-sig paths above)
bip48_vectors = {
# m/48' native segwit (2h script type)
(SC.MAINNET, SC.NATIVE_SEGWIT, False): "m/48'/0'/0'/2'/0/5",
(SC.TESTNET, SC.NATIVE_SEGWIT, False): "m/48'/1'/0'/2'/0/5",
(SC.REGTEST, SC.NATIVE_SEGWIT, False): "m/48'/1'/0'/2'/0/5",
(SC.MAINNET, SC.NATIVE_SEGWIT, True): "m/48'/0'/0'/2'/1/5",
(SC.TESTNET, SC.NATIVE_SEGWIT, True): "m/48'/1'/0'/2'/1/5",
(SC.REGTEST, SC.NATIVE_SEGWIT, True): "m/48'/1'/0'/2'/1/5",

# m/48' nested segwit (1h script type)
(SC.MAINNET, SC.NESTED_SEGWIT, False): "m/48'/0'/0'/1'/0/5",
(SC.TESTNET, SC.NESTED_SEGWIT, False): "m/48'/1'/0'/1'/0/5",
(SC.REGTEST, SC.NESTED_SEGWIT, False): "m/48'/1'/0'/1'/0/5",
(SC.MAINNET, SC.NESTED_SEGWIT, True): "m/48'/0'/0'/1'/1/5",
(SC.TESTNET, SC.NESTED_SEGWIT, True): "m/48'/1'/0'/1'/1/5",
(SC.REGTEST, SC.NESTED_SEGWIT, True): "m/48'/1'/0'/1'/1/5",

# m/48' with unrecognised script type falls back to CUSTOM_DERIVATION
(SC.MAINNET, SC.CUSTOM_DERIVATION, False): "m/48'/0'/0'/3'/0/5",
}

for expected_result, derivation_path in bip48_vectors.items():
actual_result = embit_utils.parse_derivation_path(derivation_path)

if expected_result[0] == SC.MAINNET:
assert actual_result["network"] == expected_result[0]
assert actual_result["clean_match"] is True
elif expected_result[0] is None:
assert actual_result["network"] is None
assert actual_result["clean_match"] is False
else:
assert expected_result[0] in actual_result["network"]
assert actual_result["clean_match"] is True

assert actual_result["script_type"] == expected_result[1]
assert actual_result["is_change"] == expected_result[2]

if len(expected_result) == 4:
assert actual_result["index"] == expected_result[3]
else:
assert actual_result["index"] == int(derivation_path.split("/")[-1])
40 changes: 40 additions & 0 deletions tests/test_flows_seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,8 @@ class TestMessageSigningFlows(FlowTest):
MAINNET_DERIVATION_PATH = "m/84h/0h/0h/0/0"
TESTNET_DERIVATION_PATH = "m/84h/1h/0h/0/0"
CUSTOM_DERIVATION_PATH = "m/99h/0/0"
BIP48_NATIVE_SEGWIT_PATH = "m/48h/0h/0h/2h/0/0"
BIP48_NESTED_SEGWIT_PATH = "m/48h/0h/0h/1h/0/0"
SHORT_MESSAGE = "I attest that I control this bitcoin address blah blah blah"
NO_WHITESPACE_MESSAGE = """{"height":841407,"lightning_bolt12":"lno1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}"""
MULTIPAGE_MESSAGE = """Chancellor on brink of second bailout for banks
Expand Down Expand Up @@ -540,6 +542,14 @@ def load_custom_derivation_into_decoder(self, view: View):
self.load_signmessage_into_decoder(view, self.CUSTOM_DERIVATION_PATH, self.SHORT_MESSAGE)


def load_bip48_native_segwit_into_decoder(self, view: View):
self.load_signmessage_into_decoder(view, self.BIP48_NATIVE_SEGWIT_PATH, self.SHORT_MESSAGE)


def load_bip48_nested_segwit_into_decoder(self, view: View):
self.load_signmessage_into_decoder(view, self.BIP48_NESTED_SEGWIT_PATH, self.SHORT_MESSAGE)


def inject_mesage_as_paged_message(self, view: View):
# Because the Screen won't actually run, we have to do the Screen's work here
from seedsigner.gui.components import reflow_text_into_pages, GUIConstants
Expand Down Expand Up @@ -729,6 +739,36 @@ def load_invalid_signmessage_qr(view: scan_views.ScanView):
assert self.controller.resume_main_flow is None


def test_sign_message_bip48_multisig_flow(self):
"""
Should scan a `signmessage` QR with a BIP48 multisig derivation path and
complete the signing flow for both native segwit (m/48'/0'/0'/2') and
nested segwit (m/48'/0'/0'/1') script types.
"""
self.settings.set_value(SettingsConstants.SETTING__MESSAGE_SIGNING, SettingsConstants.OPTION__ENABLED)

def expect_successful_signing(load_message: Callable):
self.run_sequence([
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
FlowStep(scan_views.ScanView, before_run=load_message),
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),
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, screen_return_value=0),
FlowStep(seed_views.SeedSignMessageSignedMessageQRView, screen_return_value=0),
FlowStep(MainMenuView),
])

# BIP48 native segwit (m/48'/0'/0'/2'/0/0)
expect_successful_signing(self.load_bip48_native_segwit_into_decoder)

# BIP48 nested segwit (m/48'/0'/0'/1'/0/0)
expect_successful_signing(self.load_bip48_nested_segwit_into_decoder)


def test_sign_message_unsupported_derivation_flow(self):
"""
Should redirect to NotYetImplementedView if a message's derivation path isn't yet supported
Expand Down