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 (version 5.6.0), (version 0.3.1) and (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: web3 tinyec PyCryptodome tinyec.ec SubGroup, Curve Crypto.Random.random randint web3 Web3 from import from import from import 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 . This type of cryptography relies first on the choice of (parametrized by three values h, p and n), (parametrized by two values a and b) and some point g = (x,y). In Ethereum the chosen parameters are those of and are given in . Elliplic Curve Cryptography some mathematical field some elliptic curve the “secp256k1” standard curve 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 command (for h we simply have h = 1): int("hexadecimal string", 16) p = int( , ) n = int( , ) "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F" 16 "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( , ) y = int( , ) g = (x,y) "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798" 16 "483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8" 16 It is possible to check that the couple (x, y) lies on the curve, by typing: print(y** % p == (x** + ) % p) 2 3 7 Using the package, we can then create the Python objects corresponding to the mathematical field and the elliptic function as follows: tinyec field = SubGroup(p, g, n, h) curve = Curve(a = , b = , field = field, name = ) 0 7 '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 package: PyCrypto private_key = randint( , n) 1 By applying S-times the 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. group operation on this curve 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 (in our program x’ is and y’ is ). We could use the basic Python function, but here we will use the similar function from the Web3 package since we have to load the package anyways for part 2: from the hash of the concatenation of the hexadecimal values of x’ and y’ public_key.x public_key.y hex public_key_hex = Web3.toHex(public_key.x)[ :] + Web3.toHex(public_key.y)[ :] 2 2 The command removes the 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 to the last 40 characters (or 20 bytes) of the hash: [2:] 0x 0x address = Web3.keccak(hexstr = public_key_hex).hex() address = + address[ :] "0x" -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 package: web3 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 together with the which are both available in the : AES-256 algorithm scrypt key derivation function PyCryptodome package Crypto.Cipher AES Crypto.Protocol.KDF scrypt Crypto.Util.Padding pad, unpad Crypto.Random get_random_bytes json from import from import from import from import import 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 = salt = get_random_bytes( ) b"password" 16 The scrypt key derivation function can then be used to obtain a key for AES encryption like this: key = scrypt(password, salt, , N = ** , r = , p = ) 32 2 20 8 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 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: number suggested for file encryption. private_key = Web3.toHex(private_key)[ :] data = str(private_key).encode( ) 2 '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, : iv, : ct} open(address + , ) json_file: json.dump(output, json_file) "salt" "initialization vector" "encrypted private key" with '.txt' 'w' as 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 files which are similar json files with . 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: keystore more information (about the encryption and hashing algorithms for example) open(address + ) f: data = json.load(f) salt = data[ ] iv = data[ ] ct = data[ ] salt = bytes.fromhex(salt) iv = bytes.fromhex(iv) ct = bytes.fromhex(ct) key = scrypt(password, salt, , N = ** , r = , p = ) cipher = AES.new(key, AES.MODE_CBC, iv) pt = unpad(cipher.decrypt(ct), AES.block_size) with '.txt' as 'salt' 'initialization vector' 'encrypted private key' 32 2 20 8 1 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.