When I read the document written by Phu Minh, I was curious about learning different concepts about blockchain. Once I started to read the code, I wanted to match it with Python to understand also the differences with JavaScript.
The objective of this post is finding the differences in both languages and serve as the Python appendix of the original post.
Even though the original document comes from a Python example, I wanted to have an exact match with the JavaScript
code to compare.
Let's also fit the python
code in the promised 60 lines.
Blockchain
Although the idea is to mimic the entire post and use the same sections to follow the code,
For the Blockchain definition, I prefer the following:
Blockchain is a system of recording information in a way that makes it difficult or impossible to change, hack, or cheat.
Setup
We are using Python for this project, so be sure to install it if you haven't.
As I have said, a block is just an object that has some information on it, so we should have a Block class like this:
class Block:
def __init__(self, timestamp=None, data=None):
self.timestamp = timestamp or time()
# this.data should contain information like transactions.
self.data = [] if data is None else data
The class definition is quite similar in both languages. In Python, we use self
instead of this
and init is a constructor
method.
Comments are also similar in both languages. In Python, we use #
to comment vs. //
in javascript.
Fot the sha256
algorithn, I will use the hashlib library vs the crypto
package in javascript.
from hashlib import sha256
class Block:
def __init__(self, timestamp=None, data=None):
self.timestamp = timestamp or time()
self.data = [] if data is None else data
self.hash = self.getHash()
self.prevHash = None # previous block's hash
def getHash(self):
hash = sha256()
hash.update(str(self.prevHash).encode('utf-8'))
hash.update(str(self.timestamp).encode('utf-8'))
hash.update(str(self.data).encode('utf-8'))
return hash.hexdigest()
In the getHash method, from an empty hash, we update it with the rest of components. The hash is the result of the concatenation of the previous hash, the timestamp and the data. All of then with the .encode('utf-8')
to convert the string to bytes.
The blockchain
Let's move over to the blockchain class.
class Blockchain:
def __init__(self):
# This property will contain all the blocks.
self.chain = []
Again, the class definition is similar in both languages.
To create the genesis block, we just call the Block with the current timestamp using time. To do that, we need to import the time library.
The string conversion is done with str
instead of toString
.
from time import time
class Blockchain:
def __init__(self):
# Create our genesis block
self.chain = [Block(str(int(time())))]
And the method to get the latest block is similar. We use len
to get the length of the chain instead of length
in javascript.
def getLastBlock(self):
return self.chain[len(self.chain) - 1]
To add the block to the blockchain, we just call the addBlock
method. The code is almost the same except the append
(push
in javascript).
def addBlock(self, block):
# Since we are adding a new block, prevHash will be the hash of the old latest block
block.prevHash = self.getLastBlock().hash
# Since now prevHash has a value, we must reset the block's hash
block.hash = block.getHash()
self.chain.append(block)
Validation
In the validation method, we start using range
as a big difference. Also, because we don't use constants in Python, we just use normal variables.
For the conditional, python uses or
instead of ||
in javascript.
def isValid(self):
# Iterate over the chain, we need to set i to 1 because there are nothing before the genesis block, so we start at the second block.
for i in range(1, len(self.chain)):
currentBlock = self.chain[i]
prevBlock = self.chain[i - 1]
# Check validation
if (currentBlock.hash != currentBlock.getHash() or prevBlock hash != currentBlock.prevHash):
return False
return True
Proof-of-work
We can implement this system by adding a mine
method and a nonce
property to our block. Be careful because nonce
must be declared before calling the self.getHash()
method. If not, you will get the error AttributeError: 'Block' object has no attribute 'nonce'
.
class Block:
def __init__(self, timestamp=None, data=None):
self.timestamp = timestamp or time()
self.data = [] if data is None else data
self.prevHash = None # previous block's hash
self.nonce = 0
self.hash = self.getHash()
# Our hash function.
def getHash(self):
hash = sha256()
hash.update(str(self.prevHash).encode('utf-8'))
hash.update(str(self.timestamp).encode('utf-8'))
hash.update(str(self.data).encode('utf-8'))
hash.update(str(self.nonce).encode('utf-8'))
return hash.hexdigest()
def mine(self, difficulty):
# Basically, it loops until our hash starts with
# the string 0...000 with length of <difficulty>.
while self.hash[:difficulty] != '0' * difficulty:
# We increases our nonce so that we can get a whole different hash.
self.nonce += 1
# Update our new hash with the new nonce value.
self.hash = self.getHash()
To create the difficulty property:
self.difficulty = 1
And the addBlock
method:
def addBlock(self, block):
block.prevHash = self.getLastBlock().hash
block.hash = block.getHash()
block.mine(self.difficulty)
self.chain.append(block)
Testing out the chain
First, import the module and use the Blockchain
class the same way using JeChain object:
from blockchain import Block
from blockchain import Blockchain
from time import time
JeChain = Blockchain()
# Add a new block
JeChain.addBlock(Block(str(int(time())), ({"from": "John", "to": "Bob", "amount": 100})))
# (This is just a fun example, real cryptocurrencies often have some more steps to implement).
# Prints out the updated chain
print(JeChain)
It should looks like this:
[
{
"data": [],
"timestamp": "1636153236",
"nonce": 0,
"hash": "4caa5f684eb3871cb0eea217a6d043896b3775f047e699d92bd29d0285541678",
"prevHash": null
},
{
"data": {
"from": "John",
"to": "Bob",
"amount": 100
},
"timestamp": "1636153236",
"nonce": 14,
"hash": "038f82c6e6605acfcad4ade04e454eaa1cfa3d17f8c2980f1ee474eefb9613e9",
"prevHash": "4caa5f684eb3871cb0eea217a6d043896b3775f047e699d92bd29d0285541678"
}
]
but only after adding the __repr__
method to the Blockchain class:
import json
def __repr__(self):
return json.dumps([{'data': item.data, 'timestamp': item.timestamp, 'nonce': item.nonce, 'hash': item.hash, 'prevHash': item.prevHash} for item in self.chain], indent=4)
Updated bonus: Difficulty and block time
For the blockTime just:
self.blockTime = 30000
Have a look to the ternary used for the difficulty system. In Python, the ternary operator is (if_test_is_false, if_test_is_true)[test]
, resulting in:
def addBlock(self, block):
block.prevHash = self.getLastBlock().hash
block.hash = block.getHash()
block.mine(self.difficulty)
self.chain.append(block)
self.difficulty += (-1, 1)[int(time()) - int(self.getLastBlock().timestamp) < self.blockTime]
The final python code (Without proper formatting) in 60 lines is:
# -*- coding: utf-8 -*-
from hashlib import sha256
import json
from time import time
class Block:
def __init__(self, timestamp=None, data=None):
self.timestamp = timestamp or time()
self.data = [] if data is None else data
self.prevHash = None
self.nonce = 0
self.hash = self.getHash()
def getHash(self):
hash = sha256()
hash.update(str(self.prevHash).encode('utf-8'))
hash.update(str(self.timestamp).encode('utf-8'))
hash.update(str(self.data).encode('utf-8'))
hash.update(str(self.nonce).encode('utf-8'))
return hash.hexdigest()
def mine(self, difficulty):
while self.hash[:difficulty] != '0' * difficulty:
self.nonce += 1
self.hash = self.getHash()
class Blockchain:
def __init__(self):
self.chain = [Block(str(int(time())))]
self.difficulty = 1
self.blockTime = 30000
def getLastBlock(self):
return self.chain[len(self.chain) - 1]
def addBlock(self, block):
block.prevHash = self.getLastBlock().hash
block.hash = block.getHash()
block.mine(self.difficulty)
self.chain.append(block)
self.difficulty += (-1, 1)[int(time()) - int(self.getLastBlock().timestamp) < self.blockTime]
def isValid(self):
for i in range(1, len(self.chain)):
currentBlock = self.chain[i]
prevBlock = self.chain[i - 1]
if (currentBlock.hash != currentBlock.getHash() or prevBlock.hash != currentBlock.prevHash):
return False
return True
def __repr__(self):
return json.dumps([{'data': item.data, 'timestamp': item.timestamp, 'nonce': item.nonce, 'hash': item.hash, 'prevHash': item.prevHash} for item in self.chain], indent=4)
Hopefully you will enjoy and learn with both posts!
Top comments (8)
Great article :)
This is considered harmful because you are assigning a mutable object in a method definition, all instances of
Block
will be mutating the samedata
reference.Consider changing to
Also
timestamp=time()
will be evaluated in the time the class is defined, when the program is started, if this is a long running process then you are stuck with a certaintime()
Fully agree @rochacbruno
The idea of the code was keeping it exactly the same as the Javascript for learning purposes but your comment is a perfect helper for the post. β₯
I have just changed it as per your comments.
Thank you!
Interesting article. I wanted to take a stab at cleaning it up a little to utilize more pythonic syntax.
Thanks @theflanman for your contribution. It is always cool to see code like yours to learn.
Do you think adding typing could be a good addition to your code? like:
Also I β₯ to see how you are using @property decorator in the example. I have prepared a small snippet to see the effect when using it:
I really like how you are using the iterator in the
__repr__
and other dunder methods.Have Fun!
The consensus we've reached at work has mostly been that typing should really only be bothered to help users and devs where it's not clear what expected input and output is. Things like
__init__
methods or other dunder methods are expected to return a certain type of object so they don't need it.What I might recommend is typing your inputs a little more. As an example, explicitly use ints to represent timestamps, and use the function time.time_ns() for timestamps, then use structs to generate more accurate representations to hash.
Here I've specified that data is a list of anything, timestamp is an int, and prevHash is bytes, and that Block.hash generates bytes. This informs a user/developer that's interfacing with it that this is what the methods are expecting to take or should be expected to return. The only thing I might also add for type hinting in the Block class is specifying the difficulty parameter in Block.mine() is an integer, as well as a check that it's greater than or equal to 1, raising an exception if it isn't.
I had a challenge following the article step by step as I'd assign class variables before even declaring them... It was kinda hard to follow, unless this article wasn't geared towards beginners...
Other than that... Great stuff.
Hi @mungaivic
If you have any questions, donβt hesitate to contact me. I will very glad to help.
It is interesting to me understanding how to improve the way we could explain different pieces of code in a better way.
@imjoseangel can I get your contact details...