Building a Minimalistic Ethereum Wallet in Python

·

In this guide, we will build a simple Ethereum wallet from scratch using Python. We will explore core cryptographic concepts and learn how to interact with the Ethereum blockchain. This first part focuses on generating an Ethereum-compatible key pair, deriving an Ethereum address from the public key, and encrypting the private key using a password.


Prerequisites and Setup

To follow this tutorial, you will need the following Python packages:

You can install them using pip:

pip install web3==5.6.0 tinyec==0.3.1 pycryptodome==3.9.7

Let's start by importing the required modules:

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

An Ethereum wallet is a set of cryptographic keys that enables users to send and receive tokens on the blockchain. It relies on asymmetric cryptography, meaning it must generate and securely store public and private keys.


Generating a Cryptographic Key Pair

Ethereum uses Elliptic Curve Cryptography (ECC) based on the secp256k1 standard. This system depends on specific mathematical parameters:

Here are the hexadecimal values for these parameters (converted to integers):

p = int("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16)
n = int("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16)
a = 0
b = 7
x = int("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", 16)
y = int("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8", 16)
g = (x, y)

You can verify that point (x, y) lies on the curve:

print(y**2 % p == (x**3 + 7) % p)  # Output: True

Now, create the curve object using tinyec:

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

Generate a private key as a random integer between 1 and n:

private_key = randint(1, n)

The public key is derived by multiplying the private key by the generator point:

public_key = private_key * curve.g

Elliptic curve cryptography ensures that deriving the private key from the public key is computationally infeasible.


Deriving an Ethereum Address

The Ethereum address is derived from the public key coordinates (x', y'). Concatenate their hexadecimal values (without the 0x prefix):

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

Apply the Keccak-256 hash function and take the last 40 characters:

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

For example, with the private key f8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315, you should get the address 0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9.

To add a checksum for error detection, use:

address = Web3.toChecksumAddress(address)

This yields a mixed-case address like 0x001d3F1ef827552Ae1114027BD3ECF1f086bA0F9.


Encrypting the Private Key with a Password

Users typically prefer storing encrypted private keys rather than remembering long hexadecimal strings. We use the AES-256 encryption algorithm with the scrypt key derivation function.

Import additional modules:

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

Encryption Steps

  1. Generate a salt and derive an encryption key:

    password = b"your_secure_password"  # Replace with a strong password
    salt = get_random_bytes(16)
    key = scrypt(password, salt, 32, N=2**20, r=8, p=1)
  2. Convert the private key to bytes and encrypt it:

    private_key_hex = Web3.toHex(private_key)[2:]
    data = private_key_hex.encode('utf-8')
    cipher = AES.new(key, AES.MODE_CBC)
    ct_bytes = cipher.encrypt(pad(data, AES.block_size))
  3. Store the encrypted data in a JSON file:

    output = {
        "salt": salt.hex(),
        "initialization vector": cipher.iv.hex(),
        "encrypted private key": ct_bytes.hex()
    }
    with open(address + '.txt', 'w') as f:
        json.dump(output, f)

The output file will look like this:

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

Decryption Steps

To recover the private key:

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

salt = bytes.fromhex(data['salt'])
iv = bytes.fromhex(data['initialization vector'])
ct = bytes.fromhex(data['encrypted private key'])

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)
private_key_hex = pt.decode('utf-8')
private_key = int(private_key_hex, 16)

This confirms that the encryption and decryption process works correctly.

👉 Explore advanced encryption methods


Frequently Asked Questions

What is an Ethereum wallet?

An Ethereum wallet is a software application that stores cryptographic keys enabling users to interact with the Ethereum blockchain. It manages private and public keys, facilitates transactions, and often provides additional features like token management and smart contract interactions.

Why use elliptic curve cryptography?

Elliptic curve cryptography offers strong security with relatively small key sizes. The secp256k1 curve used by Ethereum provides a robust foundation for generating keys and signing transactions, ensuring efficiency and resistance to attacks.

How secure is the encryption method described?

The AES-256 encryption combined with the scrypt key derivation function is highly secure. The parameters used (N=2²⁰, r=8, p=1) are designed to be computationally intensive, mitigating brute-force attacks. However, users must choose strong passwords and protect the keystore file.

Can I use this wallet for mainnet transactions?

While the wallet generated here is technically functional, it is intended for educational purposes. For handling real funds, use well-audited wallet software like MetaMask or hardware wallets, which include additional security features and ease-of-use optimizations.

What is the checksum address format?

The checksum address incorporates capital letters based on the address hash, providing a built error-detection mechanism. This helps prevent losses due to typos when entering addresses manually.

How can I improve the security of my keystore file?

Store the keystore file in a secure location, such as an encrypted drive or hardware security module. Use a strong, unique password and consider enabling two-factor authentication for any system accessing the file.


In this tutorial, we built a basic Ethereum wallet capable of generating keys, deriving addresses, and encrypting private keys. In the next part, we will explore how to use the web3 package to interact with the Ethereum blockchain, check balances, and send transactions.