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.

Toby Speight
87.2k14 gold badges104 silver badges322 bronze badges
asked Jan 14, 2018 at 21:28
\$\endgroup\$
4
  • 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\$ Commented Jan 15, 2018 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\$ Commented Jan 15, 2018 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\$ Commented Jan 15, 2018 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\$ Commented Jan 15, 2018 at 19:13

4 Answers 4

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.

answered Jan 14, 2018 at 22:11
\$\endgroup\$
1
  • \$\begingroup\$ @MrGrj Ah, makes sense, :) \$\endgroup\$ Commented Jan 15, 2018 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())
answered Jan 14, 2018 at 21:44
\$\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)
answered Sep 2, 2018 at 19:43
\$\endgroup\$
0
1
\$\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)
answered Jun 9, 2018 at 9:23
\$\endgroup\$
2
  • \$\begingroup\$ Are you sure this should be an answer, and not a comment? What exactly are you suggesting here? \$\endgroup\$ Commented Jun 9, 2018 at 11:15
  • \$\begingroup\$ @Coal_ Hi, He want generate a address. But there is an error in the code. I point it out. \$\endgroup\$ Commented Jun 9, 2018 at 12:16

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.