Writing a blockchain in 60 readable lines of TypeScript

ninofiliu

Nino Filiu

Posted on March 29, 2021

Writing a blockchain in 60 readable lines of TypeScript

Blockchain: it gets more confusing every year because people explain it with weird metaphors instead of talking 5mn about the actual maths & code involved

And it frustrate me a lot because the concept of a blockchain is actually pretty simple:

  • a blockchain is a list of blocks
  • a block is
    • some data
    • the hash of the previous block
    • some token
    • the hash of the above

A block is valid if the hash has a magic prefix, which is when it begins by a certain number of zeros, so if you want to add some data to the blockchain, you'll have to pack it into a block, and to do so you'll have to find a token that produces a block hash that begins by a certain number of zeros, which is computationally intensive, and that's how you get proof of work.

Ok to be honest, I'm just talking about a particular type of blockchain there, but that's not the point. Coding the following blockchain helped me understand some crypto basics and I hope that'll help you too. I am not bragging about how clever I am to code the same tech as Bitcoin in 60 lines or whatever.

Anyway here's the code

I am using Deno and TypeScript.

Let's start by importing some hasher we'll need later on

import { createHash } from "https://deno.land/std@0.91.0/hash/mod.ts";
Enter fullscreen mode Exit fullscreen mode

Let's define blocks and agents. An agent represents the kind of programs running on your computer that can trade Bitcoins with other agents around the world.

type Block = {
  data: string;
  prev: string;
  token: string;
  hash: string;
};
type Agent = {
  addAgent(agent: Agent): void;
  receiveBlock(block: Block): void;
  addData(data: Block["data"]): void;
};
Enter fullscreen mode Exit fullscreen mode

We're gonna use md5 as our hash function. It's definitely not the most secure one but we don't really care. Let's define our magic prefix 00000 there too so we don't repeat it afterward. The more 0 there are, the more difficult it is to mine a new block.

const hashOf = (str: string) => createHash("md5")
  .update(str)
  .toString();
const magicPrefix = Array(5).fill("0").join("");
Enter fullscreen mode Exit fullscreen mode

Now let's create our agent factory. Internally, it keeps the whole chain in memory, and a list of all agents it needs to broadcast to when mining a new block.

The first block is the "Genesis Block" which is exempted from pointing to the previous block's hash, or having a magic prefix.

const createAgent = (): Agent => {
  const chain: Block[] = [{
    data: "",
    prev: "",
    token: "",
    hash: hashOf(""),
  }];
  const agents: Agent[] = [];
  return {
    addAgent(agent) { /* ... */ },
    addData(data) { /* ... */ },
    receiveBlock(block) { /* ... */ },
  };
};
Enter fullscreen mode Exit fullscreen mode

The addAgent method does not need further explanations:

addAgent(agent) {
  agents.push(agent);
},
Enter fullscreen mode Exit fullscreen mode

addData is basically where the mining happens. It's the computationally intensive loop to find the token that'll produce a block hash with a magic prefix.

I chose hashOf(Math.random().toString()) to generate a random string because that's a very concise way to do so but hashing is not needed there.

addData(data) {
  while (true) {
    const prev = chain[chain.length - 1].hash;
    const token = hashOf(Math.random().toString());
    const hash = hashOf(data + prev + token);
    if (hash.startsWith(magicPrefix)) {
      const block: Block = { data, prev, token, hash };
      chain.push(block);
      for (const agent of agents) {
        agent.receiveBlock(block);
      }
      return;
    }
  }
},
Enter fullscreen mode Exit fullscreen mode

receiveBlock validates if a new block can be added on top of the chain according to the conditions above, and if everything is ok, adds it, else throws.

receiveBlock(block) {
  if (block.prev != chain[chain.length - 1].hash) {
    throw new Error(
      "Hash does not point to the previous hash in the chain",
    );
  }
  if (!block.hash.startsWith(magicPrefix)) {
    throw new Error("Hash does not start with the magic prefix");
  }
  const actualHash = hashOf(block.data + block.prev + block.token);
  if (actualHash !== block.hash) {
    throw new Error("Hash is not the hash of data|prev|token");
  }
  chain.push(block);
},
Enter fullscreen mode Exit fullscreen mode

And... that's it!

You can run "simulations" with such agents, for example this one where two agents add greeting blocks. It should run without printing anything and without throwing:

const alice = createAgent();
const bob = createAgent();
alice.addAgent(bob);
bob.addAgent(alice);
alice.addData("Hello Bob! -Alice");
bob.addData("Hello Alice! -Bob");
Enter fullscreen mode Exit fullscreen mode

or this "simulation" where we try to inject a malevolent block into the blockchain but it gets caught:

const alice = createAgent();
const data = "bad things";
const prev = hashOf("");
alice.receiveBlock({
  data,
  prev,
  token: "",
  hash: hashOf(data + prev),
});
// error: Uncaught Error: Hash does not start with the magic prefix
Enter fullscreen mode Exit fullscreen mode

So... that's it?

Well, yes, at the core, there's not a lot more to blockchain than this. But in order to go from there to building a real cryptocurrency, you'd probably have to

  • replace the string data payload by signed and timestamped transactions
  • find ways to compensate miners for their work
  • build the p2p network layer
  • convince people that this cryptocurrency has value
  • etc

TL;DR it's usually not the blockchain part that is hard but rather what's around it

💖 💪 🙅 🚩
ninofiliu
Nino Filiu

Posted on March 29, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related