From bba1e15a243cf97be174898447f7912fc3b9cb58 Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Sun, 29 Mar 2026 20:55:25 +0200 Subject: [PATCH 1/4] incorporate_libngu_improvements --- shared/desc_utils.py | 53 ++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/shared/desc_utils.py b/shared/desc_utils.py index 4cdff450..6a76c39e 100644 --- a/shared/desc_utils.py +++ b/shared/desc_utils.py @@ -279,29 +279,21 @@ def compile(self): d = self.serialize() return ser_compact_size(len(d)) + d + @staticmethod + def chain_from_version(version): + # https://github.com/satoshilabs/slips/blob/master/slip-0132.md + if version in [0x0488b21e, 0x049d7cb2, 0x04b24746, 0x0295b43f, 0x02aa7ed3]: + return "BTC" + else: + assert version in [0x043587cf, 0x044a5262, 0x045f1cf6, 0x024289ef, 0x02575483] + return "XTN" + @classmethod def parse_key(cls, key_str): assert key_str[1:4].lower() == b"pub", "only extended pubkeys allowed" - # extended key - # or xpub or tpub as we use descriptors (SLIP-132 NOT allowed) - hint = key_str[0:1].lower() - if hint == b"x": - chain_type = "BTC" - elif hint == b"t": - chain_type = "XTN" - else: - # slip (ignore any implied address format) - chain_type = "BTC" if hint in b"yz" else "XTN" - node = ngu.hdnode.HDNode() - node.deserialize(key_str) - try: - assert node.privkey() is None, "no privkeys" - except ValueError: - # ValueError is thrown from libngu if key is public - pass - - return node, chain_type + version = node.deserialize(key_str) + return node, cls.chain_from_version(version) def validate(self, my_xfp, disable_checks=False): assert self.chain_type == chains.current_key_chain().ctype, "wrong chain" @@ -311,9 +303,16 @@ def validate(self, my_xfp, disable_checks=False): xfp = self.origin.cc_fp is_mine = (xfp == my_xfp) - # raises ValueError on invalid pubkey (should be in libngu) + # raises ValueError on invalid pubkey in libngu + # https://github.com/switck/libngu/blob/master/ngu/hdnode.c#L148 # invalid public key not allowed even with disable checks - ngu.secp256k1.pubkey(self.node.pubkey()) + # ngu.secp256k1.pubkey(self.node.pubkey()) + + try: + assert self.node.privkey() is None, "no privkeys" + except ValueError: + # ValueError is thrown from libngu if key is public + pass if not disable_checks: depth = self.node.depth() @@ -410,7 +409,6 @@ def from_cc_json(cls, vals, af_str): # new firmware, prefer key expression return cls.from_string(vals[key_exp]) - # TODO node, _, _, _ = chains.slip132_deserialize(vals[af_str]) ek = chains.current_chain().serialize_public(node) return cls.from_cc_data(vals["xfp"], vals["%s_deriv" % af_str], ek) @@ -418,13 +416,10 @@ def from_cc_json(cls, vals, af_str): @classmethod def from_psbt_xpub(cls, ek_bytes, xfp_path): xfp, *path = xfp_path - koi = KeyOriginInfo(a2b_hex(xfp2str(xfp)), path) - # TODO this should be done by C code, no need to base58 encode/decode - # byte-serialized key should be decodable - ek = ngu.codecs.b58_encode(ek_bytes) - node, chain_type = cls.parse_key(ek.encode()) - - return cls(node, koi, KeyDerivationInfo(), chain_type=chain_type) + koi = KeyOriginInfo(ustruct.pack(" Date: Sun, 29 Mar 2026 20:55:35 +0200 Subject: [PATCH 2/4] improve tests stability --- testing/test_miniscript.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/testing/test_miniscript.py b/testing/test_miniscript.py index 4b0d8baa..29e5fe8b 100644 --- a/testing/test_miniscript.py +++ b/testing/test_miniscript.py @@ -3178,9 +3178,11 @@ def test_same_key_set_miniscript(get_cc_key, bitcoin_core_signer, create_core_wa title, story = cap_story() if 'OK TO SEND' not in title: - pick_menu_item(fname) - time.sleep(0.1) - title, story = cap_story() + try: + pick_menu_item(fname) + time.sleep(0.1) + title, story = cap_story() + except: pass assert title == "OK TO SEND?" assert "msc2" in story @@ -3194,8 +3196,9 @@ def test_same_key_set_miniscript(get_cc_key, bitcoin_core_signer, create_core_wa @pytest.mark.parametrize("orig_der", [False, True]) def test_specific_wallet_signing_xpubs(orig_der, get_cc_key, bitcoin_core_signer, create_core_wallet, offer_minsc_import, press_select, bitcoind, start_sign, - cap_story, end_sign, clear_miniscript, goto_home): + cap_story, end_sign, clear_miniscript, goto_home, use_regtest): goto_home() + use_regtest() clear_miniscript() msc = "wsh(or_d(pk(@D),and_v(v:multi(2,@A,@B,@C),older(65535))))" @@ -3255,8 +3258,14 @@ def test_specific_wallet_signing_xpubs(orig_der, get_cc_key, bitcoin_core_signer end_sign(accept=True) item = po.xpubs[0] - # wrong key - key_wrong = item[0][:-1] + b"\x10" + # wrong key - but has to be valid - otherwise "bad pubkey is raised" + node = BIP32Node.from_master_secret(os.urandom(32)) + if orig_der: + a, _ = bk.split("]") + der = "/".join(a[1:].split("/")[1:]) + node = node.subkey_for_path(der) + + key_wrong = node.node.serialize_public() po.xpubs[0] = (key_wrong, item[1]) start_sign(po.as_bytes(), miniscript="msc") From 26b0c083fd7f62efd41256419fe747d37f965f2c Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Wed, 8 Apr 2026 15:42:51 +0200 Subject: [PATCH 3/4] move to chains.py --- shared/chains.py | 9 +++++++++ shared/desc_utils.py | 14 +++----------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/shared/chains.py b/shared/chains.py index afed090b..a4029a6e 100644 --- a/shared/chains.py +++ b/shared/chains.py @@ -511,4 +511,13 @@ def verify_recover_pubkey(sig, digest): except: raise ValueError('invalid signature') + +def type_from_xpub_version(xpub_ver): + # https://github.com/satoshilabs/slips/blob/master/slip-0132.md + if xpub_ver in [0x0488b21e, 0x049d7cb2, 0x04b24746, 0x0295b43f, 0x02aa7ed3]: + return "BTC" + else: + assert xpub_ver in [0x043587cf, 0x044a5262, 0x045f1cf6, 0x024289ef, 0x02575483] + return "XTN" + # EOF diff --git a/shared/desc_utils.py b/shared/desc_utils.py index 6a76c39e..cec7e424 100644 --- a/shared/desc_utils.py +++ b/shared/desc_utils.py @@ -279,21 +279,12 @@ def compile(self): d = self.serialize() return ser_compact_size(len(d)) + d - @staticmethod - def chain_from_version(version): - # https://github.com/satoshilabs/slips/blob/master/slip-0132.md - if version in [0x0488b21e, 0x049d7cb2, 0x04b24746, 0x0295b43f, 0x02aa7ed3]: - return "BTC" - else: - assert version in [0x043587cf, 0x044a5262, 0x045f1cf6, 0x024289ef, 0x02575483] - return "XTN" - @classmethod def parse_key(cls, key_str): assert key_str[1:4].lower() == b"pub", "only extended pubkeys allowed" node = ngu.hdnode.HDNode() version = node.deserialize(key_str) - return node, cls.chain_from_version(version) + return node, chains.type_from_xpub_version(version) def validate(self, my_xfp, disable_checks=False): assert self.chain_type == chains.current_key_chain().ctype, "wrong chain" @@ -419,7 +410,8 @@ def from_psbt_xpub(cls, ek_bytes, xfp_path): koi = KeyOriginInfo(ustruct.pack(" Date: Sat, 4 Apr 2026 09:22:25 +0200 Subject: [PATCH 4/4] better fitting UX message for MK versions --- shared/ccc.py | 2 +- testing/test_sssp.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/ccc.py b/shared/ccc.py index ab635180..138cbecf 100644 --- a/shared/ccc.py +++ b/shared/ccc.py @@ -1092,7 +1092,7 @@ async def sssp_enable(): First step is to define a new PIN code that is used when you want to bypass or \ disable this feature. ''', - title="Spending Policy") + title="Spending Policy" if version.has_qwerty else "Spend Policy") if ch != 'y': # just a tourist diff --git a/testing/test_sssp.py b/testing/test_sssp.py index 328ec98c..fc4ff524 100644 --- a/testing/test_sssp.py +++ b/testing/test_sssp.py @@ -35,7 +35,7 @@ def doit(pin=None, mag=None, vel=None, whitelist=None, w2fa=None, has_violation= title, story = cap_story() # it is possible that PIN was set beforehand - if title == "Spending Policy": + if title == ("Spending Policy" if is_q1 else "Spend Policy"): assert "stops you from signing transactions unless conditions are met" in story assert "locked into a special mode" in story assert "First step is to define a new PIN" in story