Introduction

Even putting aside the hype around cryptocurrencies, it’s impossible to deny how big an innovation they are. Computers and the internet brought us free and unlimited copies of data: You can create a file and send it to thousands of people for free. The invention of blockchains brings scarcity to the internet: If you have a coin, only you have it.

Through this guide, we’ll go over what a cryptocurrency is by building one from scratch in Ruby. If you don’t know Ruby, don’t worry, it’s a surprisingly simple language, and as long as you have a bit of coding knowledge, you’ll follow along just fine.

Building first a blockchain and then the cryptocurrency will give us a good understanding of what makes it work and its safety and scalability challenges In this guide, each step is explained and then coded. If you want to have a look at the end result right away, check out the Github repository of Melon Coin, the cryptocurrency we’ll be building. Just keep in mind that the code is optimized for learning, not for scalability or security. Numerous changes would be necessary for it to go into production that we discuss in the last part of this guide.

I would also recommend you have a look at the original Bitcoin whitepaper. Unsurprisingly, it is very helpful when starting to build a cryptocurrency.

With that out of the way, let’s start coding!

The blockchain

The most key concept to understand when starting a cryptocurrency is the blockchain. As the name implies, everything relies on a chain of blocks that goes back to the cryptocurrency launch and continually churns out new blocks.

It’s a chain because every block references the previous block. Blocks each have a hash that is dependent on their content. Because the hash of the previous block is stored in the block itself, its own hash depends on the value of the previous block. If a block in the chain is altered, all of the hashes will change (we’ll soon see how this security is done).

So let’s build a chain of blocks!

blocks = []

height = 0
loop do
  height += 1

  blocks << {
    height: height,
    last_block: height - 1,
    transactions: transactions_for_this_block,
  }
end

In this example, “height” is the number of the block based on its position in the blockchain. The first block has a height of 1, the second 2, etc.

This minimalist system has, of course, a big downside: Nothing prevents the edition of a past block. So even though all blocks reference the previous one, they are valid even if altered. Although technically a blockchain, it isn’t a secure one. That’s what we need to change.

Making the blockchain unalterable

A recurring tool in blockchains is cryptographic hash functions and in particular, SHA-256, which we’ll be using a lot in the rest of this code. Those functions take an input and return an output that has a fixed size. The key properties you should be aware of are:

  • An input always returns the same hash
  • Different messages can’t have the same hash
  • You can’t use the hash to know anything about its input (ideal for privacy)
  • The input can’t be formatted in a way that will lead to a specific output (we’ll use this for mining)

With this in mind, we can make the blockchain unalterable by hashing the content of the blocks. To use our previous code:

require "digest"

blocks = []

height = 0
loop do
  height += 1

  block = {
    height: height,
    # We're now using the last block's hash instead of its height.
    last_block: blocks.last&.hash,
    transactions: transactions_for_this_block,
  }

  block["hash"] = Digest::SHA256.hexdigest(block.to_json)

  blocks << block
end

The main change here is the hashing of the entire block as a JSON. We call this the block hash. Any change to the block, including its transactions, will change its block hash. And because future blocks use the block hash inside their block that is then itself hashed, you can’t edit older blocks without breaking the entire chain.

Because hashes have a small size (and fixed size) compared to their content, it also means that it’s easy for participants in a blockchain to keep a record of all previous blocks without having to store the entire blockchain. They can only store the block hashes (or, in practice, the header of the block as we’ll soon do), and even though they can’t by themselves know what’s inside those blocks, they can at least know if someone is trying to rewrite history as hashes wouldn’t match.

Agreeing on a single blockchain with Proof of Work

We’ve created a blockchain that can’t be tampered with. Now, we need to collaborate with others to end up with a single blockchain instead of one per miner running the code.

With typical currencies, sources of truth such as banks keep a record of transactions. There’s no single source we can trust in decentralized blockchains; therefore, we need a way to make all participants agree on what constitutes the actual blockchain.

The solution Satoshi Nakamoto found is to make the creation of blocks challenging. Every miner would compete to finish the challenge before the others. As soon as one completes the challenge, it tells the other miners, and everyone moves on to the next block to start a new challenge. In this scenario, it takes a lot of work to complete the challenge, but as we’ll implement, later on, it’s trivial for other miners to confirm the challenge is completed. This is what is called Proof of Work.

Again, hashes provide us with the solution. In our previous example, our block hash has an hexadecimal representation meaning it’s a string of the characters 0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f. As we said previously, it’s impossible to change the input of the hashing function to get a specific output. The only way to know the output is to run the function.

We can use this to create a challenge: A block would only be valid if it starts with a certain number of “0”s. Because the same input will always return the same output, we need to change the block slightly at each iteration to get a different hash. This tiny variation is called a nonce, an arbitrary value that only serves to change the hash. We’ll then try a lot of different nonces until we find one that starts with enough 0.

require "digest"

blocks = []

height = 0
loop do
  height += 1

  block = {
    height: height,
    # We're now using the last block's hash instead of its height.
    last_block: blocks.last&.hash,
    transactions: transactions_for_this_block,
  }

  # We'll loop until we find a hash that starts with four '0'.
  loop do
    # At each iteration, we're increase the nonce by one. This
    # will change the hash and give us another change at finding
    # the right block hash.
    block["nonce"] = block["nonce"].to_i + 1
    block["hash"] = Digest::SHA256.hexdigest(block.reject { |k| k == "hash" }.to_json)

    if block["hash"][0, 4] == "0000"
      # We found a block that passes the challenge!
      break
    end
  end

  blocks << block
end

Later on, we’ll add interactions with other miners: If we finish the challenge, we need to tell others about the block. If someone else finishes the challenge before us, we need to know about it as soon as possible as continuing to work on the current block would just be a waste of time (since everyone agrees on using the longest available blockchain as it is the one that has had the most work put into it).

In our example, the challenge requires finding a hash starting with four 0. This is relatively easy for a computer. In practice, the difficulty would need to be adjusted based on the number of participants. Increasing the level of difficulty leads to more time between block creations. To understand the pros and cons behind changing the levels of frequency, read this question on StackExchange.

Note that this entire challenge relies on everyone agreeing on our convention. It’s only because enough machines agree to the rules of the game that it works. In a way, it’s very similar to how fiat currencies work: If enough people agree to the rules, then it exists.

Putting it together in a small application

Our pseudo-code can now be turned into a real application. We’ll need in particular to add a way of storing blocks in case the application restarts and for future developments. For this application, we’ll be using SQLite for its ease of use. We’re also creating a couple of Ruby classes to organize the code. Have a look at commit 501fe5 for more details.

Transactions

Now that we have a blockchain let’s use it by adding data to the blocks. Our goal is to keep what we achieved in the previous chapter: just as the blocks themselves, it shouldn’t be possible to alter the data associated with each block. This feature makes cryptocurrencies possible as we’ll have a ledger of transactions that everyone can agree on.

We’ll be calling the data we add to the block transactions. For this chapter, they will be simple blocks of text. As you’ll see, ensuring that those transactions can’t be altered is most of the work and what enables more complex use-cases such as cryptos.

Adding text transactions to the blocks

Adding text to the block is easy. In our pseudo-code, it’s a matter of adding a transaction field (which has been the case in past examples). Since the hash of the block includes the entire block, the transactions are already unalterable! To alter the transactions, you would need to find the same hash as the original block, which is impossible.

Understanding the use-case for Merkle trees

We could say we’re done with adding transactions — and if you’re only trying to get a very high-level view of the topic, you can move on to the next chapter. But our current implementation requires more storage than it should. This is a problem both for:

  • Nodes that keep the entire transaction history: Every transaction that ever happened would need to be stored if we ever want to recompute the block hash successfully. As the blockchain grows, this might not be sustainable. Having a way to prune older transaction data that no longer matters would help partially fix this problem.
  • Validating a recent transaction means downloading the entire block: Again, without all the transactions, it’s impossible to get the block hash and, therefore, to validate the data. For less powerful devices or where bandwidth is at a premium, this could be a problem depending on the size of blocks. Figuring out a solution would enable Simplified Payment Verification as described in the original Bitcoin whitepaper, where a node only keeps track of headers and validate transactions individually. This approach isn’t as safe as running a full node but is a good compromise, for example for mobile payments.

The solution envisioned in the Bitcoin whitepaper is to use a Merkle tree.

A Merkle tree also relies on hashes. In particular, the fact that the output of a hash is of a fixed size and that different inputs can’t share the same output. Instead of hashing all transactions at once, it gradually constructs a tree. Let’s look at a small example (the diagrams are from the Bitcoin Book’s explanation of Merkle which I recommend if you want more details):

A small example of Merkle tree

To get to the Merkle root (which is our equivalent of the hash containing all transactions), we start at the bottom: Every transaction is grouped by pair. After each transaction is hashed, pairs are hashed together, forming what is called a leaf. For example HAB = Hash(HA + HB). Leaves are then hashed together until we reach a final hash which is our Merkle root.

As we’re using hashes, any change to transactions or to their order changes the Merkle root. So as long as the Merkle root is included in the block’s data that we hash, we can guarantee the integrity of the transactions.

With a Merkle tree, we need less information to confirm a transaction is part of a block. With a naïve hash, we would need all of them, but now, we can work with a lot less data. Again an example from the Bitcoin Book:

Merkle tree is more efficient to validate only one transaction

If we’re trying to validate that transaction K is part of the block, using Transaction K and the hash of transaction L (HL), we can build a leaf and find HKL. We now need only 3 other hashes (HABCDEFGH, HIJ and HMNOP) to arrive back at the Root. That’s a lot less information! Consider as an example that Bitcoin blocks hold more than 2,000 transactions, and you can see the impact of this approach.

Implementing a Merkle tree

We could implement a Merkle tree from scratch, but we’re going to use a Ruby gem for the purpose of simplicity. The aptly named merkle_tree gem is a great implementation.

Going back to our pseudo-code, we are going to make a significant change: Instead of having our block hash include all the data of the block, it’s only going to use a few fields. In particular, transactions aren’t going to be part of the hash anymore. Instead only the Merkle root will be part of the hash. As always, with decentralized blockchains, we need to revalidate to confirm a bad actor isn’t tricking us. Only including the Merkle root will confirm a transaction is part of a block without downloading all transactions, as we saw in the previous section.

require "digest"
require "merkle_tree"

blocks = []

height = 0
loop do
  height += 1

  # We'll see later on to get those transactions. For now,
  # let's assume this method returns an array.
  transactions = fetch_transactions

  block = {
    height: height,
    # We're now using the last block's hash instead of its height.
    last_block: blocks.last&.hash,
    transactions: transactions,
    merkle_root: MerkleTree.new(*transactions).root.value,
  }

  # We'll loop until we find a hash that starts with four '0'.
  loop do
    # At each iteration, we're increase the nonce by one. This
    # will change the hash and give us another change at finding
    # the right block hash.
    block["nonce"] = block["nonce"].to_i + 1

    # The block header hash includes transactions indirectly through the Merkle
    # root. Thanks to the properties of hashes, it offers the same level of safety
    # while being more practical for users of the blockchain.
    block_header = block["nonce"] + block["last_block"] + block["merkle_root"]
    block["hash"] = Digest::SHA256.hexdigest(block_header)

    if block["hash"][0, 4] == "0000"
      # We found a block that passes the challenge!
      break
    end
  end

  blocks << block
end

Wallets and signing transactions

We have a blockchain. Let’s make that into a cryptocurrency.

A refresher on public-key cryptography

Public-key cryptography is a system based on public and private keys. Anyone can create keys. As the name implies, the private key isn’t shared with anyone but has a few interesting properties:

  • The public key can be obtained from the private key, but the reverse isn’t true. This means you can safely share your public key without revealing your private key.
  • Your private key can sign messages. Anyone can then verify that the signature matches a public key. Without the private key associated with the public key, it’s impossible to sign the message.
  • When done correctly, just as with hashes, private and public keys are unique. If you generate a new private key, nobody else is going to have the same.

Those two features are what we’ll need to get started building. If you want to know more, Wikipedia’s article on public-key cryptography is a good start.

Using public-key cryptography to send and receive coins safely

As we saw, private keys, and by consequence public keys, are unique. We can therefore use them as addresses (similar to a bank account number). When sending coins, we’ll be saying, “I’m sending x coins to this public key, and only someone able to prove ownership of the public key (via the private key) can use them.” Later on, the public key owner can send a transaction to move the coins and sign this transaction with a private key. Anyone can then compare the signature and the public key to confirm the ownership.

Implementing this type of cryptography is complex, likely to lead to mistakes, and well beyond my skills. So we’ll be using the ruby_ecdsa gem.

Before starting to write code, we need to explain how we’re going to work with addresses in more detail.

There exists numerous cryptographic algorithm. They might not have the same format of public keys. This is problematic as having standardized addresses is a lot more practical. We also want addresses that are easy to type. For this reason, we’re going to be hashing all addresses. This will ensure they all have the same format. As we’ll see, we’ll still be able to send coins securely. This approach also has the added benefit of increasing privacy and safety if someone in the future figures out how to retrieve more information than expected from public keys.

We can now write an example of transactions sending coins from one address to another:

require "ecdsa"
require "securerandom"

# 1. We generate a private key.
group = ECDSA::Group::Secp256k1
private_key = 1 + SecureRandom.random_number(group.order - 1)
# 2. The public key is obtained from the private key.
public_key_coordinates = group.generator.multiply_by_scalar(private_key)
public_key = ECDSA::Format::PointOctetString.encode(public_key_coordinates)

message = {
  # The sender's public key is encoded in B
  from: Base58.binary_to_base58(public_key),
  # The destination is the SHA256 of the public key.
  destination: "27a84712e4b22c415fc544d55cdee82327a829f96d03329457f76ebf9af4dcaa",
  amount: "1.2119"
}

# 3. We create the signature so anyone can validate we own the coins being sent.
# We sign the hash of the message instead of the complete message for performance
# reasons.
digest = Digest::SHA2.digest(message.to_json)
signature = nil
temp_key = 1 + SecureRandom.random_number(group.order - 1)
signature = ECDSA.sign(group, private_key, digest, temp_key)
# The signature is encoded as a DER string and then in Base 58.
encoded_signature = ECDSA::Format::SignatureDerString.encode(signature)

# 4. We add a transaction ID.
id = Digest::SHA256.hexdigest(encoded_signature)

# 5. We have a complete transaction!
transaction = {
  id: id,
  signature: encoded_signature,
  message: message,
}

As you see, we encoded data using Base 58 twice. This is because transactions are stored as JSONs, and we don’t want binary data within them. It’s up to us to then decide on the binary-to-text encoding we want to use. Base 58 was invented by Satoshi Nakamoto for Bitcoin and is very similar to Base 64 with a couple of key advantages:

  • It only includes alphanumeric characters, which makes it easy to copy the entire string by double-clicking
  • Ambiguous characters (0,o,O,l,i) are avoided

A transaction ID is included in the transaction. Having an ID is essential to reference transactions, and by using a hash of the signature, we know it will be unique. Miners not allowing two transactions with the same ID avoids double-spending:

Preventing double-spending:
When users create and sign transactions, they are broadcasted to members of the network. There needs to be a solution to ensure the transactions aren’t executed twice. This responsibility will fall on the miners in our implementation as they will keep track of transaction IDs. A block containing a transaction ID that was already seen in the future would be invalid.
We can see already a scaling problem: Keeping track of all transaction IDs will become expensive over time, and asking miners to keep billions of IDs in memory isn’t practical. An easy fix would be to have an expiration time to the transaction (with a reasonable upper limit at, for example, one week). This way, miners could only store transaction IDs less than a week old and still avoid double-spending. It’s also better for users: They are no ways of canceling transactions actively, so a time limit ensures there isn’t a risk of a transaction being executed six months in the future.

Mining rewards

Mining rewards are the incentive that keeps the network running: There’s a cost (often significant) in running a node for a cryptocurrency. Without a predictable way of making revenues, mining wouldn’t happen, and the integrity of the blockchain would vanish.

The most common reward mechanism and the one chosen by Bitcoin is giving miners fees from each transaction and creating coins for each block they create. Those rules are perfectly malleable, and it’s up to the designers of a cryptocurrency to make an arbitrage between fees, motivating miners, and inflation.

We’re going to go with the creation of one coin for every block mined. Miners will also get the total of all fees of the transactions in the block.

A mining reward transaction message would be:

message = {
  # We add a type to the message. This isn't strictly necessary but we're here to
  # learn! And this will help debug if we need to.
  type: "mining_reward",
  # There's no sender as this is a mining reward.
  from: nil,
  destination: "27a84712e4b22c415fc544d55cdee82327a829f96d03329457f76ebf9af4dcaa",

  # The amount is 1 coin + SUM(fees of all transactions)
  amount: "7.2861"
}

Of course, we’ll need to make a few changes to regular transactions, such as adding fees. Here’s how it looks in the final application: transaction_builder.rb.

There isn’t anything more to do for now. What makes this work is the validation that every miner will perform on every block. If a miner tries to inflate the reward, the block will be rejected. A miner not following the rule is just wasting resources and, therefore, money.

P2P network: Working with other miners

Networking is a critical aspect of cryptocurrencies. We need numerous computers to work together even though they do not trust one another. For our purpose, this will mainly mean validating blocks extensively. For a cryptocurrency to be production-ready, there would be a lot more work involved, but we’ll skip most of it for this guide.

To keep things simple, we’re going to add a small API using Sinatra. It will then be up to the miners to regularly connect to their peers to see if new blocks have been mined.

Receiving pending transactions

We want our node to receive pending transactions meaning transactions that coin holders would like to see added to the blockchain. Coin holders would create a transaction, as we saw in the last chapter, and send its information to a decent amount of miners to maximize the chances of it being quickly added to the blockchain.

Miners are incentivized to process the transaction as they keep the associated fee. This also means a sender can increase its chance of being quickly processed by increasing the fees.

Note that from now on, I’ll stop using pseudo-code and instead use parts of the final application. Thankfully, Ruby is easy to understand.

To receive transactions, we need a new API endpoint:

# From node.rb: https://github.com/antoinefink/melon/blob/master/node.rb

post "/transactions/submit" do
  transaction = JSON.parse(request.body.read)

  if transaction["message"]["type"] != "funds_transfer"
    halt(400, "Transaction has an invalid type")
  end

  # We verify that sender is the actual owner of the coins. This is done by
  # confirming the signature of the message is valid. Only someone with the
  # private key would be able to sign the message of the transaction.
  if @@blockchain.valid_cryptography?(transaction) == false
    halt(400, "Transaction is cryptography is invalid")
  end

  # The miner fetches the amount of coins available to the wallet sending
  # funds and ensures the balance doesn't fall bellow 0.
  if @@blockchain.address_has_enough_funds?(transaction["message"]) == false
    halt(400, "The sender's address doesn't have enough funds")
  end

  # The transaction looks good! We add it to the pending transactions list.
  PendingTransaction.create(transaction)

  $logger.info("Received one pending transaction (#{transaction["id"]})")

  "accepted"
end

# From blockchain.rb: https://github.com/antoinefink/melon/blob/master/blockchain.rb

def valid_cryptography?(transaction)
  # The signature of the message needs to be valid and come from the sender.
  if transaction["id"] != Digest::SHA256.hexdigest(transaction["signature"])
    $logger.warn("Transaction ID isn't the SHA256 of the signature")
    return false
  end

  # The signature should be valid.
  if transaction["message"]["type"] != "mining_reward"
    public_key = ECDSA::Format::PointOctetString.decode(
      Base58.base58_to_binary(transaction["message"]["from"]),
      ECDSA::Group::Secp256k1,
    )

    digest = Digest::SHA256.digest(transaction["message"].sort.to_h.to_json)

    signature = ECDSA::Format::SignatureDerString.decode(
      Base58.base58_to_binary(transaction["signature"])
    )

    if ECDSA.valid_signature?(public_key, digest, signature) == false
      $logger.warn("Transaction signature is invalid")
      return false
    end
  end
end

def address_has_enough_funds?(message_transaction)
  from = Digest::SHA256.hexdigest(message_transaction["from"])
  total = BigDecimal(message_transaction["amount"]) + BigDecimal(message_transaction["fee"])

  address_balance(from) > total
end

Dealing with currency amounts with BigDecimal
You’ll notice in the “address_has_enough_funds?” methods that we are parsing and comparing amounts using Ruby’s BigDecimal library. If you ever worked with currencies and float, you know why: Currency calculations involving decimals are notoriously imprecise by default. For most calculations, it’s a reasonable tradeoff. If you’re dealing with π, it’s even necessary!
For currencies, we have to be precise. BigDecimal gives us that guarantee.

Anyone with a valid (signed) transaction can contact miners to include it in the next block. It’s in the miner’s interest to include transactions to maximize their profits.

Receiving and sending new blocks

For our blockchain, we’re keeping things very simple. Every node is responsible for figuring out new blocks are available by making HTTP requests to other peers. If a new block has been found, it’s then downloaded, validated, and added to the local blockchain.

In our final code, this looks like this:

# Connects to all known peers and requests their highest block. If a block is
# found, we'll do the necessary validations and then alter our local
# blockchain.
def find_higher_blocks_on_the_network
  $logger.info("Looking for higher blocks on the network")

  Array(@peers).each do |peer|
    peer_response = JSON.parse(HTTParty.get("http://#{peer}/blocks/last").body)

    return if peer_response.nil?

    block = Block.initialize_from_json(self, peer_response)

    if block.height > last_block_height.to_i
      $logger.info("Found an higher block: #{block.height} (from #{last_block_height.to_i})")
      validate_and_switch_to_fork(peer, block.height)
    end
  end
end

Handling forks

Forks are a fact of life for blockchains. Given enough time, miners are bound to find a block at approximately the same instant. They will broadcast their finding, and all other miners will agree that the blockchain has a new block, but not which one.

The system resolves this problem by following a simple rule: The valid blockchain is the longest. This is because the longest chain has proven the highest amount of work.

In case of a blockchain fork, the longest chain is valid

Note that this where the term 51% attack comes from: An attacker with more than 50% of the mining power can remove, reorder or add transactions (although he can’t change them as transactions are signed).

For our blockchain, handling forks is done by regularly connecting to peers and requesting their latest block. If its height is above our local blockchain, we download the missing block and apply them. That’s where we might notice we’re on a fork. Let’s take an example:

Our local blockchain is at height 50, and we discover a peer has a blockchain of height 53. We download blocks 51, 52, and 53. However, that’s where we notice block 51 includes a last_block hash for the block of height 50 that’s different from the hash of our own block 50. This could be because of a technical bug in the peer, for a malicious reason, or (and this is what we’ll consider) because a fork has happened. At that point, we need to continue downloading past blocks until we find a common point, meaning the last_block hash matches one of our own blocks.

Once the common point is found, all the blocks are validated successfully; we switch over to the longest blockchain.

Here’s how we implement this part:


# We fetch all the blocks of the fork until we reach a common point.
# The blocks are then gradually validated and applied while we revert at the
# same time the blocks we had created.
def validate_and_switch_to_fork(peer, target_height)
  new_blocks = []

  height = target_height

  loop do
    new_blocks << Block.initialize_from_json(self, JSON.parse(HTTParty.get("http://#{peer}/blocks/#{height}").body))

    # Once we reach the genesis block or a common point, we can stop fetching
    # blocks.
    break if new_blocks.last.height == 0 ||
        new_blocks.last.previous_block_header_hash == find_block_by_height(height)&.previous_block_header_hash

    height += -1
  end

  # We now reverse our local blockchain before we start applying the fork.
  reverse_blocks_and_transactions_until(new_blocks.last.height - 1)

  new_blocks.reverse.each do |block|
    validate_and_apply_block(block)
  end
end

def validate_and_apply_block(block)
  validate(block)
  save_block(block)

  $logger.info("Finished validating and applying block ##{block.height}")
end

# Validates the block by performing verifications in order:
# 2. Height and previous block hash
# 3. Time: Can't be before the previous block.
# 4. Transactions: Wallets should only spend fund they own. Signatures and
#    IDs should be valid.
# 5. There should be exactly one mining reward transaction.
# 6. Merkle tree root: Should be valid.
# 7. Block hash: Is it valid and is the level of difficulty what's expected.
def validate(new_block)
  previous_block = find_block_by_height(new_block.height - 1)

  # 1. Height.
  if previous_block&.height.to_i + 1 != new_block.height.to_i
    raise "Height isn't valid"
  end

  if previous_block.nil? == false
    # 2. Previous block hash
    if previous_block.block_header_hash != new_block.previous_block_header_hash
      raise "Previous block header hash is wrong"
    end

    # 2. Time: Can't be before the previous block.
    if previous_block.time > new_block.time
      raise "New block can't have been created before the previous one"
    end
  end

  # 3. Transactions: Wallets should only spend fund they own.
  new_block.transactions.each do |transaction|
    if WalletTransfer.find(transaction["id"]).nil? == false
      raise "A transaction with the same ID already exists on the blockchain"
    end

    case transaction["message"]["type"]
    when "funds_transfer"
      if valid_cryptography?(transaction) == false
        raise "Cryptography of the transaction is invalid"
      end

      # The amount + fee needs to be available to the wallet.
      if address_has_enough_funds?(transaction["message"]) == false
        raise "From address doesn't have enough funds"
      end

    when "mining_reward"
      fees = new_block.transactions.reject { |t| t["message"]["type"] == "mining_reward" }
        .map { |t| BigDecimal(t["message"]["fee"]) }
        .reduce(&:+) || BigDecimal(0)

      mining_reward = BigDecimal(transaction["message"]["amount"])
      if mining_reward != fees + 1
        raise "Mining reward amount is invalid (#{mining_reward.to_s("F")})"
      end
    end
  end

  # 4. There should be exactly one mining reward transaction.
  mining_rewards = new_block.transactions.reject { |t| t["message"]["type"] != "mining_reward" }
  if mining_rewards.size != 1
    raise "Unexpected amount of mining rewards"
  end

  # 5. Merkle tree root: Should be valid.
  merkle_root = MerkleTree.new(*new_block.transactions.map(&:to_json).sort { |a, b| a["id"] <=> b["id"] }).root.value
  if new_block.merkle_root != merkle_root
    raise "Merkle root is invalid"
  end

  # 6. Block hash: Is it valid and is the level of difficulty what's expected.
  if new_block.compute_block_header_hash != new_block.block_header_hash
    raise "Block header hash is invalid"
  end

  if new_block.block_header_hash[0, DIFFICULTY_LEVEL] != "0" * DIFFICULTY_LEVEL
    raise "Block doesn't match expected difficulty level"
  end
end

# Validate all the cryptographical components of the transactions.
# Therefore it doesn't include ensuring the wallet has the required funds to
# make the transaction.
def valid_cryptography?(transaction)
  # The signature of the message needs to be valid and come from the sender.
  if transaction["id"] != Digest::SHA256.hexdigest(transaction["signature"])
    $logger.warn("Transaction ID isn't the SHA256 of the signature")
    return false
  end

  # The signature should be valid.
  if transaction["message"]["type"] != "mining_reward"
    public_key = ECDSA::Format::PointOctetString.decode(
      Base58.base58_to_binary(transaction["message"]["from"]),
      ECDSA::Group::Secp256k1,
    )

    digest = Digest::SHA256.digest(transaction["message"].sort.to_h.to_json)

    signature = ECDSA::Format::SignatureDerString.decode(
      Base58.base58_to_binary(transaction["signature"])
    )

    if ECDSA.valid_signature?(public_key, digest, signature) == false
      $logger.warn("Transaction signature is invalid")
      return false
    end
  end
end

Conclusion

We’re now done with the blockchain! In this guide, I showed extracts of the code, and there’s more going on, but not that much! So feel free to have a look at the code of Melon Coin.

For this guide and to save time, I took numerous shortcuts so you can’t farm melons right away :) Some improvements are necessary, some recommended. In particular:

  • Mining difficulty is static: It should be adjusted based on the computing power of all miners combined.
  • Proof of Stake could be better than Proof of Work: As Bitcoin uses Proof of Stake and it’s also easier to build, that’s the path we chose. But it has some downsides, including 51% attacks while the coin is still new and energy consumption. Proof of Stake provides an interesting solution to those problems.
  • Transactions should include an expiry time based on the height or time: This change would make validations easier as it wouldn’t be necessary for nodes to keep the entire history of the blockchain on hand.
  • Block size is currently unlimited: Although it is debatable what constitutes the right block size, there should be one.
  • Calculating the balance of an address involves going through all previous transactions. This, of course, doesn’t scale well.
  • Pending transactions should be extracted based on their fee per byte.
  • New peers should be able to connect, and both receive a list of peers and be added themselves to the list of peers

I hope you found my exploration interesting, and hopefully, you learned something too. If you have questions or feedback, please share them below or via email!