Post
Topic
Board Development & Technical Discussion
Topic OP
How does getting the private key and nonce from a nonce reuse work?
by
fairmuffin
on 01/04/2025, 18:29:54 UTC
Hi there!

So I've been researching about nonces lately, and found an address that reused many nonces across its transactions. The address is empty obviously; however, the Python code generated by Sonnet 3.7 doesn't seem to give the correct one, and I am not super sure what's the issue. Here's the code for instance:

Code:
import hashlib
import binascii
import base58
from ecdsa import SigningKey, SECP256k1, util

def extract_r_s_from_signature(signature):
    # Remove the [ALL] suffix if present
    signature = signature.split('[ALL]')[0]
   
    # The signature is in DER format
    # Extract the raw hex string
    hex_str = signature
   
    # Skip the first 4 bytes (30 + length + 02 + r_length) to get to r
    r_start = 8  # Skip '3044' and '0220'
    r_end = r_start + 64  # r is 32 bytes (64 hex chars)
    r_hex = hex_str[r_start:r_end]
   
    # After r, there's another 02 marker and length byte for s
    s_start = r_end + 4  # Skip '02' and '20' (or '21' if s has a leading zero)
    s_hex = hex_str[s_start:]
   
    return int(r_hex, 16), int(s_hex, 16)

def calculate_private_key(z1, z2, s1, s2, r):
    # Calculate private key from two different signatures with the same r value
    s1_minus_s2 = (s1 - s2) % SECP256k1.order
    z1_minus_z2 = (z1 - z2) % SECP256k1.order
   
    # Calculate the modular inverse
    s1_minus_s2_inv = pow(s1_minus_s2, -1, SECP256k1.order)
   
    # Calculate private key
    private_key = (z1_minus_z2 * s1_minus_s2_inv) % SECP256k1.order
    return private_key

def calculate_nonce(z, s, r, private_key):
    # Calculate the nonce (k) used in the signature
    r_inv = pow(r, -1, SECP256k1.order)
    nonce = (r_inv * ((s * private_key) - z)) % SECP256k1.order
    return nonce

def private_key_to_wif(private_key_hex, compressed=True):
    # Add version byte (0x80 for mainnet)
    extended_key = "80" + private_key_hex
   
    # Add compression flag if needed
    if compressed:
        extended_key += "01"
   
    # First round of SHA-256
    first_sha = hashlib.sha256(binascii.unhexlify(extended_key)).digest()
   
    # Second round of SHA-256
    second_sha = hashlib.sha256(first_sha).digest()
   
    # First 4 bytes of the second SHA-256 hash are the checksum
    checksum = binascii.hexlify(second_sha[:4]).decode()
   
    # Add the checksum to the extended key
    wif_key = extended_key + checksum
   
    # Convert to base58
    wif = base58.b58encode(binascii.unhexlify(wif_key)).decode()
   
    return wif

def verify_private_key(private_key_hex, public_key_hex):
    # Convert private key to SigningKey
    private_key_bytes = bytes.fromhex(private_key_hex)
    sk = SigningKey.from_string(private_key_bytes, curve=SECP256k1)
   
    # Get public key from private key
    vk = sk.get_verifying_key()
   
    # Adjust for compressed public key format
    computed_x = vk.pubkey.point.x()
    computed_y = vk.pubkey.point.y()
   
    # Check if y is even or odd for the compressed format prefix
    prefix = '02' if computed_y % 2 == 0 else '03'
    computed_compressed = prefix + hex(computed_x)[2:].zfill(64)
   
    return computed_compressed == public_key_hex

def main():
    # Data from the first signature
    address = "1BTrViTDXhWrdw5ErBWSyP5LdzYmeuDTr2"
    public_key = "03c88e78a3f105d99b7b0643f3cfca56bad5ffd2c8e1bc055d8c6d51475bc6b2cf"
    tx_hash1 = "223d80bffcb8cc519f23d6e7795693c5c0b25a1f3c477a96632f875c067d2439"
    r_value = "0c9a907263e472822c3afc1df2f87c95b9c8f9956ab891a3f7b3f482fc16814d"
    sig1 = "304402200c9a907263e472822c3afc1df2f87c95b9c8f9956ab891a3f7b3f482fc16814d022055dde0ae98f2ffad66a888c3ea22d8de0635062b65ff06b75191e4085035fa61"
   
    # Data from the second signature
    tx_hash2 = "8b044016b8307dd8aefe5dcb61cfac97c01122f578fc7e4192472c45405e0a74"
    sig2 = "304402200c9a907263e472822c3afc1df2f87c95b9c8f9956ab891a3f7b3f482fc16814d022061f915743d2dadd8856c53405a4fb8e1e0ee974a852dbc2c89649e790dd157ac"
   
    # Print the actual R-value for debugging
    print(f"Expected R-value: {r_value}")
   
    # Directly use the R-value from the input
    r = int(r_value, 16)
   
    # Extract S values from signatures - we'll only extract S since we know R
    _, s1 = extract_r_s_from_signature(sig1)
    _, s2 = extract_r_s_from_signature(sig2)
   
    print(f"S1: {hex(s1)[2:]}")
    print(f"S2: {hex(s2)[2:]}")
   
    # Convert transaction hashes to integers
    z1 = int(tx_hash1, 16)
    z2 = int(tx_hash2, 16)
   
    # Calculate the private key
    private_key = calculate_private_key(z1, z2, s1, s2, r)
    private_key_hex = hex(private_key)[2:].zfill(64)
   
    # Generate compressed WIF format private key (for Electrum)
    wif_compressed = private_key_to_wif(private_key_hex, compressed=True)
   
    # Calculate the nonce used
    nonce = calculate_nonce(z1, s1, r, private_key)
    nonce_hex = hex(nonce)[2:].zfill(64)
   
    # Verify if the calculated private key corresponds to the public key
    is_valid = verify_private_key(private_key_hex, public_key)
   
    print(f"Found private key (hex): {private_key_hex}")
    print(f"Compressed WIF for Electrum: {wif_compressed}")
    print(f"Nonce (k) used: {nonce_hex}")
    print(f"Private key verification: {'Successful' if is_valid else 'Failed'}")
   
    # Print instructions for using in Electrum
    print("\n=== ELECTRUM IMPORT INSTRUCTIONS ===")
    print("1. Open Electrum wallet")
    print("2. Go to Wallet -> Private Keys -> Import")
    print("3. Paste the Compressed WIF key")
    print("4. Electrum will scan for transactions and show any balance associated with this key")
    print("5. Transfer any funds to a new, secure wallet immediately")

if __name__ == "__main__":
    main()

Address involved:
1BTrViTDXhWrdw5ErBWSyP5LdzYmeuDTr2
Transaction 1 I chose: 223d80bffcb8cc519f23d6e7795693c5c0b25a1f3c477a96632f875c067d2439
Transaction 2 I chose: 8b044016b8307dd8aefe5dcb61cfac97c01122f578fc7e4192472c45405e0a74

What I am doing wrong?