[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
- https://www.python.org/
- https://flask.palletsprojects.com/en/2.0.x/
- https://docs.python-requests.org/
- https://pypi.org/project/mnemonic/0.20/
- https://pypi.org/project/ecdsa/
- https://docs.microsoft.com/en-us/windows/wsl/install-win10
- https://www.docker.com/
- https://code.visualstudio.com/
- https://insomnia.rest/
Images
The rest is screenshots or made using draw.ioPrevious dev blogs of the series
- Dev Blog 1
- Dev Blog 2
- Dev Blog 3
- Dev Blog 4
- Dev Blog 5
- Dev Blog 6
- Dev Blog 7
- Dev Blog 8
- Dev Blog 9
- Dev Blog 10
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!
Can you please tell me what is still missing here to be like production ready or it is ready?
Hey @bajwolf,
This is a simple, educational implementation, and not meant to be used as an actual blockchain system, but to understand how such a system operates. Of course, it's not a simulator, as the individual parts such as nodes, miners and the dns server (or node index) communicate with each other. What's missing is basically more testing and optimizations. I don't think that it would work in a real-life scenario...as is.
Sincerely,
@drifter1
OMG, this is so amazing! I always wanted to know how people could create their own blockchain, and in Python!! It' so cool
Thanks for the post
Glad that you enjoy this series the same way I did! 😃
I have lots of ideas, and I also want to get into other types of content (such as videos, streams, etc.). So, feedback really helps...it keeps me going!Hey @luis96xd,
Best Regards,
@drifter1
Making videos would be so cool!
Hello again,
I'm currently thinking of:
Do you maybe have some specific suggestions for content? 😃
Sincerely,
@drifter1
Wow, this topics you mentioned sound so interesting! and advanced :O
What if you make a video about creating a circuit simulator?
Or some Artificial Intelligence topic
What about using an API related to Blockchain or Web3 Apps :D