In the first of this two-post blog series, also available on Medium, we started to approach the notion of a “Blockchain” by first principles. We had a look at Blockchain-related terms and definitions and implemented a first basic, single-node Blockchain and web app.

In this second and final post, we will extend this basic setup by:

  • adding transaction handling capabilities introducing our very own cryptocurrency, the “BeCoin”, in the process
  • implementing decentralization in the form of a multi-node setup, including largest-chain considerations to obtain node consensus

By working through this post, you will end up with a fully functional, albeit, still simple and, of course, not production-ready, decentralized Blockchain network and related web app. Just like the first part, this post’s main purpose is educational in nature and inspired by work at SuperDataScience. Let’s get started.


Transaction handling

There are a number of extensions we need to make to the single-node Blockchain setup developed in the first post to allow for the processing of transaction data.

The code snippet below shows where we left things at the end of part one. This codebase supports the creation of a chain of blocks, their mining using a proof-of-work protocol and the validation of the resulting Blockchain object. What’s missing from this, however, are actual transactions, i.e., the blocks mined do not hold any transaction data and, as a result, the generated Blockchain does not really serve any meaningful purpose.

import datetime
import hashlib
import json
from flask import Flask, jsonify


class Blockchain:
    
    def __init__(self):
        self.chain = []
        self.create_block(nonce = 1, previous_hash = '0')
        
    def create_block(self, nonce, previous_hash):
        block = {'index' : len(self.chain) + 1,
                 'timestamp' : str(datetime.datetime.now()),
                 'nonce' : nonce,
                 'previous_hash' : previous_hash}
        self.chain.append(block)
        return block

    def get_previous_block(self):
        return self.chain[-1]
    
    def proof_of_work(self, previous_nonce):
        new_nonce = 1
        check_nonce = False
        while check_nonce is False:
            hash_operation = hashlib.sha256(str(new_nonce**2 - previous_nonce**2).encode()).hexdigest()
            if hash_operation[:4] == '0000':
                check_nonce = True
            else:
                new_nonce += 1
        return new_nonce
    
    def hash(self, block):
        encoded_block = json.dumps(block, sort_keys = True).encode()
        return hashlib.sha256(encoded_block).hexdigest()
    
    def is_chain_valid(self, chain):
        previous_block = chain[0]
        block_index = 1
        while block_index < len(chain):
            block = chain[block_index]
            if block['previous_hash'] != hashlib.sha256(json.dumps(previous_block, sort_keys = True).encode()).hexdigest():
                return False
            previous_nonce = previous_block['nonce']
            nonce = block['nonce']
            hash_operation = hashlib.sha256(str(nonce**2 - previous_nonce**2).encode()).hexdigest()
            if hash_operation[:4] != '0000':
                return False
            previous_block = block
            block_index += 1
        return True

To change this, we, firstly, need to extend the list of imports by both the requests Python library and the Flask request module as well as the uuid and urllib.parse Python libraries to make targetted URL-based communication across a multi-node Blockchain setup possible. That is:

  • import requests
  • from flask import Flask, jsonify, request
  • from uuid import uuid4
  • from urllib.parse import urlparse

Secondly, the transaction data needs to be added to the Blockchain class in the form of a suitable data structure. Let’s add a transaction list, self.transactions = [], to the class constructor, __init__(self), for this purpose.

 def __init__(self):
        self.chain = []
        self.transactions = []
        self.create_block(proof = 1, previous_hash = '0')

In addition, the new method add_transaction(self, sender, receiver, amount) appends a specific transaction between a sender and a receiver featuring a certain amount of cryptocurrency in the form of a dictionary to the list of transactions of the block under consideration. Note that this is where our very own cryptocurrency, called the “BeCoin”, comes into play.

The method simply returns the index of the next block, i.e., the block the new transaction will be added to.

 def add_transaction(self, sender, receiver, amount):
        self.transactions.append({'sender' : sender,
                                 'receiver' : receiver,
                                 'amount' : amount})
        previous_block = self.get_previous_block()
        return previous_block['index'] + 1

And that’s all what needs to be done for our initial Blockchain setup to start handling transactions and thus to start serving the purpose the Blockchain concept was employed for following the 2007–2008 financial crisis. Namely, to function as a peer-to-peer electronic cash system allowing for sending electronic cash, i.e. cryptocurrency, from one party to another without the need for a financial institution as a central intermediary [1]. To see this more clearly, however, we still need to extend our single-node to a decentralized multi-node Blockchain setup next.

⚡Note that, as already mentioned in part 1, the notion of Blockchain was developed prior to its peer-to-peer electronic cash application in [1] and that it is not limited to financial applications in any way. [2]⚡


Decentralization and longest-chain consensus

Similarly to the transaction capability added above, our Blockchain class needs to be extended by a suitable data structure representing the multi-node setup first. We use a set data structure for this purpose, i.e., self.node = set(), is added to the class constructor.

def __init__(self):
        self.chain = []
        self.transactions = []
        self.create_block(proof = 1, previous_hash = '0')
        self.node = set()

The new add_node(self, address) method then adds the IP address and port of a new node to the set of nodes of the Blockchain under consideration. Remember that the single-node setup developed in part 1 featured the node at local IP address 0.0.0.0 and port 5000. This single-node setup can simply be extended by additional nodes represented by different ports at the same IP address. To make this happen, the node’s IP address and port need to be extracted from the node’s URL such as ‘http://0.0.0.1:5001’ with the help of the parsed_url method of the urllib.parse library, with netloc subsequently holding the extracted IP address and port in the form of, for example, ‘0.0.0.0:5001’.

def add_node(self, address):
        parsed_url = urlparse(address)
        self.node.add(parsed_url.netloc)

Now that we can extend our single-node Blockchain setup to a multi-node peer-to-peer Blockchain network, the question arises as to how obtain consistency, or “consensus”, amongst this set of nodes, with nodes likely to hold different versions of the Blockchain at any particular point in time due to, for example, network latency.

This is where the “rule of the longest chain” comes in, i.e., the chain held by a Blockchain node, which is longer than the versions of the chain held by the other nodes is going to be selected as the valid chain and thus going to replace the chains at the other nodes.

Let’s implement this rule in the form of replace_chain(self) method taking self, i.e. the specific Blockchain object under consideration, as argument only. The two local variables, longest_chain and max_length, will eventually hold the chain representing the longest chain and its length, respectively, with max_length initialised to the length of the chain at self.

All that’s left to be done now is to use the get_chain() method developed in part 1 to determine the length of the chains held at all the other nodes in the network. This is where the requests library comes into play. Using its get() method, we can make an HTTP get request to the other nodes in the network calling their local get_chain() method and receiving the node’s chain and its length in response.

Provided that both the value of length is larger than the current value of max_length and the Blockchain copy, chain, proves valid, max_length is to be updated with the value of length and chain becomes the new longest_chain. Crucially, it also becomes the new chain at self, i.e. self.chain = longest_chain, and the method simply returns the boolean value true as a result.

def replace_chain(self):
        network = self.node
        longest_chain = None
        max_length = len(self.chain)
        for node in network:
            response = requests.get(f'http://{node}/get_chain')
            if response.status_code == 200:
                length = response.json()['length']
                chain = response.json()['chain']
                if length > max_length and self.is_chain_valid(chain):
                    max_length = length
                    longest_chain = chain
        if longest_chain:
            self.chain = longest_chain
            return True
        return False

We’ve added Blockchain methods for transaction processing, including cryptocurrency, and consensus. So, what’s left to be done? Well, we have to bring all of these things together by enhancing our web app, so as to actually be able to use this new functionality.

That is, first of all, the block mining web app method needs to be extended by the ability to add the new transaction data to a newly mined block and to reward the miner with some of our new BeCoins in the process.

Secondly, we need to both add the capability of adding a new node to an existing Blockchain network and subsequently enforce consensus within the resulting decentralised Blockchain setup via HTTP requests.

We tackle these aspects in the next and final section.


Mining blocks with transaction data and enforcing consensus with HTTP requests

Before having a look at the new or modified HTTP request functions required for the Blockchain Web app we started to develop in the first blog post, an unique identifier for any particular Blockchain node is required. This identifier can then be used for, for example, identifiying and rewarding a miner for creating a new block at this specific node.

To obtain a suitable identifier, the uuid4() method can be used to generate a 128-bit random UUID (Universally Unique Identifier), also known as GUID (Globally Unique Identifier), in the form of a string separated by hyphens. The replace() method is used in this context to remove the hyphens, which then results in the identifier of the new node.

With this issue out of the way, let’s have a look at the mine_block() web app method developed in the first post next. It does not yet handle any transaction data. To change this, the mine_block() method’s response dictionary needs to be extended by a, for example, ‘transactions’ key referencing a new block’s transaction data.

Note that these transaction data will always include a transaction added within the method itself, with the sender rewarding the miner, receiver, here represented by the public key placeholder ‘Receiver’ with, in this case, 10 BeCoins for mining the new block.

app = Flask(__name__)
app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False

# Creating a random address for the node
node_address = str(uuid4()).replace('-', '')

# Creating the Blockchain        
blockchain = Blockchain()

# Mining a new block
@app.route('/mine_block', methods = ['GET'])
def mine_block():
    previous_block = blockchain.get_previous_block()
    previous_nonce = previous_block['nonce']
    nonce = blockchain.proof_of_work(previous_nonce)
    previous_hash = blockchain.hash(previous_block)
    blockchain.add_transaction(sender = node_address, receiver = 'Receiver', amount = 10)
    block = blockchain.create_block(nonce, previous_hash)
    response = {'message' : 'You just mined a block.',
                'index' : block['index'],
                'timestamp' : block['timestamp'],
                'nonce' : block['nonce'],
                'previous_hash' : block['previous_hash'],
                'transactions' : block['transactions']}
    return jsonify(response), 200

But how do we get transactions added to the block about to be mined in the first place? Since any Blockchain interactions happen on HTTP request basis, this can be achieved in the form of an HTTP post request featuring the sender, receiver and amount values to be added. This is exactly the functionality provided by the add_transaction() request listed below.

@app.route('/add_transaction', methods = ['POST'])
def add_transaction():
    json = request.get_json()
    transaction_keys = ['sender', 'receiver', 'amount']
    if not all (key in json for key in transaction_keys):
        return 'Some elements of the transaction are missing', 400
    index = blockchain.add_transaction(json['sender'], json['receiver'], json['amount'])
    response = {'message' : f'This transaction will be added to block {index}'}
    return jsonify(response), 201

The method expects sender, receiver and amount values. It then adds these transaction data to the block to be mined next as identified by the next block’s index.

When posting this HTTP request, the request caller will receive either a 400 error message, if the three required pieces of information are not provided, or a 201 post success message, if the transaction data have been added successfully.

As a final step, the connect_node() and the replace_chain() web app methods allow for the HTTP request-driven decentralisation of the Blockchain in the form of a peer-to-peer network of connected nodes holding Blockchain copies, which are kept in consensus using the Blockchain replace_chain() method developed above.

@app.route('/connect_node', methods = ['POST'])
def connect_node():
    json = request.get_json()
    nodes = json.get('nodes')
    if nodes is None:
        return "No node", 400
    for node in nodes:
        blockchain.add_node(node)
    response = {'message' : 'All the nodes are now connected. The BeCoin Blockchain now contains the following nodes',
                'total_nodes' : list(blockchain.nodes)}
    return jsonify(response), 201

It is assumed within the connect_node() request implementation that new nodes are provided in the previously established format within the JSON file posted as part of the HTTP request, i.e., for example, “http://0.0.0.0:5001”, “http://0.0.0.0:5002”, etc. The method call json.get(‘nodes’) will then retrieve exactly this list of new nodes from the HTTP post request and the nodes are subsequently added to the decentralised Blockchain network with the help of the add_node() Blockchain method developed above.

When posting the HTTP request, the caller will obtain either a 400 error message, in case the required node information is not provided, or a 201 post success message listing the new size of the network, in case it has successfully been extended.

To enforce consensus within our decentralised Blockchain network, the replace_chain() web app method calls the replace_chain() method of the Blockchain object. For information purposes, it returns the unchanged or new longest chain, i.e. blockchain.chain in either case, to the caller.

@app.route('/replace_chain', methods = ['GET'])    
def replace_chain():
    is_chain_replaced = blockchain.replace_chain()
    if is_chain_replaced:
        response = {'message' : 'The nodes had different chains, so the chain was replaced with the longest one.',
                    'new_chain' : blockchain.chain}
    else:
        response = {'message' : 'The chain is the largest one.',
                    'actual_chain' : blockchain.chain}
    return jsonify(response), 200

With consensus in place, we are done in terms of actual implementation work for the educational purposes of this two-part blog series. So, time for putting this setup to the test.


Blockchain setup in action

To simulate a distributed, decentralised, multi-node Blockchain setup, you can execute the Blockchain codebase within separate consoles thereby “simulating” a setup of separate, distributed servers. For example, a three-node setup simply requires the execution of the codebase within three separate consoles. You only need to make sure in this context that the codebase makes use of different ports representing the different nodes.

More specifically, to launch a sample network of three nodes, let’s execute the codebase within separate consoles making sure that the very last line of code has been changed in line with the respective port number, i.e., in this example, app.run(host = ‘0.0.0.0’, port = 5001), app.run(host = ‘0.0.0.0’, port = 5002) and app.run(host = ‘0.0.0.0’, port = 5003), respectively. If the codebase has been successfully executed for the three nodes, an API client such as Postman can then be used to interact with the Blockchain objects at the three nodes, as already demonstrated in the first of these two blog posts.

To start off the Blockchain interactions, for example, launch an HTTP get request using the get_chain endpoint for each of the three nodes to have a look at the respective genesis blocks. The result should look similar to the contents of the Postman screenshots shown below.

Node1 of the initial three-node decentralised Blockchain and its genesis block

Node2 of the initial three-node decentralised Blockchain and its genesis block

Third and final node of the initial three-node decentralised Blockchain and its genesis block

Note that, immediately following their initialisations, the Blockchain objects at the different nodes are neither connected into a network nor in consensus. They effectively represent three unrelated, stand-alone Blockchain nodes.

In order to change this, let’s launch the HTTP post connect_node request passing in the relevant nodes in the expected JSON format. Posting this request, for example, from node 5001 implies that nodes 5002 and 5003 are to be connected to node 5001, and nodes 5002 and 5003 only. This happens by simply adding the nodes 5002 and 5003 to the local set of nodes stored at the Blockchain object representing node 5001. Posting the request should produce a result similar to the scenario shown below.

Connecting nodes 5002 and 5003 to node 5001

This step is to be repeated from and for nodes 5002 and 5003 with reference to connection candidate pairs 5001, 5003 and 5001, 5002, respectively, in the JSON request body. As a result, we obtain a network of three peer-to-peer-connected, distributed Blockchain nodes.

Next, to give the consensus method a test run, let’s request, for example, the mining of a new block at node 5001 by launching the HTTP get request mine_block (with node “5001” rewarding itself with 10 BeCoins for doing so in the process).

Following the successful block mining at node 5001, the chains stored at the different nodes are necessarily of different length and thus out-of-synch. So, it’s time to use the replace_chain request to enforce consensus amongst the three nodes, as illustrated below.

Blockchain consensus reinforced using the longest-chain rule at node 5002

Blockchain consensus reinforced using the longest-chain rule at node 5003

The nodes 5002 and 5003 now carry identical copies of the longest chain, namely the chain which was previously extended by a newly mined block at node 5001.

To round things off, let’s simulate the sending of a certain amount of BeCoins from a sender to a receiver address using the new transaction handling capability. As you may recall, this requires adding the transaction data to the next block to be added to the chain first, which is then newly mined. For example, as shown below, let’s send 1000 BeCoins from the (here: simulated) public key “PublicKeya” of a sender to the (simulated) public key “PublicKeyb” of the receiver at node 5001.

Sending BeCoins by adding the transaction to the next block to be mined (block 7) ...

... and then mining it

As before, the Blockchain objects stored at the three nodes are now out-of-sync again and the longest-chain rule would need to be reapplied to reinforce Blockchain consensus by calling the replace_chain get request at node 5002 and node 5003, respectively.

This completes the test runs of the various functionality added during this second and final part of this blog post series.

So, that’s a wrap! We’ve made our way from a single-node Blockchain setup holding no transaction data to a multi-node, peer-to-peer-distributed, decentralised Blockchain network capable of handling transaction data and featuring its own BeCoin cryptocurrency, alongside a Flask web app for Blockchain interaction purposes. That is, you now have got various bits and pieces available for setting up, running and interacting with a simple Blockchain scenario helping you to learn and experiment with fundamental Blockchain functionality from the ground up.


References

[1] Satoshi Nakamoto, “Bitcoin: A Peer-to-Peer Electronic Cash System”, Oct. 2008

[2] Stuart Haber and W. Scott Stornetta, “How to Time-Stamp a Digital Document”, in: A. J. Menezes and S. A. Vanstone (eds.), “Advances in Cryptology — CRYPTO ‘90”, LNCS 537, pp. 437–455, Springer-Verlag, Berlin, 1991