Writing a blockchain in 60 readable lines of TypeScript
Nino Filiu
Posted on March 29, 2021
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";
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;
};
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("");
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) { /* ... */ },
};
};
The addAgent
method does not need further explanations:
addAgent(agent) {
agents.push(agent);
},
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;
}
}
},
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);
},
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");
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
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
Posted on March 29, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.