Coding a Blockchain: An Introduction to Blockchains
In this part, we'll begin coding our blockchain. We'll :
Learn about hash functions a core element in the architecture of blockchains.
Use hash functions to construct Blocks.
Link the blocks together to build a Blockchain.
Run the blockchain.
This series is comprised of the following posts:
Hash Functions
To start this part we'll be looking at hash functions. These are mathematical algorithms that take input data and convert it into a fixed-size string of characters called output or digest. These hash functions possess two crucial properties: they consistently produce the same digest for the same input, and they generate unique digests for distinct inputs, as represented in the image below.
This means that any change to the input of the hash function, no matter how small, results in a completely different digest, as can be seen in the latter four rows of the above image.
These properties make hash functions invaluable tools in the realm of blockchains. We use them can input the data within a block into a hash function, and the resulting digest becomes a representation of that data. This digest is then placed in the following block, effectively linking the two blocks together. This means that any change to the data would mean a different digest so the following block would now have incorrect information regarding the previous block's digest making it clear that data was tampered with.
A brief note: often, the digest of a hash function is commonly referred to as a "hash". We'll frequently use this term to describe the digest when we apply the hash function throughout this series. For example when writing "we take the hash of the data and insert it in the block", the term "hash" refers to the digest of the hash function that used the data as the input.
Coding a Blockchain
Project Setup
If haven't installed node.js, and ts-node on your computer, follow the instructions on those links for the installation.
To begin setting up our environment run the following commands on your system console:
npm init es6
npm i --save-dev @types/node
To perform tests we'll be using a javascript testing framework called Jest. This framework lets us easily and rapidly create tests to check the correct functioning of our modules. To install it run the following:
npm install --save-dev ts-jest @jest/globals ts-jest-resolver
To configure the tests, create a file named jest.config.cjs
and copy the following:
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
resolver: "ts-jest-resolver",
};
Next, to run our program change the scripts
field on package.json
file to:
"scripts": {
"dev": "ts-node --esm src/index.ts",
"test": "jest"
},
And lastly, create a tsconfig.json
file and paste the following:
{
"compilerOptions": {
"module": "NodeNext",
"esModuleInterop": true,
"target": "es2015"
}
}
Let's create two folders, the first named src
where we'll place our code modules, and the second folder will be named tests
where we'll place our tests for the modules we create.
In our blockchain, we'll be using the hash function sha256, which is commonly used in many systems including the Bitcoin blockchain. To use it we'll be installing the following package.
npm i js-sha256
Blocks
Let's begin the coding process by creating a file that we'll name block.ts
inside our src
folder where we'll define the structure of a block.
To define the structure of the blocks let's first define what properties a blockchain should have. The blocks need to be organized in such a way that we have a continuous series of blocks that reference the preceding one, and changing the data in any one of the blocks would make all of the following blocks incorrect. Based on this, we need to be able to identify the position that the block has on the chain, we also need to have a reference to the previous block and a way to easily verify if the data in the previous block has been altered.
With this in mind, we'll construct our blocks having the following elements:
height
: indicates the position of the block on the chain.data
: the data we wish to store inside the block, typically a record of cryptocurrency transfers between users but, for now, it'll be astring
.previousBlockHash
: the hash of the previous block that is used to establish a connection between all blocks in the chain.hash
: the hash of this block.
We'll use our previously discussed hash function sha256 to create the block hash. As input, we'll use a combination of the height
, data
, previousBlockHash
and set the digest as our block hash. This will make it so that any change to the other fields of the block will result in a completely different hash making it easy to detect tampering.
We'll also add a method to verify that the block is valid. At this point, the only field that we need to verify is the hash
, so we calculate the hash and compare it to the corresponding element on the block. If we find a different value we throw an error with the explanation of what is wrong.
import {sha256} from "js-sha256"
export class Block{
height: number
data: string
prevBlockHash: string
hash: string
constructor(height: number, data: string, prevBlockHash: string, hash: string){
this.height = height
this.data = data
this.prevBlockHash = prevBlockHash
this.hash = hash
}
static initialize(height: number, data: string, prevBlockHash: string){
const newBlock = new Block(height,data, prevBlockHash, "")
newBlock.hash = newBlock.calculateHash()
return newBlock
}
calculateHash(){
return sha256(this.height + this.data + this.prevBlockHash)
}
verify(){
const hash = this.calculateHash()
if(hash !== this.hash){
throw new Error(`Block ${this.height} hash should be ${hash},
found ${this.hash}`)
}
}
}
To test what we've just made we'll create a new file inside the tests
folder called block.test.ts
where we'll write the tests. We'll prepare two tests, on the first we'll test that the block is initialized with all the correct parameters and that the verification is successful. In the second test, we'll assert that changing a field of the block will result in the verification throwing an error.
import {describe, expect, test} from "@jest/globals";
import{Block} from "../src/block.js";
describe("Block tests", () => {
test("Block should initialize and pass verification", () => {
const block = Block.initialize(0,"Test", "prevHash")
expect(block.height).toBe(0)
expect(block.data).toBe("Test")
expect(block.prevBlockHash).toBe("prevHash")
expect(block.hash).toBe(block.calculateHash())
block.verify()
});
test("Block that had any field altered should fail verification",() =>{
const block = Block.initialize(0,"Test", "")
block.height = 1
expect(() => block.verify()).toThrowError()
})
})
On the console, we can type npm test
and check if our module is working as we expected.
Blockchain
With our block structure defined, we can now proceed to create the blockchain structure itself, which is simply an array of interconnected blocks. We start by creating a new file called blockchain.ts
, and define the element blocks
, as an array of Blocks.
When we initialize the blockchain we'll also need to create a block, this first block on a blockchain is known as the genesis block. It holds a unique status since it lacks a reference to a previous block, serving as the foundation upon which the entire blockchain is built.
Let's now create a method to add a new block to our blockchain taking as an argument the block we wish to add. We'll start by using the method created previously to verify the validity of the block. Then we'll fetch the current height of the blockchain, and the hash
of the previous block, and compare them to fields set on the block. If any of these fields are in conflict we throw an error with the explanation. Finally, if the block is found to be valid we push it into the blockchain array, adding it to the chain.
To finish we'll add a method to validate the blockchain. To do this we need to verify that all fields in the blocks are in accordance with each other. We skip the verification of the genesis block since it doesn't contain the previousBlockHash
field. However, for all subsequent blocks, we must ensure that each block has the correct height relative to its position within the array, confirm that the previousBlockHash
field matches the hash of the preceding block, and verify that the block's hash is accurately computed. This validation method is crucial to maintaining the integrity and security of our blockchain, as it allows any user to easily verify that the blockchain has not been tampered with.
import { Block } from "./block.js";
export class Blockchain{
blocks: Block[]
constructor(blocks: Block[]){
this.blocks = blocks
}
static initialize(genesisBlockData: string){
const genesisBlock = Block.initialize(0, genesisBlockData, "")
return new Blockchain([genesisBlock])
}
addBlock(block: Block){
block.verify()
const height = this.blocks.length
const prevBlockHash = this.blocks[height - 1].hash
if (block.height !== height){
throw new Error(`Blockhain current height is ${height - 1},
provided Block height is ${block.height}`)
}
if(block.prevBlockHash !== prevBlockHash){
throw new Error(`Last block hash is ${prevBlockHash},
provided Block height is ${block.height}`)
}
this.blocks.push(block)
}
validateChain(){
for(let i = 1; i < this.blocks.length; i++){
const block = this.blocks[i]
const prevBlockHash = this.blocks[i-1].hash
if(block.height !== i){
throw new Error(`Block ${i} height should be ${i},
found ${block.height}`)
}
if(block.prevBlockHash != prevBlockHash){
throw new Error(`Block ${i} prevBlockHash should
be ${prevBlockHash}, found${block.prevBlockHash}`)
}
block.verify()
}
}
}
To test this new module we'll create a new file called blockchain.test.ts
and as in the block test, we'll begin by checking that the blockchain is initialized correctly. Then we'll create a block and test that we can properly add it to the blockchain. Lastly, we'll verify that if we try to add blocks with incorrect height
or if the prevBlockHash
field is different from the hash of the last block an error will be thrown.
import {describe, expect, test} from '@jest/globals';
import {Blockchain} from "../src/blockchain.js";
import {Block} from "../src/block.js";
describe("Blockchain tests", () => {
test('Blockchain should correctly initalize', () => {
const blockchain = Blockchain.initialize("Test")
expect(blockchain.blocks.length).toBe(1)
expect(blockchain.blocks[0].height).toBe(0)
blockchain.validateChain()
});
test('Blochain should add valid block',() =>{
const blockchain = Blockchain.initialize("Test")
const block = Block.initialize(1,"Block 1",blockchain.blocks[0].hash)
blockchain.addBlock(block)
blockchain.validateChain()
})
test('Blockchain should not add block with incorrect height or prevBlockHash',() =>{
const blockchain = Blockchain.initialize("Test")
const blockWrongHeight = Block.initialize(2,"Block 1",blockchain.blocks[0].hash)
const blockWrongPrevBlockHash = Block.initialize(1,"Block 1","test")
expect(() => blockchain.addBlock(blockWrongHeight)).toThrowError()
expect(() => blockchain.addBlock(blockWrongPrevBlockHash)).toThrowError()
})
})
Running the Blockchain
Now, it's time to put our blockchain to the test. Let's create a new file named index.ts
and instantiate our blockchain. We'll create and add a couple of blocks to the chain and perform the verification. If everything functions as expected, we should see the message "Blockchain is valid"
printed out on the console.
Next, we'll tamper with the data of one of the blocks and rerun the verification method. This time we should observe the message "Blockchain is invalid"
printed on the console, along with an error message indicating that the previousBlockHash
element on the second block is different from the hash of the previous block.
To run the file insert npm run dev
into your console, and verify that the expected messages are printed out.
import { Block } from "./block.js";
import { Blockchain } from "./blockchain.js";
function main(){
const blockchain = Blockchain.initialize("Hello world")
const block1 = Block.initialize(1, "First Block", blockchain.blocks[0].hash)
blockchain.addBlock(block1)
const block2 = Block.initialize(2, "Second Block", blockchain.blocks[1].hash)
blockchain.addBlock(block2)
blockchain.validateChain()
console.log("Blockchain is valid")
blockchain.blocks[1].data = "Tampered data"
blockchain.blocks[1].hash = blockchain.blocks[1].calculateHash()
try {
blockchain.validateChain()
console.log("Blockchain is valid")
} catch (error) {
if(error instanceof Error){
console.error("Blockchain is Invalid, cause:",error.message)
}
}
console.log("Finished")
}
main()
You can also make different tampering to the blocks on the chain, or add invalid blocks and observe other kinds of error messages appearing on the console.
Conclusion
We've gained some insight into the inner workings of blockchain technology. We've seen how the blockchain meticulously organizes its data and how a seemingly minor alteration in a previous block makes the entire structure invalid.
In the next part of this series, we'll go into learning about digital signatures which are a fundamental part of any internet communication, and then use that to add a cryptocurrency token to our blockchain ensuring that no party can transfer the tokens without the authorization of the owner.
The code in this article can be found here.
This series is comprised of the following posts: