From e7849bce941acdfa7250a435c2dcfe08332a37bf Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 28 Feb 2023 16:08:11 +0000 Subject: [PATCH] descriptor.py: clean-up and test PubkeyProvider.get_full_derivation_* --- electrum/bip32.py | 8 ++++++-- electrum/descriptor.py | 28 ++++++++++++---------------- electrum/tests/test_descriptor.py | 17 +++++++++++++++++ 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/electrum/bip32.py b/electrum/bip32.py index 74ee1aeb8..225427f78 100644 --- a/electrum/bip32.py +++ b/electrum/bip32.py @@ -334,14 +334,18 @@ def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]: # makes concatenating paths easier continue prime = 0 - if x.endswith("'") or x.endswith("h"): + if x.endswith("'") or x.endswith("h"): # note: some implementations also accept "H", "p", "P" x = x[:-1] prime = BIP32_PRIME if x.startswith('-'): if prime: raise ValueError(f"bip32 path child index is signalling hardened level in multiple ways") prime = BIP32_PRIME - child_index = abs(int(x)) | prime + try: + x_int = int(x) + except ValueError as e: + raise ValueError(f"failed to parse bip32 path: {(str(e))}") from None + child_index = abs(x_int) | prime if child_index > UINT32_MAX: raise ValueError(f"bip32 path child index too large: {child_index} > {UINT32_MAX}") path.append(child_index) diff --git a/electrum/descriptor.py b/electrum/descriptor.py index f11adef65..909c9de2c 100644 --- a/electrum/descriptor.py +++ b/electrum/descriptor.py @@ -16,7 +16,7 @@ import enum -from .bip32 import convert_bip32_path_to_list_of_uint32, BIP32Node, KeyOriginInfo +from .bip32 import convert_bip32_path_to_list_of_uint32, BIP32Node, KeyOriginInfo, BIP32_PRIME from . import bitcoin from .bitcoin import construct_script, opcodes, construct_witness from . import constants @@ -254,35 +254,31 @@ class PubkeyProvider(object): assert not self.is_range() return unhexlify(self.pubkey) - def get_full_derivation_path(self, pos: int) -> str: + def get_full_derivation_path(self, *, pos: Optional[int] = None) -> str: """ Returns the full derivation path at the given position, including the origin """ - path = self.origin.get_derivation_path() if self.origin is not None else "m/" + if self.is_range() and pos is None: + raise ValueError("pos must be set for ranged descriptor") + path = self.origin.get_derivation_path() if self.origin is not None else "m" path += self.deriv_path if self.deriv_path is not None else "" if path[-1] == "*": path = path[:-1] + str(pos) return path - def get_full_derivation_int_list(self, pos: int) -> List[int]: + def get_full_derivation_int_list(self, *, pos: Optional[int] = None) -> List[int]: """ Returns the full derivation path as an integer list at the given position. Includes the origin and master key fingerprint as an int """ + if self.is_range() and pos is None: + raise ValueError("pos must be set for ranged descriptor") path: List[int] = self.origin.get_full_int_list() if self.origin is not None else [] if self.deriv_path is not None: - der_split = self.deriv_path.split("/") - for p in der_split: - if not p: - continue - if p == "*": - i = pos - elif p[-1] in "'phHP": - assert len(p) >= 2 - i = int(p[:-1]) | 0x80000000 - else: - i = int(p) - path.append(i) + der_suffix = self.deriv_path + assert (wc_count := der_suffix.count("*")) <= 1, wc_count + der_suffix = der_suffix.replace("*", str(pos)) + path.extend(convert_bip32_path_to_list_of_uint32(der_suffix)) return path def __lt__(self, other: 'PubkeyProvider') -> bool: diff --git a/electrum/tests/test_descriptor.py b/electrum/tests/test_descriptor.py index 6c1b80104..096f48f74 100644 --- a/electrum/tests/test_descriptor.py +++ b/electrum/tests/test_descriptor.py @@ -34,6 +34,8 @@ class TestDescriptor(ElectrumTestCase): self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h") self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") + self.assertEqual(desc.pubkeys[0].get_full_derivation_path(), "m/84h/1h/0h/0/0") + self.assertEqual(desc.pubkeys[0].get_full_derivation_int_list(), [16777216, 2147483732, 2147483649, 2147483648, 0, 0]) self.assertEqual(desc.to_string_no_checksum(), d) e = desc.expand() self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) @@ -51,6 +53,8 @@ class TestDescriptor(ElectrumTestCase): self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.get_derivation_path(), "m/48h/0h/0h/2h") self.assertEqual(desc.subdescriptors[0].pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.subdescriptors[0].pubkeys[0].deriv_path, "/0/0") + self.assertEqual(desc.subdescriptors[0].pubkeys[0].get_full_derivation_path(), "m/48h/0h/0h/2h/0/0") + self.assertEqual(desc.subdescriptors[0].pubkeys[0].get_full_derivation_int_list(), [16777216, 2147483696, 2147483648, 2147483648, 2147483650, 0, 0]) self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.fingerprint.hex(), "00000002") self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.get_derivation_path(), "m/48h/0h/0h/2h") @@ -109,6 +113,8 @@ class TestDescriptor(ElectrumTestCase): self.assertEqual(desc.pubkeys[0].origin, None) self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") + self.assertEqual(desc.pubkeys[0].get_full_derivation_path(), "m/0/0") + self.assertEqual(desc.pubkeys[0].get_full_derivation_int_list(), [0, 0]) self.assertEqual(desc.to_string_no_checksum(), d) e = desc.expand() self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) @@ -138,6 +144,8 @@ class TestDescriptor(ElectrumTestCase): self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h/0/0") self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") self.assertEqual(desc.pubkeys[0].deriv_path, None) + self.assertEqual(desc.pubkeys[0].get_full_derivation_path(), "m/84h/1h/0h/0/0") + self.assertEqual(desc.pubkeys[0].get_full_derivation_int_list(), [16777216, 2147483732, 2147483649, 2147483648, 0, 0]) self.assertEqual(desc.to_string_no_checksum(), d) e = desc.expand() self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) @@ -164,6 +172,8 @@ class TestDescriptor(ElectrumTestCase): self.assertEqual(desc.pubkeys[0].origin, None) self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") self.assertEqual(desc.pubkeys[0].deriv_path, None) + self.assertEqual(desc.pubkeys[0].get_full_derivation_path(), "m") + self.assertEqual(desc.pubkeys[0].get_full_derivation_int_list(), []) self.assertEqual(desc.to_string_no_checksum(), d) def test_parse_empty_descriptor(self): @@ -176,6 +186,13 @@ class TestDescriptor(ElectrumTestCase): self.assertIsNotNone(desc) self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h") + @as_testnet + def test_parse_descriptor_unknown_notation_for_hardened_derivation(self): + with self.assertRaises(ValueError): + desc = parse_descriptor("wpkh([00000001/84x/1x/0x]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)") + with self.assertRaises(ValueError): + desc = parse_descriptor("wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0x)") + def test_checksums(self): with self.subTest(msg="Valid checksum"): self.assertIsNotNone(parse_descriptor("sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#5js07kwj"))