Create your own blockchain using Python (pt. 3)

Transactions and security

Guillaume Belanger
9 min readJul 8, 2021

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:

  1. Alice is Alice
  2. 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.

Scytale (source: https://en.wikipedia.org/wiki/Scytale)

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.

Source: https://en.wikipedia.org/wiki/Public-key_cryptography

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).

Source: https://en.wikipedia.org/wiki/Digital_signature

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!

Source: Blockchain Basics & Cryptography, MIT 15.S12 Blockchain and Money, Fall 2018, Gary Gensler (Youtube)

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.

  1. Signing
  2. Broadcasting
  3. 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:

  1. The signature checks out (Alice is Alice)
  2. 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:

  1. Albert pays 30 to Bertrand
  2. Albert pays 10 to Camille
  3. 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

--

--

Guillaume Belanger

Guillaume is a software developer from Montreal who writes about bip bop stuff.