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: Hashing

Before we jump into blocks, we're going to recall what we learned about hashing in the previous hands-on session (and redefine the relevant functions).

We'll take things a step further in this session, as we dive into blocks. Then, in the next session, we will move over to blockchains (or chains of blocks) and distributed copies.

In [16]:
import hashlib


def create_hash(a):
    """Return the sha256 of the provided string."""
    
    return hashlib.sha256(a.encode('utf-8')).hexdigest()


def print_hash(a):
    """Print the sha256 of the provided string."""
    
    print(("create_hash('%s')\t=>\t" % (a)) + create_hash(a) + '\n')

Blocks

Now that we understand that hashing underpins the blockchain, we'll take that knowledge and use it to build blocks, which package all relevant fields about a set of transactions into a single unit. We'll dive into the blocks that, when chronologically ordered, make up the chain of blocks — or blockchain!

What Makes Up a Block

Though can store any set of transactions in the "data" field, we also have two other fields to contend with now: block number and nonce. Both are integers!

These fields will be concatenated in order to make up a hash for the entire block. Then, based on the structure of said hash, we'll either be able to declare its block as "valid" or not.

In [118]:
def create_block_hash(number, nonce, data):
    """Create a simple block's hash by concatenating all required fields."""
    
    block_fields = str(number) + str(nonce) + str(data)
    
    return create_hash(block_fields)


def print_block(number, nonce, data, block_hash):
    """Print the contents of the block in a helpful manner."""
        
    output = "Block |"

    output += "\tNumber\t\t=>\t"   + str(number)     + "\n"
    output += "\tNonce\t\t=>\t"    + str(nonce)      + "\n"
    output += "\tData\t\t=>\t"     + str(data)       + "\n"
    output += "\tHash\t\t=>\t"     + str(block_hash) + "\n"

    print(output)

Validity of a Block

We know that the hash of a block is made up of its block number, nonce and data field.

The problem is that not every hash is valid. In fact, it's purposefully difficult to find a hash that is valid. That's because, if it were easy to obtain a valid hash, blocks would appear so quickly that the network would grow too quickly and become unstable.

Fortunately, it's very easy to determine what makes a hash valid or invalid... in fact, it can be as simple as counting the number of leading zeros. For the sake of this exercise, we'll assume that a hash needs to start with four leading zeros to be valid, and create the following function:

In [119]:
def is_valid_hash(block_hash, print_output=False):
    """Determine the validity of a hash, based on the number of zeros it starts with."""
    
    difficulty = 4
    is_valid = block_hash.startswith("0" * difficulty)
    
    if print_output:
        print('Got a valid hash!') if is_valid else print('Not a valid hash!')
        
    else:
        return is_valid

Why the Nonce

We'll now talk about what the purpose of the nonce is.

So far, we know that the hash will have to encompass the block number as well as the data field. Hashing them will produce a fixed string of 64 characters that will not change as long as the block number and data field remain constant... which they will.

Therefore, what is the field that we can change in order to try to obtain a different hash — one that meets the validity criteria we established above? The nonce!

The nonce — a number that is only used once — is an arbitrary integer that we'll try to guess over and over until we find a hash that meets our criteria.

This is a clever process, which is simply based on the knowledge that a different piece of content will lead to a different hash. But, since we don't know what that value should be, and we don't want to modify our data or block number, then we introduce an abitrary new number (the nonce).

Furthermore, since we don't know what number we'll have to guess in order to find a nonce that makes the hash work, we'll have to automate the job of finding it... and we'll call this mining.

In [120]:
def mine_block(number, data):
    """Obtain a nonce for the block that leads to a valid hash, considering its number and data."""
    
    # Start with a guess of "1", and increment it until we obtain a valid hash. At that point,
    # return the last nonce value.
    nonce = 1

    while nonce:
        block_hash = create_block_hash(number, nonce, data)

        if is_valid_hash(block_hash):
            return str(nonce)

        nonce += 1

Examples

We'll try to create the first block with a data field that consists of the following transactions: "Tx #1: 12312312, Tx #2: 0892179, Tx #3: 5898219" and the number "1".

First we'll have to find a nonce that works under these circumstances (this function may take a while to run, since we won't be happy until we find a hash that has four leading zeros).

In [121]:
# Define the initial parameters of our block.
block_number = "1"
block_data = "Tx #1: 12312312, Tx #2: 0892179, Tx #3: 5898219"

# Start mining and get a nonce!
block_nonce = mine_block(block_number, block_data)

# Obtain a digital signature (a hash) of the contents of our block.
block_hash = create_block_hash(block_number, block_nonce, block_data)

# Print the contents of the block.
print_block(block_number, block_nonce, block_data, block_hash)

# Verify that our block is valid by analyzing its hash.
is_valid_hash(block_hash, print_output=True)
Block |	Number		=>	1
	Nonce		=>	48276
	Data		=>	Tx #1: 12312312, Tx #2: 0892179, Tx #3: 5898219
	Hash		=>	000008f9aa9e55fb926611a0f85df08a558a8475ab3914fcdb245f56b007654d

Got a valid hash!

We'll now see that, by changing the data in the slightest way — say, by appending another transaction to our list — the nonce becomes completely different:

In [108]:
# Assemble our new block of data with an additional transaction.
new_block_data = "Tx #1: 12312312, Tx #2: 0892179, Tx #3: 5898219; Tx #4: 79977187"

# Try to reuse the old nonce to obtain a hash (hint: the old nonce won't work!).
new_block_hash = create_block_hash(block_number, block_nonce, new_block_data)

# Print the contents of the block.
print_block(block_number, block_nonce, new_block_data, new_block_hash)

# See that the new hash is not valid, because we didn't bother to mine for a new nonce.
is_valid_hash(new_block_hash, print_output=True)
Block Number	=>	1
Block Nonce	=>	48276
Block Data	=>	Tx #1: 12312312, Tx #2: 0892179, Tx #3: 5898219; Tx #4: 79977187
Block Hash	=>	6b9cb4de0dfe7f76dc9a8ed8f5515420e18777c06248485f69a3c229b90b45bd

Not a valid hash!

We just have to make sure that we mine for a new nonce every time we modify a field in the block, such as the number or the data. Then, we'll be able to obtain a valid hash!

In [113]:
# Mine for a new nonce.
new_block_nonce = mine_block(block_number, new_block_data)

# Override our invalid hash with one that encompasses the new nonce.
new_block_hash = create_block_hash(block_number, new_block_nonce, new_block_data)

# Print the contents of the block.
print_block(block_number, new_block_nonce, new_block_data, new_block_hash)

# Verify that the new hash is valid!
is_valid_hash(new_block_hash, print_output=True)
Block Number	=>	1
Block Nonce	=>	19479
Block Data	=>	Tx #1: 12312312, Tx #2: 0892179, Tx #3: 5898219; Tx #4: 79977187
Block Hash	=>	0000d076431ab7c887ac8bb78584d19a43e300d2e9080e2cfcd07189138af779

Got a valid hash!

Next Steps: Blocks!

Now that we understand how hashing works, we'll move onto how it's used with blocks.