In this tutorial series, we are constructing a Layer 1 blockchain from the ground up using Python. The development is broken down into four essential components:
- Wallet
- Blockchain & Mining
- Database
- Peer-to-Peer (P2P) Network
This first installment focuses exclusively on creating a fundamental wallet. We will cover how to generate cryptographic key pairs, sign transactions, and establish the basic interaction between the wallet and the blockchain.
Understanding the Blockchain Wallet
A wallet is the primary gateway for any user to interact with a blockchain network. It is not a physical container for digital assets but rather a software tool that manages the cryptographic keys responsible for owning and controlling those assets on the distributed ledger.
In this guide, we are building a simple yet powerful wallet in Python. This wallet will generate Ethereum-compatible public and private key pairs, create digitally signed transactions, and serve as the secure interface for managing account balances on our custom blockchain.
Prerequisites and Setup
Before diving into the code, ensure your development environment is correctly configured.
Recommended Python Version: 3.11.6 or later
Operating System: Linux Ubuntu (though other OSs can work with adjustments)
Database: LevelDB for persistent storage
Step-by-Step Wallet Implementation
Let's break down the Python code required to bring our wallet to life. The following sections provide a detailed, line-by-line explanation of the core functionality.
1. Importing Necessary Modules
The first step involves importing all required libraries. We leverage existing Ethereum tools for robust cryptography and our custom modules for blockchain interaction.
# Ethereum cryptography modules
from eth_account import Account
from eth_account.messages import encode_defunct
# Custom blockchain modules
from blockchain import Blockchain
from db import get_account_state, get_transaction, update_account_state- The
eth_accountlibrary handles the complex tasks of creating accounts and signing messages. - The
encode_defunctfunction prepares messages for the Ethereum signing process. - Our custom modules (
blockchain,db) manage the core chain logic and database interactions.
2. Initializing the Blockchain
Every action originates from the blockchain instance, which coordinates transactions and block management.
# Entry point of the script
if __name__ == "__main__":
# Initialize the Blockchain instance
blockchain = Blockchain()- This code initializes an instance of our
Blockchainclass. This object will be our central point for adding transactions and querying data.
3. Configuring Sender and Receiver Accounts
A transaction requires two parties: a sender and a receiver. Here’s how we set them up.
# Define the sender's Ethereum address and private key
sender_address = '0xbd281AE5D72050dEB0243b91a81018709AFA1994'
sender_key = '39092b4d8f20dd79c73928e501230b714a7730956755738be7523b7a19773ece'
# Create an account object for the sender using the private key
sender_account = Account.from_key(sender_key)
# Update the sender's account balance to 1,000,000 units
update_account_state(sender_address, 1_000_000)
# Dynamically create a new Ethereum account for the receiver
receiver_account = Account.create()- A predefined sender account is configured using a known Ethereum address and its corresponding private key.
- The
Account.from_key()method creates an account object from the private key, enabling signing capabilities. - The sender's balance is manually set in the state database using
update_account_state. - A new, cryptographically secure account for the receiver is generated on-the-fly using
Account.create().
4. Creating and Signing a Transaction
The core function of any wallet is to authorize a transfer of value. This is achieved through digital signatures.
amount = 10 # amount to be transferred
# Create a message string that includes sender, receiver, and amount details
message = f"{sender_account.address}:{receiver_account.address}:{amount}"
# Encode the message to be compatible with Ethereum signing process
encoded_message = encode_defunct(text=message)
# Sign the encoded message with the sender's private key to produce a signature
signature = Account.sign_message(encoded_message, sender_account.key).signature.hex()
# Create a transaction dictionary containing all necessary transaction details
transaction = {
'Sender': sender_account.address,
'Receiver': receiver_account.address,
'Amount': str(amount),
'signature': signature
}- A plain-text message is constructed containing all critical transaction details.
- This message is encoded into a format suitable for Ethereum's signing algorithm.
- The sender's private key is used to cryptographically sign the encoded message, producing a unique signature that proves authorization.
- All transaction data, including the signature, is packaged into a dictionary object.
5. Adding the Transaction to the Blockchain
Once signed, the transaction is broadcast to the network to be included in a block.
# Loop to add multiple transactions (simulating 10,000 transactions)
for x in range(10_000):
# Add the transaction to the blockchain and get a unique transaction key (txn_key)
txn_key = blockchain.add_transaction(transaction)
# Print the details of the transaction and current balance of sender and receiver
print(f"{sender_account.address[-5:]} : {amount} --> {receiver_account.address[-5:]}")
print(f"{sender_account.address[-5:]} Balance = {get_account_state(sender_account.address)['balance']}\n{receiver_account.address[-5:]} Balance = {get_account_state(receiver_account.address)['balance']}")- The
add_transactionmethod of our blockchain object is called to submit the transaction. It returns a unique key for that transaction. - A loop simulates a high volume of transactions to test network performance.
- After each transaction, the current balances of both accounts are fetched from the database and printed, providing immediate feedback.
6. Displaying Mined Blocks
After processing transactions, we can inspect the resulting blocks in the chain.
# After all transactions, display all the blocks in the blockchain
for block in blockchain.blocks:
print(block)- This loop iterates through all blocks stored in the
blockchain.blocksarray and prints their contents, allowing us to verify that transactions were successfully grouped and mined.
7. Retrieving a Transaction
It's crucial to be able to query any transaction after it has been processed.
# If the transaction key is valid, retrieve the transaction using the key and display it
if txn_key:
retrieved_transaction = get_transaction(txn_key)
if retrieved_transaction:
print("Retrieved transaction:", retrieved_transaction)
else:
print("Transaction not found.")
else:
print("Invalid transaction.")- Using the unique transaction key (
txn_key) from earlier, theget_transactionfunction retrieves the full transaction details from the database. - This demonstrates the ability to audit and verify any transaction on the blockchain after its completion.
👉 Explore more strategies for blockchain development
Frequently Asked Questions
What is the difference between a private key and a public key?
A private key is a secret, randomly generated number that allows you to control funds associated with its corresponding addresses. It is used to sign transactions. The public key is derived from the private key and can be shared openly to receive funds. Your wallet address is a shortened representation of your public key.
Why is signing a transaction necessary?
Signing a transaction cryptographically proves that the owner of the private key associated with the funds has authorized the transfer. It prevents forgery and unauthorized spending, ensuring that only the rightful owner can move their assets on the blockchain.
What does 'Ethereum-compatible' mean for a wallet?
An Ethereum-compatible wallet uses the same cryptographic standards (secp256k1 elliptic curve) and address formatting as Ethereum. This means the key pairs and addresses generated by our wallet can interact with the Ethereum mainnet, testnets, and other Ethereum Virtual Machine (EVM)-compatible blockchains.
How is a wallet address generated from a public key?
The process involves taking the Keccak-256 hash of the public key. This produces a 256-bit number. The last 20 bytes (40 hexadecimal characters) of this hash are taken and prefixed with '0x' to form the familiar Ethereum-style wallet address (e.g., 0x7cB...).
Is it safe to use a private key directly in code as shown?
No, the example hardcodes a private key for demonstration clarity only. In a production environment or for managing real funds, private keys must be stored securely using encrypted keystore files (like those used by Geth or MetaMask), hardware wallets, or secure environment variables never committed to version control.
What is the role of LevelDB in this wallet setup?
LevelDB serves as the persistent storage layer for our blockchain. It stores crucial data such as account states (balances), the transactions themselves, and the blocks that make up the chain. The update_account_state and get_transaction functions in our code interact with this database.
Conclusion and Next Steps
In this first part, we have successfully constructed a functional wallet that serves as the foundation for our Layer 1 blockchain. We've implemented its core abilities: generating secure key pairs, constructing transactions, creating cryptographic signatures, and interacting with the blockchain database.
The wallet is the user's command center, and its proper implementation is critical for security and functionality. 👉 Get advanced methods for securing digital assets
In the next part of this series, we will delve into the heart of the system by building the core Blockchain class. We will explore the mechanics of transaction validation, the process of block creation and linking through mining, and the algorithms that maintain consensus across the decentralized network.