paint-brush
How To Build a Minimalistic Ethereum Wallet in Python [Part 1]by@jspiel
6,785 reads
6,785 reads

How To Build a Minimalistic Ethereum Wallet in Python [Part 1]

by Jérôme SpielmannMarch 19th, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In this article, we are going to build a simple Ethereum wallet from scratch using Python. In part 1, we will use some cryptography and try to interact with the blockchain. We will generate a key pair compatible with the public key and encrypt the private key using a password. The cryptographic standard used by Bitcoin is based on Elliplic Curve Cryptography. An Ethereum wallet is simply a specific set of cryptographic keys together with methods to send and receive tokens using these keys. The wallet should at least be able to generate and store public and private keys. We start by loading some of them into Python:

Company Mentioned

Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - How To Build a Minimalistic Ethereum Wallet in Python [Part 1]
Jérôme Spielmann HackerNoon profile picture

In this article, we are going to build a simple Ethereum wallet from scratch using Python. During the process, we will use some cryptography and try to interact with the Ethereum blockchain. In part 1, we will generate a key pair which is compatible with the Ethereum protocol, obtain the Ethereum address from the public key and encrypt the private key using a password.

In this tutorial, we will need the

web3
(version 5.6.0),
tinyec
(version 0.3.1) and
PyCryptodome
(version 3.9.7) packages, if you haven’t already installed them you can download them using pip. We start by loading some of them into Python:

from tinyec.ec import SubGroup, Curve
from Crypto.Random.random import randint
from web3 import Web3

An Ethereum wallet is simply a specific set of cryptographic keys together with methods that allow an user to interact with the blockchain by sending and receiving tokens using these keys. Since the Ethereum blockchain is based on asymmetric cryptography, this means that the wallet should at least be able to generate and store public and private keys. The creation of these keys will be the starting point of the first part of this tutorial.

Generating a Key Pair

The cryptographic standard used by Ethereum (which is the same as the one used by Bitcoin) is based on Elliplic Curve Cryptography. This type of cryptography relies first on the choice of some mathematical field (parametrized by three values h, p and n), some elliptic curve (parametrized by two values a and b) and some point g = (x,y). In Ethereum the chosen parameters are those of the “secp256k1” standard curve and are given in this PDF of “Standards for Efficient Cryptography”.

In the standard notation, the values are given in hexadecimal values and we can transform them into integers by using the

int("hexadecimal string", 16)
command (for h we simply have h = 1): 

p = int("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16)
n = int("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16)

The values of a and b that define the elliptic curve of the protocol are a = 0 and b =7 and the point g is defined by the hexadecimal number “04 79BE667E F9DCBBAC …” given in the above file. Here, the “04” simply indicates that both the x and y coordinates are given and the 8 first blocks correspond to the value of x and the 8 next to the value of y. Thus, we write:

x = int("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", 16)
y = int("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8", 16)
g = (x,y)

It is possible to check that the couple (x, y) lies on the curve, by typing:

print(y**2 % p == (x**3 + 7) % p)

Using the

tinyec
package, we can then create the Python objects corresponding to the mathematical field and the elliptic function as follows:

field = SubGroup(p, g, n, h)
curve = Curve(a = 0, b = 7, field = field, name = 'secp256k1')

Now to generate the key pair, we start by selecting the private key S at random from the list of integers between 0 and n. We use the random function from the

PyCrypto
package:

private_key = randint(1, n)

By applying S-times the group operation on this curve to the point g, we obtain a new point denoted S*g, which has coordinates (x’, y’). The private key is the coordinate pair (x’, y’) of this new point.

public_key = private_key * curve.g

The point of elliptic curve crytography is that there is no way of finding S, even when g and (x’, y’) are known, without checking each possible value between 1 and S. This fact guarantees the safety of the Ethereum protocol.

Obtaining the Ethereum Address from the Key Pair

We now have a key pair and we can derive the Ethereum address from the public key. In the Ethereum protocol, the address is derived from the hash of the concatenation of the hexadecimal values of x’ and y’ (in our program x’ is

public_key.x
and y’ is
public_key.y
). We could use the basic Python
hex
function, but here we will use the similar function from the Web3 package since we have to load the package anyways for part 2:

public_key_hex = Web3.toHex(public_key.x)[2:] + Web3.toHex(public_key.y)[2:]

The command

[2:]
removes the
0x
in front of the hexadecimal values of x’ and y’ which to have be removed to obtain the address. Once this hexadecimal string has been generated from the values of x’ and y’, we use the Keccak-256 SHA function on this string and obtain the address by adding
0x
to the last 40 characters (or 20 bytes) of the hash:

address = Web3.keccak(hexstr = public_key_hex).hex()
address = "0x" + address[-40:]

To test the method, we can check that

private_key = int("f8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315", 16)

yields

0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9

as in the example given in the Ethereum Book.

While the address obtained in this manner is a valid Ethereum address, we can go further by capitalizing certain letters in the address so that it yields a checksum that can protect against typing or reading mistakes. We won’t go into more details here, since a custom built function to do this already exists in the

web3
package:

address = Web3.toChecksumAddress(address)

If we now print the address, we obtain:

0x001d3F1ef827552Ae1114027BD3ECF1f086bA0F9

Storing the Private Key using a Password

In general, an user will not want to remember the private key, but only some password which gives access to the key. A way to do this is to encrypt the private key using a password and write the encrypted key to a file. Then, each time the user opens the wallet, we can decrypt the private key using the password.

Obviously, if an attacker gains access to the password and the file she also gains access to the private key and thus to the content of the wallet. However, if she gains access only to the file, she should not be able to obtain the private key by brute-force, if we use a strong enough hashing method and password. In any case, the password and the file should be kept safe.

To encrypt the private key, we will use the AES-256 algorithm together with the scrypt key derivation function which are both available in the PyCryptodome package:

from Crypto.Cipher import AES
from Crypto.Protocol.KDF import scrypt
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes
import json

The above lines also import some utilities for padding, generating random bytes and manipulating the json format. Our strategy will be the following: first, we salt the password and obtain a key for the AES encryption using scrypt. Then, we encrypt the private key (using some randomly chosen initialization vector), convert the salt, the initialization vector and the encrypted private key into hexadecimal strings and store these to a json file.

In the following lines, we set the password (which has to be a byte string) and we generate 16 bytes randomly for the salt:

password = b"password"
salt = get_random_bytes(16)

The scrypt key derivation function can then be used to obtain a key for AES encryption like this:

key = scrypt(password, salt, 32, N = 2**20, r = 8, p = 1)

In the line above, we have specified that we want a length of 32 bytes (which is required for AES-256 encryption) and we have set the CPU cost parameter to 2²⁰ which is the number suggested for file encryption. Note that, the computation of the key can take a few seconds because of these settings. We then transform the private key into a hexadecimal string again and encode the string into bytes:

private_key = Web3.toHex(private_key)[2:]
data = str(private_key).encode('utf-8')

Now, we only have to call the AES method to obtain the encrypted key:

cipher = AES.new(key, AES.MODE_CBC)
ct_bytes = cipher.encrypt(pad(data, AES.block_size))

and we can write

salt = salt.hex()
iv = cipher.iv.hex()
ct = ct_bytes.hex()

output = {"salt" : salt, "initialization vector" : iv, "encrypted private key" : ct}

with open(address + '.txt', 'w') as json_file:
	json.dump(output, json_file)

to reconvert the values to hexadecimal strings and write them into a json file. This will yield a file looking similar to the following (the values will be different if you try, since the initialization vector and the salt are different):

{"salt": "e373892fe0cc6e743388e96df8a085cc", "initialization vector": "3514e6247d7557d112e10b2aca997608", "encrypted private key": "1ed635cd7bebb69abaaf97deee6e5fae3de732d1cdefc5ec0e2516725108909a5c18efb9a0a420978004bbef4c289a9cbd49120fc8ac1e5833e9f0160599b0962e1c700e71a7293a91e51118750f5e1e"}

In general, Ethereum wallets create keystore files which are similar json files with more information (about the encryption and hashing algorithms for example). In any case, once such a file is created, it is simple to decrypt and reobtain the private key using a few lines of code:

with open(address + '.txt') as f:
	data = json.load(f)

salt = data['salt']
iv = data['initialization vector'] 
ct = data['encrypted private key']

salt = bytes.fromhex(salt)
iv = bytes.fromhex(iv)
ct = bytes.fromhex(ct)

key = scrypt(password, salt, 32, N = 2**20, r = 8, p = 1)

cipher = AES.new(key, AES.MODE_CBC, iv)
pt = unpad(cipher.decrypt(ct), AES.block_size)

To verify our result we can type 

print(pt.decode('utf-8'))

and see that we obtain our private key.

This concludes the first part of this tutorial. In the next part, we will see how to interact with the blockchain using the

web3
package.