Browse Source

descriptor.py: clean-up and test PubkeyProvider.get_full_derivation_*

master
SomberNight 3 years ago
parent
commit
e7849bce94
No known key found for this signature in database
GPG Key ID: B33B5F232C6271E9
  1. 8
      electrum/bip32.py
  2. 28
      electrum/descriptor.py
  3. 17
      electrum/tests/test_descriptor.py

8
electrum/bip32.py

@ -334,14 +334,18 @@ def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]:
# makes concatenating paths easier # makes concatenating paths easier
continue continue
prime = 0 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] x = x[:-1]
prime = BIP32_PRIME prime = BIP32_PRIME
if x.startswith('-'): if x.startswith('-'):
if prime: if prime:
raise ValueError(f"bip32 path child index is signalling hardened level in multiple ways") raise ValueError(f"bip32 path child index is signalling hardened level in multiple ways")
prime = BIP32_PRIME 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: if child_index > UINT32_MAX:
raise ValueError(f"bip32 path child index too large: {child_index} > {UINT32_MAX}") raise ValueError(f"bip32 path child index too large: {child_index} > {UINT32_MAX}")
path.append(child_index) path.append(child_index)

28
electrum/descriptor.py

@ -16,7 +16,7 @@
import enum 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 . import bitcoin
from .bitcoin import construct_script, opcodes, construct_witness from .bitcoin import construct_script, opcodes, construct_witness
from . import constants from . import constants
@ -254,35 +254,31 @@ class PubkeyProvider(object):
assert not self.is_range() assert not self.is_range()
return unhexlify(self.pubkey) 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 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 "" path += self.deriv_path if self.deriv_path is not None else ""
if path[-1] == "*": if path[-1] == "*":
path = path[:-1] + str(pos) path = path[:-1] + str(pos)
return path 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. Returns the full derivation path as an integer list at the given position.
Includes the origin and master key fingerprint as an int 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 [] path: List[int] = self.origin.get_full_int_list() if self.origin is not None else []
if self.deriv_path is not None: if self.deriv_path is not None:
der_split = self.deriv_path.split("/") der_suffix = self.deriv_path
for p in der_split: assert (wc_count := der_suffix.count("*")) <= 1, wc_count
if not p: der_suffix = der_suffix.replace("*", str(pos))
continue path.extend(convert_bip32_path_to_list_of_uint32(der_suffix))
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)
return path return path
def __lt__(self, other: 'PubkeyProvider') -> bool: def __lt__(self, other: 'PubkeyProvider') -> bool:

17
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].origin.get_derivation_path(), "m/84h/1h/0h")
self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B")
self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") 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) self.assertEqual(desc.to_string_no_checksum(), d)
e = desc.expand() e = desc.expand()
self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) 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].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].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B")
self.assertEqual(desc.subdescriptors[0].pubkeys[0].deriv_path, "/0/0") 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.fingerprint.hex(), "00000002")
self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.get_derivation_path(), "m/48h/0h/0h/2h") 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].origin, None)
self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B") self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B")
self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0") 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) self.assertEqual(desc.to_string_no_checksum(), d)
e = desc.expand() e = desc.expand()
self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) 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].origin.get_derivation_path(), "m/84h/1h/0h/0/0")
self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7")
self.assertEqual(desc.pubkeys[0].deriv_path, None) 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) self.assertEqual(desc.to_string_no_checksum(), d)
e = desc.expand() e = desc.expand()
self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa")) 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].origin, None)
self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7") self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7")
self.assertEqual(desc.pubkeys[0].deriv_path, None) 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) self.assertEqual(desc.to_string_no_checksum(), d)
def test_parse_empty_descriptor(self): def test_parse_empty_descriptor(self):
@ -176,6 +186,13 @@ class TestDescriptor(ElectrumTestCase):
self.assertIsNotNone(desc) self.assertIsNotNone(desc)
self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h") 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): def test_checksums(self):
with self.subTest(msg="Valid checksum"): with self.subTest(msg="Valid checksum"):
self.assertIsNotNone(parse_descriptor("sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#5js07kwj")) self.assertIsNotNone(parse_descriptor("sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#5js07kwj"))

Loading…
Cancel
Save