Last active
January 10, 2022 21:43
-
-
Save Nikolaj-K/d548a12a45599070ea89ff376803758b to your computer and use it in GitHub Desktop.
From secret private key to address and WIF - on Bitcoin
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Discussed in the video | |
https://youtu.be/LYN3h5DjeXw | |
See e.g. | |
https://allbitcoinprivatekeys.com/1 | |
* For the following we use Bitcoin specification conventions. | |
* Essentially all those notions were around before and later blockchain | |
projects are all variations of Bitcoins conventions. | |
Relevant for this video: | |
* (1) Unformated Private key | |
- a.k.a. secret number | |
- a.k.a. exponent | |
* (2) Public key | |
- a.k.a. points | |
* (3) Multiplication of points on an eliptic curve | |
- Note: Exponentiation is just repeated multiplication, thus (1) | |
* (4) Base encodings | |
* (6) Hash functions | |
* (5) Checksums | |
* (6) Blockchain addresses | |
* (7) Wallet input format private key (WIF) | |
Topic of this video: | |
How to get (6) and (7) from (1), i.e. explicitly computing two functions. | |
Here (3), (4) and (5) are largely taken as blackbox here. But see videos on my channel. | |
Previous related videos: | |
Hash functions: | |
* VIDEO: What's a hash and what's the point? | |
https://youtu.be/wZiZjXOpaEs | |
* VIDEO: Hash function by analogy? | |
https://youtu.be/L-4T-pIJuQw | |
* VIDEO: SHA256 live coding on bit-level with Python | |
https://youtu.be/UziK-Hqzwi4 | |
* VIDEO: Blockchain explained by analogy (focused on the chain of blocks itself) | |
https://youtu.be/w3sI8WVX-cc | |
Digital signatures: | |
* VIDEO: The Elliptic Curve Digital Signature Algorithm and raw transactions on Bitcoin | |
https://youtu.be/YZafZ3Rvd8I | |
See link therein as well as: | |
For use of some of this in a Bitcoin protocol transaction (tx), see | |
https://klmoney.wordpress.com/bitcoin-dissecting-transactions-part-1/ | |
https://klmoney.wordpress.com/bitcoin-dissecting-transactions-part-2-building-a-transaction-by-hand/ | |
* VIDEO: The private key: How to digitally sign a message? | |
https://youtu.be/FVc-UOHcajc | |
Relevant external Bitcoin wiki links: | |
* https://en.bitcoin.it/wiki/Secp256k1 | |
* https://en.bitcoin.it/wiki/Base58Check_encoding | |
* https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses | |
* https://en.bitcoin.it/wiki/Wallet_import_format | |
See also the Bitcoin Stackexchange. | |
In fact, other projects wiki's and StackExchanges will help too. | |
Needs you to install all the libaries I import from right below and is only guaraneed to run on pyhton 3.7+ | |
Don't write me emails asking how to install python or something of the sort. | |
If you ask me something, use an adult sentence structure and attire. | |
Disclaimer: Due to potential bugs, don't use functions from this script for anything of value! | |
I have refactored the encodings and played with this script | |
a thousand times, so don't use its routines for anything of value! | |
You will anyway find many implementations of those things | |
online, especially in Cpp, Java, Rust, Javascript, Python. | |
""" | |
import binascii | |
import ecdsa | |
import hashlib | |
from base58 import b58encode # https://github.com/serengil/crypto/blob/master/python/base58.py | |
# See also https://en.bitcoin.it/wiki/Base58Check_encoding | |
def secret_to_address(secret, legacy=False): | |
""" | |
1. Exponent (secret private key) to point on curve | |
2. Point to x-cooridnate | |
3. Coordinate to checksummed obscrurification (address) | |
See also | |
https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses | |
etc. | |
""" | |
pubk_pair = from_secret_pubk_point(secret) | |
compressed_pubk, pubk = _pubk_to_compressed_pubk(*pubk_pair) | |
address = _pubk_to_address(pubk) if legacy else _pubk_to_address(compressed_pubk) | |
return address | |
def from_secret_pubk_point(secret): | |
""" | |
See also https://en.bitcoin.it/wiki/Secp256k1 | |
1. Compute point from secret number (a.k.a (unformated) private key, a.k.a. exponent) | |
2. Format pubk_vk | |
3. Project out coordinates | |
""" | |
CURVE = ecdsa.SECP256k1 | |
sk = ecdsa.SigningKey.from_secret_exponent(secret, curve=CURVE) | |
pubk_vk = sk.verifying_key # the point | |
# Note: Rougly g^(secret + x) = g^secret * g^x, where g is a point that comes with the curve spec. | |
# Note: 0 < secret < _r (see SECP256k1 for max secret) | |
pubk = binascii.b2a_hex(pubk_vk.to_string()).decode('ascii') | |
pubk_x = pubk[:64] | |
pubk_y = pubk[64:] | |
#print(f"pubk:\t{pubk}\npubk_x:\t{pubk_x}\npubk_y:\t{pubk_y}\n") | |
return pubk_x, pubk_y | |
def _pubk_to_compressed_pubk(pubk_x, pubk_y): | |
""" | |
Prefix pubk_x with info about pubk_y. | |
Note: | |
print(f'Uncompressed pubk: 04 {pubk_x} {pubk_y}') | |
""" | |
EVEN_PREFIX = '02' | |
UNEVEN_PREFIX = '03' | |
LEGACY_PREFIX = '04' | |
# Prepend parity information | |
y_parity = ord(bytearray.fromhex(pubk_y[-2:])) % 2 | |
prefix = EVEN_PREFIX if y_parity==0 else UNEVEN_PREFIX | |
compressed_pubk = prefix + pubk_x | |
#print(f"y_parity:\t\t{y_parity}, compressed_pubk: {compressed_pubk}") | |
pubk = LEGACY_PREFIX + pubk_x + pubk_y | |
return compressed_pubk, pubk | |
def _pubk_to_address(pubk): | |
""" | |
Hash and checksum the pubk. | |
Note: No discussion of Pay-to-ScriptHash addresses ("3..") here but only Pay-to-PubKeyHash ("1..") | |
Newer SegWit addresses ("bc..") are a deviation in some details. | |
""" | |
pubk_array = bytearray.fromhex(pubk) | |
# Compute key_hash | |
sha = hashlib.sha256() # See Nikolaj-K videos | |
sha.update(pubk_array) | |
rip = hashlib.new('ripemd160') | |
rip.update(sha.digest()) | |
PREFIX = '00' | |
key_hash = PREFIX + rip.hexdigest() # .hexdigest() is hex ASCII | |
#print(f"key_hash:\t\t{key_hash}") | |
# Compute checksum to append to key_hash | |
sha = hashlib.sha256() | |
sha.update(bytearray.fromhex(key_hash)) | |
checksum = sha.digest() | |
sha = hashlib.sha256() | |
sha.update(checksum) | |
checksum = sha.hexdigest()[0:8] | |
# Append checksum | |
address_hex = key_hash + checksum | |
# expected encoding | |
bs = bytes(bytearray.fromhex(address_hex)) | |
address = b58encode(bs).decode('utf-8') | |
return address | |
def secret_to_wif(secret): | |
""" | |
See also | |
https://en.bitcoin.it/wiki/Wallet_import_format | |
etc. | |
""" | |
# Cast / Format | |
PREFIX = "80" | |
hex_string = hex(secret)[2:].zfill(64) # remove 0x and fill with zeros | |
pre_hash = PREFIX + hex_string | |
# Compute checksum via hashing | |
hash_1 = hashlib.sha256(binascii.unhexlify(pre_hash)).hexdigest() | |
hash_2 = hashlib.sha256(binascii.unhexlify(hash_1)).hexdigest() | |
checksum = hash_2[:8] | |
# Append checksum | |
pre_hash_checksum = pre_hash + checksum | |
# Cast / Format | |
from_hex_string = int(pre_hash_checksum, 16) | |
def _get(idx): | |
ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' | |
m = 58**idx | |
idx = from_hex_string // m % 58 | |
return ALPHABET[idx] | |
IDXS = range(100) # sufficiently long | |
wif_str = "".join(map(_get, IDXS)) | |
# Reverse | |
rev_wif_str = wif_str[::-1].lstrip('1') | |
return rev_wif_str | |
if __name__ == "__main__": | |
EXAMPLE_PRIVATE_KEYS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 17, 18, 19, 7000000003, 9001000007000000003, 666] | |
for secret in EXAMPLE_PRIVATE_KEYS: | |
# Compute wallet-input-format of private key | |
wif = secret_to_wif(secret) | |
# Compute adresses | |
address_legacy = secret_to_address(secret, True) | |
address = secret_to_address(secret) | |
print( | |
f"priv key (secret):\t{secret}\n" | |
f"priv key (secret wif):\t{wif}\n" | |
f"legacy address:\t\t{address_legacy}\n" | |
f"address:\t\t{address}\n" + 80*"-" | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment