From 92a776cfc30d72a1613f7bcf6385bf8ebf56ad5a Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Mon, 13 Jan 2025 11:12:27 +0100 Subject: [PATCH 1/3] testnet4 --- releases/Next-ChangeLog.md | 1 + shared/address_explorer.py | 3 +-- shared/chains.py | 10 +++------- shared/export.py | 11 ++--------- shared/flow.py | 2 +- shared/ownership.py | 3 +-- 6 files changed, 9 insertions(+), 21 deletions(-) diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md index d9749848..c2a75ae8 100644 --- a/releases/Next-ChangeLog.md +++ b/releases/Next-ChangeLog.md @@ -14,6 +14,7 @@ This lists the new changes that have not yet been published in a normal release. - Bugfix: Prevent yikes in ownership search - Change: Do not allow to purge settings of current active tmp seed when deleting it from Seed Vault - Change: Do not include sighash in PSBT input data, if sighash value is SIGHASH_ALL +- Change: Testnet3 -> Testnet4 (all parameters are the same) # Mk4 Specific Changes diff --git a/shared/address_explorer.py b/shared/address_explorer.py index c672e062..2bb52ef0 100644 --- a/shared/address_explorer.py +++ b/shared/address_explorer.py @@ -179,8 +179,7 @@ class AddressListMenu(MenuSystem): # Create list of choices (address_index_0, path, addr_fmt) choices = [] for name, path, addr_fmt in chains.CommonDerivations: - if '{coin_type}' in path: - path = path.replace('{coin_type}', str(chain.b44_cointype)) + path = path.replace('{coin_type}', str(chain.b44_cointype)) if self.account_num != 0 and '{account}' not in path: # skip derivations that are not affected by account number diff --git a/shared/chains.py b/shared/chains.py index 26af1410..a1055bc6 100644 --- a/shared/chains.py +++ b/shared/chains.py @@ -29,8 +29,6 @@ Slip132Version = namedtuple('Slip132Version', ('pub', 'priv', 'hint')) class ChainsBase: curve = 'secp256k1' - menu_name = None # use 'name' if this isn't defined - core_name = None # name of chain's "core" p2p software # b44_cointype comes from # @@ -290,8 +288,7 @@ class ChainsBase: class BitcoinMain(ChainsBase): # see ctype = 'BTC' - name = 'Bitcoin' - core_name = 'Bitcoin Core' + name = 'Bitcoin Mainnet' slip132 = { AF_CLASSIC: Slip132Version(0x0488B21E, 0x0488ADE4, 'x'), @@ -310,9 +307,9 @@ class BitcoinMain(ChainsBase): b44_cointype = 0 class BitcoinTestnet(BitcoinMain): + # testnet4 (was testnet3 up until 2025 but all parameters are the same) ctype = 'XTN' - name = 'Bitcoin Testnet' - menu_name = 'Testnet: BTC' + name = 'Bitcoin Testnet 4' slip132 = { AF_CLASSIC: Slip132Version(0x043587cf, 0x04358394, 't'), @@ -334,7 +331,6 @@ class BitcoinTestnet(BitcoinMain): class BitcoinRegtest(BitcoinMain): ctype = 'XRT' name = 'Bitcoin Regtest' - menu_name = 'Regtest: BTC' slip132 = { AF_CLASSIC: Slip132Version(0x043587cf, 0x04358394, 't'), diff --git a/shared/export.py b/shared/export.py index 661e8f8b..52cc75f3 100644 --- a/shared/export.py +++ b/shared/export.py @@ -73,14 +73,7 @@ be needed for different systems. sym=chain.ctype, ct=chain.b44_cointype, xfp=xfp)) for name, path, addr_fmt in chains.CommonDerivations: - - if '{coin_type}' in path: - path = path.replace('{coin_type}', str(chain.b44_cointype)) - - if '{' in name: - name = name.format(core_name=chain.core_name) - - show_slip132 = ('Core' not in name) + path = path.replace('{coin_type}', str(chain.b44_cointype)) yield ('''## For {name}: {path}\n\n'''.format(name=name, path=path)) yield ('''First %d receive addresses (account=0, change=0):\n\n''' % num_rx) @@ -103,7 +96,7 @@ be needed for different systems. node = sv.derive_path(hard_sub, register=False) yield ("%s => %s\n" % (hard_sub, chain.serialize_public(node))) - if show_slip132 and addr_fmt != AF_CLASSIC and (addr_fmt in chain.slip132): + if addr_fmt != AF_CLASSIC and (addr_fmt in chain.slip132): yield ("%s => %s ##SLIP-132##\n" % ( hard_sub, chain.serialize_public(node, addr_fmt))) diff --git a/shared/flow.py b/shared/flow.py index b00bbfd6..de3065a7 100644 --- a/shared/flow.py +++ b/shared/flow.py @@ -304,7 +304,7 @@ If you disable sighash flag restrictions, and ignore the \ warnings, funds can be stolen by specially crafted PSBT or MitM. Keep blocked unless you intend to sign special transactions.'''), - ToggleMenuItem('Testnet Mode', 'chain', ['Bitcoin', 'Testnet3', 'Regtest'], + ToggleMenuItem('Testnet Mode', 'chain', ['Bitcoin', 'Testnet4', 'Regtest'], value_map=['BTC', 'XTN', 'XRT'], on_change=change_which_chain, story="Testnet must only be used by developers because \ diff --git a/shared/ownership.py b/shared/ownership.py index b361e1d6..6c811bc0 100644 --- a/shared/ownership.py +++ b/shared/ownership.py @@ -216,8 +216,7 @@ class OwnershipCache: addr_fmt = ch.possible_address_fmt(addr) if not addr_fmt: # might be valid address over on testnet vs mainnet - nm = ch.name if ch.ctype != 'BTC' else 'Bitcoin Mainnet' - raise UnknownAddressExplained('That address is not valid on ' + nm) + raise UnknownAddressExplained('That address is not valid on ' + ch.name) possibles = [] From de0a679eefdc26ce5c4cf8074e432038c04ac736 Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Mon, 23 Sep 2024 11:20:01 +0200 Subject: [PATCH 2/3] add ability to switch between slip132 and bip32 representations of extended public keys in `Export XPUB` --- releases/Next-ChangeLog.md | 2 + shared/actions.py | 45 ++++++++++++++------ shared/flow.py | 2 +- testing/test_export.py | 85 ++++++++++++++++++++++++++------------ 4 files changed, 93 insertions(+), 41 deletions(-) diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md index c2a75ae8..d378ba1e 100644 --- a/releases/Next-ChangeLog.md +++ b/releases/Next-ChangeLog.md @@ -7,6 +7,7 @@ This lists the new changes that have not yet been published in a normal release. - Enhancement: Hide Secure Notes & Passwords in Deltamode. Wipe seed if notes menu accessed. - Enhancement: Hide Seed Vault in Deltamode. Wipe seed if Seed Vault menu accessed. +- Enhancement: Ability to switch between BIP-32 XPUB and SLIP-132 garbage in `Export XPUB` - Bugfix: Sometimes see a struck screen after _Verifying..._ in boot up sequence. On Q, result is blank screen, on Mk4, result is three-dots screen. - Bugfix: Do not allow to enable/disable Seed Vault feature when in temporary seed mode @@ -17,6 +18,7 @@ This lists the new changes that have not yet been published in a normal release. - Change: Testnet3 -> Testnet4 (all parameters are the same) + # Mk4 Specific Changes ## 5.4.1 - 2024-??-?? diff --git a/shared/actions.py b/shared/actions.py index c2a49ebe..4dd27364 100644 --- a/shared/actions.py +++ b/shared/actions.py @@ -1009,6 +1009,7 @@ async def export_xpub(label, _2, item): chain = chains.current_chain() acct = 0 + slip132 = False # non-slip is default from Oct 2024 # decode menu code => standard derivation mode = item.arg @@ -1024,24 +1025,44 @@ async def export_xpub(label, _2, item): else: remap = {44:0, 49:1, 84:2}[mode] _, path, addr_fmt = chains.CommonDerivations[remap] - path = path.format(account='{acct}', coin_type=chain.b44_cointype, change=0, idx=0)[:-4] - - # always show SLIP-132 style, because defacto - show_slip132 = (addr_fmt != AF_CLASSIC) + path = path.format(account=acct, coin_type=chain.b44_cointype, + change=0, idx=0)[:-4] while 1: - msg = '''Show QR of the XPUB for path:\n\n%s\n\n''' % path + msg = 'Show QR of the XPUB for path:\n\n%s\n\n' % path + esc = "" + if path != "m": + esc += "1" + msg += "Press (1) to select account other than %s. " % (acct or "zero") + if addr_fmt != AF_CLASSIC: + esc += "2" + slp_af = addr_fmt + if slip132: + slp_af = AF_CLASSIC - if '{acct}' in path: - msg += "Press (1) to select account other than zero. " + slp = chain.slip132[slp_af].hint + "pub" + msg += " Press (2) to show %s %s." % ( + slp, "(BIP-32)" if slip132 else "(SLIP-132)" + ) if glob.NFC: - msg += "Press %s to share via NFC. " % (KEY_NFC if version.has_qwerty else "(3)") + if version.has_qwerty: + esc += KEY_NFC + key_hint = KEY_NFC + else: + esc += "3" + key_hint = "(3)" + msg += " Press %s to share via NFC. " % key_hint - ch = await ux_show_story(msg, escape='13') + ch = await ux_show_story(msg, escape=esc) if ch == 'x': return + if ch == "2": + slip132 = not slip132 + continue if ch == '1': acct = await ux_enter_bip32_index('Account Number:') or 0 - path = path.format(acct=acct) + pth_split = path.split("/") + pth_split[-1] = ("%dh" % acct) + path = "/".join(pth_split) continue # assume zero account if not picked @@ -1053,7 +1074,7 @@ async def export_xpub(label, _2, item): # render xpub/ypub/zpub with stash.SensitiveValues() as sv: node = sv.derive_path(path) if path != 'm' else sv.node - xpub = chain.serialize_public(node, addr_fmt) + xpub = chain.serialize_public(node, addr_fmt if slip132 else AF_CLASSIC) from ownership import OWNERSHIP OWNERSHIP.note_wallet_used(addr_fmt, acct) @@ -1063,8 +1084,6 @@ async def export_xpub(label, _2, item): else: await show_qr_code(xpub, False) - break - def electrum_export_story(background=False): # saves memory being in a function diff --git a/shared/flow.py b/shared/flow.py index de3065a7..42633b1e 100644 --- a/shared/flow.py +++ b/shared/flow.py @@ -176,7 +176,7 @@ XpubExportMenu = [ # xxxxxxxxxxxxxxxx MenuItem("Segwit (BIP-84)", f=export_xpub, arg=84), MenuItem("Classic (BIP-44)", f=export_xpub, arg=44), - MenuItem("P2WPKH/P2SH (49)", f=export_xpub, arg=49), + MenuItem("P2WPKH/P2SH "+("(BIP-49)"if version.has_qwerty else "(49)"), f=export_xpub, arg=49), MenuItem("Master XPUB", f=export_xpub, arg=0), MenuItem("Current XFP", f=export_xpub, arg=-1), ] diff --git a/testing/test_export.py b/testing/test_export.py index 8d7a58a3..80a229b9 100644 --- a/testing/test_export.py +++ b/testing/test_export.py @@ -519,15 +519,15 @@ def test_export_public_txt(way, dev, pick_menu_item, goto_home, press_select, mi @pytest.mark.qrcode +@pytest.mark.parametrize('chain', ["BTC", "XTN"]) @pytest.mark.parametrize('acct_num', [ None, 0, 99, 8989]) -@pytest.mark.parametrize('use_nfc', [False, True]) -def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home, +def test_export_xpub(chain, acct_num, dev, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress, enter_number, cap_screen_qr, - use_mainnet, nfc_read_text, is_q1, press_select, press_cancel, + settings_set, nfc_read_text, is_q1, press_select, press_cancel, press_nfc, expect_acctnum_captured): # XPUB's via QR - use_mainnet() - + settings_set("chain", chain) + chain_num = 0 if chain == "BTC" else 1 goto_home() pick_menu_item('Advanced/Tools') pick_menu_item('Export Wallet') @@ -537,11 +537,11 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home for m in top_items: is_xfp = False if '-84' in m: - expect = "m/84h/0h/{acct}h" + expect = f"m/84h/{chain_num}h/{{acct}}h" elif '-44' in m: - expect = "m/44h/0h/{acct}h" + expect = f"m/44h/{chain_num}h/{{acct}}h" elif '49' in m: - expect = "m/49h/0h/{acct}h" + expect = f"m/49h/{chain_num}h/{{acct}}h" elif 'Master' in m: expect = "m" elif 'XFP' in m: @@ -551,17 +551,21 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home time.sleep(0.3) if is_xfp: got = cap_screen_qr().decode('ascii') - if use_nfc: - press_nfc() - assert got == xfp2str(simulator_fixed_xfp).upper() - press_cancel() + time.sleep(.1) + press_nfc() + time.sleep(.2) + nfc_got = nfc_read_text() + time.sleep(.2) + assert nfc_got == got == xfp2str(simulator_fixed_xfp).upper() + press_cancel() # cancel animation + press_cancel() # cancel QR continue time.sleep(0.3) title, story = cap_story() - assert expect in story + assert expect.format(acct=0) in story - if 'acct' in expect: + if expect != "m": assert "Press (1) to select account" in story if acct_num is not None: need_keypress('1') @@ -571,24 +575,52 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home expect = expect.format(acct=acct_num) title, story = cap_story() assert expect in story - assert "Press (1) to select account" not in story + assert "Press (1) to select account" in story - expect = expect.format(acct=0) - if not use_nfc: - press_select() - got_pub = cap_screen_qr().decode('ascii') - else: - if f'Press {KEY_NFC if is_q1 else "(3)"}' not in story: - raise pytest.skip("NFC disabled") + expect = expect.format(acct=0) + + press_select() + got_pub = cap_screen_qr().decode('ascii') + + if f'Press {KEY_NFC if is_q1 else "(3)"}' in story: assert 'NFC' in story press_nfc() time.sleep(0.2) - got_pub = nfc_read_text() + got_nfc_pub = nfc_read_text() time.sleep(0.1) - #press_select() + press_cancel() # cancel animation + press_cancel() # cancel QR + assert got_nfc_pub == got_pub - if got_pub[0] not in 'xt': - got_pub,*_ = slip132undo(got_pub) + time.sleep(.1) + _, story = cap_story() + assert got_pub[0] in 'xt' + if "Press (2)" in story: + if chain == "BTC": + assert f"{'z' if expect[:5] == 'm/84h' else 'y'}pub (SLIP-132)" in story + else: + assert f"{'v' if expect[:5] == 'm/84h' else 'u'}pub (SLIP-132)" in story + need_keypress("2") + time.sleep(.1) + _, story = cap_story() + assert ("%spub (BIP-32)" % ("x" if chain == "BTC" else "t")) in story + assert "Press (2)" in story + + press_select() + got_slip_pub = cap_screen_qr().decode('ascii') + got_unslip, *_ = slip132undo(got_slip_pub) + assert got_unslip == got_pub + + if f'Press {KEY_NFC if is_q1 else "(3)"}' in story: + assert 'NFC' in story + press_nfc() + time.sleep(0.2) + got_nfc_slip_pub = nfc_read_text() + time.sleep(0.1) + press_cancel() # cancel animation + assert got_slip_pub == got_nfc_slip_pub + + press_cancel() # cancel QR expect_acctnum_captured(acct_num) @@ -598,7 +630,6 @@ def test_export_xpub(use_nfc, acct_num, dev, cap_menu, pick_menu_item, goto_home if expect != 'm': wallet = wallet.subkey_for_path(expect[2:].replace('h', "'")) assert got.sec() == wallet.sec() - press_cancel() @pytest.mark.parametrize("chain", ["BTC", "XTN", "XRT"]) From ac761c23d560db05efb5658cdf3594b779c1f0d3 Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Mon, 28 Oct 2024 16:29:00 +0100 Subject: [PATCH 3/3] add message about successful master seed recovery when trying to use master as tmp --- releases/Next-ChangeLog.md | 3 ++- shared/pincodes.py | 5 ++++- testing/test_ephemeral.py | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md index d378ba1e..faddbca5 100644 --- a/releases/Next-ChangeLog.md +++ b/releases/Next-ChangeLog.md @@ -8,6 +8,8 @@ This lists the new changes that have not yet been published in a normal release. - Enhancement: Hide Secure Notes & Passwords in Deltamode. Wipe seed if notes menu accessed. - Enhancement: Hide Seed Vault in Deltamode. Wipe seed if Seed Vault menu accessed. - Enhancement: Ability to switch between BIP-32 XPUB and SLIP-132 garbage in `Export XPUB` +- Enhancement: Use the fact that master seed cannot be used as ephemeral and add UX message + for successful master seed verification. - Bugfix: Sometimes see a struck screen after _Verifying..._ in boot up sequence. On Q, result is blank screen, on Mk4, result is three-dots screen. - Bugfix: Do not allow to enable/disable Seed Vault feature when in temporary seed mode @@ -18,7 +20,6 @@ This lists the new changes that have not yet been published in a normal release. - Change: Testnet3 -> Testnet4 (all parameters are the same) - # Mk4 Specific Changes ## 5.4.1 - 2024-??-?? diff --git a/shared/pincodes.py b/shared/pincodes.py index e4160187..a4a70a40 100644 --- a/shared/pincodes.py +++ b/shared/pincodes.py @@ -473,6 +473,7 @@ class PinAttempt: def tmp_secret(self, encoded, chain=None, bip39pw=''): # Use indicated secret and stop using the SE; operate like this until reboot from glob import settings + from utils import xfp2str from nvstore import SettingsObject val = bytes(encoded + bytes(AE_SECRET_LEN - len(encoded))) @@ -483,7 +484,9 @@ class PinAttempt: target_nvram_key = None if encoded is not None: # disallow using master seed as temporary - master_err = "Cannot use master seed as temporary." + xfp = xfp2str(settings.master_get("xfp", 0)) + master_err = ("Cannot use master seed as temporary. BUT you have just successfully " + "tested recovery of your master seed [%s].") % xfp target_nvram_key = settings.hash_key(val) if SettingsObject.master_nvram_key: assert self.tmp_value diff --git a/testing/test_ephemeral.py b/testing/test_ephemeral.py index c57c26f9..e4198161 100644 --- a/testing/test_ephemeral.py +++ b/testing/test_ephemeral.py @@ -1405,6 +1405,7 @@ def test_import_master_as_tmp(reset_seed_words, goto_eph_seed_menu, cap_story, title, story = cap_story() assert "FAILED" == title assert 'Cannot use master seed as temporary.' in story + assert 'tested recovery of your master seed' in story press_cancel() # go to ephemeral seed and then try to create new ephemeral seed from master @@ -1433,6 +1434,7 @@ def test_import_master_as_tmp(reset_seed_words, goto_eph_seed_menu, cap_story, title, story = cap_story() assert "FAILED" == title assert 'Cannot use master seed as temporary.' in story + assert 'tested recovery of your master seed' in story press_cancel() # now import same seed but represented as master extended key