Build Status codecov

BlockPad is a real-time collaborative rich text editor powered by a blockchain.


npm install


  1. Launch two instances of BlockPad with different HTTP and P2P ports. Specify the WebSocket URL for the peers.

    HTTP_PORT=3001 P2P_PORT=6001 npm start
    HTTP_PORT=3002 P2P_PORT=6002 PEERS=ws://localhost:6001 npm start
  2. Go to http://localhost:3001 and http://localhost:3002 in your browser and start typing.


This system consists of the following components:

  • A blockchain: the data structure that stores the content of the text editor.
  • A peer-to-peer (P2P) server: the server that updates the blockchain between peers.
  • An HTTP server: the server that provides the APIs for accessing the blockchain and the peers.
  • A text editor: the web interface for text editing.


A block is the basic element of a blockchain and a blockchain is an array of blocks. A block consists of the following fields: index, timestamp, data, previousHash, and hash.

class Block {
    constructor(index, timestamp, data, previousHash, hash) {
        this.index = index;
        this.timestamp = timestamp; = data;
        this.previousHash = previousHash.toString();
        this.hash = hash.toString();

The first block is called the genesis block and its fields are hardcoded.

function getGenesisBlock() {
    return new Block(0, 737510400, 'Genesis block', '0', '9397591240bc3a17c0f737e72837953459df4ee23ff0ccd089af18ecaa05b991');

The fields of each new block are computed from the previous block.

function generateNextBlock(blockData) {
    const previousBlock = this.getLatestBlock();
    const timestamp = new Date().getTime();
    const nextIndex = previousBlock.index + 1;
    const previousHash = previousBlock.hash;
    const nextHash = Math.calculateHash(nextIndex, timestamp, blockData, previousHash, 0);
    return new Block(nextIndex, timestamp, blockData, previousHash, nextHash);

A block is valid if its index, previousHash, and hash are valid. A blockchain is valid if every of its block is valid.

function isValidNewBlock(newBlock, previousBlock) {
    if (previousBlock.index + 1 !== newBlock.index) {
        console.log('Invalid index');
        return false;
    } else if (previousBlock.hash !== newBlock.previousHash) {
        console.log('Invalid previous hash');
        return false;
    } else if (Math.calculateHashForBlock(newBlock) !== newBlock.hash) {
        console.log('Invalid hash: ' + Math.calculateHashForBlock(newBlock) + ' ' + newBlock.hash);
        return false;
    return true;

function isValidChain(targetChain) {
        if (!Array.isArray(targetChain) || targetChain.length === 0) return false;
        let prevBlock = targetChain[0];
        if (JSON.stringify(prevBlock) !== JSON.stringify(this.getGenesisBlock())) {
            return false;
        for (let i = 1; i < targetChain.length; i++) {
            if (this.isValidNewBlock(targetChain[i], prevBlock)) {
                prevBlock = targetChain[i];
            } else {
                return false;
        return true;

The block also implements proof-of-work, which enforces the hash values to start with a certain number of zeroes.

function mineBlock(difficulty) {
    while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join('0')) {
        this.hash = Math.calculateHashForBlock(this);
    console.log('BLOCK MINED: ' + this.hash);

P2P Server

The p2p server setups connections with the peers.

const WebSocket = require('ws');

function initServer() {
    this.server = new WebSocket.Server({port: this.p2pPort});
    this.server.on('connection', ws => this.initConnection(ws));
    console.log('Listening websocket p2p port on: ' + this.p2pPort);

function initConnection(ws) {
    this.write(ws, P2PServer.queryChainLengthMsg());

function connectToPeers(newPeers) {
    newPeers.forEach((peer) => {
        const ws = new WebSocket(peer);
        ws.on('open', () => this.initConnection(ws));
        ws.on('error', () => {
            console.log('connection failed')


It also updates the blockchain when the index of the latest received block is larger than the index of the latest block held. Either of the following can occur:

  • The latest received block is the successor of the latest block. We can safely add the received block to the chain.
  • The received blockchain have a length of 1. We have to query the chain from the peer.
  • The received blockchain is longer than the current chain. We replace the current chain with the received one.
function handleBlockchainResponse(message) {
    const receivedBlocks = JSON.parse(, b2) => (b1.index - b2.index));
    const latestBlockReceived = receivedBlocks[receivedBlocks.length - 1];
    const latestBlockHeld = this.blockchain.getLatestBlock();

    if (latestBlockReceived.index > latestBlockHeld.index) {
        console.log('blockchain possibly behind. We got: ' + latestBlockHeld.index + ' Peer got: ' + latestBlockReceived.index);
        if (latestBlockHeld.hash === latestBlockReceived.previousHash) {
            console.log("We can append the received block to our chain");
        } else if (receivedBlocks.length === 1) {
            console.log("We have to query the chain from our peer");
        } else {
            console.log("Received blockchain is longer than current blockchain");
    } else {
        console.log('Received blockchain is not longer than current blockchain. Do nothing');

HTTP Server

The HTTP server provides the RESTful APIs to the web interface to access the blockchain and the peers.

const express = require('express');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.urlencoded({extended: true}));

app.get('/blocks', (req, res) => res.send(JSON.stringify(this.blockchain)));'/mineBlock', (req, res) => {
    const newBlock = this.blockchain.generateNextBlock(;
    console.log('Block added: ' + JSON.stringify(newBlock));

Text Editor

The text editor uses Quill as the underlying editor.

<link href="" rel="stylesheet">
<div id="editor"></div>
<script src=""></script>
  var quill = new Quill('#editor', {
    theme: 'snow'

The text editor has the following functions.

  • Get the Latest Content: when the document is ready, get the latest content of the editor from the HTTP server.

    $.getJSON('blocks', function (data) {
      if (data.chain.length > 1) {
          var latestBlock = data.chain[data.chain.length - 1];
          var content = JSON.parse(;
  • Listen for Text Changes: when a peer updates the content of the text editor, reflect the changes on the editor.

    var ws = new WebSocket('ws://localhost:6001');
    ws.onmessage = function (event) {
      var message = JSON.parse(;
      var receivedBlock = JSON.parse(, - 1));
      var blockData = JSON.parse(;
      var content = blockData.content;
      var range = quill.getSelection();
  • Update Text Changes: when a text change event occurs at the editor, send the latest content to the editor.

    quill.on('text-change', function (delta, oldDelta, source) {
      if (source === 'user') {
          $.post('mineBlock', {
              "data": JSON.stringify(
                      "content": $('.ql-editor').html(),
                      "delta": delta
          console.log("A user action triggered this change.");


npm test



Real-time Collaborative Rich Text Editor powered by Blockchain

Blockpad Info

⭐ Stars 13
🔗 Source Code
🕒 Last Update a year ago
🕒 Created 4 years ago
🐞 Open Issues 1
➗ Star-Issue Ratio 13
😎 Author waitingcheung