@@ -528,74 +528,113 @@ 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 ----------
535- hex_str : str
535+ hex_str : str, optional
536536 the public key in hex string
537+
538+ In case of generating public key from message and signature:-
539+ message : str, optional
540+ The original message that was signed
541+ signature : bytes, optional
542+ A 65-byte Bitcoin signature (1-byte recovery ID + 64-byte ECDSA signature).
537543
538544 Raises
539545 ------
540546 TypeError
541547 If first byte of public key (corresponding to SEC format) is
542548 invalid.
549+ If neither hex_str nor (message, signature) are provided
550+ ValueError
551+ If message is empty when attempting recovery
552+ If signature is not exactly 65 bytes
553+ If an invalid recovery ID is detected
543554 """
544- hex_str = hex_str .strip ()
555+ if hex_str :
556+ hex_str = hex_str .strip ()
545557
546- # Normalize hex string by removing '0x' prefix and any whitespace
547- if hex_str .lower ().startswith ('0x' ):
548- hex_str = hex_str [2 :]
558+ # Normalize hex string by removing '0x' prefix and any whitespace
559+ if hex_str .lower ().startswith ('0x' ):
560+ hex_str = hex_str [2 :]
549561
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 )
562+ # expects key as hex string - SEC format
563+ first_byte_in_hex = hex_str [:2 ] # 2 hex chars = 1 byte
564+ hex_bytes = h_to_b (hex_str )
553565
554- taproot = False
566+ taproot = False
555567
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)
568+ # check if compressed or not
569+ if len (hex_bytes ) > 33 :
570+ # uncompressed - SEC format: 0x04 + x + y coordinates (x,y are 32 byte
571+ # numbers)
560572
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
573+ # remove first byte and instantiate ecdsa key
574+ self .key = VerifyingKey .from_string (hex_bytes [1 :], curve = SECP256k1 )
575+ elif len (hex_bytes ) > 31 :
576+ # key is either compressed or in x-only taproot format
565577
566- # taproot public keys are exactly 32 bytes
567- if len (hex_bytes ) == 32 :
568- taproot = True
578+ # taproot public keys are exactly 32 bytes
579+ if len (hex_bytes ) == 32 :
580+ taproot = True
569581
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 )
582+ # compressed - SEC FORMAT: 0x02|0x03 + x coordinate (if 02 then y
583+ # is even else y is odd. Calculate y and then instantiate the ecdsa key
584+ x_coord = int (hex_str [2 :], 16 )
573585
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- )
586+ # y = modulo_square_root( (x**3 + 7) mod p ) -- there will be 2 y values
587+ y_values = sqrt_mod (
588+ (x_coord ** 3 + 7 ) % Secp256k1Params ._p , Secp256k1Params ._p , True
589+ )
578590
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
591+ assert y_values is not None
592+ # check SEC format's first byte to determine which of the 2 values to use
593+ if first_byte_in_hex == "02" or taproot :
594+ # y is the even value
595+ if y_values [0 ] % 2 == 0 : # type: ignore
596+ y_coord = y_values [0 ] # type: ignore
597+ else :
598+ y_coord = y_values [1 ] # type: ignore
599+ elif first_byte_in_hex == "03" :
600+ # y is the odd value
601+ if y_values [0 ] % 2 == 0 : # type: ignore
602+ y_coord = y_values [1 ] # type: ignore
603+ else :
604+ y_coord = y_values [0 ] # type: ignore
585605 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 )
606+ raise TypeError ("Invalid SEC compressed format" )
607+
608+ uncompressed_hex = f"{ x_coord :064x} { y_coord :064x} "
609+ uncompressed_hex_bytes = h_to_b (uncompressed_hex )
610+ self .key = VerifyingKey .from_string (uncompressed_hex_bytes , curve = SECP256k1 )
611+ elif message or signature :
612+ if not message :
613+ raise ValueError ("Empty message provided for public key recovery." )
614+
615+ if (len (signature ) != 65 ):
616+ raise ValueError ("Invalid signature length, must be exactly 65 bytes" )
617+
618+ # The compressed signature is of the format: recovery_id (1 byte) | r (32 bytes) | s (32 bytes)
619+ # We subtract the prefix(27) for uncompressed signatures and an additional 4 (31) for compressed signatures to get the recovery id
620+ recovery_id = signature [0 ] - 31
621+ if not (0 <= recovery_id <= 3 ): # A valid recovery ID is between 0 and 3
622+ raise ValueError (f"Invalid recovery ID: expected 31-34, got { signature [0 ]} " )
623+
624+ signature = signature [1 :] #Remove recovery id from signature
625+
626+ # All bitcoin signatures include the magic prefix. It is just a string
627+ # added to the message to distinguish Bitcoin-specific messages.
628+ message_magic = add_magic_prefix (message )
629+ # create message digest
630+ message_digest = hashlib .sha256 (hashlib .sha256 (message_magic ).digest ()).digest ()
631+
632+ recovered_keys = VerifyingKey .from_public_key_recovery_with_digest (
633+ signature , message_digest , curve = SECP256k1 , hashfunc = hashlib .sha256 , sigdecode = sigdecode_string
634+ )
635+ self .key = recovered_keys [recovery_id ]
636+ else :
637+ raise TypeError ("Either 'hex_str' or ('message', 'signature') must be provided." )
599638
600639 @classmethod
601640 def from_hex (cls , hex_str : str ) -> PublicKey :
@@ -665,11 +704,12 @@ def is_y_even(self) -> bool:
665704 return y % 2 == 0
666705
667706 @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!" )
707+ def from_message_signature (cls , message , signature ):
708+ """Recovers a public key from a Bitcoin-signed message and a 65-byte compressed signature.
709+ """
710+ #Note: Only works for compressed signatures because DER encoding does not contain the recovery id
711+ return cls (message = message , signature = signature )
712+ # raise BaseException("NO-OP!")
673713
674714 @classmethod
675715 def verify_message (cls , address : str , signature : str , message : str ) -> bool :
0 commit comments