Building an iota tangle from scratch in python and flask

in #iota7 years ago

Building an iota tangle from scratch in python and flask

Introduction
Before i start i want to talk about 2 very useful resources that was very influential on this project. first of all a post on hackernoon.com by an author Daniel van Flymen. He implements a blockchain similar to the one implemented by blockchain. This post can be found here https://hackernoon.com/learn-blockchains-by-building-one-117428612f46. I will not go into the definitions of what a blockchain is because this post gives all of the necessary details about what a blockchain is to how it works. He gives very clean code that is very easy to follow. A great post you should check it out.

Second is a video by siraj reval

. He gives a very clear and easy to follow explanation of how the iota tangle works. His explanation is the way in which i have chosen to implement my code. If the implementation, or my understanding of how the tangle works is incorrect please leave a comment and i will update my code and repost it. For this reason i will not be going into depth about what the tangle is too much.

What is a tangle
Well now that the formalities are out of the way what is a tangle. Based on the video above a tangle is basically a directed acyclic graph, (DAG), which uses a proof of work algorithm. However instead of having miners to do the proof of work algorithm the act of making a transaction requires the client to validate 2 other random transactions which were made by some other random client. This DAG is then shared among every client in the network using a consensus algorithm

What will be implemented
This implementation is a basic implementation. It will focus on building the tangle and being able to share it with other clients. It will not be a way to share money reliably nodes will not have unique identifiers like wallet id’s etc. these may come later in follow up posts if people like it.

Who is this guide aimed at? You should be comfy reading and writing some basic Python, as well as have some understanding of how HTTP requests work, since we’ll be talking to our Blockchain over HTTP.

What do I need? Make sure that Python 3.6+ (along with pip) is installed. You’ll also need to install Flask and the Requests library. Also you will need an http client like postman or curl. I will be using postman as I simply like the gui. But you can use anything. Postman can be found here https://www.getpostman.com.

The final code can be found here https://github.com/ljlabs/tangle-pow

Step 1 building the tangle
Open a text editor and create a new files called tangle.py, api.py, and properties.py we will be using 3 files to help make the code easier to understand each part.

The file properties.py will be our basic configuration for the network and thus will have no logic only constants

RequiredProofs = 2 # the number of times a transaction needs to be processed
# befor it is seen as valid
numberOfValidationNodesNeeded = 2 # the number of nodes needed to connect to when
# addeing a new node to the network for the purpose
# of validating them when making a transaction

The skeleton for tangle.py will be

import properties as prop
import json
from time import time
import hashlib
import requests
from urllib.parse import urlparse

class Tangle(object):
def init(self):
pass # initialize the network

@staticmethod
def valid_proof(self):
   pass # used to make sure that the pow was correct

def proof_of_work(self):
   pass # calculate the proof of work

@staticmethod
def hash():
   pass # compute the hash
  
def createNode(self):
    pass # create a new node


def send_transaction(self, data):
   pass # send a transaction


###########################################################################
# this is the consensus algorithm
###########################################################################

def valid_tangle(self, tangle):
    pass # we will use this to ensure that the tangles we get from peers are correct

def register_peer(self, address):
    pass # we will use this a add new peers to our p2p network

And finally the skeleton for the api.py

import hashlib
import json
from textwrap import dedent
from time import time
from uuid import uuid4
from tangle import Tangle
from urllib.parse import urlparse

import requests
from flask import Flask, jsonify, request

Instantiate our Node

app = Flask(name)

Instantiate the Blockchain

tangle = Tangle()

@app.route('/transactions/new', methods=['POST'])
def new_transaction():
pass # make a new transaction on the network

@app.route('/tangle', methods=['GET'])
def full_chain():
pass # returns the current tangle

Consensus

@app.route('/peers/register', methods=['POST'])
def register_nodes():
pass # add new peers to the network

@app.route('/peers/resolve', methods=['GET'])
def consensus():
pass # check for other maybe newer tangles on the network

@app.route('/peers', methods=['GET'])
def list_peers():
pass # return a list of peers

if name == 'main':
app.run(host='0.0.0.0', port=5001)

What do the nodes look like
Each of the nodes in our tangle will look like this

Node = {
'index': "int value",
'timestamp': time(),
'data': "this is the transaction data that is being stored",
'proof': "proof of work",
'previous_hashs': "the hash of the previous 2 nodes that it is connected to",
'previous_nodes': 'index values',
'next_nodes': 'indexes of the nodes pointing to it',
'validity': the number of times the node has been proven
}

At this point the way the tangle works should be easy to see. Each node has its data and identifying characteristics as well as some information about how it fits into the tangle and how many times it has been validated by the network

Creating transactions
This is by far the most work that will have to be implemented as making a transaction also validates other transactions making it the most important step in the creation of our tangle

class Tangle(object):
def init(self):
self.nodes = []
self.peers = set()
for i in range(prop.numberOfValidationNodesNeeded):
# Create the genesis block
self.nodes.append(self.createNode(None, [], len(self.nodes), validity = prop.RequiredProofs))

  @staticmethod
  def valid_proof(last_proof, last_hash, proof):
      # ensures that the node has the correct number of zeros

      guess = (str(last_proof) + str(last_hash) + str(proof)).encode()
      guess_hash = hashlib.sha256(guess).hexdigest()
      return guess_hash[:4] == "0000"

  def proof_of_work(self, last_proof, last_hash):
      # computes the proof of work
      proof = 0
      while self.valid_proof(last_proof, last_hash, proof) is False:
          proof += 1
      return proof


  @staticmethod
  def hash(node):
      # make a hash of the block
      # We must make sure that the Dictionary is Ordered, or we'll have inconsistent hashes
      node_string = json.dumps(node, sort_keys=True).encode()
      return hashlib.sha256(node_string).hexdigest()

  def validate_node(self, node):
      if self.nodes[node['index']]['validity'] < prop.RequiredProofs:
          last_proof = self.nodes[node['index']]['proof'] # this nodes proof
          last_hash = ""
          for prevHash in self.nodes[node['index']]['previous_hashs']:
              last_hash += prevHash # the hashes of the nodes this node connects
          self.nodes[node['index']]['proof'] = self.proof_of_work(last_proof, last_hash)
          self.nodes[node['index']]['validity'] += 1

  def createNode(self, data, prevNodes, newIndex, validity=0):# these prevNodes are the indexes in the dag that points to the previous nodes
      prevHashes = []
      '''
      may need to update every node that points to this when sending transaction
      '''
      for i in prevNodes:
          prevHashes.append(self.hash(self.nodes[i]))
          # now we tell the nodes that we are pointing to that we are poiinting to them
          self.nodes[i]['next_nodes'].append(newIndex)

      Node = {
          'index': newIndex,
          'timestamp': time(),
          'data': data,
          'proof': 0,
          'previous_hashs': prevHashes,          # hashes of the nodes we are connecting to
          'previous_nodes': prevNodes,                # indexes of the nodes we are connecting to
          'next_nodes': [],                           # indexes of future nodes that will connect to us
          'validity': validity,
      }
      return Node


  def send_transaction(self, data):
      # find 2 nodes in the network that are un proven
      nodesToattach = []
      nodesIndexes = []
      newIndex = len(self.nodes)
      worstCaseScinario = []
      worstCaseScinarioindexes = []
      '''
      this function should be changed to search randomly
      '''
      for i in range(len(self.nodes)-1, -1, -1):
          node=self.nodes[i]
          if node['validity'] < prop.RequiredProofs:
              nodesToattach.append(node)
              nodesIndexes.append(node['index'])
          else:
              if worstCaseScinario == [] or len(worstCaseScinario) < prop.numberOfValidationNodesNeeded:
                  worstCaseScinario.append(node)
                  worstCaseScinarioindexes.append(node['index'])
          if len(nodesToattach) == prop.numberOfValidationNodesNeeded:
              break
      # if there are not enough un varified transactions then use verified transactions
      while len(nodesToattach) < prop.numberOfValidationNodesNeeded:
          nodesToattach.append(worstCaseScinario.pop())
          nodesIndexes.append(worstCaseScinarioindexes.pop())
      # process nodes to attatch
      for node in nodesToattach:
          self.validate_node(node)
      # now that those nodes are proven
      # we can now attatch our node to the dag
      self.nodes.append(self.createNode(data, nodesIndexes, newIndex))
      return newIndex

To break down the steps
First we have to find 2 nodes in the network which are unverified. If not enough unverified transactions are found then verified transactions will have to be used. A transaction is considered valid if its validity level is >= the property RequiredProofs set in the properties.py file earlier This should be done at random. If a transaction that has been previously verified is taken then we will not validate it again.

Then we need to verify those 2 chosen nodes:
We compute its proof of work using an algorithm very similar to hashcash described here https://en.wikipedia.org/wiki/Hashcash. We increment its validity of each of the nodes

We then create the new transaction node:
We set its previous hashes to a list of the hashes of the nodes that we just verified
We set its previous nodes to the index of the nodes that we had previously created.
We timestamp the new node.
We set its index to the index at the end of the list which holds all of the lists
Its next nodes are set to [] as there are no nodes pointing to this node yet
And the validity is set to zero as no node has validated it yet

And finally we tell the 2 nodes that we have just verified that the new node that we have created is pointing to it.

Building the api
To have other peers in our p2p network know about us, and to be able to make transaction we will need to have a network facing interface this is where flask comes in.

@app.route('/transactions/new', methods=['POST'])
def new_transaction():
# update tangle
tangle.resolve_conflicts()
# begin transaction
values = request.get_json()

# Check that the required fields are in the POST'ed data
required = ['sender', 'recipient', 'amount']
if not all(k in values for k in required):
    return 'Missing values', 400

# Create a new Transaction
index = tangle.send_transaction(values)

response = {'message': 'Transaction will be added to Block ' + str(index)}
# tell peers to update tangle
for peer in tangle.peers:
    requests.get("http://"+str(peer) + "/peers/resolve")
return jsonify(response), 201

@app.route('/tangle', methods=['GET'])
def full_chain():
response = {
'tangle': tangle.nodes,
'length': len(tangle.nodes),
}
return jsonify(response), 200

We create 2 end point
/transactions/new
Allowing us to create new transactions
/tangle
Allowing us to view our current tangle graph

Consensus
The final portion of the project
This is the more interesting portion. Till this point we have a tangle that accepts new transactions as well as allows us to view our transaction history now comes the part to make that network decentralised.

We begin by updating our api.py file to is final form

import hashlib
import json
from textwrap import dedent
from time import time
from uuid import uuid4
from tangle import Tangle
from urllib.parse import urlparse

import requests
from flask import Flask, jsonify, request

Instantiate our Node

app = Flask(name)

Generate a globally unique address for this node

node_identifier = str(uuid4()).replace('-', '')

Instantiate the Blockchain

tangle = Tangle()

@app.route('/transactions/new', methods=['POST'])
def new_transaction():
# update tangle
tangle.resolve_conflicts()
# begin transaction
values = request.get_json()

# Check that the required fields are in the POST'ed data
required = ['sender', 'recipient', 'amount']
if not all(k in values for k in required):
    return 'Missing values', 400

# Create a new Transaction
index = tangle.send_transaction(values)

response = {'message': 'Transaction will be added to Block ' + str(index)}
# tell peers to update tangle
for peer in tangle.peers:
    requests.get("http://"+str(peer) + "/peers/resolve")
return jsonify(response), 201

@app.route('/tangle', methods=['GET'])
def full_chain():
response = {
'tangle': tangle.nodes,
'length': len(tangle.nodes),
}
return jsonify(response), 200

Consensus

@app.route('/peers/register', methods=['POST'])
def register_nodes():
values = request.get_json()
print(values)
print("asdugdag")
peers = None
if type("") == type(values):
print(json.loads(values))
peers = json.loads(values)['peers']
else:
peers = values.get('peers')
if peers is None:
return "Error: Please supply a valid list of nodes", 400

for peer in peers:
    tangle.register_peer(peer)


response = {
    'message': 'New peers have been added',
    'total_nodes': list(tangle.peers),
}

return jsonify(response), 201

@app.route('/peers/resolve', methods=['GET'])
def consensus():
replaced = tangle.resolve_conflicts()

if replaced:
    response = {
        'message': 'Our chain was replaced',
        'new_chain': tangle.nodes
    }
else:
    response = {
        'message': 'Our chain is authoritative',
        'chain': tangle.nodes
    }

return jsonify(response), 200

@app.route('/peers', methods=['GET'])
def list_peers():
response = {
'known_peers': list(tangle.peers),
}

return jsonify(response), 201

if name == 'main':
app.run(host='0.0.0.0', port=5001)

As well as our tangle.py


import properties as prop
import json
from time import time
import hashlib
import requests
from urllib.parse import urlparse

"""
nodes in the dag are represented in json as folows
they can be transmitted across the network in this form and then can be
initialized in this form
Node = {
'index': "int value",
'timestamp': time(),
'data': "this is the transaction data that is being stored",
'proof': "proof of work",
'previous_hashs': "the hash of the previous 2 nodes that it is connected to",
'previous_nodes': 'index values',
'next_nodes': 'indexes of the nodes pointing to it',
'validity': the number of times the node has been proven
}

"""

class Tangle(object):
def init(self):
self.nodes = []
self.peers = set()
for i in range(prop.numberOfValidationNodesNeeded):
# Create the genesis block
self.nodes.append(self.createNode(None, [], len(self.nodes), validity = prop.RequiredProofs))

@staticmethod
def valid_proof(last_proof, last_hash, proof):
    # ensures that the node has the correct number of zeros

    guess = (str(last_proof) + str(last_hash) + str(proof)).encode()
    guess_hash = hashlib.sha256(guess).hexdigest()
    return guess_hash[:4] == "0000"

def proof_of_work(self, last_proof, last_hash):
    # computes the proof of work
    proof = 0
    while self.valid_proof(last_proof, last_hash, proof) is False:
        proof += 1
    return proof


@staticmethod
def hash(node):
    # make a hash of the block
    # We must make sure that the Dictionary is Ordered, or we'll have inconsistent hashes
    node_string = json.dumps(node, sort_keys=True).encode()
    return hashlib.sha256(node_string).hexdigest()

def validate_node(self, node):
    if self.nodes[node['index']]['validity'] < prop.RequiredProofs:
        last_proof = self.nodes[node['index']]['proof'] # this nodes proof
        last_hash = ""
        for prevHash in self.nodes[node['index']]['previous_hashs']:
            last_hash += prevHash # the hashes of the nodes this node connects
        self.nodes[node['index']]['proof'] = self.proof_of_work(last_proof, last_hash)
        self.nodes[node['index']]['validity'] += 1

def createNode(self, data, prevNodes, newIndex, validity=0):# these prevNodes are the indexes in the dag that points to the previous nodes
    prevHashes = []
    '''
    may need to update every node that points to this when sending transaction
    '''
    for i in prevNodes:
        prevHashes.append(self.hash(self.nodes[i]))
        # now we tell the nodes that we are pointing to that we are poiinting to them
        self.nodes[i]['next_nodes'].append(newIndex)

    Node = {
        'index': newIndex,
        'timestamp': time(),
        'data': data,
        'proof': 0,
        'previous_hashs': prevHashes,          # hashes of the nodes we are connecting to
        'previous_nodes': prevNodes,                # indexes of the nodes we are connecting to
        'next_nodes': [],                           # indexes of future nodes that will connect to us
        'validity': validity,
    }
    return Node


def send_transaction(self, data):
    # find 2 nodes in the network that are un proven
    nodesToattach = []
    nodesIndexes = []
    newIndex = len(self.nodes)
    worstCaseScinario = []
    worstCaseScinarioindexes = []
    '''
    this function should be changed to search randomly
    '''
    for i in range(len(self.nodes)-1, -1, -1):
        node=self.nodes[i]
        if node['validity'] < prop.RequiredProofs:
            nodesToattach.append(node)
            nodesIndexes.append(node['index'])
        else:
            if worstCaseScinario == [] or len(worstCaseScinario) < prop.numberOfValidationNodesNeeded:
                worstCaseScinario.append(node)
                worstCaseScinarioindexes.append(node['index'])
        if len(nodesToattach) == prop.numberOfValidationNodesNeeded:
            break
    # if there are not enough un varified transactions then use verified transactions
    while len(nodesToattach) < prop.numberOfValidationNodesNeeded:
        nodesToattach.append(worstCaseScinario.pop())
        nodesIndexes.append(worstCaseScinarioindexes.pop())
    # process nodes to attatch
    for node in nodesToattach:
        self.validate_node(node)
    # now that those nodes are proven
    # we can now attatch our node to the dag
    self.nodes.append(self.createNode(data, nodesIndexes, newIndex))
    return newIndex


###########################################################################
# this is the consensus algorithm
###########################################################################

def valid_tangle(self, tangle):
    for node in tangle:
        if node['index'] >= prop.numberOfValidationNodesNeeded: # dont test genesis nodes
            validitiyOfNode = node['validity']
            # make sure that the same number of nodes saying that they have
            # validated him as his validity level
            nextNodes = node['next_nodes']
            print(nextNodes)
            if validitiyOfNode < len(nextNodes):
                return False
            # make sure these nodes are pointing to him
            for Nnode in nextNodes:
                print(tangle[Nnode])
                if node['index'] not in tangle[Nnode]['previous_nodes']:
                    return False
    return True

def register_peer(self, address):
    parsed_url = urlparse(address)
    self.peers.add(parsed_url.netloc)

def resolve_conflicts(self):
    neighbours = self.peers
    new_tangle = None

    # We're only looking for chains longer than ours
    max_length = len(self.nodes)

    # Grab and verify the chains from all the peers in our network
    for peer in neighbours:
        response = requests.get("http://"+str(peer) + "/tangle")

        if response.status_code == 200:
            length = response.json()['length']
            tangle = response.json()['tangle']

            # Check if the length is longer and the chain is valid
            if length > max_length and self.valid_tangle(tangle):
                max_length = length
                new_tangle = tangle

    # Replace our chain if we discovered a new, valid chain longer than ours
    if new_tangle:
        self.nodes = new_tangle
        return True

    return False

This now allows us to make calls to our network register new peers to that network and check if our peers have newer versions of our tangle it does this by checking if their tangle is longer than ours

Finishing up

At this point we can spin up the api by calling python api.py on 2 computers or on 2 different port numbers which is what i had done one on port 5000 and the other on port 5001

Firstly we should make a call to our api at http://127.0.0.1:5000/peers/register
{
"peers": ["http://X.X.X.X:5000"]
}
Where X.X.X.X is the url of the second instance of the application on your lan that you had spin up

Using an http client make a post request to http://127.0.0.1:5000/transactions/new
With the json object

{
"sender": "this is such fucking bull shit",
"recipient": "anne",
"amount": 5
}

And we should see a response

{
"message": "Transaction will be added to Block 2"
}

Now we can either make a call to http://127.0.0.1:5000/tangle or http://X.X.X.X:5000/tangle and both will give us the same tangle

And this concludes the tangle. If this was helpful please tell me in the comments. This is my first post so please go easy on me. It is probably not up to the scratch of other posts i am a software developer and writing is new to me. Please feel free to take my code on github and edit it in any project. Please tell me if this was helpful and if i should keep making new posts about building blockchain technology