Skip to content

Commit 1d02a80

Browse files
Added public key creation from message and signature
1 parent a93da8b commit 1d02a80

1 file changed

Lines changed: 77 additions & 51 deletions

File tree

bitcoinutils/keys.py

Lines changed: 77 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -528,74 +528,99 @@ class PublicKey:
528528
returns the corresponding P2trAddress object
529529
"""
530530

531-
def __init__(self, hex_str: str) -> None:
531+
def __init__(self, hex_str: str = None, message: str = None, signature: bytes = None) -> None:
532532
"""
533533
Parameters
534534
----------
535535
hex_str : str
536536
the public key in hex string
537+
538+
In case of generating public key from message and signature:-
539+
message : str
540+
the message
541+
signature : str
542+
the compressed signature in bytes
537543
538544
Raises
539545
------
540546
TypeError
541547
If first byte of public key (corresponding to SEC format) is
542548
invalid.
543549
"""
544-
hex_str = hex_str.strip()
550+
if hex_str:
551+
hex_str = hex_str.strip()
545552

546-
# Normalize hex string by removing '0x' prefix and any whitespace
547-
if hex_str.lower().startswith('0x'):
548-
hex_str = hex_str[2:]
553+
# Normalize hex string by removing '0x' prefix and any whitespace
554+
if hex_str.lower().startswith('0x'):
555+
hex_str = hex_str[2:]
549556

550-
# expects key as hex string - SEC format
551-
first_byte_in_hex = hex_str[:2] # 2 hex chars = 1 byte
552-
hex_bytes = h_to_b(hex_str)
557+
# expects key as hex string - SEC format
558+
first_byte_in_hex = hex_str[:2] # 2 hex chars = 1 byte
559+
hex_bytes = h_to_b(hex_str)
553560

554-
taproot = False
561+
taproot = False
555562

556-
# check if compressed or not
557-
if len(hex_bytes) > 33:
558-
# uncompressed - SEC format: 0x04 + x + y coordinates (x,y are 32 byte
559-
# numbers)
563+
# check if compressed or not
564+
if len(hex_bytes) > 33:
565+
# uncompressed - SEC format: 0x04 + x + y coordinates (x,y are 32 byte
566+
# numbers)
560567

561-
# remove first byte and instantiate ecdsa key
562-
self.key = VerifyingKey.from_string(hex_bytes[1:], curve=SECP256k1)
563-
elif len(hex_bytes) > 31:
564-
# key is either compressed or in x-only taproot format
568+
# remove first byte and instantiate ecdsa key
569+
self.key = VerifyingKey.from_string(hex_bytes[1:], curve=SECP256k1)
570+
elif len(hex_bytes) > 31:
571+
# key is either compressed or in x-only taproot format
565572

566-
# taproot public keys are exactly 32 bytes
567-
if len(hex_bytes) == 32:
568-
taproot = True
573+
# taproot public keys are exactly 32 bytes
574+
if len(hex_bytes) == 32:
575+
taproot = True
569576

570-
# compressed - SEC FORMAT: 0x02|0x03 + x coordinate (if 02 then y
571-
# is even else y is odd. Calculate y and then instantiate the ecdsa key
572-
x_coord = int(hex_str[2:], 16)
577+
# compressed - SEC FORMAT: 0x02|0x03 + x coordinate (if 02 then y
578+
# is even else y is odd. Calculate y and then instantiate the ecdsa key
579+
x_coord = int(hex_str[2:], 16)
573580

574-
# y = modulo_square_root( (x**3 + 7) mod p ) -- there will be 2 y values
575-
y_values = sqrt_mod(
576-
(x_coord**3 + 7) % Secp256k1Params._p, Secp256k1Params._p, True
577-
)
581+
# y = modulo_square_root( (x**3 + 7) mod p ) -- there will be 2 y values
582+
y_values = sqrt_mod(
583+
(x_coord**3 + 7) % Secp256k1Params._p, Secp256k1Params._p, True
584+
)
578585

579-
assert y_values is not None
580-
# check SEC format's first byte to determine which of the 2 values to use
581-
if first_byte_in_hex == "02" or taproot:
582-
# y is the even value
583-
if y_values[0] % 2 == 0: # type: ignore
584-
y_coord = y_values[0] # type: ignore
586+
assert y_values is not None
587+
# check SEC format's first byte to determine which of the 2 values to use
588+
if first_byte_in_hex == "02" or taproot:
589+
# y is the even value
590+
if y_values[0] % 2 == 0: # type: ignore
591+
y_coord = y_values[0] # type: ignore
592+
else:
593+
y_coord = y_values[1] # type: ignore
594+
elif first_byte_in_hex == "03":
595+
# y is the odd value
596+
if y_values[0] % 2 == 0: # type: ignore
597+
y_coord = y_values[1] # type: ignore
598+
else:
599+
y_coord = y_values[0] # type: ignore
585600
else:
586-
y_coord = y_values[1] # type: ignore
587-
elif first_byte_in_hex == "03":
588-
# y is the odd value
589-
if y_values[0] % 2 == 0: # type: ignore
590-
y_coord = y_values[1] # type: ignore
591-
else:
592-
y_coord = y_values[0] # type: ignore
593-
else:
594-
raise TypeError("Invalid SEC compressed format")
595-
596-
uncompressed_hex = f"{x_coord:064x}{y_coord:064x}"
597-
uncompressed_hex_bytes = h_to_b(uncompressed_hex)
598-
self.key = VerifyingKey.from_string(uncompressed_hex_bytes, curve=SECP256k1)
601+
raise TypeError("Invalid SEC compressed format")
602+
603+
uncompressed_hex = f"{x_coord:064x}{y_coord:064x}"
604+
uncompressed_hex_bytes = h_to_b(uncompressed_hex)
605+
self.key = VerifyingKey.from_string(uncompressed_hex_bytes, curve=SECP256k1)
606+
elif message and signature:
607+
if(len(signature) != 65):
608+
raise ValueError("Invalid signature length")
609+
610+
recovery_id = signature[0] - 31 #Extract recovery id
611+
612+
signature = signature[1:] #Remove recovery id from signature
613+
614+
message_magic = add_magic_prefix(message)
615+
# create message digest
616+
message_digest = hashlib.sha256(hashlib.sha256(message_magic).digest()).digest()
617+
618+
recovered_keys = VerifyingKey.from_public_key_recovery_with_digest(
619+
signature, message_digest, curve=SECP256k1, hashfunc = hashlib.sha256, sigdecode=sigdecode_string
620+
)
621+
self.key = recovered_keys[recovery_id]
622+
else:
623+
raise TypeError("Parameters missing")
599624

600625
@classmethod
601626
def from_hex(cls, hex_str: str) -> PublicKey:
@@ -665,11 +690,12 @@ def is_y_even(self) -> bool:
665690
return y % 2 == 0
666691

667692
@classmethod
668-
def from_message_signature(cls, signature):
669-
# TODO implement (add signature=None in __init__, etc.)
670-
# TODO plus does this apply to DER signatures as well?
671-
# return cls(signature=signature)
672-
raise BaseException("NO-OP!")
693+
def from_message_signature(cls, message, signature):
694+
"""Creates a public key from a message signature
695+
"""
696+
#Note: Only works for compressed signatures because DER encoding does not contain the recovery id
697+
return cls(message=message, signature=signature)
698+
# raise BaseException("NO-OP!")
673699

674700
@classmethod
675701
def verify_message(cls, address: str, signature: str, message: str) -> bool:

0 commit comments

Comments
 (0)