Create your own blockchain using Python (pt. 3)
Transactions and security
In the first part of this tutorial series, we built our own blockchain where each block contained a timestamp, transaction data and a hash of the previous block. However, we overlooked security. Let’s say Alice wants to send 10 cheeses to Bob, it is important for our blockchain protocol to validate that:
- Alice is Alice
- Alice has the necessary funds to conduct the transaction
In this section, we will take a deeper dive on the subject of transactions and security by first deep-diving into public-key cryptography. We will then apply those cryptographic tools to validate the two points mentioned above.
Public-key Cryptography
Cryptography is communication in the presence of adversaries. There exists various ways to accomplish this and methods have evolved through the years, mainly in the context of wars. For example, the scytale shown below was a tool used by the ancient greeks in military campaign to encrypt messages. This type of cryptography is part of a category called symmetric cryptography since both the sender and the receiver need to have the same key in order to encrypt and decrypt the message.
Today a more modern type of cryptography called public-key cryptography is used for most secure online activities. Here is the Wikipedia definition of public-key cryptography.
Public-key cryptography, or asymmetric cryptography, is a cryptographic system that uses pairs of keys: public keys (which may be known to others), and private keys (which may never be known by any except the owner). The generation of such key pairs depends on cryptographic algorithms which are based on mathematical problems termed one-way functions. Effective security requires keeping the private key private; the public key can be openly distributed without compromising security.
This type of cryptography has many properties and one of them is that it allows for messages to be targeted. Indeed, like shown in the image below, when Bob sends a message to Alice, he uses Alice’s public key to encrypt it. This makes it that the only person in the world that can decrypt it is Alice.
Digital signatures allow for robust authentication. In the example below, Alice can combine a message with her private key to create a short digital signature on the message. Anyone with the sender’s corresponding public key (Bob in this case) can combine that message with a claimed digital signature; if the signature matches the message, the origin of the message is verified (i.e., it must have been made by the owner of the corresponding private key).
In opposition with real signatures, digital signatures are unique for each message because they are generated using the message.
Bitcoin wallets and addresses
If you have a bitcoin wallet, you should be able to easily see your bitcoin address. Those addresses are public and unique and they are used to identify senders and receivers of transactions. The way they are generated is by your wallet first creating a private and public key pair and then hashing your public key two times and encoding that last hash in base 58. Tada, magic!
Let’s create a basic blockchain wallet in Python. First, we need to generate a key pair:
# wallet/wallet.pyfrom Crypto.PublicKey import RSA
def initialize_wallet():
private_key = RSA.generate(2048)
public_key = key.publickey().export_key()
return private_key, public_key
To build our bitcoin address, we now need to hash our public key 2 times (one times via sha256 and one time via RIPEMD160) so the first thing we need to do is update our calculate_hash
method that we created in pt.1 to include the RIPEMD160 hash function:
# wallet/utils.pyfrom Crypto.Hash import RIPEMD160, SHA256
def calculate_hash(data, hash_function: str = "sha256"):
if type(data) == str:
data = bytearray(data, "utf-8")
if hash_function == "sha256":
h = SHA256.new()
h.update(data)
return h.hexdigest()
if hash_function == "ripemd160":
h = RIPEMD160.new()
h.update(data)
return h.hexdigest()
We also need to encode our hash in base 58. Let’s use the base58
python library:
pip3 install base58
From key pair generation to hashing two times and then encoding it in base 58, your Python code now looks like so:
# wallet/wallet.pyimport base58
from Crypto.PublicKey import RSA
from utils import calculate_hash
class Owner:
def __init__(self, private_key: RSA.RsaKey, public_key: bytes, bitcoin_address: bytes):
self.private_key = private_key
self.public_key = public_key
self.bitcoin_address = bitcoin_address
def initialize_wallet():
private_key = RSA.generate(2048)
public_key = private_key.publickey().export_key()
hash_1 = calculate_hash(public_key, hash_function="sha256")
hash_2 = calculate_hash(hash_1, hash_function="ripemd160")
bitcoin_address = base58.b58encode(hash_2)
return Owner(private_key, public_key, bitcoin_address)
Bitcoin transactions
Transaction data
For bitcoin, each transaction message will contain three information:
- Sender bitcoin address
- Recipient bitcoin address
- Amount being sent
We write one function that would take this information as input and generate a dictionary and another that converts this dictionary to bytes.
# wallet/utils.pyimport json
def generate_transaction_data(sender_bitcoin_address, receiver_bitcoin_address, amount: int) -> dict:
return {
"sender": sender_bitcoin_address,
"receiver": receiver_bitcoin_address,
"amount": amount
}
def convert_transaction_data_to_bytes(transaction_data: dict):
new_transaction_data = transaction_data.copy()
new_transaction_data["sender"] = str(transaction_data["sender"])
new_transaction_data["receiver"] = str(transaction_data["receiver"])
new_transaction_data["amount"] = str(transaction_data["amount"])
return json.dumps(new_transaction_data, indent=2).encode('utf-8')
We create a class called Transaction
in which we implement a method generate_data
that leverages those two new methods in order to generate our transaction data in bytes format.
# wallet/wallet.pyfrom utils import generate_transaction_data, convert_transaction_data_to_bytes
class Transaction:
def __init__(self, owner: Owner, receiver_bitcoin_address: bytes, amount: int):
self.owner = owner
self.receiver_bitcoin_address = receiver_bitcoin_address
self.amount = amount
def generate_data(self) -> bytes:
transaction_data = generate_transaction_data(self.owner.bitcoin_address, self.receiver_bitcoin_address, self.amount)
return convert_transaction_data_to_bytes(transaction_data)
Transaction process
Transactions are completed in a three parts process. Let’s look at those with the example where Alice wants to send 10 cheese to Bob.
- Signing
- Broadcasting
- Confirming
1. Signing
Signing is very much like authentication: in order to make a transaction, Alice needs to authenticate against the bitcoin nodes. Alice’s wallet will generate a signature based on the message and her private key. The wallet then groups the signature along with the message into a small file. Part of our Transaction
class, we will create a sign
method that will take care of generating a signature:
# wallet/wallet.pyimport binasciifrom Crypto.Hash import SHA256
from Crypto.Signature import pkcs1_15
class Transaction:
def __init__(self, owner: Owner, receiver_bitcoin_address: bytes, amount: int):
self.owner = owner
self.receiver_bitcoin_address = receiver_bitcoin_address
self.amount = amount
def sign(self) -> str:
transaction_data = self.generate_data()
hash_object = SHA256.new(transaction_data)
signature = pkcs1_15.new(self.owner.private_key).sign(hash_object)
return binascii.hexlify(signature).decode("utf-8")
Now that the transaction data and signature are generated, the wallet groups them into a dictionary using a new send_to_nodes
method:
# wallet/wallet.pyclass Transaction:
def __init__(self, owner: Owner, receiver_bitcoin_address: bytes, amount: int, signature: str = ""):
self.owner = owner
self.receiver_bitcoin_address = receiver_bitcoin_address
self.amount = amount
self.signature = signature
def generate_data(self) -> bytes:
transaction_data = generate_transaction_data(self.owner.bitcoin_address, self.receiver_bitcoin_address, self.amount)
return convert_transaction_data_to_bytes(transaction_data)
def sign(self):
transaction_data = self.generate_data()
hash_object = SHA256.new(transaction_data)
signature = pkcs1_15.new(self.owner.private_key).sign(hash_object)
self.signature = binascii.hexlify(signature).decode("utf-8")
def send_to_nodes(self):
return {
"sender_address": self.owner.bitcoin_address,
"receiver_address": self.receiver_bitcoin_address,
"amount": self.amount,
"signature": self.signature
}
Here’s how the wallet would use the sign method:
receiver_bitcoin_address = b'blabliblou'
amount = 10
transaction = Transaction(owner, receiver_bitcoin_address, amount)
transaction.sign()
2. Broadcasting
Once the transaction is signed, the wallet will send the file to the network nodes that hold copies of the blockchain. Each node that receives the file verifies that it is legit by validating that:
- The signature checks out (Alice is Alice)
- The sender has the funds to make the transaction (Alice has the funds)
To verify that the signature checks out, the node hashes the message and validates it against the signature:
# node.pyfrom Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from block import Block
class Node:
def __init__(self, blockchain: Block):
self.blockchain = blockchain
@staticmethod
def validate_signature(public_key: bytes, signature: bytes, transaction_data: bytes):
public_key_object = RSA.import_key(public_key)
transaction_hash = SHA256.new(transaction_data)
pkcs1_15.new(public_key_object).verify(transaction_hash, signature)
Here, validate_signature
will throw an exception if the signature doesn’t match with the transaction hash.
The node also needs to validate that the sender has the funds to make the transaction. Let’s re-use the same example from pt.1 of this series:
- Albert pays 30 to Bertrand
- Albert pays 10 to Camille
- Bertrand pays 5 to Camille
Let’s say the node wants to validate that the following transaction is valid:
- Camille pays 5 to Albert
We expect this transaction to be approved since Camille at this point has a total of 15 which is greater than 5. Now, how do we implement this validation in Python? The problem we have is that the blockchain does not store balances, it stores transactions. So, one way to know wether Camille has the funds to make the transaction is by iterating through every transaction that her address was involved in starting from the beginning of the blockchain and calculate the final balance for that address. Let’s implement this in Python:
# node.pyfrom block import Block
class Node:
def __init__(self, blockchain: Block):
self.blockchain = blockchain
def validate_funds(self, sender_address: bytes, amount: int) -> bool:
sender_balance = 0
current_block = self.blockchain
while current_block:
if current_block.transaction_data["sender"] == sender_address:
sender_balance = sender_balance - current_block.transaction_data["amount"]
if current_block.transaction_data["receiver"] == sender_address:
sender_balance = sender_balance + current_block.transaction_data["amount"]
current_block = current_block.previous_block
if amount <= sender_balance:
return True
else:
return False
Note 1: In practice, transactions are stored and validated via double-entry bookkeeping. We will revisit this subject in the next section of the tutorial.
Note 2: In our example, Albert’s money came from nowhere, which obviously doesn’t make sense. We will come back to how money gets generated later in this series.
Once the file is verified by the nodes, the file is passed to other nodes that repeat the same process. When a node receives the file, it holds it in a holding area called the mem_pool (memory pool) which is meant for valid but still unconfirmed transactions. Unconfirmed means that the transaction is not yet part of the blockchain.
3. Confirming
Miners group transactions together that are kept in their mem_pool and create a block of transactions. Once a miner wins the proof of work competition, he will get his block onto the chain and the transaction is considered to be confirmed.
Note 3: We introduced some new terms like nodes, miners and proof of work and those terms are key components of the bitcoin network. The problem is that we can’t talk about transactions without talking about the bitcoin network and we can’t talk about the bitcoin network without talking about transactions. That being said, we will come back to those specific subjects in a future section of this tutorial.
A more useful way to store transactions: Double-entry bookkeeping
Up to now in our tutorial we have been using single-entry bookkeeping inside of our blockchain (i.e. one entry per transaction). Our entries looked something like “sender, receiver, amount”. In the next section, we will look at double-entry bookkeeping and why most blockchains use this form of bookeeping to store transactions.
Code repository
Create your own blockchain using Python
References
- Bitcoin: A Peer-to-Peer Electronic Cash System, Satoshi Nakamoto: https://bitcoin.org/bitcoin.pdf
- Blockchain Basics & Cryptography, MIT 15.S12 Blockchain and Money, Fall 2018, Gary Gensler (Youtube)
- Base 58 Encoding: https://learnmeabitcoin.com/technical/base58
- Public key cryptography: https://en.wikipedia.org/wiki/Public-key_cryptography
- Scytale: https://en.wikipedia.org/wiki/Scytale
- Digital signatures: https://en.wikipedia.org/wiki/Digital_signature
- Transaction validation: https://golden.com/wiki/Transaction_validation-XKEG3JW
- Mastering Bitcoin: Programming the Open Blockchain, Andreas M. Antonopoulos, O’Reilly Media; 2 edition (July 11 2017)