Building a minimal Blockchain in Ruby

You have probably heard about blockchain before and you are tired of asking yourself, “What the hell is it?”, so you want to start reading something about it. But, you have just ten minutes to spend between a coffee break and the next task on your Kanban board… well, then you have come to the right place!

Let’s start by explaining what a blockchain really is. A blockchain is a shared and immutable data structure. Exactly like a chain, each item (or block) is permanently linked to the next one. The first blockchain was born to keep track of the transactions of an incoming virtual cryptocurrency, known as Bitcoin. However, it’s important to keep in mind that the purpose of a blockchain is not limited to financial transactions, but can also be applied in many different areas like IoT, healthcare, supply chain, and so on.

Each block of the chain must contain the following elements:

  • Data of transactions
  • A unique fingerprint called hash
  • The hash of the previous block

A single block can contain one or more transactions. We can see a transaction as a simple money (or value) transfer action, from one user to another, identified by an ID and a time stamp.

To avoid the possibility of data manipulation, each block needs a unique fingerprint, computed by using the block itself as input. The hash function works exactly in this way, it generates a unique hash code by using the stored transactions. So, in the case of data manipulation, the comparison between the stored hash and the recalculated one will get a negative result back.

To validate the entire chain, we need to create a link between the blocks. We can obtain this feature by storing, for each block, the hash of the previous one. In this way, each hash code is based on the previous one, allowing the entire chain to be marked as invalid in case of the corruption of one or more blocks.

Another interesting feature of blockchain is that the entire data structure is shared with each user (or node), following the principles of the P2P (Peer-To-Peer) network architecture, thus making the possibility of manipulative actions more difficult. Every time a new block of transactions is created, the entire chain must be validated by all nodes and the identity of the senders must be verified. Only when all nodes are in agreement, the new block is added to the chain. This process is called consensus.

But how can the identity of the senders be verified? Every node inside the network has two different identification keys, one public, accessible by all nodes, and one private, known only by the owner. When a user creates a new transaction by transferring some money to another user, he signs the transaction data with his own private key. To validate the transaction, a single node has to ask for the sender’s public key. Once obtained, it will be able to validate the sender’s signature.

From a business point of view, we can see the blockchain as a shared, replicated ledger with smart contracts. Inside a business network, the assets transfer allows the building of value for a company. We can see an asset as anything capable of being owned or controlled to produce value. A blockchain can help to simplify the transfer of assets between stakeholders inside a business network, sharing between them a log of all transactions (ledger). Any transaction identifies an asset transfer between participants and can only occur under particular conditions (smart contracts).

Let’s take a look at an example

Now that we know more about blockchain, we are ready to take a look at a simple example. In this ready-to-use code repository, you can find a minimal blockchain structure, written in Ruby. Before starting, you need to install the Ruby interpreter on your computer by following these instructions. Also, I suggest you take a look at this 20 minutes tutorial about Ruby, to get started with this fantastic programming language.

From the root folder of the repository, run the following command by using a command prompt:

ruby run.rb

The execution could take some time. At the end of the process, you will be able to see the following logs:

Wallet A balance: 50.0$
Wallet B balance: 0$
Wallet C balance: 0$

Transaction from Wallet A to Wallet B of 10.0$
Transaction done, added new block to chain.

Wallet A balance: 40.0$
Wallet B balance: 10.0$
Wallet C balance: 0$

Transaction from Wallet B to Wallet A of 5.0$
Transaction done, added new block to chain.

Wallet A balance: 45.0$
Wallet B balance: 5.0$
Wallet C balance: 0$

Corrupted transaction from Wallet A to Wallet C of 45.0$

Error: invalid signature, the transaction will not be performed.
Error: invalid or corrupted chain, no block will be added.

Wallet A balance: 45.0$
Wallet B balance: 5.0$
Wallet C balance: 0$

What happened here? By taking a look at the end of the run.rb file, we can get more details:

First, a new chain is instantiated. A chain represents the immutable data structure that will be shared between nodes.

chain = Chain.new()

The Chain class exposes the add_block method, which allows adding a new block of transactions (in this example, each block is able to contain just one single transaction). Before adding the new block, the entire chain must be validated by using the is_chain_valid method. If the transaction has been processed in the right way (so the sender’s signature is valid), then the block is finally added to the chain.

def add_block(transaction)
    if is_chain_valid and transaction.process
        block = Block.new(blocks.last, transaction)
        blocks.push(block)
    else
        puts("Error: invalid or corrupted chain, no block will be added.")
    end
end

def is_chain_valid
    if(blocks.length > 0)
        blocks.last.is_block_valid
    else 
        true
    end
end

A single block takes the previous one and the transaction data as parameters. The method is_block_valid allows checking the block validity, comparing the stored hash with the recalculated one. By performing this action even for the previous blocks, it’s possible to check, in a recursive way, the entire chain validity. The calculate_hash method allows the unique fingerprint to be generated and stored inside the block.

def is_block_valid
    if hash.eql? calculate_hash
        if prev_block
            prev_block.is_block_valid
	else
	    true
	end
    else 
        false
    end
end

def calculate_hash
    hash = ""
    nonce = 0
    while not hash.start_with?("00000")
        hash = Digest::SHA256.hexdigest "#{prev_block.hash}#{time_stamp}#{transaction.id}#{nonce}"
        nonce += 1
    end
end

As you can see, inside the calculate_hash method, the hash is computed until the generated string starts with “00000”, requiring a large amount of CPU time. In this way, we are mining our blocks, implementing a proof-of-work system that aims to discourage the substitution of one or more blocks of the chain, since the computation of the hash, for each one of them, could require a high computational cost.

Any user/node inside the network can be identified by a wallet, which is a structure that allows users to keep track of incoming transactions (from another user), outgoing transactions (to another user), and to send funds or calculate the actual balance. Also, inside the wallet, it’s possible to store the user’s private and public keys.

def calculate_balance
    amount = initial_amount
    incoming_transactions.each do |incoming_transaction|
        amount += incoming_transaction.funds
    end
    outgoing_transactions.each do |outgoing_transaction|
        amount -= outgoing_transaction.funds
    end
    amount
end
	
def send_funds(receiver_wallet, funds)
    if calculate_balance >= funds
        signature = OpenSSL::PKey::RSA.new(private_key, PEM_PASSWORD).sign(OpenSSL::Digest::SHA256.new, "#{public_key}#{receiver_wallet.public_key}#{funds}")
        Transaction.new(self, receiver_wallet, funds, signature)
	end
end

A single transaction takes the sender’s and the receiver’s wallets as parameters, with the funds to transfer and the sender’s signature. The process method allows the validation of the sender’s signature by using his public key, then stores the transaction as an outgoing one inside the sender’s wallet, and as an incoming one inside the receiver’s wallet.

def process
    if is_singnature_valid
        sender_wallet.outgoing_transactions.push(self)
        receiver_wallet.incoming_transactions.push(self)
    else
        puts("Error: invalid signature, the transaction will not be performed.")
    end
end

def is_singnature_valid
    data = "#{sender_wallet.public_key}#{receiver_wallet.public_key}#{funds}"
    OpenSSL::PKey::RSA.new(sender_wallet.public_key, PEM_PASSWORD).verify(OpenSSL::Digest::SHA256.new, signature, data)
end

Back in the main process of this example program, after having instantiated the chain, three new wallets are created:

wallet_A = Wallet.new(50.0)
wallet_B = Wallet.new(0)
wallet_C = Wallet.new(0)

And two new transactions are made from Wallet A to Wallet B and vice-versa, obtaining the following result:

transaction_1 = wallet_A.send_funds(wallet_B, 10.0)
chain.add_block(transaction_1)

transaction_2 = wallet_B.send_funds(wallet_A, 5.0)
chain.add_block(transaction_2)

# Output:
# Wallet A balance: 45.0$
# Wallet B balance: 5.0$
# Wallet C balance: 0$

So far, everything works fine. But, if we try to make an unauthorised transaction from Wallet A to Wallet C by using a corrupted signature, the validation will fail and the transaction will not be added to the chain inside of a new block. As a result, a validation error will be shown:

corrupted_transaction = Transaction.new(wallet_A, wallet_C, 45.0, "ANY_CORRUPTED_SIGNATURE")
chain.add_block(corrupted_transaction)

# Output:
# Error: invalid signature, the transaction will not be performed.
# Error: invalid or corrupted chain, no block will be added.

And that’s it!
For practice, this example could be extended to support more of one transaction for each block. Try to do it! 😉

Happy coding!