{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Build your own Blockchain! - IN5420 Programming Exercise"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Acknowledgments\n",
    "\n",
    "Thanks to Omkar Shanbhag and Simon Guo from the Berkley Blockchain Lab for the initial version of this notebook.\n",
    "\n",
    "## Getting Started\n",
    "\n",
    "This notebook is an introduction to Blockchain implementations, aimed at translating into code the concepts we learned during the course.\n",
    "\n",
    "In this notebook we will implement several aspects of Blockchain technology, including: \n",
    "\n",
    "- The Blockchain data structure using OOP \n",
    "- The concept of difficulty of finding the next block hash \n",
    "- A simple PoW simulation \n",
    "- A simulation of multiple miners with varying computational powers \n",
    "- A data analytics task to evaluate PoW hardness"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# import statements\n",
    "import hashlib as hasher \n",
    "import random as rand\n",
    "import time as tt\n",
    "import datetime as date\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Part 1 - Blockchain Data Structure Design"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this section we will define the class *Block* and implement an init function that creates a new block given some parameters, as well as a function *hash_block*, that computes the hash of this block based on its class variables. \n",
    "\n",
    "The init function is implemented for you below and creates a block with the following specifications \n",
    "- height: the height of the block on the chain (the genesis block has height=0)\n",
    "- timestamp: time that the block is added on to the chain\n",
    "- data: the data the block contains (it usually includes the merket root, but we can fill it with whatever for this)\n",
    "- previous_hash: the hash value of the previous block \n",
    "- hash: hash of this block computed using the *hash_block* function\n",
    "- nonce: the variable value that changes, altering the hash output (default value=0, irrelevant in this section)\n",
    "\n",
    "**( ! ) Task 1: implement hash_block( )**\n",
    "\n",
    "We need to implement the hash_block( ) which computes the hash value of this block. This function has no input parameters. \n",
    "\n",
    "Desired Functionality:\n",
    "- Concatenate string representations of all the class variables \n",
    "- Computes the SHA256 hash of this concatenation \n",
    "\n",
    "**HINTS**\n",
    "\n",
    "- Use the first import statement\n",
    "- Initialize a new hasher\n",
    "- Look into the following: \n",
    "    - utf-8 encoding \n",
    "    - .update() function \n",
    "    - .hexdigest() function\n",
    "    - In order for the test function below to work properly, create the string concatenation in the following order with no spaces or any other characters in between \n",
    "       - height\n",
    "       - timestamp\n",
    "       - previous block hash\n",
    "       - data\n",
    "       - nonce\n",
    "       \n",
    "If you want to use a different variable order that incorporates everything that's totally fine, but test function wont work!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Block:\n",
    "    def __init__(self, height, timestamp, previous_hash, data, nonce=0):\n",
    "        self.height = height\n",
    "        self.timestamp = timestamp\n",
    "        self.previous_hash = previous_hash\n",
    "        self.data = data\n",
    "        self.nonce = nonce # set to zero as default. Not used in the first part\n",
    "        \n",
    "        self.hash = self.hash_block()\n",
    "        \n",
    "    def __str__(self):\n",
    "        return f\"[Block {self.height}. Timestamp: {self.timestamp}. Data: {self.data}. Nonce: {self.nonce}]\"\n",
    "    \n",
    "    def update_nonce(self, new_nonce):\n",
    "        self.nonce = new_nonce\n",
    "        self.hash = self.hash_block()\n",
    "\n",
    "    def hash_block(self):\n",
    "        # Your code here"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**( ! ) Test your implementation of Task 1 here**\n",
    "\n",
    "Run the block of code below"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def test_task_1(height, time, previous_hash, data):\n",
    "    new_block = Block(height, time, previous_hash, data)\n",
    "    check_string = '771bd3218b062af12b12105602248140434874acf5dc592af8945788b298380c'\n",
    "    print_statement = \"PASSED!!! Move on to next Question\" if str(new_block.hash) == check_string else \"FAILED!!! Try Again\"\n",
    "    print(print_statement)\n",
    "    \n",
    "height = 0\n",
    "time = '2022-01-24 15:05:06.612876'\n",
    "previous_hash = '6ffd1464f68ef4aeb385d399244efa19293ba5c842c464a82c02f8256ef71428'\n",
    "data = 'IN5420'\n",
    "\n",
    "test_task_1(height, time, previous_hash, data)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Functions for Building the Chain**\n",
    "\n",
    "Now that the Block class is completed, let's figure out how to make a chain made of several blocks. First, we need to have a function that creates a genesis block, which serves as the first block of our chain, and then implement the function *next_block( )* which builds a new block on top of a given block.\n",
    "\n",
    "*create_genesis_block( )* has been implemented for you. It creates a block with the following specifications. \n",
    "- height = 0\n",
    "- timestamp = Now (whenever the function is being called) \n",
    "- previous_hash = \"0\"\n",
    "- data = \"Genesis Block\"\n",
    "\n",
    "\n",
    "**( ! ) Task 2: Implement next_block( )**\n",
    "\n",
    "Implement the function *next_block( )* which takes in 2 parameters:\n",
    "- last_block = an instance of class Block that is the block that we're building our next block on top of \n",
    "- nonce = Dont do anything with this right now - just pass it in to the Block that you create using the default \n",
    "\n",
    "Implement the function so that it returns a new instance of the class Block with the following specifications\n",
    "- height = height of last_block + 1\n",
    "- timestamp = Now (whenever the function is being called)\n",
    "- previous_hash = hash of last_block\n",
    "- data = \"Hey! I'm block {height}\"  (for example block at height 5 would have data: \"Hey! I'm block 5\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Creates the first block with current time and generic data\n",
    "def create_genesis_block():\n",
    "    # Manually construct a block with height zero and arbitrary previous hash\n",
    "    return Block(0, date.datetime.now(), \"0\", \"Genesis Block\" )\n",
    "\n",
    "# Function that creates the next block, given the last block on the chain you want to mine on\n",
    "def next_block(last_block, nonce=0):\n",
    "    # Your code for Task 2 here"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**( ! ) Test your Task 2 implementation here**: Run the block of code below\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def test_task_2(genesis_block):\n",
    "    block_1 = next_block(genesis_block)\n",
    "    if block_1.height == 1 and block_1.data == \"Hey! I'm block 1\" and block_1.previous_hash == genesis_block.hash and str(type(block_1.timestamp)) == \"<class 'datetime.datetime'>\":\n",
    "        print(\"PASSED!!! Move on to next part\" )\n",
    "    else:\n",
    "        print(\"FAILED!!! Try again :(\")\n",
    "    \n",
    "\n",
    "genesis_block = create_genesis_block()\n",
    "test_task_2(genesis_block)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Spinning up a Chain**\n",
    "\n",
    "Now that you have implemented the data structure as well as the functions needed to create the chain, let's see how to spin up an actual blockchain instance.\n",
    "\n",
    "Below we initialize three different variables: \n",
    "- blockchain - this is a python list which we initialize with one block inside (the genesis block) \n",
    "- previous_block - this points to our genesis block (since it references the first element in blockchain)\n",
    "- num_blocks - this specifies the number of additional blocks we want to add to our chain \n",
    "\n",
    "**( ! ) Task 3: implement complete_chain( )**\n",
    "\n",
    "Complete the implementation of the function *complete_chain( )*. This function takes in three inputs, which correspond to the initializations that we made. It returns nothing however, upon completion, the list \"blockchain\" that we initialized earlier has been turned into an array of length num_blocks + 1 in which each element is an instance of class Block and each element's self.previous_hash == the previous element's self.hash. Therefore we have created our own mini blockchain!! \n",
    "\n",
    "The for loop and the print statements of complete_chain have been implemented for you, you need to add the statements that ***create a new block on top of previous_block, add it to the block chain, and edit previous block so that the for loop can continue correctly*** \n",
    "\n",
    "****HINT**** : Literally just do all the things listed in the section immediately above in order \n",
    "\n",
    "If it works out, you should get the same number of print statements as num_blocks"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#[ORIGINAL CODE]\n",
    "# Create the blockchain and add the genesis block\n",
    "blockchain = [create_genesis_block()]\n",
    "\n",
    "# Create our initial reference to previous block which points to the genesis block\n",
    "previous_block = blockchain[0]\n",
    "\n",
    "# How many blocks should we add to the chain after the genesis block\n",
    "num_blocks = 10\n",
    "\n",
    "def complete_chain(num_blocks, blockchain, previous_block):\n",
    "    # Add blocks to the chain\n",
    "    for i in range(0, num_blocks):\n",
    "        # Your code for Task 3 Here\n",
    "        # TIP: name the newly created block \"block_to_add\"\n",
    "        \n",
    "        # Your code for Task 3 ends Here\n",
    "        print(\"Block #{} has been added to the blockchain!\".format(block_to_add.height))\n",
    "        print(\"Hash: {}\\n\".format(block_to_add.hash))\n",
    "        tt.sleep(1)        \n",
    "\n",
    "complete_chain(num_blocks, blockchain, previous_block)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**( ! ) Test your Task 3 implementation here**: Run the block of code below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def test_task_3(blockchain, num_blocks):\n",
    "    correct = True\n",
    "    if len(blockchain) != num_blocks + 1:\n",
    "        correct = False\n",
    "    for i in range(len(blockchain)-1):\n",
    "        if blockchain[i + 1].previous_hash != blockchain[i].hash:\n",
    "            correct = False\n",
    "            break\n",
    "    print_statement = \"PASSED!!! Move on to the next Part\" if correct else \"FAILED!!! Try Again :(\"\n",
    "    print(print_statement)\n",
    "\n",
    "test_task_3(blockchain, num_blocks)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Part 2 - Proof of Work Simulation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this part you will implement a simulation of the Proof-of-Work mechanism that Bitcoin (among other blockchains) uses. Let us define again some of the concepts that we discussed in class.\n",
    "\n",
    "- **nonce**: a randomly generated value that is concatenated to the block to vary the resulting digest.\n",
    "\n",
    "- **difficulty** : specified by the network (in theory. Here it is specified by us). Defines the number of leading zeros and, as a consequence, limits the number of hashes that are valid out of all possible values. Higher difficulty indicates a lower number of valid hashes. \n",
    "\n",
    "The functions **generate_nonce( )** and **generate_difficulty_bound( )** have been implemented for you.\n",
    "\n",
    "Complete the implementation of **find_next_block( )**. This function's purpose is to try different blocks with the same data, height etc. but different nonces that satisfy the difficulty specified. The difficulty bound has already been generated for you and the first block has been created. You have to complete the implementation so that: \n",
    "- You find a nonce such that the hash of the block is less than the difficulty bound. \n",
    "- All data from new block has to be the same (including the timestamp, for simplicity purposes) and the only thing changing is the nonce \n",
    "- For our learning purposes, increment the hashes_tried parameter appropriately everytime you try a hash so that we can see the number of hashes tried and how this correlates to our difficulty value. \n",
    "\n",
    "**( ! ) Task 4: implement find_next_block( )**\n",
    "\n",
    "**HINTS**\n",
    "- How do you turn a hash string into its hex prepresentation that you can compare???\n",
    "- if your implementation is working, you should notice something interesting about all the hashes of the blocks that you are getting when you run the code cell labeled \"Create Proof of work Blockchain\"**\n",
    "\n",
    "\n",
    "\n",
    "**you can test question 4 in the a few cells below this one, after running all the cells in between**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_nonce(length=20):\n",
    "    return ''.join([str(rand.randint(0, 9)) for i in range(length)])\n",
    "\n",
    "def generate_difficulty_bound(number_leading_zeros=1):\n",
    "    return \"0\" * number_leading_zeros\n",
    "\n",
    "# Given a previous block and a difficulty value, find a nonce that results in a lower hash value\n",
    "def find_next_block(last_block, difficulty, nonce_length):\n",
    "    difficulty_bound = generate_difficulty_bound(difficulty)\n",
    "    start = tt.time()\n",
    "    new_block = next_block(last_block)\n",
    "    hashes_tried = 1 \n",
    "    #Your code for Task 4 Starts here \n",
    "\n",
    "    #Your code for Task 4 Ends here \n",
    "    time_taken = time.process_time() - start\n",
    "    return(time_taken, hashes_tried, new_block)\n",
    "        "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The cell below creates our proof of work blockchain in a similar way that we do in the earlier section. \n",
    "\n",
    "Some initializations:\n",
    "- blockchain_pow: Our new python list that signifies our proof of work blockchain, with the genesis block inside\n",
    "- previous_block: The first block to use as previous block to build upon\n",
    "- num_blocks: number of additional blocks to add to the chain\n",
    "- difficulty: difficulty of the network \n",
    "- nonce_length: length of the randomly generated nonce"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Create Proof of Work Blockchain \n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create the blockchain and add the genesis block\n",
    "blockchain_pow = [create_genesis_block()]\n",
    "\n",
    "#Create our initial reference to previous block which points to the genesis block\n",
    "previous_block = blockchain_pow[0]\n",
    "\n",
    "# How many blocks should we add to the chain after genesis block\n",
    "num_blocks = 10\n",
    "\n",
    "# number of leading zeroes that must be in the beginning of the hash\n",
    "difficulty = 4\n",
    "\n",
    "#length of nonce that will be generated and added\n",
    "nonce_length = 20\n",
    "\n",
    "# Add blocks to the chain based on difficulty with nonces of length nonce_length\n",
    "def create_pow_blockchain(num_blocks, difficulty, blockchain_pow, previous_block, nonce_length, print_data=1):\n",
    "    hash_array = []\n",
    "    time_array = []\n",
    "    for i in range(0, num_blocks):\n",
    "        time_taken, hashes_tried, block_to_add = find_next_block(previous_block, difficulty, nonce_length)\n",
    "        blockchain_pow.append(block_to_add)\n",
    "        previous_block = block_to_add\n",
    "        hash_array.append(hashes_tried)\n",
    "        time_array.append(time_taken)\n",
    "        # Tell everyone about it!\n",
    "        if print_data:\n",
    "            print(\"Block #{} has been added to the blockchain!\".format(block_to_add.height))\n",
    "            print(\"{} Hashes Tried!\".format(hashes_tried))\n",
    "            print(\"Time taken to find block: {}\".format(time_taken))\n",
    "            print(\"Hash: {}\\n\".format(block_to_add.hash))\n",
    "    return(hash_array, time_array)\n",
    "\n",
    "hash_array, time_array = create_pow_blockchain(num_blocks, difficulty, blockchain_pow, previous_block, nonce_length)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**( ! ) Test your Task 4 implementation here**\n",
    "\n",
    "Run the block of code below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def test_task_4(blockchain_pow, num_blocks):\n",
    "    correct = True\n",
    "    difficulty_bound = generate_difficulty_bound(difficulty)\n",
    "    if len(blockchain_pow) != num_blocks + 1:\n",
    "        correct = False\n",
    "    for i in range(len(blockchain_pow) - 1):\n",
    "        if blockchain_pow[i + 1].previous_hash != blockchain_pow[i].hash:\n",
    "            correct = False\n",
    "            break\n",
    "        if blockchain_pow[i + 1].hash[:difficulty] != difficulty_bound:\n",
    "            correct = False\n",
    "            break\n",
    "    print_statement = \"PASSED!!! Move on to the next Part\" if correct else \"FAILED!!! Try Again :(\"\n",
    "    print(print_statement)\n",
    "            \n",
    "test_task_4(blockchain_pow, num_blocks)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Once you have passed the above test case, you can play around with the difficulties and nonce lengths and see what happens "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Part 3 - Distributed Network and Computing Power Simulation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this section, instead of simulating a single node executing the PoW in isolation, we will create a group of nodes that compete to propose blocks. These nodes have different computing powers, specified by us. Below we define the *MinerNodeNaive* class which has an init function to create an instance, as well as a *try_hash( )* function that allows the miner to try a hash and see if it produces a valid block. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Naive miner class that races with other miners to see who can get a certain number of blocks first \n",
    "class MinerNodeNaive: \n",
    "    def __init__(self, name, compute):\n",
    "        self.name = name \n",
    "        self.compute = compute\n",
    "    \n",
    "    def try_hash(self, difficulty, chain):\n",
    "        last_block = chain[-1]\n",
    "        difficulty_bound = generate_difficulty_bound(difficulty)\n",
    "        this_height = last_block.height + 1\n",
    "        this_timestamp = date.datetime.now()\n",
    "        this_hash = last_block.hash\n",
    "        this_data = \"Hey! I'm block \" + str(this_height)\n",
    "        \n",
    "        new_block = Block(this_height, this_timestamp, this_data, this_hash)\n",
    "        \n",
    "        if new_block.hash[:difficulty] == difficulty_bound:\n",
    "            chain.append(new_block)\n",
    "            # Tell everyone about it!\n",
    "            print(\"Block #{} has been added to the blockchain!\".format(new_block.height))\n",
    "            print(\"Block found by: {}\".format(self.name))\n",
    "            print(\"Hash: {}\\n\".format(new_block.hash))\n",
    "        "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**( ! ) Task 5: Describe the Following Cells**\n",
    "\n",
    "In the next cells we try to simulate a group of miners with different computing powers. However this isn't completely indicative of how a real system works, and is limited by our single threaded implementation. Read through the code, run all the cells and explain what is happening in a paragrpah. Also explain how this is different from real world vanilla PoW systems and how this simulation isn't completely accurate of what is actually happening while nodes race to find the next block. There is a cell at the bottom where you can enter your description.  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Initialize multiple miners on the network \n",
    "UiO_Miner = MinerNodeNaive(\"UiO Miner\", 10)\n",
    "berkeley_Miner = MinerNodeNaive(\"Berkeley Miner\", 10)\n",
    "stanford_Miner = MinerNodeNaive(\"Stanford Miner\", 7)\n",
    "MIT_Miner = MinerNodeNaive(\"MIT Miner\", 5)\n",
    "UCLA_Miner = MinerNodeNaive(\"UCLA Miner\", 3)\n",
    "\n",
    "miner_array = [UiO_Miner, berkeley_Miner, stanford_Miner, MIT_Miner, UCLA_Miner]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def create_compute_simulation(miner_array):\n",
    "    compute_array = []\n",
    "    for miner in miner_array:\n",
    "        for i in range(miner.compute):\n",
    "            compute_array.append(miner)\n",
    "    return(compute_array)\n",
    "\n",
    "compute_simulation_array = create_compute_simulation(miner_array)\n",
    "rand.shuffle(compute_simulation_array)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "chain_length = 10\n",
    "chain_difficulty = 4\n",
    "blockchain_distributed = [create_genesis_block()]\n",
    "genesis_block_dist = blockchain_distributed[0]\n",
    "\n",
    "while len(blockchain_distributed) <= chain_length:\n",
    "    sampled_miner = rand.sample(compute_simulation_array, 1)[0] \n",
    "    sampled_miner.try_hash(chain_difficulty, blockchain_distributed)\n",
    "        "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**( ! ) Task 5 Description Here**\n",
    "\n",
    "**## Your description Here ##**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "# Blockchain Data Analytics"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "In this section we are going to analyze the average time and number of hashes needed to propose a valid block, given a fixed difficulty level. Look and understand the code, run all of the cells, and answer the question at the bottom. \n",
    "\n",
    "NOTE: difficulty level 4 may take some time to complete so do not be alarmed if the next cell takes a while to run"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "blockchain = [create_genesis_block()]\n",
    "previous_block = blockchain[0]\n",
    "num_blocks = 100\n",
    "\n",
    "# 4 different types of difficulty to analyze\n",
    "difficulty_1 = 1\n",
    "difficulty_2 = 2\n",
    "difficulty_3 = 3\n",
    "difficulty_4 = 4\n",
    "\n",
    "nonce_length = 20\n",
    "\n",
    "hash_array_1, time_array_1 = create_pow_blockchain(num_blocks, difficulty_1, blockchain, previous_block, nonce_length, 0)\n",
    "print(\"Difficulty Level: {} complete\".format(difficulty_1))\n",
    "hash_array_2, time_array_2 = create_pow_blockchain(num_blocks, difficulty_2, blockchain, previous_block, nonce_length, 0)\n",
    "print(\"Difficulty Level: {} complete\".format(difficulty_2))\n",
    "hash_array_3, time_array_3 = create_pow_blockchain(num_blocks, difficulty_3, blockchain, previous_block, nonce_length, 0)\n",
    "print(\"Difficulty Level: {} complete\".format(difficulty_3))\n",
    "hash_array_4, time_array_4 = create_pow_blockchain(num_blocks, difficulty_4, blockchain, previous_block, nonce_length, 0)\n",
    "print(\"Difficulty Level: {} complete\".format(difficulty_4))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dfHashesNeeded = pd.DataFrame(columns=[\"difficulty\", \"hashNumber\"])\n",
    "dfTimeNeeded = pd.DataFrame(columns=[\"difficulty\", \"time\"])\n",
    "\n",
    "for index in range(num_blocks):\n",
    "    dfHashesNeeded = dfHashesNeeded.append({\"difficulty\":1, \"hashNumber\": hash_array_1[index]}, ignore_index=True)\n",
    "    dfHashesNeeded = dfHashesNeeded.append({\"difficulty\":2, \"hashNumber\": hash_array_2[index]}, ignore_index=True)\n",
    "    dfHashesNeeded = dfHashesNeeded.append({\"difficulty\":3, \"hashNumber\": hash_array_3[index]}, ignore_index=True)\n",
    "    dfHashesNeeded = dfHashesNeeded.append({\"difficulty\":4, \"hashNumber\": hash_array_4[index]}, ignore_index=True)\n",
    "    \n",
    "    dfTimeNeeded = dfTimeNeeded.append({\"difficulty\":1, \"time\": time_array_1[index]}, ignore_index=True)\n",
    "    dfTimeNeeded = dfTimeNeeded.append({\"difficulty\":2, \"time\": time_array_2[index]}, ignore_index=True)\n",
    "    dfTimeNeeded = dfTimeNeeded.append({\"difficulty\":3, \"time\": time_array_3[index]}, ignore_index=True)\n",
    "    dfTimeNeeded = dfTimeNeeded.append({\"difficulty\":4, \"time\": time_array_4[index]}, ignore_index=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plotHashTrend(dataset):\n",
    "\n",
    "    dataset.difficulty = dataset.difficulty.astype(float)\n",
    "    dataset.hashNumber = dataset.hashNumber.astype(float)\n",
    "    plt.figure(figsize=(14, 10))\n",
    "    \n",
    "    b = sns.boxplot(x=\"difficulty\", y=\"hashNumber\", data=dataset)\n",
    "\n",
    "    b.axes.set_title(f\"Hash Plot\", fontsize=30)\n",
    "    b.set_xlabel(\"Difficulty\", fontsize=20)\n",
    "    b.set_ylabel(\"Number of hashes tried\", fontsize=20)\n",
    "    b.tick_params(labelsize=20)\n",
    "\n",
    "    plt.show()\n",
    "    \n",
    "plotHashTrend(dfHashesNeeded)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "def plotTimeTrend(dataset):\n",
    "\n",
    "    dataset.difficulty = dataset.difficulty.astype(float)\n",
    "    dataset.hashNumber = dataset.time.astype(float)\n",
    "    plt.figure(figsize=(14, 10))\n",
    "    \n",
    "    b = sns.boxplot(x=\"difficulty\", y=\"time\", data=dataset)\n",
    "\n",
    "    b.axes.set_title(f\"Time Plot\", fontsize=30)\n",
    "    b.set_xlabel(\"Difficulty\", fontsize=20)\n",
    "    b.set_ylabel(\"Block proposal time (seconds)\", fontsize=20)\n",
    "    b.tick_params(labelsize=20)\n",
    "\n",
    "    plt.show()\n",
    "    \n",
    "plotTimeTrend(dfTimeNeeded)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "diff_factor_1 = np.mean(hash_array_1)/np.mean(hash_array_0)\n",
    "diff_factor_2 = np.mean(hash_array_2)/np.mean(hash_array_1)\n",
    "diff_factor_3 = np.mean(hash_array_3)/np.mean(hash_array_2)\n",
    "print(\"Factor of difficulty increase from 1 to 2: {}\".format(diff_factor_1))\n",
    "print(\"Factor of difficulty increase from 2 to 3: {}\".format(diff_factor_2))\n",
    "print(\"Factor of difficulty increase from 3 to 4: {}\".format(diff_factor_3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**( ! ) Task 6**\n",
    "\n",
    "Look at the factor of difficulty increase from each level to the subsequent level, printed from the cell above.\n",
    "\n",
    "What whould the factor of difficulty increase be for each level: **Your answer Here**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**( ! ) Optional Extra Tasks**\n",
    "\n",
    "Feel free to extend the code and investigate/test cases you are curious about. Curiosity will be positively evaluated ;)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
