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: Blockchain (Non-Distributed)

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:

  • The 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).
  • The 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).
  • The 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.

In [68]:
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
In [69]:
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

Distributed Blockchain

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.

In [66]:
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

Examples

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.

In [87]:
# 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)
Blockchain | Vivian


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


Block |	Number		=>	2
	Nonce		=>	24765
	Data		=>	Tuesday
	Prev. Hash	=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf
	Hash		=>	0000b251bd673dd811aa01145d202b1a6a695a57e070cb5e83f542a39c219118


Block |	Number		=>	3
	Nonce		=>	67551
	Data		=>	Wednesday.
	Prev. Hash	=>	0000b251bd673dd811aa01145d202b1a6a695a57e070cb5e83f542a39c219118
	Hash		=>	000017c9d2fe10444fd5cc2d679e89162932b071511474a148072c90546ce025



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!

In [88]:
# 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)
Blockchain | Joseph


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


Block |	Number		=>	2
	Nonce		=>	24765
	Data		=>	Tuesday
	Prev. Hash	=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf
	Hash		=>	0000b251bd673dd811aa01145d202b1a6a695a57e070cb5e83f542a39c219118


Block |	Number		=>	3
	Nonce		=>	67551
	Data		=>	Wednesday.
	Prev. Hash	=>	0000b251bd673dd811aa01145d202b1a6a695a57e070cb5e83f542a39c219118
	Hash		=>	000017c9d2fe10444fd5cc2d679e89162932b071511474a148072c90546ce025



Blockchain | George


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


Block |	Number		=>	2
	Nonce		=>	24765
	Data		=>	Tuesday
	Prev. Hash	=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf
	Hash		=>	0000b251bd673dd811aa01145d202b1a6a695a57e070cb5e83f542a39c219118


Block |	Number		=>	3
	Nonce		=>	67551
	Data		=>	Wednesday.
	Prev. Hash	=>	0000b251bd673dd811aa01145d202b1a6a695a57e070cb5e83f542a39c219118
	Hash		=>	000017c9d2fe10444fd5cc2d679e89162932b071511474a148072c90546ce025



In [89]:
# 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)
True
True
True

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!

In [90]:
# 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)
Blockchain | Joseph


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


Block |	Number		=>	2
	Nonce		=>	24765
	Data		=>	Friday
	Prev. Hash	=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf
	Hash		=>	86191e900654a45b64a238e94d42562d3211f7f963d2e84ebb88e5aae370aeb1


Block |	Number		=>	3
	Nonce		=>	67551
	Data		=>	Wednesday.
	Prev. Hash	=>	0000b251bd673dd811aa01145d202b1a6a695a57e070cb5e83f542a39c219118
	Hash		=>	000017c9d2fe10444fd5cc2d679e89162932b071511474a148072c90546ce025



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!

In [91]:
# Visualize Vivian's and George's blockchains to verify that Joseph's differs.
print(vivian)
print(george)
Blockchain | Vivian


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


Block |	Number		=>	2
	Nonce		=>	24765
	Data		=>	Tuesday
	Prev. Hash	=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf
	Hash		=>	0000b251bd673dd811aa01145d202b1a6a695a57e070cb5e83f542a39c219118


Block |	Number		=>	3
	Nonce		=>	67551
	Data		=>	Wednesday.
	Prev. Hash	=>	0000b251bd673dd811aa01145d202b1a6a695a57e070cb5e83f542a39c219118
	Hash		=>	000017c9d2fe10444fd5cc2d679e89162932b071511474a148072c90546ce025



Blockchain | George


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


Block |	Number		=>	2
	Nonce		=>	24765
	Data		=>	Tuesday
	Prev. Hash	=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf
	Hash		=>	0000b251bd673dd811aa01145d202b1a6a695a57e070cb5e83f542a39c219118


Block |	Number		=>	3
	Nonce		=>	67551
	Data		=>	Wednesday.
	Prev. Hash	=>	0000b251bd673dd811aa01145d202b1a6a695a57e070cb5e83f542a39c219118
	Hash		=>	000017c9d2fe10444fd5cc2d679e89162932b071511474a148072c90546ce025



In [92]:
# 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)
True
False
False

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.

In [97]:
# Re-mine Joseph's second block and try to validate his full copy of the blockchain.
joseph_block2.mine()

joseph.blockchain.validate()
Invalid blockchain due to mismatched prev_hash.
The guilty block is: #2.
In [100]:
# 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)
Valid blockchain!


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


Block |	Number		=>	2
	Nonce		=>	117769
	Data		=>	Friday
	Prev. Hash	=>	000015783b764259d382017d91a36d206d0600e2cbb3567748f46a33fe9297cf
	Hash		=>	000047f3537304790973f92a56b10aeb73b1192e4e91badbcca9fc8a98ddf59c


Block |	Number		=>	3
	Nonce		=>	178815
	Data		=>	Wednesday.
	Prev. Hash	=>	000047f3537304790973f92a56b10aeb73b1192e4e91badbcca9fc8a98ddf59c
	Hash		=>	000007049a7f466d949996ec699edb8c1cf80bd9a17c8bf914c20afafc22e729



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.

In [101]:
# Compare Joseph's blockchain to Vivian's and George's.
print(joseph.blockchain == vivian.blockchain)
print(joseph.blockchain == george.blockchain)
True
False
False

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.

Congratulations!

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.