Welcome to BrainLab's hands-on training sessions! The purpose is to illustrate the concepts behind the demos in the course in a way that allows you to explore different functions, arguments and examples as you master the material.
In the previous hands-on session, we learned what it takes to tie blocks together in a blockchain, and defined Block
and Blockchain
objects.
We'll start by redefining the objects with a few minor additions:
Block
object will include an __eq__(self, other)
method in order to be able to determine whether its contents are similar to another block's (we'll compare its number
, nonce
, data
, prev_hash
and hash
fields).Blockchain
object will also include an __eq__(self, other)
in order to be able to determine whether its blocks are similar to another blockchain's (we'll individually compare its blocks). Blockchain
object will include a copy(self)
method in order for us to be able to easily replicate it.These will be essential in order for us to easily copy a blockchain and see what happens when every user on the network has her own copy!
Recall that the overarching premise of the blockchain is that it has to be immutable. If one party modifies her copy, nefariously or otherwise, then the rest of the network has to be able to spot it.
import hashlib
class Block:
"""Block class to package all relevant data and helper methods."""
def __init__(self, blockchain, is_first_block=False):
"""Initiate the block under the assumption that it must either be
(1) The first block in the chain, in which case its data will be prepopulated or
(2) Any other block, in which case we'll be able to derive some fields from the
last block on the chain.
"""
if is_first_block:
# As the first block, its number will naturally be "1".
self.number = "1"
# Its previous block's hash will simply consist of zeros (64 zeros, since
# that's the number of characters in a sha-256 hash), data field will be blank
# and a nonce of "11316" will be used to create a hash that meets the required
# level of difficulty.
self.prev_hash = "0" * 64
self.nonce = "11316"
self.set_data("")
else:
# Since this is not the first block on the chain, we will need to derive some
# information from the last block on the current blockchain.
last_block = blockchain.get_last_block()
# Increment the last block's number to get its own.
self.number = int(last_block.number) + 1
# Base its last block's hash on the previous block's hash.
self.prev_hash = last_block.hash
# Required fields, which will be populated via separate methods later.
self.nonce = None
self.data = None
self.hash = None
def set_data(self, data):
"""Update the data of the block as well as its own hash."""
self.data = data
self.set_hash()
def set_prev_hash(self, prev_hash):
"""Update the previous hash field of the block as well as its own hash."""
self.prev_hash = prev_hash
self.set_hash()
def set_hash(self):
"""Create a simple block's hash by concatenating all required fields."""
block_fields = str(self.number) + \
str(self.nonce) + \
str(self.data) + \
str(self.prev_hash)
# Use Python's native SHA-256 function.
self.hash = hashlib.sha256(block_fields.encode("utf-8")).hexdigest()
def has_valid_hash(self):
"""Determine the validity of a hash, based on the number of zeros it starts with."""
difficulty = 4
return self.hash.startswith("0" * difficulty)
def mine(self):
"""Obtain a nonce for the block that leads to a valid hash, considering its other fields."""
# Start with a guess of "1", and increment it until we obtain a valid hash. At that point,
# store the last nonce value.
self.nonce = 1
while self.nonce:
self.set_hash()
if self.has_valid_hash():
break
self.nonce += 1
def __eq__(self, other):
"""Determine whether this block's fields are similar to the other's."""
return self.__dict__ == other.__dict__
def __str__(self):
"""Use to print the contents of the block in a helpful manner."""
output = "Block |"
output += "\tNumber\t\t=>\t" + str(self.number) + "\n"
output += "\tNonce\t\t=>\t" + str(self.nonce) + "\n"
output += "\tData\t\t=>\t" + str(self.data) + "\n"
output += "\tPrev. Hash\t=>\t" + str(self.prev_hash) + "\n"
output += "\tHash\t\t=>\t" + str(self.hash)
return output
class Blockchain:
"""Blockchain class to package all relevant blocks and helper methods."""
def __init__(self):
"""Create an empty list to store our chain of blocks, or blockchain!"""
self.blocks = []
def append_block(self, block):
"""Append a block object (defined above) to our chain of blocks."""
self.blocks.append(block)
def get_last_block(self):
"""Return the most recent block in the chain."""
return self.blocks[-1]
def validate(self):
"""Ensure that our blockchain is valid by:
(1) Verifying that every block has a valid hash. If an older block changes its
data and does not mine for a new nonce, then this check will fail.
(2) Verifying that every block's previous hash field links to the correct
previous block. If an older block is tampered with and its hash changes without
the knowledge of the next block in the chain, then this check will fail.
"""
# In a chain of blocks A -> B -> C, this variable is used to compare C's previous
# hash field against B's own hash field.
expected_prev_hash = None
# Iterate through the blocks, starting with the most recent one.
for block in reversed(self.blocks):
# Check (1) above. Specify and output the number of the guilty block!
if not block.has_valid_hash():
print("Invalid blockchain due to invalid hash.")
print("The guilty block is: #%s." % block.number)
break
# Check (2) above. Specify and output the number of the guilty block!
if expected_prev_hash and (block.hash != expected_prev_hash):
print("Invalid blockchain due to mismatched prev_hash.")
print("The guilty block is: #%s." % block.number)
break
# Store the current block's previous hash as stated above.
expected_prev_hash = block.prev_hash
# Conclude that our blockchain is valid if we didn't find any faulty blocks
# and, therefore, did not have to break out of the checking process.
else:
print("Valid blockchain!")
def copy(self):
"""Creates a full copy of the blockchain (including its blocks).
https://docs.python.org/3/library/copy.html#module-copy
"""
return copy.deepcopy(self.blocks)
def __eq__(self, other):
"""Determine whether this blockchain's blocks are similar to the other's."""
for count, block in enumerate(self.blocks):
if block != other.blocks[count]:
return False
return True
def __str__(self):
"""Use to print the contents of the blockchain in a helpful manner."""
output = "\n\n"
for block in self.blocks:
output += block.__str__() + "\n\n\n"
return output
We say that the blockchain is secure and immutable precisely because it is distributed and prepared to have tens of thousands of copies around the world (for instance, Bitcoin's blockchain).
In a network with this many copies of the blockchain, even if a single instance is compromised or tampered with, the data will endure via the other copies of the ledger.
We'll now create a DistributedBlockchain
object that will wrap our Blockchain
object above with information about its owner, or peer
. Note that this doesn't mean that the peer
will own the blockchain — since nobody does — but that she may have a copy of the full blockchain running on her own infrastructure.
class DistributedBlockchain:
"""Distributed blockchain class to package its peer, relevant blocks and helper methods."""
def __init__(self, peer, blockchain=None):
"""Keep track of the peer (owner of this instance of the blockchain) as well as on
the blockchain object above with the relevant blocks.
"""
self.peer = peer
# Store a blockchain if it's provided via the constructor.
if blockchain:
self.blockchain = blockchain
# If a blockchain is not provided to start it, start a fresh instance with a
# prepopulated initial block.
else:
self.blockchain = Blockchain()
first_block = Block(self.blockchain, is_first_block=True)
self.append_block(first_block)
def append_block(self, block):
"""Append a block object (defined above) to our chain of blocks."""
self.blockchain.append_block(block)
def __eq__(self, other):
"""Determine whether this blockchain is similar to the other's."""
return self.blockchain == other.blockchain
def __str__(self):
"""Use to print the contents of the distributed blockchain in a helpful manner."""
output = "Blockchain | " + str(self.peer) + "\n"
output += self.blockchain.__str__()
return output
We'll now dive into some examples, where three participants known as vivian
, joseph
and george
will have their own copies of a blockchain with three blocks.
Then, we'll go into the different scenarios that will show what happens when one party tries to deviate from the rest.
# Define a sample blockchain under Vivian's account.
vivian = DistributedBlockchain('Vivian')
# Create, mine and append a second block.
block2 = Block(vivian.blockchain)
block2.set_data('Tuesday')
block2.mine()
vivian.append_block(block2)
# Create, mine and append a third block.
block3 = Block(vivian.blockchain)
block3.set_data('Wednesday.')
block3.mine()
vivian.append_block(block3)
# Let's see how our copy of the blockchain is doing thus far.
print(vivian)
We'll now incorporate Joseph
and George
into our network. Say, for instance, that they've just learned about our incredible blockchain and are now looking to house it fully on their own machines in order to become miners.
The first step for them will be to make full copies of the blockchain, as shown below!
# Instantiate copies of Vivian's instance of the blockchain for Joseph and George.
joseph = DistributedBlockchain('Joseph', vivian.copy())
george = DistributedBlockchain('George', vivian.copy())
# We'll now print Joseph's and George's instances, in order to verify that we
# actually got full copies of Vivian's instance of the blockchain.
print(joseph)
print(george)
# We can also use our equality operators to verify that every block is similar.
print(joseph.blockchain == vivian.blockchain)
print(george.blockchain == vivian.blockchain)
print(joseph.blockchain == george.blockchain)
It's important to know that we're dealing with three distributed copies of the blockchain, not a single blockchain that any one of these parties may change.
The latter scenario would be extremely dangerous, since it'd mean that one party can modify the common ledger that we're talking about. Precisely, what's interesting about the blockchain is that we can trust parties we don't know to have copies of the same ledger.
What happens, then, if Joseph decides to modify the data in block #2? How will that affect what Vivian and George know about the same blockchain?
Let's try it out!
# Let's change the data of block #2 to say "Friday" instead of "Tuesday"
# in Joseph's blockchain. (Note that, since the list is implemented in Python,
# whose lists start with 0, the second block will be accessed via the 1 index).
joseph_block2 = joseph.blockchain.blocks[1]
joseph_block2.set_data('Friday')
# How does this impact Joseph's blockchain?
print(joseph)
Clearly, we can compare Joseph's blockchain against Vivian's and George's above to see that it has changed. Its second block is no longer similar to the other blockchain's, as its hash is not even valid!
# Visualize Vivian's and George's blockchains to verify that Joseph's differs.
print(vivian)
print(george)
# In fact, while Vivian's and George's remain similar, they don't
# match Joseph's!
print(vivian.blockchain == george.blockchain)
print(joseph.blockchain == vivian.blockchain)
print(joseph.blockchain == george.blockchain)
Now, as we saw in the previous hands-on sessions, it's important to mention that Joseph could re-mine his second block in order to obtain a valid hash, and then re-mine the third in order to incorporate the second block's hash in its prev_hash
field.
Let's see how that could fix Joseph's blockchain, and how it would compare to Vivian's and George's.
# Re-mine Joseph's second block and try to validate his full copy of the blockchain.
joseph_block2.mine()
joseph.blockchain.validate()
# Indeed, we have to set block #3's prev_hash field to match block #2's hash.
joseph_block3 = joseph.blockchain.blocks[2]
joseph_block3.prev_hash = joseph_block2.hash
joseph_block3.mine()
# Is Joseph's blockchain valid now?
joseph.blockchain.validate()
print(joseph.blockchain)
Joseph's blockchain is valid now, so we could think that he got away with changing the value of the data field in block #2 to "Friday", thereby violating the principle of immutability that we've emphasized the blockchain is all about!
However, that's not quite the case.
Even if Joseph's blockchain does not contain any invalid hashes or broken links, it's fairly useless insofar as it does not match Vivian's or George's versions.
# Compare Joseph's blockchain to Vivian's and George's.
print(joseph.blockchain == vivian.blockchain)
print(joseph.blockchain == george.blockchain)
Joseph has effectively forked his blockchain away from Vivian's and George's.
In order to effectively modify a block in his version of the blockchain and remain complicit with Vivian and George, he would have to modify Vivian's and George's versions as well.
Now, if we apply these concepts to a popular blockchain such as Bitcoin's, then Joseph would not only have had to hack into Vivian's and George's, but also the other tens of thousands of nodes in the network... an exceedingly difficult feat in terms of computing resources and cybersecurity.
We've now gone through the hands-on training sessions of BrainLab's "Blockchain for Absolute Beginners" course.
We started by exploring the concept and importance of hashing insofar as it allowed us to create digital signatures of our data and verify their immutability.
Then, we created blocks that encompassed block numbers, nonces, data and hashes into single items (which we carried onto the Block
object above).
Blockchains, represented by the Blockchain
object, taught us to tie blocks together via their prev_hash
fields, which also served to ensure their immutability.
Lastly, we saw how a distributed and replicated blockchain lets an unbounded number of parties come together in a network to maintain copies of the same ledger. And, by that logic, ensure that an individual who modifies the data in a single instance does not affect the data that everyone else has.