Proof-of-Work and Proof-of-Stake

2023-05-10 blockchain

The purpose of this notebook is to look at proof of work blockchain and proof of stake blockchain in a very primitive manner.

We start with defining transactions and block objects for the PoW and PoS

from dataclasses import dataclass
import json
from hashlib import sha256
from datetime import datetime
from dataclasses import asdict
import numpy as np
import time



@dataclass
class Transaction:
  sender: str
  receiver: str
  amount: float


@dataclass
class Pow_Block:
  version: int
  timestamp: float
  nonce: int
  difficulty: int
  previous_hash: str
  transactions: list[Transaction]



@dataclass
class Pos_Block:
  version: int
  timestamp: float
  validator: str
  previous_hash: str
  transactions: list[Transaction]

Proof of Work

In proof of work, miners are competing in finding the so called nonce (number only used once) that satisfies a certain difficulty. If the nonce generates a number lower than the difficulty number, the nonce is valid and the miner who found it wins and can claim the rewards.

The nonce generates a hash that is converted to a number. Call that number $A$. The difficulty generates a number which we call $B$. If $A<B$, nonce is valid.



HEX_BASE_TO_NUMBER = 16
DIFFICULTY = 1
#PREV_BLOCK_HASH = "68ffd13b24f9d73399a80aad9de06f676001fed56648314526cd23a4d18fef16"
TRANSACTIONS = (
    Transaction("sender1", "receiver1", 1),
    Transaction("sender2", "receiver2", 0.5),
    Transaction("sender3", "receiver3", 2.7),
)

def create_block_pow(nonce: int, difficulty: int, transactions: tuple, previous_hash) -> Pow_Block:
    cur_timestamp = datetime.now().timestamp()
    return Pow_Block(
        version=1,
        timestamp=cur_timestamp,
        nonce=nonce,
        previous_hash=previous_hash,
        difficulty=difficulty,
        transactions=transactions
    )

def encode_block_pow(block: Pow_Block) -> str:
    encoded_block = json.dumps(asdict(block)).encode()
    return sha256(sha256(encoded_block).digest()).hexdigest()

def calculate_hash_pow(nonce: int, transactions) -> str:
    block = encode_block_pow(nonce, DIFFICULTY, transactions)
    encoded_block_hash = encode_block_pow(block)
    return encoded_block_hash



BYTE_IN_BITS = 256
HEX_BASE_TO_NUMBER = 16
SECONDS_TO_EXPIRE = 20


GENESIS_BLOCK = Pow_Block(
    version=1,
    timestamp=1231006505,
    difficulty=1,
    nonce=42,
    previous_hash="000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
    transactions=(Transaction("Satoshi Nakamoto", "Satoshi Nakamoto", 50),)
)


def calculate_difficulty_target(difficulty_bits: int) -> int:
    return 2 ** (BYTE_IN_BITS - difficulty_bits)


class Pow_Blockchain:
    VERSION = 1
    DIFFICULTY = 10
    MINUTES_TOLERANCE = 1

    def __init__(self):
        self.chain = [GENESIS_BLOCK]

    def get_last_block(self) -> Pow_Block:
        return self.chain[-1]
    
    def add_block(self, block: Pow_Block) -> bool:
        is_valid = self._validate_block(block)
        if is_valid:
            self.chain.append(block)

        return is_valid
    
    def get_difficulty(self) -> int:
        return self.DIFFICULTY


    def _validate_block(self, candidate: Pow_Block) -> bool:
        if candidate.version != self.VERSION:
            return False

        last_block = self.get_last_block()
        if candidate.previous_hash != encode_block_pow(last_block):
            return False
        
        if candidate.difficulty != self.DIFFICULTY:
            return False


        candidate_hash = encode_block_pow(candidate)
        candidate_decimal = int(candidate_hash, HEX_BASE_TO_NUMBER)

        target = calculate_difficulty_target(self.DIFFICULTY)
        is_block_valid = candidate_decimal < target

        return is_block_valid
    



def mine_proof_of_work(nonce: int, difficulty: int, prev_hash: str, transactions:tuple) -> tuple[bool, Pow_Block]:
    block = create_block_pow(nonce, difficulty, transactions, prev_hash)
    encoded_block = encode_block_pow(block)
    block_encoded_as_number = int(encoded_block, HEX_BASE_TO_NUMBER)
    decimal_target = calculate_difficulty_target(difficulty)

    if block_encoded_as_number < decimal_target:
        return True, block

    return False, block

Start blockchain

import numpy as np
pow_blockchain = Pow_Blockchain()


nonce = 0
start_time = time.time()
found = False
prev_hash = encode_block_pow(pow_blockchain.get_last_block())

for i in range(10):
    nonce = 0
    start_time = time.time()
    found = False
    prev_hash = encode_block_pow(pow_blockchain.get_last_block())
    while not found:
        found, block = mine_proof_of_work(nonce, pow_blockchain.get_difficulty(), prev_hash, Transaction("Satoshi Nakamoto", "Satoshi Nakamoto", np.random.uniform()))

        if found:
            pow_blockchain.add_block(block)
        else:
            nonce += 1

        end_time = time.time()
        elapsed_time = end_time - start_time

        if elapsed_time > SECONDS_TO_EXPIRE:
            raise TimeoutError(
                f"Couldn't find a block within the given timeframe"
            )
for block in pow_blockchain.chain:
    print(block)
    print("\n")
Pow_Block(version=1, timestamp=1231006505, nonce=42, difficulty=1, previous_hash='000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', transactions=(Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=50),))


Pow_Block(version=1, timestamp=1683676835.03425, nonce=1271, difficulty=10, previous_hash='629982432748d68179f7f81e85b67925a59fb2b5082eeb5f6fc9257028204ea4', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=0.04643169129473279))


Pow_Block(version=1, timestamp=1683676835.045253, nonce=533, difficulty=10, previous_hash='003e03f4189866a540edccaf5a8854a5f611a7707ff9c0df572c41f1e098fb03', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=0.23683450574403841))


Pow_Block(version=1, timestamp=1683676835.091262, nonce=2219, difficulty=10, previous_hash='003abd0d23f25fc664171fbc7f6a25998ec28ee204b31cd45b74ff2c0ccf4309', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=0.30504848451670386))


Pow_Block(version=1, timestamp=1683676835.103265, nonce=580, difficulty=10, previous_hash='00274768d89c7f4372eb15ff34a3c0c92da8f7acc98d37bce46c0dfbbcefad61', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=0.8891598058714782))


Pow_Block(version=1, timestamp=1683676835.105266, nonce=96, difficulty=10, previous_hash='0033bfbdecf964db73b45fcdf19b0255143799583a4def154570cd1f859e8358', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=0.21893269713324048))


Pow_Block(version=1, timestamp=1683676835.12527, nonce=993, difficulty=10, previous_hash='001436dd4a0710ee2dc4f443fe3ae851eaec7df44f77afdd4f86785fcfa197b4', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=0.13291033703083))


Pow_Block(version=1, timestamp=1683676835.135273, nonce=486, difficulty=10, previous_hash='003037d579bb7aeef3befb4c281050ed0d2703d7a70b847b4f471e8417d59d59', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=0.138879958758775))


Pow_Block(version=1, timestamp=1683676835.162279, nonce=1286, difficulty=10, previous_hash='000453475413a3c6c46c7bfa7c8a63fe5b068ed9d1b1e80a213d9dc166d220f8', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=0.6620923154562147))


Pow_Block(version=1, timestamp=1683676835.189285, nonce=1339, difficulty=10, previous_hash='0020b8db96297591627d09101ab738fd167e3d98821c6200e60915366e8adc47', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=0.24158860154623452))


Pow_Block(version=1, timestamp=1683676835.198287, nonce=447, difficulty=10, previous_hash='002b019a97422540e7523a898a40c451f7aa9acd1796fdcd0a5fc2a1e3d97d78', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=0.5398827320781564))

Proof of Stake

In Proof of Stake, miners/validators are required to stake their tokens/balance in order to be chosen as the next block creator. Therefore, the miner that stakes the most amount of its currency has the highest chance of being chosen as the leader and creating the next block.

Compared to Proof of Work, where miners compete with each other in terms of computation power, here they compete in terms of currency.

GENESIS_BLOCK_POS = Pos_Block(
    version=1,
    timestamp=1231006505,
    previous_hash="000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
    validator='Louie',
    transactions=Transaction('','',0)
)

def encode_block_pos(block):
    """
    calcuate block sha256 hash value
    """
    record  = "".join([
        str(block.version),
        str(block.timestamp),
        block.validator,
        block.previous_hash
    ])
    return sha256(record.encode()).hexdigest()



class Pos_Blockchain:
    VERSION = 1

    def __init__(self):
        self.chain = [GENESIS_BLOCK_POS]

    def get_last_block(self) -> Pos_Block:
        return self.chain[-1]
    
    def add_block(self, block: Pos_Block) -> bool:
        is_valid = self._validate_block(block)
        if is_valid:
            self.chain.append(block)

        return is_valid
    

    def get_difficulty(self) -> int:
        return self.DIFFICULTY


    def _validate_block(self, candidate: Pos_Block) -> bool:
        if candidate.version != self.VERSION:
            return False

        last_block = self.get_last_block()
        if candidate.previous_hash != encode_block_pos(last_block):
            return False
        



        return True








def create_block_pos(transactions: tuple, validator: str, previous_hash) -> Pow_Block:
        cur_timestamp = datetime.now().timestamp()
        return Pos_Block(
            version=1,
            timestamp=cur_timestamp,
            validator=validator,
            previous_hash=previous_hash,
            transactions=transactions
        )

def lottery_proof_of_stake(validators, block, prev_hash, transactions):
    participants = [user['id'] for user in validators]
    participants_amt = np.array([user['stake'] for user in validators])
    total_amt = np.sum(participants_amt)
    winner = np.random.choice(a = participants, p = participants_amt/total_amt)
    block = create_block_pos(transactions, winner, prev_hash)
    return block

    
    
validators = [{'id':'Huey', 'stake':33}, {'id':'Dewey', 'stake':10}, {'id':'Louie', 'stake':99}]


pos_blockchain = Pos_Blockchain()


start_time = time.time()
found = False
prev_hash = encode_block_pos(pos_blockchain.get_last_block())

for i in range(10):
    prev_hash = encode_block_pos(pos_blockchain.get_last_block()) 
    block = lottery_proof_of_stake(validators, block, prev_hash, Transaction("Satoshi Nakamoto", "Satoshi Nakamoto", 100*np.random.uniform()))
    pos_blockchain.add_block(block)


for block in pos_blockchain.chain:
    print(block)
    print("\n")
Pos_Block(version=1, timestamp=1231006505, validator='Louie', previous_hash='000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', transactions=Transaction(sender='', receiver='', amount=0))


Pos_Block(version=1, timestamp=1683676866.845412, validator='Dewey', previous_hash='df4413f8c82d31a43997633f241f3969788d7870410653a04abfa246335dd429', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=74.1256835500038))


Pos_Block(version=1, timestamp=1683676866.845412, validator='Louie', previous_hash='b94d10d3a11af4e0c95b62f46883935fdccd761924ef849c99805f45ff99fb0f', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=24.151181171183932))


Pos_Block(version=1, timestamp=1683676866.845412, validator='Louie', previous_hash='c4886eca584e50da1dd120fe8d242b1d06eb295361974d00565fa175e87674c4', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=79.4071220119231))


Pos_Block(version=1, timestamp=1683676866.845412, validator='Louie', previous_hash='427e88cae6b25a7e648efced676cea59cbbf1f6141fee8cc17884b2dcc3905f3', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=35.83240492783906))


Pos_Block(version=1, timestamp=1683676866.845412, validator='Louie', previous_hash='175e1d1f82a454f73cab7175bd9ccd1f69bab8a31e1bcb0b5437768a15e74441', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=64.68286159916468))


Pos_Block(version=1, timestamp=1683676866.845412, validator='Huey', previous_hash='3ea4c3fbc5ccd3e7a513717705874f06d3db4d70ca33938cb546239d55dcfdad', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=66.83634658085363))


Pos_Block(version=1, timestamp=1683676866.845412, validator='Louie', previous_hash='78bf9ae2cc5b67a8f4f773820fed55bf6c6036c7f3687d60f9f14930e6588d89', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=66.79188204734481))


Pos_Block(version=1, timestamp=1683676866.845412, validator='Louie', previous_hash='2202892e13eb974736cfbf9e77138f7142996ab0ebc3b572571e7596eeef6070', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=77.54172125698501))


Pos_Block(version=1, timestamp=1683676866.845412, validator='Louie', previous_hash='ea9e218d1e7dbdca541796500a13b80e2e039f73a04f17080a16683b74fc2e04', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=15.281043236113367))


Pos_Block(version=1, timestamp=1683676866.845412, validator='Huey', previous_hash='3ba3b8089dc65d0b1336aa60e7e854d39d035150dc728f735737642e0b4b9bce', transactions=Transaction(sender='Satoshi Nakamoto', receiver='Satoshi Nakamoto', amount=15.538494296269745))