From d69a2b497580bd469250dcaa39d21ee8a5d826d3 Mon Sep 17 00:00:00 2001 From: Lennart Date: Tue, 21 Nov 2023 12:51:54 +0100 Subject: [PATCH] [ADD] Allow to create key using point (x,y) on curve --- bitcoinlib/keys.py | 69 +++++++++++++++++++++++++++++----------------- tests/test_keys.py | 18 ++++++++---- 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/bitcoinlib/keys.py b/bitcoinlib/keys.py index cc17b70a..d3c37976 100644 --- a/bitcoinlib/keys.py +++ b/bitcoinlib/keys.py @@ -150,6 +150,9 @@ def get_key_format(key, is_private=None): elif isinstance(key, bytes) and len(key) == 32: key_format = 'bin' is_private = True + elif isinstance(key, tuple): + key_format = 'point' + is_private = False elif len(key) == 130 and key[:2] == '04' and not is_private: key_format = 'public_uncompressed' is_private = False @@ -737,7 +740,7 @@ def __init__(self, import_key=None, network=None, compressed=True, password='', 12127227708610754620337553985245292396444216111803695028419544944213442390363 :param import_key: If specified import given private or public key. If not specified a new private key is generated. - :type import_key: str, int, bytes + :type import_key: str, int, bytes, tuple :param network: Bitcoin, testnet, litecoin or other network :type network: str, Network :param compressed: Is key compressed or not, default is True @@ -807,32 +810,46 @@ def __init__(self, import_key=None, network=None, compressed=True, password='', if not self.is_private: self.secret = None - pub_key = to_hexstring(import_key) - if len(pub_key) == 130: - self._public_uncompressed_hex = pub_key - self.x_hex = pub_key[2:66] - self.y_hex = pub_key[66:130] - self._y = int(self.y_hex, 16) - self.compressed = False - if self._y % 2: - prefix = '03' - else: - prefix = '02' - self.public_hex = pub_key - self.public_compressed_hex = prefix + self.x_hex - else: - self.public_hex = pub_key - self.x_hex = pub_key[2:66] + if self.key_format == 'point': self.compressed = True - self._x = int(self.x_hex, 16) - self.public_compressed_hex = pub_key - self.public_compressed_byte = bytes.fromhex(self.public_compressed_hex) - if self._public_uncompressed_hex: - self._public_uncompressed_byte = bytes.fromhex(self._public_uncompressed_hex) - if self.compressed: - self.public_byte = self.public_compressed_byte + self._x = import_key[0] + self._y = import_key[1] + self.x_bytes = self._x.to_bytes(32, 'big') + self.y_bytes = self._y.to_bytes(32, 'big') + self.x_hex = self.x_bytes.hex() + self.y_hex = self.y_bytes.hex() + prefix = '03' if self._y % 2 else '02' + self.public_hex = prefix + self.x_hex + self.public_compressed_hex = prefix + self.x_hex + self.public_byte = (b'\3' if self._y % 2 else b'\2') + self.x_bytes else: - self.public_byte = self.public_uncompressed_byte + pub_key = to_hexstring(import_key) + if len(pub_key) == 130: + self._public_uncompressed_hex = pub_key + self.x_hex = pub_key[2:66] + self.y_hex = pub_key[66:130] + self._y = int(self.y_hex, 16) + self.compressed = False + if self._y % 2: + prefix = '03' + else: + prefix = '02' + self.public_hex = pub_key + self.public_compressed_hex = prefix + self.x_hex + else: + self.public_hex = pub_key + self.x_hex = pub_key[2:66] + self.compressed = True + self._x = int(self.x_hex, 16) + self.public_compressed_hex = pub_key + self.public_compressed_byte = bytes.fromhex(self.public_compressed_hex) + if self._public_uncompressed_hex: + self._public_uncompressed_byte = bytes.fromhex(self._public_uncompressed_hex) + if self.compressed: + self.public_byte = self.public_compressed_byte + else: + self.public_byte = self.public_uncompressed_byte + elif self.is_private and self.key_format == 'decimal': self.secret = int(import_key) self.private_hex = change_base(self.secret, 10, 16, 64) @@ -1422,7 +1439,7 @@ def __init__(self, import_key=None, key=None, chain=None, depth=0, parent_finger :param import_key: HD Key to import in WIF format or as byte with key (32 bytes) and chain (32 bytes) - :type import_key: str, bytes, int + :type import_key: str, bytes, int, tuple :param key: Private or public key (length 32) :type key: bytes :param chain: A chain code (length 32) diff --git a/tests/test_keys.py b/tests/test_keys.py index 61e98dc2..b39fbcba 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -74,6 +74,13 @@ def test_keys_inverse(self): self.assertEqual(k_inv.x, inv_x) self.assertEqual(k_inv.y, inv_y) + def test_keys_inverse2(self): + k = HDKey() + pub_k = k.public() + self.assertEqual(k.address(), pub_k.address()) + self.assertEqual((-k).address(), pub_k.inverse().address()) + self.assertEqual((-k).address(), k.inverse().address()) + def test_dict_and_json_outputs(self): k = HDKey() k.address(script_type='p2wsh', encoding='bech32') @@ -99,12 +106,13 @@ def test_path_expand(self): self.assertRaisesRegexp(BKeyError, "Please provide path as list with at least 1 item", path_expand, 5) - def test_key_inverse(self): + def test_keys_create_public_point(self): k = HDKey() - pub_k = k.public() - self.assertEqual(k.address(), pub_k.address()) - self.assertEqual((-k).address(), pub_k.inverse().address()) - self.assertEqual((-k).address(), k.inverse().address()) + p = (k.x, k.y) + k2 = HDKey(p) + self.assertEqual(k, k2) + self.assertEqual(k.public(), k2) + self.assertEqual(k.address(), k2.address()) class TestGetKeyFormat(unittest.TestCase):