Blockchain Fundamentals and Integrated Node Service

0 likes

So, you still keep hearing a lot about bitcon and the blockchain technology? This article provides an overview of blockchain fundamentals, covering the key concepts, architecture, and security/reliability aspects, as well as a detailed integration of a private blockchain node service implemented using TypeScript and Node.js. On this article, you'll find code oriented content with high level examples for each component, from the blockchain node software and consensus module to smart contracts, storage, networking, and API endpoints.

Implementing a private blockchain can represent a powerhose whose your fintech or financial product can be built upon.

Conceptual Overview

First, it's important to learn the fundamentals that are going to be explored here.

Blockchain Fundamentals

  • Immutable Ledger:
    Every block in a blockchain contains a set of transactions and is cryptographically linked to the previous block, ensuring an immutable history.
  • Decentralization:
    Validation is performed by multiple nodes, which prevents any single entity from manipulating the ledger.
  • Consensus Mechanism:
    Protocols like PBFT or Raft are used to reach agreement on the blockchain’s state, even in the presence of network faults or malicious behavior.

Blockchain Architecture

  • Blocks:
    Each block consists of transactions, a timestamp, a nonce, and the hash of the previous block, forming an unbreakable chain.
  • Transactions:
    Represent actions such as deposits, transfers, or withdrawals and are digitally signed to guarantee authenticity.
  • Smart Contracts:
    Self-executing contracts written in code that automate agreements and decentralized operations.

Security and Reliability

  • Cryptography:
    Secure hashing algorithms (e.g., SHA-256) and digital signature schemes (e.g., ECDSA) ensure data integrity and authenticity.
  • Permissioned Networks:
    In private blockchains, only authorized nodes participate, providing tighter control and enhanced security.
  • Redundancy:
    A pool of nodes ensures that the system remains operational even if some nodes fail.
  • Observability:
    Monitoring tools track system performance and trigger alerts to maintain high reliability.

Software Architecture

  • Modular Design:
    Each component (blockchain engine, consensus module, smart contract VM, storage, networking, API, etc.) is built as an independent module, simplifying maintenance and scaling.
  • Integration via APIs & Messaging:
    Components communicate through well-defined REST APIs and P2P messaging, ensuring smooth interoperability.
  • Extensibility:
    The system is designed to allow future enhancements and optimizations without disrupting overall functionality.

High Level Code Implementation

Below are detailed code examples for each component that make up the integrated blockchain node service.

Blockchain Node Software

This code example outlines the core functionality of a blockchain node implemented in TypeScript. It is organized into two main parts: the Block class and the Blockchain class.

Transactions & Blocks:

The Transaction interface defines a basic transaction structure including the sender, recipient, amount, timestamp, and a signature to ensure authenticity. The Block class encapsulates each block in the chain, holding:

  • An index to denote the block's position.
  • A timestamp marking when the block was created.
  • A list of transactions included in the block.
  • The previousHash, which links the block to its predecessor.
  • A hash generated by the computeHash method.
  • A nonce used for mining.

Hashing & Mining:

The computeHash method computes a SHA-256 hash based on the block’s properties (index, previous hash, timestamp, transactions, and nonce). The mineBlock method implements a simple proof-of-work algorithm that iteratively increases the nonce until the block's hash meets a specified difficulty level (i.e., it starts with a certain number of zeros).

Blockchain Management:

The Blockchain class manages the sequence of blocks. It begins with a genesis block, maintains a list of pending transactions, and sets a mining difficulty. New transactions can be added to the pending list and later mined into a new block via minePendingTransactions. The isChainValid method ensures the integrity of the blockchain by verifying that each block’s hash is correctly computed and that each block references the correct previous block.

Express Server Initialization:

An Express application is created and configured to parse JSON payloads. The server is set to listen on port 3000.

Blockchain Integration:

A new instance of the Blockchain class (imported from blockchain.ts) is created to manage the blockchain data, transactions, and mining operations.

API Endpoints:

  • GET /chain:
    Returns the current blockchain as a JSON array, allowing clients to retrieve the complete chain.
  • POST /transaction:
    Accepts a transaction object in the request body, adds it to the pending transactions, and responds with a success message.
  • POST /mine:
    Takes a miner’s address from the request body, mines a new block using all pending transactions, appends the new block to the blockchain, and returns the updated chain.

blockchain.ts

import { createHash } from "crypto";

export interface Transaction {
  from: string;
  to: string;
  amount: number;
  timestamp: number;
  signature: string;
}

export class Block {
  public index: number;
  public timestamp: number;
  public transactions: Transaction[];
  public previousHash: string;
  public hash: string;
  public nonce: number;

  constructor(
    index: number,
    transactions: Transaction[],
    previousHash: string = ""
  ) {
    this.index = index;
    this.timestamp = Date.now();
    this.transactions = transactions;
    this.previousHash = previousHash;
    this.nonce = 0;
    this.hash = this.computeHash();
  }

  computeHash(): string {
    return createHash("sha256")
      .update(
        this.index +
          this.previousHash +
          this.timestamp +
          JSON.stringify(this.transactions) +
          this.nonce
      )
      .digest("hex");
  }

  mineBlock(difficulty: number) {
    const target = Array(difficulty + 1).join("0");
    while (this.hash.substring(0, difficulty) !== target) {
      this.nonce++;
      this.hash = this.computeHash();
    }
    console.log(`Block mined: ${this.hash}`);
  }
}

export class Blockchain {
  public chain: Block[];
  public pendingTransactions: Transaction[];
  public difficulty: number;

  constructor() {
    this.chain = [this.createGenesisBlock()];
    this.pendingTransactions = [];
    this.difficulty = 2;
  }

  createGenesisBlock(): Block {
    return new Block(0, [], "0");
  }

  getLatestBlock(): Block {
    return this.chain[this.chain.length - 1];
  }

  addTransaction(transaction: Transaction) {
    this.pendingTransactions.push(transaction);
  }

  minePendingTransactions(minerAddress: string) {
    const block = new Block(
      this.chain.length,
      this.pendingTransactions,
      this.getLatestBlock().hash
    );
    block.mineBlock(this.difficulty);
    this.chain.push(block);
    this.pendingTransactions = [];
  }

  isChainValid(): boolean {
    for (let i = 1; i < this.chain.length; i++) {
      const currentBlock = this.chain[i];
      const previousBlock = this.chain[i - 1];
      if (currentBlock.hash !== currentBlock.computeHash()) return false;
      if (currentBlock.previousHash !== previousBlock.hash) return false;
    }
    return true;
  }
}

server.ts

import express from "express";
import { Blockchain, Transaction } from "./blockchain";

const app = express();
const port = 3000;
const blockchain = new Blockchain();

app.use(express.json());

app.get("/chain", (req, res) => {
  res.json(blockchain.chain);
});

app.post("/transaction", (req, res) => {
  const tx: Transaction = req.body;
  blockchain.addTransaction(tx);
  res.json({ message: "Transaction added successfully" });
});

app.post("/mine", (req, res) => {
  const minerAddress: string = req.body.minerAddress;
  blockchain.minePendingTransactions(minerAddress);
  res.json({ message: "New block mined", chain: blockchain.chain });
});

app.listen(port, () => {
  console.log(`Blockchain node is running on port ${port}`);
});

Consensus Module

This code demonstrates a basic consensus mechanism for a permissioned blockchain using an event-driven model:

Message Types:

The ConsensusMessage interface defines two message types:

  • PROPOSE: Used when a node proposes a new block.
  • VOTE: Used when nodes cast their vote (approve or reject) for a proposed block.

Consensus Module:

The ConsensusModule class extends EventEmitter to facilitate message broadcasting and handling:

  • Node Identification & Peers:
    • Each instance is initialized with a unique nodeId and a list of peer nodes.
  • Block Proposal:
    • The proposeBlock method sets the current block to be validated and broadcasts a proposal message. It automatically votes in favor of its own proposal.
  • Message Handling:
    • The handleMessage method processes incoming messages:
    • For PROPOSE messages, the node validates the block (here simplified as always valid) and broadcasts a corresponding VOTE message.
    • For VOTE messages, the node records the vote and checks if the number of positive votes meets the required quorum.
    • Consensus Check:
    • The checkConsensus method tallies the votes and finalizes the block if the positive votes reach or exceed the quorum (a simple majority).
  • Finalizing a Block:
    • Once consensus is reached, finalizeBlock is called to approve the block and reset the vote tracking.

Example Usage:

The consensusExample.ts file simulates a consensus process between two nodes (NodeA and NodeB).

  • Each node listens for messages and relays them to the other.
  • When NodeA proposes a sample block, both nodes exchange proposal and vote messages.
  • If the combined votes meet the quorum, the block is finalized and approved by consensus.

consensus.ts

import EventEmitter from "events";

export interface ConsensusMessage {
  type: "PROPOSE" | "VOTE";
  block: any;
  sender: string;
  vote?: boolean;
}

export class ConsensusModule extends EventEmitter {
  private nodeId: string;
  private peers: string[];
  private currentBlock: any;
  private votes: Map<string, boolean>;
  private quorum: number;

  constructor(nodeId: string, peers: string[]) {
    super();
    this.nodeId = nodeId;
    this.peers = peers;
    this.votes = new Map();
    // For a permissioned blockchain, quorum is a simple majority of the nodes.
    this.quorum = Math.floor((peers.length + 1) / 2) + 1;
  }

  proposeBlock(block: any) {
    this.currentBlock = block;
    this.votes.set(this.nodeId, true);
    this.broadcast({
      type: "PROPOSE",
      block,
      sender: this.nodeId,
    });
  }

  broadcast(message: ConsensusMessage) {
    console.log(`Broadcasting from ${this.nodeId}:`, message);
    this.emit("message", message);
  }

  handleMessage(message: ConsensusMessage) {
    if (message.sender === this.nodeId) return;

    if (message.type === "PROPOSE") {
      const isValid = this.validateBlock(message.block);
      const voteMsg: ConsensusMessage = {
        type: "VOTE",
        block: message.block,
        sender: this.nodeId,
        vote: isValid,
      };
      this.broadcast(voteMsg);
    } else if (message.type === "VOTE") {
      if (this.currentBlock && message.block.hash === this.currentBlock.hash) {
        this.votes.set(message.sender, message.vote as boolean);
        this.checkConsensus();
      }
    }
  }

  validateBlock(block: any): boolean {
    return true;
  }

  private checkConsensus() {
    let positiveVotes = 0;
    for (const vote of this.votes.values()) {
      if (vote) positiveVotes++;
    }
    console.log(`Consensus votes: ${positiveVotes}/${this.quorum}`);
    if (positiveVotes >= this.quorum) {
      console.log(`Block approved by consensus: ${this.currentBlock.hash}`);
      this.finalizeBlock();
    }
  }

  private finalizeBlock() {
    console.log(`Finalizing block: ${this.currentBlock.hash}`);
    this.currentBlock = null;
    this.votes.clear();
  }
}

consensusExample.ts

import { ConsensusModule } from "./consensus";

const nodeA = new ConsensusModule("NodeA", ["NodeB"]);
const nodeB = new ConsensusModule("NodeB", ["NodeA"]);

nodeA.on("message", (msg) => {
  console.log(`NodeA sent message:`, msg);
  nodeB.handleMessage(msg);
});
nodeB.on("message", (msg) => {
  console.log(`NodeB sent message:`, msg);
  nodeA.handleMessage(msg);
});

const sampleBlock = {
  hash: "0000abc123",
};

nodeA.proposeBlock(sampleBlock);

Smart Contract Execution Environment (VM)

This section demonstrates a lightweight virtual machine (VM) designed to deploy and execute smart contracts securely using Node.js's vm module. The implementation is split into two parts:

Smart Contract Deployment & Execution:

The SmartContractVM class manages a collection of smart contracts, storing each contract’s code in a map keyed by a unique address.

  • The deployContract method registers a contract by saving its code under a specific address.
  • The executeContract method retrieves the contract code and sets up a secure sandbox environment. This sandbox includes the function arguments and any additional context data. It then wraps the contract code so that it dynamically calls the specified function with the provided arguments, capturing the result.

Usage Example:

The usageExample.ts file shows a practical example where:

  • A smart contract is deployed with two functions: add and multiply.
  • The contract is executed by calling the corresponding functions using executeContract, and the results (sum and product) are logged to the console.

smartContractVM.ts

import { Script, createContext } from "vm";

export class SmartContractVM {
  private contracts: Map<string, string>;

  constructor() {
    this.contracts = new Map();
  }

  deployContract(address: string, code: string): void {
    this.contracts.set(address, code);
  }

  executeContract(
    address: string,
    functionName: string,
    args: any[],
    contextData: Record<string, any> = {}
  ): any {
    const contractCode = this.contracts.get(address);
    if (!contractCode) {
      throw new Error("Contract not found");
    }

    const sandbox = {
      console,
      args,
      context: contextData,
      result: null,
    };

    const context = createContext(sandbox);

    const wrappedCode = `
      const contract = {};
      ${contractCode}
      if (typeof contract['${functionName}'] !== 'function') {
        throw new Error('Function "${functionName}" not found');
      }
      result = contract['${functionName}'](...args);
    `;

    const script = new Script(wrappedCode);
    script.runInContext(context);

    return sandbox.result;
  }
}

usageExample.ts (for Smart Contract VM)

import { SmartContractVM } from "./smartContractVM";

const vm = new SmartContractVM();

const contractAddress = "0xABC";
const contractCode = `
  contract.add = function(a, b) {
    return a + b;
  };

  contract.multiply = function(a, b) {
    return a * b;
  };
`;

vm.deployContract(contractAddress, contractCode);

const sum = vm.executeContract(contractAddress, "add", [5, 7]);
console.log("Sum:", sum);

const product = vm.executeContract(contractAddress, "multiply", [5, 7]);
console.log("Product:", product);

Ledger Storage / Database Layer

This section outlines a ledger storage system that uses LevelDB to persist blockchain data. The LedgerStorage class provides methods for interacting with the blockchain ledger, including:

Saving Blocks:

The saveBlock method stores a block in the database using a unique key derived from the block’s index.

Retrieving Blocks:

The getBlock method fetches a specific block by its index, handling errors if the block isn’t found.

Fetching the Latest Block:

The getLatestBlock method reads the database in reverse order (from highest to lowest index) to retrieve the most recently added block.

Retrieving All Blocks:

The getAllBlocks method streams through all stored blocks, collecting them into an array.

Closing the Database:

The close method ensures that the database connection is properly terminated.

ledgerStorage.ts

import level from "level";

export interface Block {
  index: number;
  hash: string;
  previousHash: string;
  timestamp: number;
  transactions: any[];
  nonce: number;
}

export class LedgerStorage {
  private db: level.LevelDB;

  constructor(dbPath: string) {
    this.db = level(dbPath, { valueEncoding: "json" });
  }

  async saveBlock(block: Block): Promise<void> {
    const key = `block:${block.index}`;
    await this.db.put(key, block);
  }

  async getBlock(index: number): Promise<Block | null> {
    const key = `block:${index}`;
    try {
      const block = await this.db.get(key);
      return block as Block;
    } catch (error: any) {
      if (error.notFound) {
        return null;
      }
      throw error;
    }
  }

  async getLatestBlock(): Promise<Block | null> {
    return new Promise((resolve, reject) => {
      let latestBlock: Block | null = null;
      const stream = this.db.createReadStream({
        gt: "block:",
        lt: "block:~",
        reverse: true,
        limit: 1,
      });
      stream.on("data", ({ value }) => {
        latestBlock = value;
      });
      stream.on("error", (err) => reject(err));
      stream.on("end", () => resolve(latestBlock));
    });
  }

  async getAllBlocks(): Promise<Block[]> {
    return new Promise((resolve, reject) => {
      const blocks: Block[] = [];
      const stream = this.db.createReadStream({
        gt: "block:",
        lt: "block:~",
      });
      stream.on("data", ({ value }) => {
        blocks.push(value);
      });
      stream.on("error", (err) => reject(err));
      stream.on("end", () => resolve(blocks));
    });
  }

  async close(): Promise<void> {
    await this.db.close();
  }
}

usageExample.ts (for Ledger Storage)

import { LedgerStorage, Block } from "./ledgerStorage";

(async () => {
  const ledger = new LedgerStorage("./my-ledger-db");

  const sampleBlock: Block = {
    index: 1,
    hash: "0000abc123",
    previousHash: "0000def456",
    timestamp: Date.now(),
    transactions: [],
    nonce: 0,
  };

  await ledger.saveBlock(sampleBlock);

  const retrievedBlock = await ledger.getBlock(1);
  console.log("Retrieved Block:", retrievedBlock);

  const latestBlock = await ledger.getLatestBlock();
  console.log("Latest Block:", latestBlock);

  await ledger.close();
})();

P2P Networking & Communication Stack

This section implements a peer-to-peer (P2P) networking layer using WebSockets, allowing blockchain nodes to communicate directly:

Establishing Connections:

The P2PNetwork class sets up a WebSocket server on a specified port. When a new peer connects, the handleConnection method adds it to the list of active peers and assigns handlers for incoming messages and disconnections.

Managing Peer Connections:

The connectToPeer method enables the node to initiate a connection to a remote peer by creating a new WebSocket client, adding the connection upon success, and setting up message and error handlers. The removePeer method keeps the list of peers up-to-date by removing any disconnected connections.

Message Broadcasting:

The broadcast method sends messages (serialized as JSON) to all connected peers that are in an open state, ensuring efficient propagation of network events such as new block notifications.

Handling Incoming Messages:

The handleMessage method attempts to parse incoming messages and logs them. This allows the node to react to network events, such as receiving a new block or other protocol-specific messages.

Usage Example:

The usage example demonstrates creating a P2P node on port 6001, connecting to a peer at ws://localhost:6002, and broadcasting a new block message. This setup is foundational for enabling decentralized communication between blockchain nodes.

p2pNetwork.ts

import WebSocket from "ws";
import { EventEmitter } from "events";

export class P2PNetwork extends EventEmitter {
  private peers: WebSocket[] = [];
  private server: WebSocket.Server;

  constructor(private port: number) {
    super();
    this.server = new WebSocket.Server({ port });
    this.server.on("connection", (ws: WebSocket) => {
      this.handleConnection(ws);
    });
    console.log(`P2P network listening on port ${this.port}`);
  }

  private handleConnection(ws: WebSocket) {
    console.log("New peer connected");
    this.peers.push(ws);

    ws.on("message", (data: WebSocket.Data) => {
      this.handleMessage(data);
    });

    ws.on("close", () => {
      this.removePeer(ws);
    });
  }

  private removePeer(ws: WebSocket) {
    this.peers = this.peers.filter((peer) => peer !== ws);
    console.log("Peer disconnected");
  }

  public connectToPeer(peerAddress: string) {
    const ws = new WebSocket(peerAddress);

    ws.on("open", () => {
      console.log(`Connected to peer: ${peerAddress}`);
      this.peers.push(ws);
    });

    ws.on("message", (data: WebSocket.Data) => {
      this.handleMessage(data);
    });

    ws.on("close", () => {
      this.removePeer(ws);
    });

    ws.on("error", (err) => {
      console.error(`Connection error with ${peerAddress}:`, err);
    });
  }

  public broadcast(message: any) {
    const data = JSON.stringify(message);
    this.peers.forEach((peer) => {
      if (peer.readyState === WebSocket.OPEN) {
        peer.send(data);
      }
    });
  }

  private handleMessage(data: WebSocket.Data) {
    try {
      const message = JSON.parse(data.toString());
      console.log("Received message:", message);
    } catch (error) {
      console.error("Failed to parse incoming message:", error);
    }
  }
}

usageExample.ts (for P2P Network)

import { P2PNetwork } from "./p2pNetwork";

const p2pNode = new P2PNetwork(6001);

p2pNode.connectToPeer("ws://localhost:6002");

p2pNode.broadcast({
  type: "NEW_BLOCK",
  block: { index: 1, hash: "0000abc123" },
});

API Endpoints & Client Libraries

This section provides a RESTful API server and a corresponding client library to interact with the blockchain node:

API Server (apiServer.ts):

  • Express Server Setup: An Express.js server is configured to handle JSON requests and listens on a specified port.
  • Blockchain Integration: The server creates instances of the blockchain, ledger storage, and smart contract VM. These modules handle core blockchain functionalities like transaction management, block mining, ledger persistence, and smart contract execution.

Endpoints:

  • GET /chain: Returns the current blockchain.
  • GET /block/:index: Retrieves a specific block by its index.
  • POST /transaction: Adds a new transaction to the pending list.
  • POST /mine: Mines a new block from pending transactions, saves it to the ledger, and returns the updated chain.
  • POST /contract/deploy: Deploys a smart contract by storing its code.
  • POST /contract/execute: Executes a function of a deployed smart contract and returns the result.
  • GET /metrics: Exposes system metrics (e.g., for Prometheus monitoring).

Client Library (blockchainClient.ts):

  • Provides an abstraction over the API endpoints using Axios.
  • Methods such as getChain, getBlock, addTransaction, mine, deployContract, and executeContract allow client applications to easily interact with the blockchain API.

apiServer.ts

import express from "express";
import { Blockchain, Transaction } from "./blockchain";
import { LedgerStorage } from "./ledgerStorage";
import { SmartContractVM } from "./smartContractVM";

const app = express();
const port = process.env.PORT || 3000;

app.use(express.json());

const blockchain = new Blockchain();
const ledger = new LedgerStorage("./my-ledger-db");
const smartContractVM = new SmartContractVM();

app.get("/chain", async (req, res) => {
  res.json(blockchain.chain);
});

app.get("/block/:index", async (req, res) => {
  const index = parseInt(req.params.index);
  const block = await ledger.getBlock(index);
  if (block) {
    res.json(block);
  } else {
    res.status(404).json({ error: "Block not found" });
  }
});

app.post("/transaction", (req, res) => {
  const tx: Transaction = req.body;
  blockchain.addTransaction(tx);
  res.json({ message: "Transaction added successfully" });
});

app.post("/mine", (req, res) => {
  const { minerAddress } = req.body;
  blockchain.minePendingTransactions(minerAddress);
  const latestBlock = blockchain.getLatestBlock();

  ledger
    .saveBlock(latestBlock)
    .then(() => res.json({ message: "New block mined", block: latestBlock }))
    .catch((err) => res.status(500).json({ error: err.message }));
});

app.post("/contract/deploy", (req, res) => {
  const { address, code } = req.body;
  smartContractVM.deployContract(address, code);
  res.json({ message: "Smart contract deployed successfully" });
});

app.post("/contract/execute", (req, res) => {
  const { address, functionName, args, contextData } = req.body;
  try {
    const result = smartContractVM.executeContract(
      address,
      functionName,
      args,
      contextData
    );
    res.json({ result });
  } catch (error: any) {
    res.status(500).json({ error: error.message });
  }
});

app.get("/metrics", async (req, res) => {
  res.set("Content-Type", registry.contentType);
  res.end(await registry.metrics());
});

app.listen(port, () => {
  console.log(`API server is running on port ${port}`);
});

blockchainClient.ts

import axios from "axios";

const API_BASE_URL = "http://localhost:3000";

export const BlockchainClient = {
  async getChain() {
    const response = await axios.get(`${API_BASE_URL}/chain`);
    return response.data;
  },
  async getBlock(index: number) {
    const response = await axios.get(`${API_BASE_URL}/block/${index}`);
    return response.data;
  },
  async addTransaction(transaction: any) {
    const response = await axios.post(
      `${API_BASE_URL}/transaction`,
      transaction
    );
    return response.data;
  },
  async mine(minerAddress: string) {
    const response = await axios.post(`${API_BASE_URL}/mine`, { minerAddress });
    return response.data;
  },
  async deployContract(address: string, code: string) {
    const response = await axios.post(`${API_BASE_URL}/contract/deploy`, {
      address,
      code,
    });
    return response.data;
  },
  async executeContract(
    address: string,
    functionName: string,
    args: any[],
    contextData: any = {}
  ) {
    const response = await axios.post(`${API_BASE_URL}/contract/execute`, {
      address,
      functionName,
      args,
      contextData,
    });
    return response.data;
  },
};

Cryptographic Components

This section provides a set of cryptographic utility functions essential for securing blockchain operations:

Key Pair Generation:

The generateKeyPair method generates an elliptic curve key pair using the secp256k1 curve (commonly used in blockchain). It returns a public key and a private key in PEM format, which can be used for signing and verifying data.

Data Signing:

The signData method creates a digital signature for a given piece of data using a private key. It employs the SHA256 hashing algorithm to create a digest of the data before signing, ensuring data integrity and authenticity.

Signature Verification:

The verifySignature method checks the validity of a digital signature. By using the corresponding public key, it verifies that the signature matches the data, confirming that the data was signed by the holder of the private key.

Data Hashing:

The hashData method computes a SHA256 hash of the provided data. This is useful for generating unique fingerprints for data objects, which is fundamental for ensuring the immutability and integrity of blockchain transactions.

cryptoUtils.ts

import crypto from "crypto";

export class CryptoUtils {
  static generateKeyPair(): { publicKey: string; privateKey: string } {
    const { publicKey, privateKey } = crypto.generateKeyPairSync("ec", {
      namedCurve: "secp256k1",
      publicKeyEncoding: { type: "spki", format: "pem" },
      privateKeyEncoding: { type: "pkcs8", format: "pem" },
    });
    return { publicKey, privateKey };
  }

  static signData(data: string, privateKey: string): string {
    const sign = crypto.createSign("SHA256");
    sign.update(data);
    sign.end();
    return sign.sign(privateKey, "hex");
  }

  static verifySignature(
    data: string,
    signature: string,
    publicKey: string
  ): boolean {
    const verify = crypto.createVerify("SHA256");
    verify.update(data);
    verify.end();
    return verify.verify(publicKey, signature, "hex");
  }

  static hashData(data: string): string {
    return crypto.createHash("sha256").update(data).digest("hex");
  }
}

usageExample.ts (for Cryptographic Components)

import { CryptoUtils } from "./cryptoUtils";

const { publicKey, privateKey } = CryptoUtils.generateKeyPair();
console.log("Public Key:", publicKey);
console.log("Private Key:", privateKey);

const data = JSON.stringify({ from: "Alice", to: "Bob", amount: 100 });

const signature = CryptoUtils.signData(data, privateKey);
console.log("Signature:", signature);

const isValidSignature = CryptoUtils.verifySignature(
  data,
  signature,
  publicKey
);
console.log("Signature Valid:", isValidSignature);

const dataHash = CryptoUtils.hashData(data);
console.log("Data Hash:", dataHash);

Observability & Monitoring Tools

This code implements an observability and monitoring solution using Express.js and Prometheus via the prom-client library. Key aspects include:

Metrics Collection:

The code initializes a Prometheus Registry and collects default metrics. Two custom metrics are defined:

  • HTTP Request Counter: Tracks the total number of HTTP requests, labeled by HTTP method, route, and status code.
  • HTTP Request Duration Histogram: Measures the duration of each HTTP request in seconds with the same labels.

Middleware Integration:

A custom middleware (metricsMiddleware) is applied to capture the start time of each request and update the metrics once the response finishes. It calculates the response time and increments the counter and histogram accordingly.

  • Metrics Exposure Endpoint: An endpoint (/metrics) is provided to expose all collected metrics in a format that Prometheus can scrape.
  • Basic Health Endpoint: The root endpoint (/) returns a simple message, serving as a basic health check.
  • Server Initialization: The observability server listens on a configurable port (defaulting to 4000), logging its status to the console.

observability.ts

import express from "express";
import {
  collectDefaultMetrics,
  Counter,
  Histogram,
  Registry,
} from "prom-client";

const registry = new Registry();
collectDefaultMetrics({ register: registry });

const httpRequestCounter = new Counter({
  name: "http_requests_total",
  help: "Total number of HTTP requests",
  labelNames: ["method", "route", "statusCode"],
  registers: [registry],
});

const httpRequestDuration = new Histogram({
  name: "http_request_duration_seconds",
  help: "Duration of HTTP requests in seconds",
  labelNames: ["method", "route", "statusCode"],
  registers: [registry],
});

function metricsMiddleware(
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) {
  const startEpoch = Date.now();

  res.on("finish", () => {
    const responseTimeInSeconds = (Date.now() - startEpoch) / 1000;
    const labels = {
      method: req.method,
      route: req.route ? req.route.path : req.path,
      statusCode: res.statusCode.toString(),
    };
    httpRequestCounter
      .labels(labels.method, labels.route, labels.statusCode)
      .inc();
    httpRequestDuration
      .labels(labels.method, labels.route, labels.statusCode)
      .observe(responseTimeInSeconds);
  });

  next();
}

const app = express();
const port = process.env.PORT || 4000;

app.use(metricsMiddleware);

app.get("/metrics", async (_req, res) => {
  res.set("Content-Type", registry.contentType);
  res.end(await registry.metrics());
});

app.get("/", (_req, res) => {
  res.send("Hello from the blockchain network monitoring endpoint!");
});

app.listen(port, () => {
  console.log(`Observability server is running on port ${port}`);
});

Node Pool / Peer Discovery Mechanism

This module implements a dynamic node pool for peer discovery and management in a blockchain network:

Peer Connection Management:

The NodePool class uses a WebSocket server to accept incoming connections on a specified port and maintains active peers in a Map. When a new connection is established, it logs the connection, stores the peer, and sets up handlers for incoming messages and disconnection events.

Bootstrap and Handshaking:

Upon initialization, the node pool connects to any provided bootstrap nodes. When connecting, it sends a handshake message to establish the connection. This initial handshake helps integrate new nodes into the network.

Peer Discovery Mechanism:

The discoverPeers method broadcasts a peerDiscovery message to all connected peers. When a node receives this message, it responds with a list of its known peers (peerList). This response allows nodes to discover and connect to additional peers, fostering a decentralized network.

Broadcasting Messages:

The broadcast method is used to send messages (e.g., new block notifications) to all connected peers. This is essential for propagating network events quickly across the blockchain.

Usage Example:

The usageNodePool.ts script demonstrates how to initialize the node pool, connect to bootstrap nodes, periodically trigger peer discovery every 10 seconds, and broadcast a message (like a new block announcement) after a delay.

nodePool.ts

import WebSocket from "ws";

export class NodePool {
  private peers: Map<string, WebSocket>;
  private server: WebSocket.Server;
  private bootstrapNodes: string[];

  constructor(private port: number, bootstrapNodes: string[] = []) {
    this.peers = new Map();
    this.bootstrapNodes = bootstrapNodes;

    this.server = new WebSocket.Server({ port });
    this.server.on("connection", (ws: WebSocket, req) => {
      const remoteAddress = `${req.socket.remoteAddress}:${req.socket.remotePort}`;
      console.log(`New peer connected: ${remoteAddress}`);
      this.peers.set(remoteAddress, ws);

      ws.on("message", (data: WebSocket.Data) => {
        this.handleMessage(data, remoteAddress);
      });

      ws.on("close", () => {
        console.log(`Peer disconnected: ${remoteAddress}`);
        this.peers.delete(remoteAddress);
      });
    });

    bootstrapNodes.forEach((nodeAddress) => this.connectToPeer(nodeAddress));
  }

  public connectToPeer(address: string) {
    const ws = new WebSocket(address);

    ws.on("open", () => {
      console.log(`Connected to bootstrap peer: ${address}`);
      this.peers.set(address, ws);
      ws.send(
        JSON.stringify({
          type: "handshake",
          from: `ws://localhost:${this.port}`,
        })
      );
    });

    ws.on("message", (data: WebSocket.Data) => {
      this.handleMessage(data, address);
    });

    ws.on("close", () => {
      console.log(`Bootstrap peer disconnected: ${address}`);
      this.peers.delete(address);
    });

    ws.on("error", (error) => {
      console.error(`Error connecting to peer ${address}:`, error);
    });
  }

  public broadcast(message: any) {
    const msg = JSON.stringify(message);
    this.peers.forEach((ws, address) => {
      if (ws.readyState === WebSocket.OPEN) {
        ws.send(msg);
      }
    });
  }

  public discoverPeers() {
    const discoveryMessage = {
      type: "peerDiscovery",
      from: `ws://localhost:${this.port}`,
    };
    this.broadcast(discoveryMessage);
  }

  private handleMessage(data: WebSocket.Data, sender: string) {
    try {
      const message = JSON.parse(data.toString());
      console.log(`Received message from ${sender}:`, message);

      switch (message.type) {
        case "handshake":
          break;
        case "peerDiscovery":
          const peerList = Array.from(this.peers.keys());
          const response = {
            type: "peerList",
            from: `ws://localhost:${this.port}`,
            peers: peerList,
          };
          const ws = this.peers.get(sender);
          if (ws && ws.readyState === WebSocket.OPEN) {
            ws.send(JSON.stringify(response));
          }
          break;
        case "peerList":
          (message.peers || []).forEach((peer: string) => {
            if (
              !this.peers.has(peer) &&
              peer !== `ws://localhost:${this.port}`
            ) {
              this.connectToPeer(peer);
            }
          });
          break;
        default:
          console.log(`Unhandled message type: ${message.type}`);
      }
    } catch (error) {
      console.error("Failed to handle incoming message:", error);
    }
  }
}

usageNodePool.ts

import { NodePool } from "./nodePool";

const bootstrapNodes = ["ws://localhost:6002"];
const nodePool = new NodePool(6001, bootstrapNodes);

setInterval(() => {
  console.log("Initiating peer discovery...");
  nodePool.discoverPeers();
}, 10000);

setTimeout(() => {
  nodePool.broadcast({
    type: "NEW_BLOCK",
    block: { index: 2, hash: "0000def456" },
  });
}, 5000);

Wrapping up everything and integration example

The main.ts file brings together all the individual components into a cohesive blockchain node service. At a high level, here’s how everything integrates:

HTTP API Server:

An Express-based server exposes REST endpoints for interacting with the blockchain. These endpoints allow external clients to:

  • Retrieve the current blockchain (/chain)
  • Submit new transactions (/transaction)
  • Mine pending transactions (/mine)
  • Deploy and execute smart contracts (/contract/deploy and /contract/execute)
  • Access system metrics for monitoring (/metrics)

Blockchain Core & Ledger Storage:

The blockchain core manages the chain, pending transactions, and block validation. When a new block is mined, it is added to the in-memory chain and persisted in the ledger storage (using LevelDB) for durability and future retrieval.

Consensus Module:

The consensus module ensures that only valid blocks are appended to the blockchain. When a new block is mined, it is proposed to the network through the consensus mechanism, and any consensus-related messages are broadcast over the P2P network.

Smart Contract VM:

This component provides a secure sandbox to deploy and execute smart contracts. Clients can deploy new contracts and trigger functions within them, extending the blockchain’s functionality.

Cryptographic Components:

Cryptographic utilities are used to generate key pairs, sign transactions, and verify signatures. For example, when a transaction is submitted, it is signed using a private key to ensure authenticity and integrity.

P2P Networking & Node Pool:

The P2P network module enables the node to communicate with other nodes, broadcasting events such as new blocks. In parallel, the node pool manages peer discovery and maintains active connections, ensuring the network remains decentralized and resilient.

Observability & Monitoring:

Metrics are collected and exposed (via Prometheus) to monitor system performance, such as HTTP request counts and durations, providing real-time insights into the node’s health.

Inter-Component Communication:

  • When a transaction is received, it is signed and added to the pending transactions list in the blockchain.
  • The mining process creates a new block from pending transactions, which is then saved to the ledger and proposed via the consensus module.
  • Consensus messages and new block notifications are broadcast through the P2P network, allowing other nodes to update their local copies of the chain.
  • Periodic peer discovery ensures the network remains well-connected and up-to-date with all participating nodes.

main.ts

import express from "express";
import { createServer } from "http";
import { Blockchain } from "./blockchain"; // Blockchain Node Software
import { ConsensusModule } from "./consensus"; // Consensus Module
import { SmartContractVM } from "./smartContractVM"; // Smart Contract Execution Environment (VM)
import { LedgerStorage } from "./ledgerStorage"; // Ledger Storage / Database Layer
import { CryptoUtils } from "./cryptoUtils"; // Cryptographic Components
import { P2PNetwork } from "./p2pNetwork"; // P2P Networking & Communication Stack
import { NodePool } from "./nodePool"; // Node Pool / Peer Discovery Mechanism
import { collectDefaultMetrics, Registry } from "prom-client";

const HTTP_PORT = process.env.HTTP_PORT || 3000;
const P2P_PORT = process.env.P2P_PORT || 6001;
const BOOTSTRAP_NODES = process.env.BOOTSTRAP_NODES
  ? process.env.BOOTSTRAP_NODES.split(",")
  : [];

const blockchain = new Blockchain();
const ledger = new LedgerStorage("./ledger-db");
const consensus = new ConsensusModule(`Node-${P2P_PORT}`, []);
const smartContractVM = new SmartContractVM();
const p2pNetwork = new P2PNetwork(P2P_PORT);
const nodePool = new NodePool(P2P_PORT, BOOTSTRAP_NODES);

const registry = new Registry();
collectDefaultMetrics({ register: registry });

const app = express();
app.use(express.json());

app.get("/chain", (req, res) => {
  res.json(blockchain.chain);
});

app.post("/transaction", (req, res) => {
  const tx = req.body;
  const signature = CryptoUtils.signData(
    JSON.stringify(tx),
    "your-private-key-here"
  );
  tx.signature = signature;
  blockchain.addTransaction(tx);
  res.json({ message: "Transaction added", tx });
});

app.post("/mine", async (req, res) => {
  const { minerAddress } = req.body;
  blockchain.minePendingTransactions(minerAddress);
  const latestBlock = blockchain.getLatestBlock();

  await ledger.saveBlock(latestBlock);
  consensus.proposeBlock(latestBlock);
  p2pNetwork.broadcast({ type: "NEW_BLOCK", block: latestBlock });

  res.json({ message: "Block mined", block: latestBlock });
});

app.post("/contract/deploy", (req, res) => {
  const { address, code } = req.body;
  smartContractVM.deployContract(address, code);
  res.json({ message: "Smart contract deployed" });
});

app.post("/contract/execute", (req, res) => {
  const { address, functionName, args } = req.body;
  try {
    const result = smartContractVM.executeContract(address, functionName, args);
    res.json({ result });
  } catch (error: any) {
    res.status(500).json({ error: error.message });
  }
});

app.get("/metrics", async (req, res) => {
  res.set("Content-Type", registry.contentType);
  res.end(await registry.metrics());
});

const httpServer = createServer(app);
httpServer.listen(HTTP_PORT, () => {
  console.log(`HTTP API server is running on port ${HTTP_PORT}`);
});

p2pNetwork.on("message", (data: any) => {
  try {
    const message = JSON.parse(data.toString());
    if (message.type === "NEW_BLOCK") {
      console.log(`Received new block via P2P: ${message.block.hash}`);
      if (blockchain.isChainValid()) {
        blockchain.chain.push(message.block);
        ledger.saveBlock(message.block);
      }
    }
  } catch (error) {
    console.error("Error processing P2P message:", error);
  }
});

consensus.on("message", (msg: any) => {
  p2pNetwork.broadcast(msg);
});

setInterval(() => {
  console.log("Initiating peer discovery...");
  nodePool.discoverPeers();
}, 10000);

console.log(`Node initialized:
  - HTTP API on port ${HTTP_PORT}
  - P2P network on port ${P2P_PORT}
  - Connected bootstrap nodes: ${BOOTSTRAP_NODES.join(", ")}`);

Conclusion

This article walks through a code oriented fundamentals of blockchain technology and provided a practical, integrated example of a blockchain node service built using TypeScript and Node.js. We covered key concepts: immutability, decentralization, and security, along with a modular architecture that includes a consensus module, smart contract execution environment, ledger storage, P2P networking, API endpoints, cryptographic utilities, and observability tools.

I hope this comprehensive guide, complete with detailed code examples, inspires you to build secure and scalable blockchain solutions. Happy coding!