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!