17
\$\begingroup\$

I wanted to learn how to create a Bitcoin wallet in code. I used as reference this guide which has code examples in JavaScript.

I wrote my implementation in Python. Here is the resulting code:

#!/usr/bin/env python

'''
Creates BitCoin Wallet complaint credentials:
- Public Key
- Private Key
- Private Key (Wallet Import Format)
'''

import hashlib
import base58
import ecdsa

from ecdsa.keys import SigningKey
from utilitybelt import dev_random_entropy
from binascii import hexlify, unhexlify

def random_secret_exponent(curve_order):
    while True:
        random_hex = hexlify(dev_random_entropy(32))
        random_int = int(random_hex, 16)
        if random_int >= 1 and random_int < curve_order:
            return random_int

def generate_private_key():
    curve = ecdsa.curves.SECP256k1
    se = random_secret_exponent(curve.order)
    from_secret_exponent = ecdsa.keys.SigningKey.from_secret_exponent
    return hexlify(from_secret_exponent(se, curve, hashlib.sha256).to_string())

def generate_public_key(private_key_hex):
    hash160 = ripe_hash(private_key_hex)
    public_key_and_version_hex = b"04" + hash160 
    checksum = double_hash(public_key_and_version_hex)[:4]
    return base58.b58encode(public_key_and_version_hex + checksum)

def ripe_hash(key):
    ret = hashlib.new('ripemd160')
    ret.update(hashlib.sha256(key).digest())
    return ret.digest()

def double_hash(key):
    return hashlib.sha256(hashlib.sha256(key).digest()).digest()

def generate_private_key_wif(private_key_hex):
    private_key_and_version = b"80" + private_key_hex
    checksum = double_hash(private_key_and_version)[:8]
    hashed = private_key_and_version + checksum
    return base58.b58encode(private_key_and_version + checksum)

def main():
    private_key_hex = generate_private_key()
    public_key_hex = generate_public_key(private_key_hex)
    private_key_wif_hex = generate_private_key_wif(private_key_hex)
    print("Private Key: " + private_key_hex)
    print("Public Key: " + public_key_hex)
    print("Private Key (WIF Format): " + private_key_wif_hex)

if __name__ == '__main__':
    main()

How to run:

Private Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Public Key: 2Uc973e5adf5867512dd7a21638810bd53422
Private Key (WIF Format): xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

I have my concerns about the correctness of the generated keys, as well as for the private key in WIF format. In theory it should be a short string.

\$\endgroup\$
  • 7
    \$\begingroup\$ BTW: I hope that those private keys aren't ACTUAL private keys. Those should NEVER, EVER be posted in a location even as remotely public as a code review site. \$\endgroup\$ – Nzall Jan 15 '18 at 14:44
  • \$\begingroup\$ @Nzail OK, I take notice. I replaced all the keys, either public and private, with hexadecimal random strings of the same size. \$\endgroup\$ – Diego Pino Jan 15 '18 at 17:13
  • 5
    \$\begingroup\$ Replacing them is not enough. For all purposes, you should now view the original keys as compromised and must NEVER EVER EVER use them again for bitcoin storage. Doing so runs high risk of losing the bitcoins stored in them. I already flagged your question to remove the editing history, but that isn't enough. \$\endgroup\$ – Nzall Jan 15 '18 at 17:26
  • 1
    \$\begingroup\$ @Nzall I just added your good observation to my answer to make others aware of this. I hope you don't mind ;) \$\endgroup\$ – Grajdeanu Alex. Jan 15 '18 at 19:13
17
\$\begingroup\$

IMPORTANT!!!

Not a Python tip, but rather a MUST when it comes to sensitive information (quote from @nzall's comment):

Those private keys should NEVER, EVER be posted in a location even as remotely public as a code review site. More, replacing them is not enough. For all purposes, you should now view the original keys as compromised and must NEVER EVER EVER use them again for bitcoin storage. Doing so runs high risk of losing the bitcoins stored in them

I just made this tip a bit more seeable for everybody.


Another three small observations, in addition to what @Graipher said.

In Python you can "chain" comparison operations which just means they are "and"ed together.

if random_int >= 1 and random_int < curve_order:
    return random_int

In your case, it'd be like this:

if 1 <= random_int < curve_order:
    return random_int

Read more about it here


More, I'd recommend you use .format() when it comes to printing:

print("Private Key: " + private_key_hex)
print("Public Key: " + public_key_hex)
print("Private Key (WIF Format): " + private_key_wif_hex)

Could be:

print("Private Key: {}".format(private_key_hex))
print("Public Key: {}".format(public_key_hex))
print("Private Key (WIF Format): {}".format(private_key_wif_hex))

You should also have two blank lines between your functions (instead of a single one)

This:

# ...
from binascii import hexlify, unhexlify

def random_secret_exponent(curve_order):
    while True:
        random_hex = hexlify(dev_random_entropy(32))
        random_int = int(random_hex, 16)
        if random_int >= 1 and random_int < curve_order:
            return random_int

def generate_private_key():
    curve = ecdsa.curves.SECP256k1
    se = random_secret_exponent(curve.order)
    from_secret_exponent = ecdsa.keys.SigningKey.from_secret_exponent
    return hexlify(from_secret_exponent(se, curve, hashlib.sha256).to_string())

# ...

Should be:

# ...
from binascii import hexlify, unhexlify


def random_secret_exponent(curve_order):
    while True:
        random_hex = hexlify(dev_random_entropy(32))
        random_int = int(random_hex, 16)
        if random_int >= 1 and random_int < curve_order:
            return random_int


def generate_private_key():
    curve = ecdsa.curves.SECP256k1
    se = random_secret_exponent(curve.order)
    from_secret_exponent = ecdsa.keys.SigningKey.from_secret_exponent
    return hexlify(from_secret_exponent(se, curve, hashlib.sha256).to_string())


# ...

For more details / suggestions / improvements regarding your code layout, you could take a look at PEP8, Python's official style guide.

\$\endgroup\$
  • \$\begingroup\$ @MrGrj Ah, makes sense, :) \$\endgroup\$ – Peilonrayz Jan 15 '18 at 12:00
14
\$\begingroup\$

Three very small observations:

  1. In your function generate_private_key_wif, you never use hashed, you re-compute it for the return value.

  2. In a nice self-contained module like this one, you should definitely add docstrings to your functions. Have a look at PEP257 to see how they are defined.

  3. You currently do from ecdsa.keys import SigningKey, but never use it. You also assign from_secret_exponent = ecdsa.keys.SigningKey.from_secret_exponent in your generate_private_key function. Instead do from_secret_exponent = SigningKey.from_secret_exponent or, probably better, split that line across two lines like this:

def generate_private_key():
    curve = ecdsa.curves.SECP256k1
    se = random_secret_exponent(curve.order)
    key = SigningKey.from_secret_exponent(se, curve, hashlib.sha256)
    return hexlify(key.to_string())
\$\endgroup\$
4
\$\begingroup\$

You have an error in WIF generation, you need to decode it from hex and take only 4 bytes for checksum, not 8:

def generate_private_key_wif(private_key_hex):
    private_key_and_version = b"80" + private_key_hex
    private_key_and_version = codecs.decode(private_key_and_version, 'hex')
    checksum = double_hash(private_key_and_version)[:4]
    hashed = private_key_and_version + checksum
    return base58.b58encode(hashed)
\$\endgroup\$
0
\$\begingroup\$

The generate_public_key function's parameter is private key. I find the public key is generated by hashing private key in your code. Is it not wrong? I think the public key is generated by ecdsa.SECP256k1 using private key.

#!/usr/bin/python

import ecdsa
from binascii import hexlify, unhexlify

secret = unhexlify('18E14A_the_private_key')

order = ecdsa.SigningKey.from_string(secret, curve=ecdsa.SECP256k1).curve.generator.order()

p = ecdsa.SigningKey.from_string(secret, curve=ecdsa.SECP256k1).verifying_key.pubkey.point

x_str = ecdsa.util.number_to_string(p.x(), order)
y_str = ecdsa.util.number_to_string(p.y(), order)


compressed = hexlify(chr(2 + (p.y() & 1)) + x_str).decode('ascii')
uncompressed = hexlify(chr(4) + x_str + y_str).decode('ascii')

print(compressed)
print(uncompressed)
\$\endgroup\$
  • \$\begingroup\$ Are you sure this should be an answer, and not a comment? What exactly are you suggesting here? \$\endgroup\$ – Daniel Jun 9 '18 at 11:15
  • \$\begingroup\$ @Coal_ Hi, He want generate a address. But there is an error in the code. I point it out. \$\endgroup\$ – Victor Choy Jun 9 '18 at 12:16

protected by Community Oct 27 '18 at 10:34

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.