Generating Wallet Address From Private Key

In this article, we learn how Bitcoin wallet addresses and their corresponding public keys are generated given a private key.

Table of contents.

  1. Introduction.
  2. Bitcoin address formats.
  3. ECDSA public-key generation.
  4. Compressing the Public Key.
  5. SHA-256 and RIPEMD-160 hashing.
  6. The Network Byte.
  7. Checksum Calculations.
  8. Base58Check encoding.
  9. Summary.
  10. References.

Prerequisite

Generating a Bitcoin private key

Introduction

In the prerequisite article, we learned how to generate private keys using python. In order to generate private keys and make sure that no one else knows about them, we can opt to generate them ourselves locally. If we opt to use third parties such as random.org we should know that we are not the only ones who know about it.

Unlike the Ethereum blockchain where participants have accounts and account addresses, the Bitcoin blockchain uses a wallet as the interface to interact with the blockchain. Wallets have addresses that are used by other participants in transactions, for example, if a user intends to send you Bitcoins, he/she will need the recipient's wallet address. That is, the sender creates a transaction that says that a specif amount X of Bitcoins now belongs to a wallet address W. This information is propagated across the entire network of Bitcoin blockchain nodes to ensure that only the intended recipient can spend the X bitcoins.

To acquire a Bitcoin address which is the wallet address we first download the Bitcoin wallet software. The wallet will store our private key which as mentioned should be kept private, just like a password to an account. Bitcoin wallets are not only limited to mobile, web, or desktop programs rather they can only be hardware wallets.

In this article, we will learn how Bitcoin wallet addresses and corresponding public keys are generated from a private key. The process is simple, we will apply a series of conversions to a private key in order to generate its key pair, the public-key and a Bitcoin wallet address. We will use hashing and hashing algorithms which will make sure that we have a trapdoor.

Bitcoin address formats.

Bitcoin has three common address formats. All are alphanumeric characters that are 26-35 characters long either starting with 1, 3, or bc1.

P2PKH are addresses starting with 1 and an example is shown below;
1BvBMSJksi71Hya7sMkrim4GFg7xJaNVN2

PsSH are addresses starting with 3, and an example is shown below;
3J98Jki8Kl07GhAhjueSjki8WrnqRhWNLy

Bech32 are address starting with bc1, an example is hwon below;
bc1ghr0jkid7xfkvy5l643klotsu781f52zzwf5mdq

ECDSA public-key generation.

Bitcoin uses ECDSA for its cryptography because it results in much smaller signatures compared to using RSA. Also, the former algorithm is stronger compared to the latter which also means that verification is slower compared to RSA because of the level of security.

We define an elliptic curve using the following equation;
y² = x³ + ax + b. By passing the private key through the ECDSA algorithm, we get a 64-byte integer consisting of 2 32-byte integers representing points X and Y on the curve concatenated together.

The following python code does the dicussed concept;

def generate_public_key():
    # private_key = generate_private_key()
    priv_key_bytes = codecs.decode(private_key, 'hex') # convert string into byte array
    key = ecdsa.SigningKey.from_string(priv_key_bytes, curve=ecdsa.SECP256k1).verifying_key
    key_bytes = key.to_string()
    public_key = codecs.encode(key_bytes, 'hex')
    return public_key 

public_key = generate_public_key()
print("Generated Public Key: ", public_key.decode())
print()

The private key we generate using the generate_private_key function in the prerequisite article is as follows;

6c2181e3e8a8dd271f8a074117bc4232e7250e371d2db23d9131c6659c4cc4b1

The above private key is of type string, we convert it into a byte array since in Python we can only use strings or bytes to store private and public keys. For this we use codecs.decode and we have the following byte array as a result from line 3;

b"l!\x81\xe3\xe8\xa8\xdd'\x1f\x8a\x07A\x17\xbcB2\xe7%\x0e7\x1d-\xb2=\x911\xc6e\x9cL\xc4\xb1"

We then apply the ECDSA algorithm to the above key and convert it into a string and we have the following;

b'\x9eC\xbe\x96l\x9a*1\xd2N\x83"\xaf\x08\xf05\xf0\xb4\x19\xda{\xb9.\xd0Q\x86\xe2kkeg\xa8\xe9"Uv\xf9\xd2\xb0z_j\x91\xbd\\\xf0\xa6\x18\xf7\xb0F\x8b\'\x98\x9d\x06-\xc1j\x97\x1c 8\xa9'

The above is a string of bytes.

We then use codecs to encode the above string of bytes and we have the following public key;

b'9e43be966c9a2a31d24e8322af08f035f0b419da7bb92ed05186e26b6b6567a8e9225576f9d2b07a5f6a91bd5cf0a618f7b0468b27989d062dc16a971c2038a9'

Codecs is a python library that is used for encoding and decoding. It is commonly used to encode text into a byte string and then decode the encoded byte string text into the original text. The above results in the following random public keys after each execution.

We add then a 0x04 prefix byte. This is used in Bitcoin to distinguish between several other encodings. In this case, it denotes an uncompressed public key. We use the following simple function;

def convert_pub_key(public_key):
    bit_public_key = b'0x04' + public_key
    return bit_public_key.decode()[2:]

bit_pub_key = convert_pub_key(public_key)
print("Standard Bitcoin Pub_Key: ", bit_pub_key)
print()

The result is the following standard Bitcoin public key.

b'0x049e43be966c9a2a31d24e8322af08f035f0b419da7bb92ed05186e26b6b6567a8e9225576f9d2b07a5f6a91bd5cf0a618f7b0468b27989d062dc16a971c2038a9'

In the function convert_pub_key function, we can replace return bit_public_key with return bit_public_key.decode()[2:] line to obtain a string type;

049e43be966c9a2a31d24e8322af08f035f0b419da7bb92ed05186e26b6b6567a8e9225576f9d2b07a5f6a91bd5cf0a618f7b0468b27989d062dc16a971c2038a9

Compressing the Public Key.

Compression is also important since it reduces the size of the key, this goes a long way in improving the speed of transactions while also maintaining security.
Remember that a public key on the elliptic curve is a coordinate (X, Y) on the curve. Therefore for each X there are two Ys. this means that we can keep X and the sign of Y and it will allow us to derive Y as needed.

The process involves adding 0x02 to X from the public key, if the last byte of Y is even otherwise we add 0x03 if the last byte is odd. In case, the last byte is the odd number 9, therefore we have the following compressed public key.

b'0x039e43be966c9a2a31d24e8322af08f035f0b419da7bb92ed05186e26b6b6567a8'

Remember we can decode it and have the following string;

039e43be966c9a2a31d24e8322af08f035f0b419da7bb92ed05186e26b6b6567a8

To generate a compressed public key, we use the following function;

# compress public key
def compress_pub_key(public_key):
    mid = int(len(public_key) / 2)
    x = public_key[:mid]
    if(int(public_key[-1:]) % 2 == 0):
        x = b'0x02' + x
    else:
        x = b'0x03' + x
    return x

compressed_pub_key = compress_pub_key(public_key)
print("Compressed Pub_Key: ", compressed_pub_key.decode()[2:])
print()

SHA-256 and RIPEMD-160 hashing.

In this section, we will learn how to encrypt data using a public key. We will use hashing algorithms such as SHA-256 after which we will apply RIPEMD-160 to the obtained result.
The process is as follows, we first decode the public key as done previously, then we hash the result using SHA-256 then apply the RIPEMD-160 hashing algorithm to the result of the previous operation.

**Encrypted Public Key = RIPEMD-160(SHA-256(Public Key))

The following is demonstrated in python code;

# Encrypt public key
def encrypt_pub_key(compressed_pub_key):
    public_key = compressed_pub_key.decode()[2:]
    public_key_bytes = codecs.decode(public_key, 'hex')
    sha256_pub_key = hashlib.sha256(public_key_bytes) # pass public key through SHA-256
    sha256_pub_key_digest = sha256_pub_key.digest()
    ripemd160_pub_key = hashlib.new('ripemd160') # pass hashed SHA-256 through RIPEMD
    ripemd160_pub_key.update(sha256_pub_key_digest)
    ripemd160_pub_key_digest = ripemd160_pub_key.digest()
    ripemd160_pub_key_hex = codecs.encode(ripemd160_pub_key_digest, 'hex')
    return ripemd160_pub_key_hex

encrypted_pub_key = encrypt_pub_key(compressed_pub_key)
print("Encrypted Pub_Key:", encrypted_pub_key.decode())
print()

This results in the following encrypted public key bytes;

b'0f4d06eef6c6ad74fb912e3dc47f23474a430950'

The Network Byte.

We also add a network byte to the generated address, this is because Bitcoin just like other blockchain technologies such as Ethereum has a test net and a mainnet, the former is used by developers to test features while the latter is what is used in day to day transactions on the blockchain.

For the testnet, we add 0x6f bytes to the address, for the mainnet, we add 0x00. In our case we are using the mainnet therefore we have the following compressed, encrypted public key prepended with the network byte;

b'0x000f4d06eef6c6ad74fb912e3dc47f23474a430950'

This is similar to the following;

000f4d06eef6c6ad74fb912e3dc47f23474a430950

The above is after decoding and splicing the resulting string;

The code for adding the network byte is shown below;

# adding network byte
def network_byte(encrypted_pub_key, network):
    if(network == 'mainnet'):
        return b'0x00' + encrypted_pub_key
    elif(network == 'testnet'):
        return b'0x6f' + encrypted_pub_key
    else:
        print('Invalid Network')

net_pubkey = network_byte(encrypted_pub_key, 'mainnet')
print("Bitcoin mainnet Addr: ", net_pubkey.decode()[2:])
print()

Checksum Calculations.

A checksum is used to ensure that the data is not corrupted. We calculate the checksum by passing the address through SHA-256 twice and then using taking the preceding 4 bytes of the result. That is;
checksum = SHA-256(SHA-256(encrypted public_key))[:8]
Above we use 8 since 4 bytes is equivalent to 8 hex values.

The following python code does this;

# calculate checksum of address
def calc_addr_checksum(net_pubkey):
    sha256_net_pubkey = hashlib.sha256(net_pubkey) # first SHA
    sha256_net_pubkey_digest = sha256_net_pubkey.digest()
    sha256_2_sha256_net_pubkey = hashlib.sha256(sha256_net_pubkey_digest) # second SHA
    sha256_2_net_pubkey_digest = sha256_2_sha256_net_pubkey.digest()
    sha256_2_hex = codecs.encode(sha256_2_net_pubkey_digest, 'hex')
    checksum = sha256_2_hex[:8]
    return checksum

checksum = calc_addr_checksum(net_pubkey)
print("Checksum: ", checksum.decode())
print()

In this case, we have the following checksum;

da0b514d

Now, to create a valid address, we concatenate the network public key with the generated checksum and we have the following;

b'0x000f4d06eef6c6ad74fb912e3dc47f23474a430950da0b514d'

We use the following simple function;

# create wallet address
def wallet_address(public_key, checksum):
    return public_key + checksum

wallet_addr = wallet_address(net_pubkey, checksum)
print("Wallet Address: ", wallet_addr.decode()[2:])
print()

Base58Check Encoding.

To have a valid address we first concatenate the mainnnet or testnet key and the checksum. Then convert the result into a Base58Check encoding which is what Bitcoin uses. For more on this refer to the links in the references section.

The following python code encodes the currently generate address using Base58Check encoding;

# Encode with Base58 encoding
def base58(address_hex):
    address_hex = wallet_addr.decode()[2:]
    alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
    b58_string = ''
    # Get the number of leading zeros
    leading_zeros = len(address_hex) - len(address_hex.lstrip('0'))
    # Convert hex to decimal
    address_int = int(address_hex, 16)
    # Append digits to the start of string
    while address_int > 0:
        digit = address_int % 58
        digit_char = alphabet[digit]
        b58_string = digit_char + b58_string
        address_int //= 58
    # Add '1' for each 2 leading zeros
    ones = leading_zeros // 2
    for _ in range(ones):
        b58_string = '1' + b58_string
    return b58_string

Bit_wallet_addr = base58(wallet_addr)

print("Final Bitcoin Wallet Address: ", Bit_wallet_addr)
print()

The result of the above function is the following compressed Bitcoin wallet address;

12PuQpA9eFXCRRA6T81LZXj4gixdLmh1JL

The following are the outputs at different stages in generating the bitcoin wallet address;

Generated Public Key:  9e43be966c9a2a31d24e8322af08f035f0b419da7bb92ed05186e26b6b6567a8e9225576f9d2b07a5f6a91bd5cf0a618f7b0468b27989d062dc16a971c2038a9

Standard Bitcoin Pub_Key:  049e43be966c9a2a31d24e8322af08f035f0b419da7bb92ed05186e26b6b6567a8e9225576f9d2b07a5f6a91bd5cf0a618f7b0468b27989d062dc16a971c2038a9

Compressed Pub_Key:  039e43be966c9a2a31d24e8322af08f035f0b419da7bb92ed05186e26b6b6567a8

Encrypted Pub_Key: 0f4d06eef6c6ad74fb912e3dc47f23474a430950

Bitcoin mainnet Addr:  000f4d06eef6c6ad74fb912e3dc47f23474a430950

Checksum:  da0b514d

Wallet Address:  000f4d06eef6c6ad74fb912e3dc47f23474a430950da0b514d

Final Bitcoin Wallet Address:  12PuQpA9eFXCRRA6T81LZXj4gixdLmh1JL

Summary

A Bitcoin wallet address is the source and destination of all transactions on the Bitcoin blockchain network.
The Ethereum blockchain uses accounts instead of wallet addresses.
To increase the level of security and anonymity, it is recommended to generate a new wallet address after each transaction.

Following is the full algorithm in python code:

## Part of iq.opengenus.org
import codecs
import ecdsa
import secrets
import hashlib

# generate bitcoin private key
def generate_private_key():
    bits = secrets.randbits(256)
    bits_hex = hex(bits)
    return bits_hex[2:]

private_key = "6c2181e3e8a8dd271f8a074117bc4232e7250e371d2db23d9131c6659c4cc4b1"

 # return 64-byte key, 2 32-byte representing x,y coordinates of elliptic curve
def generate_public_key():
    # private_key = generate_private_key()
    priv_key_bytes = codecs.decode(private_key, 'hex') # convert string into byte array
    key = ecdsa.SigningKey.from_string(priv_key_bytes, curve=ecdsa.SECP256k1).verifying_key
    key_bytes = key.to_string()
    public_key = codecs.encode(key_bytes, 'hex')
    return public_key 

public_key = generate_public_key()
print("Generated Public Key: ", public_key.decode())
print()

def convert_pub_key(public_key):
    bit_public_key = b'0x04' + public_key
    return bit_public_key.decode()[2:]

bit_pub_key = convert_pub_key(public_key)
print("Standard Bitcoin Pub_Key: ", bit_pub_key)
print()

# compress public key
def compress_pub_key(public_key):
    mid = int(len(public_key) / 2)
    x = public_key[:mid]
    if(int(public_key[-1:]) % 2 == 0):
        x = b'0x02' + x
    else:
        x = b'0x03' + x
    return x

compressed_pub_key = compress_pub_key(public_key)

print("Compressed Pub_Key: ", compressed_pub_key.decode()[2:])
print()

# Encrypt public key
def encrypt_pub_key(compressed_pub_key):
    public_key = compressed_pub_key.decode()[2:]
    public_key_bytes = codecs.decode(public_key, 'hex')
    sha256_pub_key = hashlib.sha256(public_key_bytes) # pass public key through SHA-256
    sha256_pub_key_digest = sha256_pub_key.digest()
    ripemd160_pub_key = hashlib.new('ripemd160') # pass hashed SHA-256 through RIPEMD
    ripemd160_pub_key.update(sha256_pub_key_digest)
    ripemd160_pub_key_digest = ripemd160_pub_key.digest()
    ripemd160_pub_key_hex = codecs.encode(ripemd160_pub_key_digest, 'hex')
    return ripemd160_pub_key_hex

encrypted_pub_key = encrypt_pub_key(compressed_pub_key)
print("Encrypted Pub_Key:", encrypted_pub_key.decode())
print()

# adding network byte
def network_byte(encrypted_pub_key, network):
    if(network == 'mainnet'):
        return b'0x00' + encrypted_pub_key
    elif(network == 'testnet'):
        return b'0x6f' + encrypted_pub_key
    else:
        print('Invalid Network')

net_pubkey = network_byte(encrypted_pub_key, 'mainnet')
print("Bitcoin mainnet Addr: ", net_pubkey.decode()[2:])
print()

# calculate checksum of address
def calc_addr_checksum(net_pubkey):
    sha256_net_pubkey = hashlib.sha256(net_pubkey) # first SHA
    sha256_net_pubkey_digest = sha256_net_pubkey.digest()
    sha256_2_sha256_net_pubkey = hashlib.sha256(sha256_net_pubkey_digest) # second SHA
    sha256_2_net_pubkey_digest = sha256_2_sha256_net_pubkey.digest()
    sha256_2_hex = codecs.encode(sha256_2_net_pubkey_digest, 'hex')
    checksum = sha256_2_hex[:8]
    return checksum

checksum = calc_addr_checksum(net_pubkey)
print("Checksum: ", checksum.decode())
print()

# create wallet address
def wallet_address(public_key, checksum):
    # checksum = b'512f43c4'
    return public_key + checksum

wallet_addr = wallet_address(net_pubkey, checksum)
print("Wallet Address: ", wallet_addr.decode()[2:])
print()

# Ecnode with Base58 encoding
def base58(address_hex):
    address_hex = wallet_addr.decode()[2:]
    alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
    b58_string = ''
    # Get the number of leading zeros
    leading_zeros = len(address_hex) - len(address_hex.lstrip('0'))
    # Convert hex to decimal
    address_int = int(address_hex, 16)
    # Append digits to the start of string
    while address_int > 0:
        digit = address_int % 58
        digit_char = alphabet[digit]
        b58_string = digit_char + b58_string
        address_int //= 58
    # Add '1' for each 2 leading zeros
    ones = leading_zeros // 2
    for _ in range(ones):
        b58_string = '1' + b58_string
    return b58_string

Bit_wallet_addr = base58(wallet_addr)

print("Final Bitcoin Wallet Address: ", Bit_wallet_addr)
print()

References

Base58Check encoding