DEV Community

Cover image for Creating a blockchain in 60 lines of Python
Jose Angel Munoz
Jose Angel Munoz

Posted on • Updated on

Creating a blockchain in 60 lines of Python

When I read the document written by Phu Minh, I was curious about learning different concepts about blockchain. Once I started to read the code, I wanted to match it with Python to understand also the differences with JavaScript.

The objective of this post is finding the differences in both languages and serve as the Python appendix of the original post.

Even though the original document comes from a Python example, I wanted to have an exact match with the JavaScript code to compare.

Let's also fit the python code in the promised 60 lines.

Blockchain

Although the idea is to mimic the entire post and use the same sections to follow the code,

For the Blockchain definition, I prefer the following:

Blockchain is a system of recording information in a way that makes it difficult or impossible to change, hack, or cheat.

Setup

We are using Python for this project, so be sure to install it if you haven't.

As I have said, a block is just an object that has some information on it, so we should have a Block class like this:

class Block:

    def __init__(self, timestamp=None, data=None):
        self.timestamp = timestamp or time()
        # this.data should contain information like transactions.
        self.data = [] if data is None else data
Enter fullscreen mode Exit fullscreen mode

The class definition is quite similar in both languages. In Python, we use self instead of this and init is a constructor method.

Comments are also similar in both languages. In Python, we use # to comment vs. // in javascript.

Fot the sha256 algorithn, I will use the hashlib library vs the crypto package in javascript.

from hashlib import sha256

class Block:

    def __init__(self, timestamp=None, data=None):
        self.timestamp = timestamp or time()
        self.data = [] if data is None else data
        self.hash = self.getHash()
        self.prevHash = None # previous block's hash

    def getHash(self):
        hash = sha256()
        hash.update(str(self.prevHash).encode('utf-8'))
        hash.update(str(self.timestamp).encode('utf-8'))
        hash.update(str(self.data).encode('utf-8'))
        return hash.hexdigest()
Enter fullscreen mode Exit fullscreen mode

In the getHash method, from an empty hash, we update it with the rest of components. The hash is the result of the concatenation of the previous hash, the timestamp and the data. All of then with the .encode('utf-8') to convert the string to bytes.

The blockchain

Let's move over to the blockchain class.

class Blockchain:
    def __init__(self):
        # This property will contain all the blocks.
        self.chain = []
Enter fullscreen mode Exit fullscreen mode

Again, the class definition is similar in both languages.

To create the genesis block, we just call the Block with the current timestamp using time. To do that, we need to import the time library.

The string conversion is done with str instead of toString.

from time import time

class Blockchain:
    def __init__(self):
        # Create our genesis block
        self.chain = [Block(str(int(time())))]
Enter fullscreen mode Exit fullscreen mode

And the method to get the latest block is similar. We use len to get the length of the chain instead of length in javascript.

    def getLastBlock(self):
        return self.chain[len(self.chain) - 1]
Enter fullscreen mode Exit fullscreen mode

To add the block to the blockchain, we just call the addBlock method. The code is almost the same except the append (push in javascript).

def addBlock(self, block):
        # Since we are adding a new block, prevHash will be the hash of the old latest block
        block.prevHash = self.getLastBlock().hash
        # Since now prevHash has a value, we must reset the block's hash
        block.hash = block.getHash()
        self.chain.append(block)
Enter fullscreen mode Exit fullscreen mode

Validation

In the validation method, we start using range as a big difference. Also, because we don't use constants in Python, we just use normal variables.

For the conditional, python uses or instead of || in javascript.

def isValid(self):
    # Iterate over the chain, we need to set i to 1 because there are nothing before the genesis block, so we start at the second block.
    for i in range(1, len(self.chain)):
        currentBlock = self.chain[i]
        prevBlock = self.chain[i - 1]

        # Check validation
        if (currentBlock.hash != currentBlock.getHash() or prevBlock hash != currentBlock.prevHash):
            return False

    return True
Enter fullscreen mode Exit fullscreen mode

Proof-of-work

We can implement this system by adding a mine method and a nonce property to our block. Be careful because nonce must be declared before calling the self.getHash() method. If not, you will get the error AttributeError: 'Block' object has no attribute 'nonce'.

class Block:

    def __init__(self, timestamp=None, data=None):
        self.timestamp = timestamp or time()
        self.data = [] if data is None else data
        self.prevHash = None # previous block's hash
        self.nonce = 0
        self.hash = self.getHash()

    # Our hash function.
    def getHash(self):

        hash = sha256()
        hash.update(str(self.prevHash).encode('utf-8'))
        hash.update(str(self.timestamp).encode('utf-8'))
        hash.update(str(self.data).encode('utf-8'))
        hash.update(str(self.nonce).encode('utf-8'))
        return hash.hexdigest()

    def mine(self, difficulty):
        # Basically, it loops until our hash starts with
        # the string 0...000 with length of <difficulty>.
        while self.hash[:difficulty] != '0' * difficulty:
            # We increases our nonce so that we can get a whole different hash.
            self.nonce += 1
            # Update our new hash with the new nonce value.
            self.hash = self.getHash()
Enter fullscreen mode Exit fullscreen mode

To create the difficulty property:

self.difficulty = 1
Enter fullscreen mode Exit fullscreen mode

And the addBlock method:

    def addBlock(self, block):
        block.prevHash = self.getLastBlock().hash
        block.hash = block.getHash()
        block.mine(self.difficulty)
        self.chain.append(block)
Enter fullscreen mode Exit fullscreen mode

Testing out the chain

First, import the module and use the Blockchain class the same way using JeChain object:

from blockchain import Block
from blockchain import Blockchain
from time import time

JeChain = Blockchain()

# Add a new block
JeChain.addBlock(Block(str(int(time())), ({"from": "John", "to": "Bob", "amount": 100})))
# (This is just a fun example, real cryptocurrencies often have some more steps to implement).

# Prints out the updated chain
print(JeChain)
Enter fullscreen mode Exit fullscreen mode

It should looks like this:

[
    {
        "data": [],
        "timestamp": "1636153236",
        "nonce": 0,
        "hash": "4caa5f684eb3871cb0eea217a6d043896b3775f047e699d92bd29d0285541678",
        "prevHash": null
    },
    {
        "data": {
            "from": "John",
            "to": "Bob",
            "amount": 100
        },
        "timestamp": "1636153236",
        "nonce": 14,
        "hash": "038f82c6e6605acfcad4ade04e454eaa1cfa3d17f8c2980f1ee474eefb9613e9",
        "prevHash": "4caa5f684eb3871cb0eea217a6d043896b3775f047e699d92bd29d0285541678"
    }
]
Enter fullscreen mode Exit fullscreen mode

but only after adding the __repr__ method to the Blockchain class:

import json

    def __repr__(self):
        return json.dumps([{'data': item.data, 'timestamp': item.timestamp, 'nonce': item.nonce, 'hash': item.hash, 'prevHash': item.prevHash} for item in self.chain], indent=4)
Enter fullscreen mode Exit fullscreen mode

Updated bonus: Difficulty and block time

For the blockTime just:

self.blockTime = 30000
Enter fullscreen mode Exit fullscreen mode

Have a look to the ternary used for the difficulty system. In Python, the ternary operator is (if_test_is_false, if_test_is_true)[test], resulting in:

    def addBlock(self, block):
        block.prevHash = self.getLastBlock().hash
        block.hash = block.getHash()
        block.mine(self.difficulty)
        self.chain.append(block)

        self.difficulty += (-1, 1)[int(time()) - int(self.getLastBlock().timestamp) < self.blockTime]
Enter fullscreen mode Exit fullscreen mode

The final python code (Without proper formatting) in 60 lines is:

# -*- coding: utf-8 -*-

from hashlib import sha256
import json
from time import time


class Block:

    def __init__(self, timestamp=None, data=None):
        self.timestamp = timestamp or time()
        self.data = [] if data is None else data
        self.prevHash = None
        self.nonce = 0
        self.hash = self.getHash()

    def getHash(self):

        hash = sha256()
        hash.update(str(self.prevHash).encode('utf-8'))
        hash.update(str(self.timestamp).encode('utf-8'))
        hash.update(str(self.data).encode('utf-8'))
        hash.update(str(self.nonce).encode('utf-8'))
        return hash.hexdigest()

    def mine(self, difficulty):
        while self.hash[:difficulty] != '0' * difficulty:
            self.nonce += 1
            self.hash = self.getHash()

class Blockchain:

    def __init__(self):
        self.chain = [Block(str(int(time())))]
        self.difficulty = 1
        self.blockTime = 30000

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

    def addBlock(self, block):
        block.prevHash = self.getLastBlock().hash
        block.hash = block.getHash()
        block.mine(self.difficulty)
        self.chain.append(block)

        self.difficulty += (-1, 1)[int(time()) - int(self.getLastBlock().timestamp) < self.blockTime]

    def isValid(self):
        for i in range(1, len(self.chain)):
            currentBlock = self.chain[i]
            prevBlock = self.chain[i - 1]

            if (currentBlock.hash != currentBlock.getHash() or prevBlock.hash != currentBlock.prevHash):
                return False

        return True

    def __repr__(self):
        return json.dumps([{'data': item.data, 'timestamp': item.timestamp, 'nonce': item.nonce, 'hash': item.hash, 'prevHash': item.prevHash} for item in self.chain], indent=4)
Enter fullscreen mode Exit fullscreen mode

Hopefully you will enjoy and learn with both posts!

Discussion (8)

Collapse
rochacbruno profile image
Bruno Rocha • Edited on

Great article :)

This is considered harmful because you are assigning a mutable object in a method definition, all instances of Block will be mutating the same data reference.

def __init__(self, timestamp=time(), data=[]):
Enter fullscreen mode Exit fullscreen mode

Consider changing to

def __init__(self, timestamp=time(), data=None):
     data = [] if data is None else data
Enter fullscreen mode Exit fullscreen mode

Also timestamp=time() will be evaluated in the time the class is defined, when the program is started, if this is a long running process then you are stuck with a certain time()

def __init__(self, timestamp=None, data=None):
     timestamp = timestamp or time()
     data = [] if data is None else data
Enter fullscreen mode Exit fullscreen mode
Collapse
imjoseangel profile image
Jose Angel Munoz Author • Edited on

Fully agree @rochacbruno

The idea of the code was keeping it exactly the same as the Javascript for learning purposes but your comment is a perfect helper for the post. β™₯

I have just changed it as per your comments.

Thank you!

Collapse
theflanman profile image
Connor Flanigan

Interesting article. I wanted to take a stab at cleaning it up a little to utilize more pythonic syntax.

from hashlib import sha256
from time import time
import json


class Block:

    def __init__(self, **kwargs):
        """
        Create a new block for the Blockchain

        :param timestamp: Timestamp of the block, defaults to the time the block object is created
        :param data: Data to store in the block, defaults to an empty list
        :param prevHash: Hash of the previous block, defaults to None.  Should always be specefied except for the genesis block.
        """
        self.timestamp = kwargs.get('timestamp', time())
        self.data = kwargs.get('data', [])
        self.prevHash = kwargs.get('prevHash', None)
        self._hash = None
        self.nonce = 0

    @property
    def hash(self):
        """
        Return the (non-python) hash of the block

        :return: The bytes of the hash of this block
        """
        if self._hash is None:
            hashFun = sha256()
            hashFun.update(self.encode(self.prevHash))
            hashFun.update(self.encode(self.timestamp))
            hashFun.update(self.encode(self.data))
            hashFun.update(self.encode(self.nonce))
            self._hash = hashFun.hexdigest()
        return self._hash

    def rehash(self):
        """
        Mark this block to re-calculate the hash the next time it's grabbed.
        """

        self._hash = None

    @staticmethod
    def encode(val):
        """
        Generate a UTF-8 bytes object to represent any object

        :param val: Value to encode
        :return: UTF-8 encoded byte representation of val
        """
        return str(val).encode('utf-8')

    def mine(self, difficulty):
        """
        Mine this block until a valid hash is found, based on leading zeros

        Basically, it loops until our hash starts with
        the string 0...000 with length of <difficulty>.

        :param difficulty:  length of the leading zeros to mine for
        """

        while self.hash[:difficulty] != '0' * difficulty:
            # We increases our nonce so that we can get a whole different hash.
            self.nonce += 1
            # Update our new hash with the new nonce value.
            self.rehash()


class Blockchain:

    def __init__(self):
        """
        initialize the blockchain with an empty, unmined "genesis" block.
        """
        self.chain = [Block()]
        self.blockTime = 30000
        self.difficulty = 1

    def __iter__(self):
        for c in self.chain:
            yield c

    def __getitem__(self, item):
        return self.chain[item]

    def append(self, data, **kwargs):
        """
        Add a new block to the blockchain from a new piece of data and an optional timestamp.

        :param data: Data to add to the new block.
        :param timestamp: UTC timecode of the new block
        """

        # Since we are adding a new block, prevHash will be the hash of the old latest block
        block = Block(data=data, prevHash=self[-1].hash, timestamp=kwargs.get('timestamp', time()))
        block.mine(self.difficulty)

        # Since now prevHash has a value, we must reset the block's hash
        self.chain.append(block)

        if time() - self[-1].timestamp < self.blockTime:
            self.difficulty += 1
        else:
            self.difficulty -= 1

    def isValid(self):
        """
        Iterates over the pairs of sequential blocks to validate that their previous hashes are set correctly

        :return: `True` if Valid, `False` otherwise
        """

        for prevBlock, currentBlock in zip(self[:-1], self[1:]):

            # Check validation
            if prevBlock.hash != currentBlock.prevHash:
                return False

        return True

    def __repr__(self):
        return json.dumps(
            [{k: getattr(item, k) for k in ['data', 'timestamp', 'nonce', 'hash', 'prevHash']}
             for item in self],
            indent=4
        )


if __name__ == '__main__':
    chain = Blockchain()
    chain.append({"from": "John", "to": "Bob", "amount": 100})
    chain.append({"from": "bob", "to": "john", "amount": 50})

    print(chain)
Enter fullscreen mode Exit fullscreen mode
Collapse
imjoseangel profile image
Jose Angel Munoz Author • Edited on

Thanks @theflanman for your contribution. It is always cool to see code like yours to learn.

Do you think adding typing could be a good addition to your code? like:

    def __init__(self, **kwargs) -> None:
        pass
Enter fullscreen mode Exit fullscreen mode

Also I β™₯ to see how you are using @property decorator in the example. I have prepared a small snippet to see the effect when using it:

class Test():
    def __init__(self, **kwargs) -> None:
        pass

    @property
    def hash(self) -> None:
        return sha256().hexdigest()

    def __repr__(self) -> str:
        return self.hash


def main():
    print(Test())
Enter fullscreen mode Exit fullscreen mode

I really like how you are using the iterator in the __repr__ and other dunder methods.

Have Fun!

Collapse
theflanman profile image
Connor Flanigan

The consensus we've reached at work has mostly been that typing should really only be bothered to help users and devs where it's not clear what expected input and output is. Things like __init__ methods or other dunder methods are expected to return a certain type of object so they don't need it.

What I might recommend is typing your inputs a little more. As an example, explicitly use ints to represent timestamps, and use the function time.time_ns() for timestamps, then use structs to generate more accurate representations to hash.

class Block:

    def __init__(self, data: Optional[List[Any]] = None, timestamp: Optional[float]=None, prevHash: Optional[bytes] = None):
        """
        Create a new block for the Blockchain

        :param timestamp: Timestamp of the block, defaults to the time the block object is created
        :param data: Data to store in the block, defaults to an empty list
        :param prevHash: Hash of the previous block, defaults to None.  Should always be specefied except for the genesis block.
        """
        if datais None:
            self.data= []
        else:
            self.data= data

        if timestamp is None:
            self.timestamp = time_ns()
        else:
            self.timestamp = timestamp

        if prevHashis None:
            self.prevHash= b''
        else:
            self.prevHash= prevHash

        self._hash = None
        self.nonce = 0

    @property
    def hash(self) -> bytes:
        """
        Return the (non-python) hash of the block

        :return: The bytes of the hash of this block
        """
        if self._hash is None:
            hashFun = sha256()
            hashFun.update(self.prevHash)
            hashFun.update(struct.pack('@l', self.timestamp))
            hashFun.update(self.encode(self.data))
            hashFun.update(struct.pack('@l', self.nonce))
            self._hash = hashFun.hexdigest()
        return self._hash

Enter fullscreen mode Exit fullscreen mode

Here I've specified that data is a list of anything, timestamp is an int, and prevHash is bytes, and that Block.hash generates bytes. This informs a user/developer that's interfacing with it that this is what the methods are expecting to take or should be expected to return. The only thing I might also add for type hinting in the Block class is specifying the difficulty parameter in Block.mine() is an integer, as well as a check that it's greater than or equal to 1, raising an exception if it isn't.

Collapse
mungaivic profile image
MungaiVic

I had a challenge following the article step by step as I'd assign class variables before even declaring them... It was kinda hard to follow, unless this article wasn't geared towards beginners...
Other than that... Great stuff.

Collapse
imjoseangel profile image
Jose Angel Munoz Author

Hi @mungaivic

If you have any questions, don’t hesitate to contact me. I will very glad to help.

It is interesting to me understanding how to improve the way we could explain different pieces of code in a better way.

Collapse
dhanan_jay profile image
Dhananjay

@imjoseangel can I get your contact details...