Programming - Implementing a Blockchain using Python and Flask [Dev Blog 11]

[Image 1]

Introduction

Hey it's a me again drifter1!

Here we are in the "finished" version of the master's task. Last time I promised that I would get you through a complete Example run. So, let's do this today, and as I already said, changes to this project will come out more rarely from now on.

And before I forget to mention it! I thought of live-streaming my coding sessions on Twitch (or something similar in the crypto-space), so tell me If you would be interested!

So, without further ado, let's get more in-depth!


GitHub Repository


Start a DNS Server

Any network needs a way in. In the case of my blockchain implementation this is done using the DNS server, which is basically a full node directory. The DNS Server periodically contacts its known nodes and so it always up-to-date to which nodes are active / online. When a node wants to connect to the network (initial network connection) it contacts the DNS in order to retrieve nodes to connect and communicate to.

When inside the /src directory, a DNS server can be started as follows:

python dns_server.py
which leads to using the default settings:
  • ip address : 127.0.0.1
  • port : 42020
  • directory : ../.dns_server

The DNS server's directory looks like this:

where nodes.json is an empty nodes array, or [].


Start some Full Nodes

Next up, we need to start the most essential part of the system, which are the full nodes. At least one such node is needed for the blockchain to be operational. Full nodes store the complete blockchain, and validate transactions and blocks.

A full node (with default settings) can be started using:

python full_node.py
which leads to a random port in the range [50000, 60000] and the directory ../.full_node

Specifying a specific port is as simple as adding -p port as an argument (argparse library). And, of course if we want to run more then one full nodes from the same code folder, the directory also needs to be set. This is also easy to do, using the -d directory argument. For example, let's run 3 more full nodes with directories of the form ../.full_nodeX, where X will be 2, 3 and 4:

python full_node.py -d ../.full_node2
python full_node.py -d ../.full_node3
python full_node.py -d ../.full_node4

This leads to the creation of the following directories:

Letting the system run for a complete node-update cycle (60 seconds), the nodes should then known each other (3 node entries per nodes.json file).

The DNS Server stores all nodes, leading to the following nodes.json file:

[
    {
        "ip_address": "127.0.0.1",
        "port": 59672
    },
    {
        "ip_address": "127.0.0.1",
        "port": 55826
    },
    {
        "ip_address": "127.0.0.1",
        "port": 58470
    },
    {
        "ip_address": "127.0.0.1",
        "port": 52018
    }
]
and in each full node's nodes.json file, the node entry referring to itself will be missing.


Run Testing Script and Miner

The testing script can be used to create a correctly formed transaction. So, let's run it once on an empty chain, in order to generate a wallet only, as no UTXO entries will exist yet:

python testing.py
In my run, this leads to the generation of address 0x9adc3034a5afa876721fab76ca2e1e1c0c6ce76b.

Let's use this address as a rewarding address of a miner using the argument -ra address:

python miner.py -ra 0x9adc3034a5afa876721fab76ca2e1e1c0c6ce76b

Running the miner and the pressing CTRL+C after a solution has been posted, ledas to the creation of the genesis block:

{
    "timestamp": 1631869461,
    "height": 0,
    "creator": "0x9adc3034a5afa876721fab76ca2e1e1c0c6ce76b",
    "reward": 1.5,
    "fees": 0,
    "nonce": "000a373e",
    "transaction_count": 1,
    "transactions": [
        {
            "timestamp": 1631869461,
            "inputs": [
                {
                    "transaction_hash": "0000000000000000000000000000000000000000000000000000000000000000",
                    "output_index": 0,
                    "output_address": "0x0000000000000000000000000000000000000000",
                    "output_value": 1.5,
                    "output_hash": "0000000000000000000000000000000000000000000000000000000000000000",
                    "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
                }
            ],
            "outputs": [
                {
                    "index": 0,
                    "address": "0x9adc3034a5afa876721fab76ca2e1e1c0c6ce76b",
                    "value": 1.5,
                    "hash": "ccd59985c83d510049f860262a55f5a2cbbaef0e60053b50b113c872f00bc9b4"
                }
            ],
            "value": 1.5,
            "fee": 0,
            "hash": "3ce4a56abbb99938f75877deb998f3730281a5b1b16b92e246c43d561c768d8f"
        }
    ],
    "hash": "00000e338ba6ff951a127a68d64b030bc1995636a1d5066c52e6bf646cf97226",
    "prev_hash": "0000000000000000000000000000000000000000000000000000000000000000"
}
which is shared amongst all full nodes.

The coinbase transaction's output leads to the first UTXO entry:

{
    "utxo_outputs": [
        {
            "block_height": 0,
            "transaction_hash": "3ce4a56abbb99938f75877deb998f3730281a5b1b16b92e246c43d561c768d8f",
            "transaction_index": 0,
            "output_index": 0
        }
    ],
    "balance": 1.5
}

And the Blockchain Info structure, was also correctly updated:

{
    "name": "test_blockchain",
    "height": 0,
    "total_transactions": 1,
    "total_addresses": 1,
    "block_time": 300,
    "block_reward": 1.5,
    "rich_list": [
        {
            "address": "0x9adc3034a5afa876721fab76ca2e1e1c0c6ce76b",
            "balance": 1.5
        }
    ]
}


Post Transaction using Testing Script

Running the testing script again, now that an UTXO entry exists, will leads to the creation of a valid transaction, which will be posted to the complete network.

The transaction sends 0.3 and 0.2 to two dummy recipient addresses, and the remainder back to the origin address, of course minus some fee. The input has been signed using the correct private key, which corresponds to the address referenced in the output (public key can be retrieved from the hash-signature pair, and hashing it and trimming it to 20 bytes + "0x" prefix leads to the address).

The JSON transaction looks as follows:

{
    "timestamp": 1631869702,
    "inputs": [
        {
            "transaction_hash": "3ce4a56abbb99938f75877deb998f3730281a5b1b16b92e246c43d561c768d8f",
            "output_index": 0,
            "output_address": "0x9adc3034a5afa876721fab76ca2e1e1c0c6ce76b",
            "output_value": 1.5,
            "output_hash": "ccd59985c83d510049f860262a55f5a2cbbaef0e60053b50b113c872f00bc9b4",
            "signature": "fbb25fd615e8bb16e1686daf10a9b011b5f0685b7a551cab5f10eb498523811aaca3240ee3f973be83cdfb637e607e795a410b4b5c818c9abae3e5f30f2ff12b"
        }
    ],
    "outputs": [
        {
            "index": 0,
            "address": "0x60a192daca0804e113d6e6d41852c611be5de0bf",
            "value": 0.3,
            "hash": "fbd6925c0bd8d12964fab3ce2a497395eb2e3941c8db00d75ff351d3d9705280"
        },
        {
            "index": 1,
            "address": "0xb24e0a3dbf9095d62418a86462792855aa24ba16",
            "value": 0.2,
            "hash": "0000000000000000000000000000000000000000000000000000000000000000"
        },
        {
            "index": 2,
            "address": "0x9adc3034a5afa876721fab76ca2e1e1c0c6ce76b",
            "value": 0.999,
            "hash": "0000000000000000000000000000000000000000000000000000000000000000"
        }
    ],
    "value": 1.499,
    "fee": 0.001,
    "hash": "5efd164880e72609b4d5e879daf640ce4b3130de2307c6c0c220d5bbcecb39b2"
}


Run Miner again

Running the miner again, will now lead to the creation of Block 1, which will contain this transaction:

{
    "timestamp": 1631870110,
    "height": 1,
    "creator": "0x9adc3034a5afa876721fab76ca2e1e1c0c6ce76b",
    "reward": 1.5,
    "fees": 0.001,
    "nonce": "00127eb7",
    "transaction_count": 2,
    "transactions": [
        {
            "timestamp": 1631870110,
            "inputs": [
                {
                    "transaction_hash": "0000000000000000000000000000000000000000000000000000000000000000",
                    "output_index": 0,
                    "output_address": "0x0000000000000000000000000000000000000000",
                    "output_value": 1.501,
                    "output_hash": "0000000000000000000000000000000000000000000000000000000000000000",
                    "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
                }
            ],
            "outputs": [
                {
                    "index": 0,
                    "address": "0x9adc3034a5afa876721fab76ca2e1e1c0c6ce76b",
                    "value": 1.501,
                    "hash": "36d48623bc158d6fb0fe00217b8155b54ff354ac7e8daecf7a4a723c63b18997"
                }
            ],
            "value": 1.501,
            "fee": 0,
            "hash": "b23350365f61f5d96c0f063d31dfe1acac8587145e43714fad91e07de9ccc376"
        },
        {
            "timestamp": 1631869702,
            "inputs": [
                {
                    "transaction_hash": "3ce4a56abbb99938f75877deb998f3730281a5b1b16b92e246c43d561c768d8f",
                    "output_index": 0,
                    "output_address": "0x9adc3034a5afa876721fab76ca2e1e1c0c6ce76b",
                    "output_value": 1.5,
                    "output_hash": "ccd59985c83d510049f860262a55f5a2cbbaef0e60053b50b113c872f00bc9b4",
                    "signature": "fbb25fd615e8bb16e1686daf10a9b011b5f0685b7a551cab5f10eb498523811aaca3240ee3f973be83cdfb637e607e795a410b4b5c818c9abae3e5f30f2ff12b"
                }
            ],
            "outputs": [
                {
                    "index": 0,
                    "address": "0x60a192daca0804e113d6e6d41852c611be5de0bf",
                    "value": 0.3,
                    "hash": "fbd6925c0bd8d12964fab3ce2a497395eb2e3941c8db00d75ff351d3d9705280"
                },
                {
                    "index": 1,
                    "address": "0xb24e0a3dbf9095d62418a86462792855aa24ba16",
                    "value": 0.2,
                    "hash": "0000000000000000000000000000000000000000000000000000000000000000"
                },
                {
                    "index": 2,
                    "address": "0x9adc3034a5afa876721fab76ca2e1e1c0c6ce76b",
                    "value": 0.999,
                    "hash": "0000000000000000000000000000000000000000000000000000000000000000"
                }
            ],
            "value": 1.499,
            "fee": 0.001,
            "hash": "5efd164880e72609b4d5e879daf640ce4b3130de2307c6c0c220d5bbcecb39b2"
        }
    ],
    "hash": "0000031dc84fdab9e0f6b09fe6fa2a9926dca6cfd7f0fa86ced6148b05ef2d16",
    "prev_hash": "00000e338ba6ff951a127a68d64b030bc1995636a1d5066c52e6bf646cf97226"
}

And of course, new UTXO's, which correspond to the addresses referenced in the Outputs:

For example, the complete UTXO for the testing script's / miner's address is now:
{
    "utxo_outputs": [
        {
            "block_height": 1,
            "transaction_hash": "b23350365f61f5d96c0f063d31dfe1acac8587145e43714fad91e07de9ccc376",
            "transaction_index": 0,
            "output_index": 0
        },
        {
            "block_height": 1,
            "transaction_hash": "5efd164880e72609b4d5e879daf640ce4b3130de2307c6c0c220d5bbcecb39b2",
            "transaction_index": 1,
            "output_index": 2
        }
    ],
    "balance": 2.5
}

Run more Miners

Let's now run 3 miners, with reward addresses, the addresses for which UTXO's now exist, and different directories:

python miner.py -ra 0x9adc3034a5afa876721fab76ca2e1e1c0c6ce76b
python miner.py -ra 0x60a192daca0804e113d6e6d41852c611be5de0bf -d ../.miner2
python miner.py -ra 0xb24e0a3dbf9095d62418a86462792855aa24ba16 -d ../.miner3

Letting them run up to block 10, led to the following Blockchain Info:

{
    "name": "test_blockchain",
    "height": 10,
    "total_transactions": 12,
    "total_addresses": 3,
    "block_time": 300,
    "block_reward": 1.5,
    "rich_list": [
        {
            "address": "0x9adc3034a5afa876721fab76ca2e1e1c0c6ce76b",
            "balance": 8.5
        },
        {
            "address": "0x60a192daca0804e113d6e6d41852c611be5de0bf",
            "balance": 4.8
        },
        {
            "address": "0xb24e0a3dbf9095d62418a86462792855aa24ba16",
            "balance": 3.2
        }
    ]
}


Run another Full Node

Let's now run another node, the fifth, and let it synchronize with the network:

python full_node.py -d ../.full_node5

After synchronizing, it ends up with the same exact data, as the other full nodes:

Here we could now continue on with stopping some full node(s), running the miners again, running the full node(s) again. They would again synchronize correctly...

Interesting right! We shall see, how we can extend the system even more, maybe in Twitch streams, as I already mentioned earlier! :)


RESOURCES:

References

  1. https://www.python.org/
  2. https://flask.palletsprojects.com/en/2.0.x/
  3. https://docs.python-requests.org/
  4. https://pypi.org/project/mnemonic/0.20/
  5. https://pypi.org/project/ecdsa/
  6. https://docs.microsoft.com/en-us/windows/wsl/install-win10
  7. https://www.docker.com/
  8. https://code.visualstudio.com/
  9. https://insomnia.rest/

Images

  1. https://pixabay.com/illustrations/blockchain-block-chain-technology-3019121/
The rest is screenshots or made using draw.io

Previous dev blogs of the series


Final words | Next up

And this is actually it for today's post!

From now on, this series will be put into slow-mode, which means posting (maybe even streaming) monthly at most!

Keep on drifting!

H2
H3
H4
3 columns
2 columns
1 column
7 Comments
Ecency