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.

Recall: Blocks

In the previous hands-on session, we learned that a block is made up of a block number, nonce, data field and hash. We understood that the hash encapsulated all of the other fields, and went through the mining process of finding a new nonce.

We'll first recall all of what we learned about blocks and place the code in a neatly packaged Python Block object, which encapsulates our data and helper functions in one place. Later on, we'll define a Blockchain class that ties blocks together.

Let's define the Block object, read it over, and dive into the differences with respect to the previous section.

In [242]:
import hashlib


class Block:
    """Define a 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 on 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 __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

Linking Blocks Together

The main difference between this hands-on session and the previous one is that we now need to equip our blocks with the means to link to each other (after all, we've said that a "blockchain" is nothing more than a chain or list of blocks).

The Previous Hash Field

If you look at the definition of a block above, you'll be able to spot the block fields that we've all come to know and love: the number, nonce, data and hash fields.

But, there's an additional one at play here, which is called the "previous hash field" (prev_hash or prev for short). As you can see in the diagram below, it's what allows e.g. block #2 to know what the previous block in the chain is. Then, just like block #2 stores block #1's hash in its prev field, block #3 stores block #2's, and so on.

Just like the block number, the previous hash field lets us order the blocks in the blockchain chronologically.

Diving into the Code

The only real difference with respect to the last hands-on session is that our hashing function, set_hash under the Block object above, now takes into account the previous block's hash field in addition to the other fields we're familiar with.

That's right! Our current block's hash depends on the previous block's hash, which means that it depends on the previous block's fields, which means that it depends on the previous block's previous block's hash, which means that it depends on those fields, and so on.

Next, we'll see why that is, and why it's crucial for the integrity of the blockchain.

Why the Previous Hash Field Has to Be a Hash

At first, it was not clear to me that the previous field needed to be the hash of the previous block. Why would this be necessary, if all we're trying to do is order blocks chronologically? Furthermore, doesn't the block number already provide the insight as to which block goes where on the chain? Is this prev_hash field redundant, then?

"No, young grasshopper" — I tell my past self.

Recall that the entire assumption behind the blockchain is that it must be immutable. We don't want to develop a chain of blocks under the assumption that nothing will ever change and then allow a third-party to just modify an old block... regardless of how small that modification is.

That's where the concept of hashing comes again!

By storing the hash of the previous block, we have the certainty that nothing in the previous block will change. If something did change in the previous block, then, as we know, its hash would change. At that point, we'd be able to compare it against the hash we had stored for it and detect that something fishy is going on.

A Note About the First Block

You'll notice that, when we instantiate a new Block, the constructor asks if we're dealing with the first block in the blockchain.

Needless to say, the first block in the chain will not have much to say about its previous block's hash field, since it won't exist.

For that reason, we prepopulate its fields to have zeros for its previous hash, a block number of 1, an empty data field, and a nonce value 11316 in order to make sure that its hash still abides by our validity standards.

Assembling the Blockchain

Now that we have a notion of the way in which blocks are linked on a chain via the prev_hash field, we're ready to assemble the code for a blockchain.

We'll define a Blockchain object which, at its core, will contain a simple list and a series of helper functions to make our lives easier.

In [214]:
class Blockchain:
    """Define a 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 __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

Diving into the Code

We've defined methods to perform the following tasks:

  • Initialize our list of blocks: __init__(self)
  • Append a new block object: append_block(self, block)
  • Get the last block: get_last_block(self)
  • Print the contents in a helpful manner: __str__(self)

We've also define a validation method, called validate(self), that performs an integrity check over the entire blockchain (starting with the last block) and alerts us if there's a block that doesn't meet our criteria.

At this point, there are two checks that we're running on each block:

  1. To verify 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 because its hash will not start with four zeros, for instance.

  2. To verify 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 on the chain, then this check will fail.

At this point, we're ready to go through some examples.

Examples

In [243]:
# Initialize our blockchain as an empty list of blocks.
blockchain = Blockchain()

# Create the first block with an empty data block and the required fields.
# (inform the constructor that we're dealing with the first block).
block1 = Block(blockchain, is_first_block=True)

# Consequently, push the first onto the blockchain!
blockchain.append_block(block1)

# Let's print the blockchain to see our one block in action.
print(blockchain)

Block |	Number		=>	1
	Nonce		=>	11316
	Data		=>	
	Prev. Hash	=>	0000000000000000000000000000000000000000000000000000000000000000
	Hash		=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf



In [244]:
# Now we're ready to add block #2 to our blockchain. We expect to see its
# prev_hash field match the first block's.
block2 = Block(blockchain)

# We'll first set the data and then we'll mine to get a nonce that leads to
# a valid hash. Lastly, we'll append it to the blockchain.
block2.set_data('Tx #318: 129899889')
block2.mine()
blockchain.append_block(block2)

# Let's see how our blockchain is looking, and validate it via our automated checks.
print(blockchain)
blockchain.validate()

Block |	Number		=>	1
	Nonce		=>	11316
	Data		=>	
	Prev. Hash	=>	0000000000000000000000000000000000000000000000000000000000000000
	Hash		=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf


Block |	Number		=>	2
	Nonce		=>	298302
	Data		=>	Tx #318: 129899889
	Prev. Hash	=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf
	Hash		=>	000005c7f16eb223212b02b7dbbc4526799ddec2bf39119447b467274bab0da6



Valid blockchain!

Our blockchain is looking good so far, so let's add a third block and prepare to see what happens when we modify data that should not be modified in an immutable ledger!

In [245]:
# Create and append block #3.
block3 = Block(blockchain)
block3.set_data('Once upon a time...')
block3.mine()
blockchain.append_block(block3)

# How's our blockchain looking so far?
print(blockchain)

Block |	Number		=>	1
	Nonce		=>	11316
	Data		=>	
	Prev. Hash	=>	0000000000000000000000000000000000000000000000000000000000000000
	Hash		=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf


Block |	Number		=>	2
	Nonce		=>	298302
	Data		=>	Tx #318: 129899889
	Prev. Hash	=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf
	Hash		=>	000005c7f16eb223212b02b7dbbc4526799ddec2bf39119447b467274bab0da6


Block |	Number		=>	3
	Nonce		=>	66286
	Data		=>	Once upon a time...
	Prev. Hash	=>	000005c7f16eb223212b02b7dbbc4526799ddec2bf39119447b467274bab0da6
	Hash		=>	0000e7687a7b11f4512eca9aaa8e985049e42c946b00b5a47b0a4aaff15878f4



Everything looks good, insofar as each block's previous hash field points to the last block, and our data is what we want it to be. What happens, though, if we decide to change the data field of Block #2?

In [246]:
block2.set_data('New data for block #2!')

print(blockchain)
blockchain.validate()

Block |	Number		=>	1
	Nonce		=>	11316
	Data		=>	
	Prev. Hash	=>	0000000000000000000000000000000000000000000000000000000000000000
	Hash		=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf


Block |	Number		=>	2
	Nonce		=>	298302
	Data		=>	New data for block #2!
	Prev. Hash	=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf
	Hash		=>	c84bfcb8e34b4d65937fb2d62cb8b8fafa5cd9c9a2558530893342447b6ddc89


Block |	Number		=>	3
	Nonce		=>	66286
	Data		=>	Once upon a time...
	Prev. Hash	=>	000005c7f16eb223212b02b7dbbc4526799ddec2bf39119447b467274bab0da6
	Hash		=>	0000e7687a7b11f4512eca9aaa8e985049e42c946b00b5a47b0a4aaff15878f4



Invalid blockchain due to invalid hash.
The guilty block is: #2.

Aha! We changed the data field of block #2 and its hash changed to one that doesn't meet our validity standards (in other words, it doesn't start with four zeros).

Well, no problem — we can re-mine block #2 and get a hash that does meet our standards! Let's see if that fixes our blockchain...

In [247]:
block2.mine()

print(blockchain)
blockchain.validate()

Block |	Number		=>	1
	Nonce		=>	11316
	Data		=>	
	Prev. Hash	=>	0000000000000000000000000000000000000000000000000000000000000000
	Hash		=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf


Block |	Number		=>	2
	Nonce		=>	173283
	Data		=>	New data for block #2!
	Prev. Hash	=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf
	Hash		=>	0000730421fb42485b90af10681499f8902579fc3568577e2fa9a553693885b4


Block |	Number		=>	3
	Nonce		=>	66286
	Data		=>	Once upon a time...
	Prev. Hash	=>	000005c7f16eb223212b02b7dbbc4526799ddec2bf39119447b467274bab0da6
	Hash		=>	0000e7687a7b11f4512eca9aaa8e985049e42c946b00b5a47b0a4aaff15878f4



Invalid blockchain due to mismatched prev_hash.
The guilty block is: #2.

We were able to get a new hash for block #2, but our blockchain was still declared to be invalid by our automatic validator! Can you see why?

It's because the hash that block #3 has in its prev_hash field does not match the hash that block #2 has for itself. Everybody's hash starts with four zeros as we'd expect, but that's not good enough because we know that a previous block has been modified.

All we can do at this point is to reset block #3's prev_hash field and re-mine it (in other words, get a valid nonce) in order to make sure that our blockchain is valid again.

In [248]:
block3.set_prev_hash(block2.hash)
block3.mine()

print(blockchain)
blockchain.validate()

Block |	Number		=>	1
	Nonce		=>	11316
	Data		=>	
	Prev. Hash	=>	0000000000000000000000000000000000000000000000000000000000000000
	Hash		=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf


Block |	Number		=>	2
	Nonce		=>	173283
	Data		=>	New data for block #2!
	Prev. Hash	=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf
	Hash		=>	0000730421fb42485b90af10681499f8902579fc3568577e2fa9a553693885b4


Block |	Number		=>	3
	Nonce		=>	20230
	Data		=>	Once upon a time...
	Prev. Hash	=>	0000730421fb42485b90af10681499f8902579fc3568577e2fa9a553693885b4
	Hash		=>	0000f372ab112114f331a1dde72df62116bb72c7f12b04da2c5d20e72c347a9a



Valid blockchain!

At this point, we've restored our blockchain and learned that, in order to make it valid, every block's prev_hash field has to match its predecessor's hash field and have a valid hash of its own.

We tampered with an old block in our blockchain (block #2), brought it to a halt, and learned that we could restore it simply by modifying and remining block #3.

In the next hands-on session, we'll see how this wouldn't help us in the "real world", since modifying our own blockchain would have no impact on other people's distributed copies.

Next Steps: Distributed Blockchain!

Now that we understand how blockchains works, we'll move onto why we need to have distributed copies of them.