In this post, I will walk through the creation of a basic PoS blockchain using Python, illustrating the foundational principles that make this technology so powerful. By the end, you’ll have a clearer understanding of how PoS blockchains work and a hands-on example that you can expand upon in your own projects. Whether you’re a seasoned developer or just starting out, this guide will help you grasp the essentials of blockchain technology in a practical, approachable way.

If you prefer a PoW blockchain example, see this post.

The Code

As a note, this code could be utilized in any coding environment but I would recommend a Jupyter Notebook. A version of the code mentioned in this post can be viewed on GitHub.

Import a few packages that will be useful.

import hashlib
import json
from datetime import datetime
import random

Start by defining the Block and Blockchain classes.

Create the Block Class

The Block class will represent a single block in the blockchain. Each block will contain the block’s index, previous block’s hash, timestamp, data (transactions), nonce, and it’s own hash. The compute_hash method calculates the SHA-256 hash of the block’s contents.

class Block:
    def __init__(self, index, previous_hash, timestamp, data, validator):
        self.index = index
        self.previous_hash = previous_hash
        self.timestamp = timestamp
        self.data = data
        self.validator = validator
        self.hash = self.calculate_hash()

    def calculate_hash(self):
        block_dict = self.__dict__
        if 'hash' in block_dict:
            del block_dict['hash']
        block_string = json.dumps(block_dict, sort_keys=True).encode()
        return hashlib.sha256(block_string).hexdigest()

Create the Blockchain Class

The Blockchain class will manage the chain of blocks and handle the consensus mechanism. The class defines the following methods:

  1. The constructor initializes the blockchain instance. It sets up several important properties:

    • self.chain: A list that holds the blocks of the blockchain.
    • self.unconfirmed_data: A list of new data (or transactions) that have not yet been added to a block.
    • self.validators: A dictionary where each validator is recorded. Validators are participants who can be selected to create blocks.
    • self.staked_tokens: A dictionary that holds the amount of staked tokens for each validator. Staked tokens determine a validator’s chance of being selected.
    • self.minimum_stake: The minimum amount of tokens a validator must stake to participate in the network.
    • self.create_genesis_block(): The genesis block is the first block in the blockchain. It is created immediately when the blockchain is initialized.
  2. create_genesis_block: This method creates the first block, or “genesis block,” in the blockchain. The genesis block typically has unique properties like an index of 0, no previous hash (since it’s the first), and no validator. It is appended to self.chain to start the blockchain.

  3. last_block: This method returns the most recent block in the blockchain. It is used when creating new blocks to reference the last block’s hash.

  4. add_data: This method adds new, unconfirmed data (or transactions) to the self.unconfirmed_data list. This data will eventually be included in the next block when it’s created.

  5. add_validator: This method allows validators to join the blockchain. It takes two parameters: the validator and their stake, which is the amount of cryptocurrency they are willing to lock up. If the stake is greater than or equal to the minimum_stake, the validator is added to both self.validators and self.staked_tokens dictionaries. Validators with stakes below the threshold are rejected with an error message.

  6. select_validator: This method selects a validator to create the next block, using a weighted random approach based on the amount of staked tokens. It works as follows:

    • It first calculates the total_stake, which is the sum of all validators’ stakes.
    • A random number (pick) is generated between 0 and total_stake.
    • The method iterates over the self.staked_tokens dictionary, accumulating the stake values. When the accumulated value surpasses pick, that validator is selected.
    • This method ensures that validators with higher stakes have a better chance of being selected, but randomness is involved to prevent deterministic outcomes.
  7. create_block: This method creates a new block on the blockchain. It performs the following actions:

    • It checks if there is any unconfirmed data in self.unconfirmed_data. If there is no data, no block is created, and the method returns False.
    • The method gets the most recent block (last_block) and creates a new Block object. The new block’s index is one greater than the last_block’s index, and its previous_hash refers to the last block’s hash.
    • The block is created with the current timestamp, the unconfirmed data, and the selected validator who is creating this block.
    • The new block is appended to the chain, and the self.unconfirmed_data list is cleared since the data has now been confirmed in a block. The method returns the index of the new block.
  8. display_chain: This method loops through the entire blockchain and prints each block’s details. It prints the attributes of each block by converting its dictionary representation block.__dict__.

class Blockchain:
    def __init__(self):
        self.chain = []
        self.unconfirmed_data = []
        self.validators = {}
        self.staked_tokens = {}
        self.minimum_stake = 10
        self.create_genesis_block()

    def create_genesis_block(self):
        genesis_block = Block(0, None, str(datetime.now()), "Genesis Block", None)
        self.chain.append(genesis_block)

    def last_block(self):
        return self.chain[-1]

    def add_data(self, new_data):
        self.unconfirmed_data.append(new_data)

    def add_validator(self, validator, stake):
        if stake >= self.minimum_stake:
            self.staked_tokens[validator] = stake
            self.validators[validator] = True
        else:
            print(f"{validator} does not meet the minimum stake requirement.")
            
    def select_validator(self):
        total_stake = sum(self.staked_tokens.values())
        selected_validator = None
        while selected_validator == None:
            pick = random.uniform(0, total_stake)
            print(pick)
            current = 0
            for validator, stake in self.staked_tokens.items():
                print(validator, stake)
                current += stake
                if current > pick:
                    selected_validator = validator
                    break
        return selected_validator

    def create_block(self, validator):
        if not self.unconfirmed_data:
            return False
        
        last_block = self.last_block()
        new_block = Block(index=last_block.index + 1, 
                          previous_hash=last_block.hash,
                          timestamp=str(datetime.now()),
                          data=self.unconfirmed_data,
                          validator=validator)
        self.chain.append(new_block)
        self.unconfirmed_data = []
        return new_block.index

    def display_chain(self):
        for block in self.chain:
            print(f"Block {block.__dict__}")

Running the code

When running this example, it will create a simple Blockchain, add some transactions, mine new blocks, and print the Blockchain’s contents. It also checks the validity of the entire chain to ensure its integrity.

# Create a Blockchain object
blockchain = Blockchain()

# Add some validators
blockchain.add_validator(999, 10)
blockchain.add_validator(1111, 50)
blockchain.add_validator(1222, 30)
blockchain.add_validator(1333, 20)
blockchain.add_validator(1444, 20)

# Add some data / transactions
blockchain.add_data("James earned 10 LSU_tokens")
blockchain.add_data("James pays Mike_the_tiger 5 LSU_tokens")
# Obtain a validator
selected_validator = blockchain.select_validator()
print(f"Validator selected: {selected_validator}")
109.81061787493455
999 10
1111 50
1222 30
1333 20
Validator selected: 1333
blockchain.create_block(selected_validator)
blockchain.display_chain()
Block {'index': 0, 'previous_hash': None, 'timestamp': '2024-09-15 17:10:14.873731', 'data': 'Genesis Block', 'validator': None, 'hash': '8e507213fcc4bdf99f2d86bbe76fc0f9d079152263b74a7a6968efa8b379fe4f'}
Block {'index': 1, 'previous_hash': '8e507213fcc4bdf99f2d86bbe76fc0f9d079152263b74a7a6968efa8b379fe4f', 'timestamp': '2024-09-15 17:11:42.801168', 'data': ['James earned 10 LSU_tokens', 'James pays Mike_the_tiger 5 LSU_tokens'], 'validator': 1333, 'hash': '469159d4f06a11514dda30d273ade304a3671b8117a754e6dcd450268586a446'}

Conclusion

This is a basic implementation of a Proof of Stake (PoS) blockchain, where validators stake tokens to increase their chances of being selected to create the next block. The selection is randomized but weighted based on how much stake each validator has. The PoS consensus method was created to address shortcomings with the Proof of Work, PoW, consensus method.