区块链研究实验室|如何使用node.js语言实现PBFT协议part3

IP归属:

区块类

接下来我们将创建区块类。在项目目录中,创建一个文件block.js并在其中创建一个类Block。Block将具有以下属性:

  • timestamp - 块的生成时间

  • lastHash  - 最后一个块的哈希

  • hash  - 当前块的哈希值

  • data  - 块所持有的事务

  • proposer  - 块的创建者的公钥

  • signature - 块的签名哈希

  • sequenceNo  - 区块的序列号

// Import SHA256 used for hashing and ChainUtil for verifying signature
const SHA256 = require("crypto-js/sha256");
const ChainUtil = require("./chain-util");

class Block {
  constructor(
    timestamp,
    lastHash,
    hash,
    data,
    proposer,
    signature,
    sequenceNo
  ) {
    this.timestamp = timestamp;
    this.lastHash = lastHash;
    this.hash = hash;
    this.data = data;
    this.proposer = proposer;
    this.signature = signature;
    this.sequenceNo = sequenceNo;
  }

  // A function to print the block
  toString() {
    return `Block - 
        Timestamp   : ${this.timestamp}
        Last Hash   : ${this.lastHash}
        Hash        : ${this.hash}
        Data        : ${this.data}
        proposer    : ${this.proposer}
        Signature   : ${this.signature}
        Sequence No : ${this.sequenceNo}`
;
  }

  // The first block by default will the genesis block
  // this function generates the genesis block with random values
  static genesis() {
    return new this(
      `genesis time`,
      "----",
      "genesis-hash",
      [],
      "P4@P@53R",
      "SIGN",
      0
    );
  }

  // creates a block using the passed lastblock, transactions and wallet instance
  static createBlock(lastBlock, data, wallet) {
    let hash;
    let timestamp = Date.now();
    const lastHash = lastBlock.hash;
    hash = Block.hash(timestamp, lastHash, data);
    let proposer = wallet.getPublicKey();
    let signature = Block.signBlockHash(hash, wallet);
    return new this(
      timestamp,
      lastHash,
      hash,
      data,
      proposer,
      signature,
      1 + lastBlock.sequenceNo
    );
  }

  // hashes the passed values
  static hash(timestamp, lastHash, data) {
    return SHA256(JSON.stringify(`${timestamp}${lastHash}${data}`)).toString();
  }

  // returns the hash of a block
  static blockHash(block) {
    const { timestamp, lastHash, data } = block;
    return Block.hash(timestamp, lastHash, data);
  }

  // signs the passed block using the passed wallet instance
  static signBlockHash(hash, wallet) {
    return wallet.sign(hash);
  }

  // checks if the block is valid
  static verifyBlock(block) {
    return ChainUtil.verifySignature(
      block.proposer,
      block.signature,
      Block.hash(block.timestamp, block.lastHash, block.data)
    );
  }

  // verifies the proposer of the block with the passed public key
  static verifyProposer(block, proposer) {
    return block.proposer == proposer ? true : false;
  }
}

module.exports = Block;

pbft-block.js

 

TransactionPool类

我们需要一个地方来存储从其他节点接收到的事务。因此,我们将创建一个TransactionPool类来存储所有事务。创建名为transaction-pool.js的文件。

 

// Import transaction class used for verification
const Transaction = require("./transaction");

// Transaction threshold is the limit or the holding capacity of the nodes
// Once this exceeds a new block is generated
const { TRANSACTION_THRESHOLD } = require("./config");

class TransactionPool {
  constructor() {
    this.transactions = [];
  }

  // pushes transactions in the list
  // returns true if it is full
  // else returns false
  addTransaction(transaction) {
    this.transactions.push(transaction);
    if (this.transactions.length >= TRANSACTION_THRESHOLD) {
      return true;
    } else {
      return false;
    }
  }

  // wrapper function to verify transactions
  verifyTransaction(transaction) {
    return Transaction.verifyTransaction(transaction);
  }

  // checks if transactions exists or not
  transactionExists(transaction) {
    let exists = this.transactions.find(t => t.id === transaction.id);
    return exists;
  }

  // empties the pool
  clear() {
    console.log("TRANSACTION POOL CLEARED");
    this.transactions = [];
  }
}

module.exports = TransactionPool;

pbft-txn-pool.js 

 

BlockPool类

为了临时存储块,我们还将生成块池。创建一个block-pool.js文件,其中blockpool类保存块,直到将其添加到链中。当收到PRE-PREPARE消息时,块被添加到块池中。

const Block = require("./block");

class BlockPool {
  constructor() {
    this.list = [];
  }

  // check if the block exisits or not
  exisitingBlock(block) {
    let exists = this.list.find(b => b.hash === block.hash);
    return exists;
  }

  // pushes block to the chain
  addBlock(block) {
    this.list.push(block);
    console.log("added block to pool");
  }

  // returns the blcok for the given hash
  getBlock(hash) {
    let exists = this.list.find(b => b.hash === hash);
    return exists;
  }
}

module.exports = BlockPool;

pbft-block-pool.js

从节点接收的许多其他数据对象需要存储。PREPARE,COMMIT和NEW_ROUND消息。

因此,将创建另外三个池,即PreparePool,CommitPool和MessagePool。MessagePool将保存NEW_ROUND消息。

PreparePool类

const ChainUtil = require("./chain-util");

class PreparePool {
  // list object is mapping that holds a list prepare messages for a hash of a block
  constructor() {
    this.list = {};
  }

  // prepare function initialize a list of prepare message for a block
  // and adds the prepare message for the current node and
  // returns it
  prepare(block, wallet) {
    let prepare = this.createPrepare(block, wallet);
    this.list[block.hash] = [];
    this.list[block.hash].push(prepare);
    return prepare;
  }

  // creates a prepare message for the given block
  createPrepare(block, wallet) {
    let prepare = {
      blockHash: block.hash,
      publicKey: wallet.getPublicKey(),
      signature: wallet.sign(block.hash)
    };

    return prepare;
  }

  // pushes the prepare message for a block hash into the list
  addPrepare(prepare) {
    this.list[prepare.blockHash].push(prepare);
  }

  // checks if the prepare message already exists
  existingPrepare(prepare) {
    let exists = this.list[prepare.blockHash].find(
      p => p.publicKey === prepare.publicKey
    );
    return exists;
  }

  // checks if the prepare message is valid or not
  isValidPrepare(prepare) {
    return ChainUtil.verifySignature(
      prepare.publicKey,
      prepare.signature,
      prepare.blockHash
    );
  }
}

module.exports = PreparePool;

pbft-prepare-pool.js 

CommitPool类

 

在收到2f + 1准备消息后添加提交消息,因此我们使用准备消息来获取块哈希而不是传递整个区块。

const ChainUtil = require("./chain-util");

class CommitPool {
  // list object is mapping that holds a list commit messages for a hash of a block
  constructor() {
    this.list = {};
  }

  // commit function initialize a list of commit message for a prepare message
  // and adds the commit message for the current node and
  // returns it
  commit(prepare, wallet) {
    let commit = this.createCommit(prepare, wallet);
    this.list[prepare.blockHash] = [];
    this.list[prepare.blockHash].push(commit);
    return commit;
  }

  // creates a commit message for the given prepare message
  createCommit(prepare, wallet) {
    let commit = {};
    commit.blockHash = prepare.blockHash;
    commit.publicKey = wallet.getPublicKey();
    commit.signature = wallet.sign(prepare.blockHash);
    return commit;
  }

  // checks if the commit message already exists
  existingCommit(commit) {
    let exists = this.list[commit.blockHash].find(
      p => p.publicKey === commit.publicKey
    );
    return exists;
  }

  // checks if the commit message is valid or not
  isValidCommit(commit) {
    return ChainUtil.verifySignature(
      commit.publicKey,
      commit.signature,
      commit.blockHash
    );
  }

  // pushes the commit message for a block hash into the list
  addCommit(commit) {
    this.list[commit.blockHash].push(commit);
  }
}

module.exports = CommitPool;

pbft-cimmit-pool.js

 

MessagePool类

MessagePool将与其他两个池类似地工作。唯一的区别是它带来的额外信息。

const ChainUtil = require("./chain-util");

class MessagePool {
  // list object is mapping that holds a list messages for a hash of a block
  constructor() {
    this.list = {};
    this.message = "INITIATE NEW ROUND";
  }

  // creates a round change message for the given block hash
  createMessage(blockHash, wallet) {
    let roundChange = {
      publicKey: wallet.getPublicKey(),
      message: this.message,
      signature: wallet.sign(ChainUtil.hash(this.message + blockHash)),
      blockHash: blockHash
    };

    this.list[blockHash] = [roundChange];
    return roundChange;
  }

  // checks if the message already exists
  existingMessage(message) {
    if (this.list[message.blockHash]) {
      let exists = this.list[message.blockHash].find(
        p => p.publicKey === message.publicKey
      );
      return exists;
    } else {
      return false;
    }
  }

  // checks if the message is valid or not
  isValidMessage(message) {
    console.log("in valid here");
    return ChainUtil.verifySignature(
      message.publicKey,
      message.signature,
      ChainUtil.hash(message.message + message.blockHash)
    );
  }

  // pushes the message for a block hash into the list
  addMessage(message) {
    this.list[message.blockHash].push(message);
  }
}

module.exports = MessagePool;

区块类

我们拥有制作区块类所需的所有类。我们现在可以创建一个文件blockchain.js Blockchain类将具有以下属性:

  • chain  - 已确认的块列表

  • validatorsList  - 给定网络的验证器列表

// Import total number of nodes used to create validators list
const { NUMBER_OF_NODES } = require("./config");

// Used to verify block
const Block = require("./block");

class Blockchain {
  // the constructor takes an argument validators class object
  // this is used to create a list of validators
  constructor(validators) {
    this.validatorList = validators.generateAddresses(NUMBER_OF_NODES);
    this.chain = [Block.genesis()];
  }

  // pushes confirmed blocks into the chain
  addBlock(block) {
    this.chain.push(block);
    console.log("NEW BLOCK ADDED TO CHAIN");
    return block;
  }

  // wrapper function to create blocks
  createBlock(transactions, wallet) {
    const block = Block.createBlock(
      this.chain[this.chain.length - 1],
      transactions,
      wallet
    );
    return block;
  }

  // calculates the next propsers by calculating a random index of the validators list
  // index is calculated using the hash of the latest block
  getProposer() {
    let index =
      this.chain[this.chain.length - 1].hash[0].charCodeAt(0) % NUMBER_OF_NODES;
    return this.validatorList[index];
  }

  // checks if the received block is valid
  isValidBlock(block) {
    const lastBlock = this.chain[this.chain.length - 1];
    if (
      lastBlock.sequenceNo + 1 == block.sequenceNo &&
      block.lastHash === lastBlock.hash &&
      block.hash === Block.blockHash(block) &&
      Block.verifyBlock(block) &&
      Block.verifyProposer(block, this.getProposer())
    ) {
      console.log("BLOCK VALID");
      return true;
    } else {
      console.log("BLOCK INVLAID");
      return false;
    }
  }

  // updates the block by appending the prepare and commit messages to the block
  addUpdatedBlock(hash, blockPool, preparePool, commitPool) {
    let block = blockPool.getBlock(hash);
    block.prepareMessages = preparePool.list[hash];
    block.commitMessages = commitPool.list[hash];
    this.addBlock(block);
  }
}
module.exports = Blockchain;

p2pserver类

我们如何向其他节点发送消息?我们将制作一个P2P服务器。在p2p-server.js文件中创建p2pserver类

为了创建一个P2P服务器,我们将使用sockets。为了使用sockets,我们将安装一个“ws”模块。这个模块使得使用sockets非常容易。

 

npm i --save ws

P2pserver类是实现一致性算法的地方。这是该项目的核心, 该类负责处理消息并广播它们。

// import the ws module
const WebSocket = require("ws");

// import the min approval constant which will be used to compare the count the messages
const { MIN_APPROVALS } = require("./config");

// decalre a p2p server port on which it would listen for messages
// we will pass the port through command line
const P2P_PORT = process.env.P2P_PORT || 5001;

// the neighbouring nodes socket addresses will be passed in command line
// this statemet splits them into an array
const peers = process.env.PEERS ? process.env.PEERS.split(",") : [];

// message types used to avoid typing messages
// also used in swtich statement in message handlers
const MESSAGE_TYPE = {
  transaction: "TRANSACTION",
  prepare: "PREPARE",
  pre_prepare: "PRE-PREPARE",
  commit: "COMMIT",
  round_change: "ROUND_CHANGE"
};

class P2pserver {
  constructor(
    blockchain,
    transactionPool,
    wallet,
    blockPool,
    preparePool,
    commitPool,
    messagePool,
    validators
  ) {
    this.blockchain = blockchain;
    this.sockets = [];
    this.transactionPool = transactionPool;
    this.wallet = wallet;
    this.blockPool = blockPool;
    this.preparePool = preparePool;
    this.commitPool = commitPool;
    this.messagePool = messagePool;
    this.validators = validators;
  }

  // Creates a server on a given port
  listen() {
    const server = new WebSocket.Server({ port: P2P_PORT });
    server.on("connection", socket => {
      console.log("new connection");
      this.connectSocket(socket);
    });
    this.connectToPeers();
    console.log(`Listening for peer to peer connection on port : ${P2P_PORT}`);
  }

  // connects to a given socket and registers the message handler on it
  connectSocket(socket) {
    this.sockets.push(socket);
    console.log("Socket connected");
    this.messageHandler(socket);
  }

  // connects to the peers passed in command line
  connectToPeers() {
    peers.forEach(peer => {
      const socket = new WebSocket(peer);
      socket.on("open", () => this.connectSocket(socket));
    });
  }

  // broadcasts transactions
  broadcastTransaction(transaction) {
    this.sockets.forEach(socket => {
      this.sendTransaction(socket, transaction);
    });
  }

  // sends transactions to a perticular socket
  sendTransaction(socket, transaction) {
    socket.send(
      JSON.stringify({
        type: MESSAGE_TYPE.transaction,
        transaction: transaction
      })
    );
  }

  // broadcasts preprepare
  broadcastPrePrepare(block) {
    this.sockets.forEach(socket => {
      this.sendPrePrepare(socket, block);
    });
  }

  // sends preprepare to a particular socket
  sendPrePrepare(socket, block) {
    socket.send(
      JSON.stringify({
        type: MESSAGE_TYPE.pre_prepare,
        block: block
      })
    );
  }

  // broadcast prepare
  broadcastPrepare(prepare) {
    this.sockets.forEach(socket => {
      this.sendPrepare(socket, prepare);
    });
  }

  // sends prepare to a particular socket
  sendPrepare(socket, prepare) {
    socket.send(
      JSON.stringify({
        type: MESSAGE_TYPE.prepare,
        prepare: prepare
      })
    );
  }

  // broadcasts commit
  broadcastCommit(commit) {
    this.sockets.forEach(socket => {
      this.sendCommit(socket, commit);
    });
  }

  // sends commit to a particular socket
  sendCommit(socket, commit) {
    socket.send(
      JSON.stringify({
        type: MESSAGE_TYPE.commit,
        commit: commit
      })
    );
  }

  // broacasts round change
  broadcastRoundChange(message) {
    this.sockets.forEach(socket => {
      this.sendRoundChange(socket, message);
    });
  }

  // sends round change message to a particular socket
  sendRoundChange(socket, message) {
    socket.send(
      JSON.stringify({
        type: MESSAGE_TYPE.round_change,
        message: message
      })
    );
  }

  // handles any message sent to the current node
  messageHandler(socket) {
    // registers message handler
    socket.on("message", message => {
      const data = JSON.parse(message);

      console.log("RECEIVED"data.type);

      // select a perticular message handler
      switch (data.type) {
        case MESSAGE_TYPE.transaction:
          // check if transactions is valid
          if (
            !this.transactionPool.transactionExists(data.transaction) &&
            this.transactionPool.verifyTransaction(data.transaction) &&
            this.validators.isValidValidator(data.transaction.from)
          ) {
            let thresholdReached = this.transactionPool.addTransaction(
              data.transaction
            );
            // send transactions to other nodes
            this.broadcastTransaction(data.transaction);

            // check if limit reached
            if (thresholdReached) {
              console.log("THRESHOLD REACHED");
              // check the current node is the proposer
              if (this.blockchain.getProposer() == this.wallet.getPublicKey()) {
                console.log("PROPOSING BLOCK");
                // if the node is the proposer, create a block and broadcast it
                let block = this.blockchain.createBlock(
                  this.transactionPool.transactions,
                  this.wallet
                );
                console.log("CREATED BLOCK", block);
                this.broadcastPrePrepare(block);
              }
            } else {
              console.log("Transaction Added");
            }
          }
          break;
        case MESSAGE_TYPE.pre_prepare:
          // check if block is valid
          if (
            !this.blockPool.exisitingBlock(data.block) &&
            this.blockchain.isValidBlock(data.block)
          ) {
            // add block to pool
            this.blockPool.addBlock(data.block);

            // send to other nodes
            this.broadcastPrePrepare(data.block);

            // create and broadcast a prepare message
            let prepare = this.preparePool.prepare(data.block, this.wallet);
            this.broadcastPrepare(prepare);
          }
          break;
        case MESSAGE_TYPE.prepare:
          // check if the prepare message is valid
          if (
            !this.preparePool.existingPrepare(data.prepare) &&
            this.preparePool.isValidPrepare(data.prepare, this.wallet) &&
            this.validators.isValidValidator(data.prepare.publicKey)
          ) {
            // add prepare message to the pool
            this.preparePool.addPrepare(data.prepare);

            // send to other nodes
            this.broadcastPrepare(data.prepare);

            // if no of prepare messages reaches minimum required
            // send commit message
            if (
              this.preparePool.list[data.prepare.blockHash].length >=
              MIN_APPROVALS
            ) {
              let commit = this.commitPool.commit(data.prepare, this.wallet);
              this.broadcastCommit(commit);
            }
          }
          break;
        case MESSAGE_TYPE.commit:
          // check the validity commit messages
          if (
            !this.commitPool.existingCommit(data.commit) &&
            this.commitPool.isValidCommit(data.commit) &&
            this.validators.isValidValidator(data.commit.publicKey)
          ) {
            // add to pool
            this.commitPool.addCommit(data.commit);

            // send to other nodes
            this.broadcastCommit(data.commit);

            // if no of commit messages reaches minimum required
            // add updated block to chain
            if (
              this.commitPool.list[data.commit.blockHash].length >=
              MIN_APPROVALS
            ) {
              this.blockchain.addUpdatedBlock(
                data.commit.blockHash,
                this.blockPool,
                this.preparePool,
                this.commitPool
              );
            }
            // Send a round change message to nodes
            let message = this.messagePool.createMessage(
              this.blockchain.chain[this.blockchain.chain.length - 1].hash,
              this.wallet
            );
            this.broadcastRoundChange(message);
          }
          break;

        case MESSAGE_TYPE.round_change:
          // check the validity of the round change message
          if (
            !this.messagePool.existingMessage(data.message) &&
            this.messagePool.isValidMessage(data.message) &&
            this.validators.isValidValidator(data.message.publicKey)
          ) {
            // add to pool
            this.messagePool.addMessage(data.message);

            // send to other nodes
            this.broadcastRoundChange(message);

            // if enough messages are received, clear the pools
            if (
              this.messagePool.list[data.message.blockHash].length >=
              MIN_APPROVALS
            ) {
              this.transactionPool.clear();
            }
          }
          break;
      }
    });
  }
}

module.exports = P2pserver;

应用程序app

现在,我们将使用使用Express模块创建的应用程序连接所有文件。使用NPM安装快速模块。

npm i express --save

我们的应用程序将实例化池,钱包,区块链,p2pserver并声明一些端点与我们的区块链进行交互。

以下是所需的最低要求,(您可以添加更多):

POST:'/ transact' - 创建事务,请求对象由要存储在事务中的数据组成

GET:'/ transactions' - 将事务池中的事务作为响应发送

GET:'/ blocks' - 发送区块链的链作为响应

// Import all required modeles
const express = require("express");
const Wallet = require("./wallet");
const bodyParser = require("body-parser");
const TransactionPool = require("./transaction-pool");
const P2pserver = require("./p2p-server");
const Validators = require("./validators");
const Blockchain = require("./blockchain");
const BlockPool = require("./block-pool");
const CommitPool = require("./commit-pool");
const PreparePool = require("./prepare-pool");
const MessagePool = require("./message-pool");
const { NUMBER_OF_NODES } = require("./config");
const HTTP_PORT = process.env.HTTP_PORT || 3001;

// Instantiate all objects
const app = express();
app.use(bodyParser.json());

const wallet = new Wallet(process.env.SECRET);
const transactionPool = new TransactionPool();
const validators = new Validators(NUMBER_OF_NODES);
const blockchain = new Blockchain(validators);
const blockPool = new BlockPool();
const preparePool = new PreparePool();
const commitPool = new CommitPool();
const messagePool = new MessagePool();
const p2pserver = new P2pserver(
  blockchain,
  transactionPool,
  wallet,
  blockPool,
  preparePool,
  commitPool,
  messagePool,
  validators
);

// sends all transactions in the transaction pool to the user
app.get("/transactions", (req, res) => {
  res.json(transactionPool.transactions);
});

// sends the entire chain to the user
app.get("/blocks", (req, res) => {
  res.json(blockchain.chain);
});

// creates transactions for the sent data
app.post("/transact", (req, res) => {
  const { data } = req.body;
  const transaction = wallet.createTransaction(data);
  p2pserver.broadcastTransaction(transaction);
  res.redirect("/transactions");
});

// starts the app server
app.listen(HTTP_PORT, () => {
  console.log(`Listening on port ${HTTP_PORT}`);
});

// starts the p2p server
p2pserver.listen();

pbft-app.js 

 

这就完成了我们的编码。通过在单独的终端中运行以下内容来测试此应用程序:

第一个节点:

 

SECRET="NODE0" P2P_PORT=5000 HTTP_PORT=3000 node app

第二节点:

SECRET="NODE1" P2P_PORT=5001 HTTP_PORT=3001 PEERS=ws://localhost:5000 node app

第三节点:

 

SECRET="NODE2" P2P_PORT=5002 HTTP_PORT=3002 PEERS=ws://localhost:5001,ws://localhost:5000 node app

您可以根据需要制作多个。 在创建更多节点之前,请更新节点配置文件的总数。

 

命中任何节点的末端,直到池填满并通过命中/ blocks末端来检查链。

 

此外,点击末端/transactions以检查事务池是否为空。

 

您还可以为提交,准备和消息池创建更多此类末端。

本文来源:陀螺科技 文章作者:区块链研究实验室
收藏
举报
区块链研究实验室
累计发布内容13篇 累计总热度10万+

陀螺科技现已开放专栏入驻,详情请见入驻指南: https://www.tuoluo.cn/article/detail-27547.html

区块链研究实验室专栏: https://www.tuoluo.cn/columns/author1286336/

本文网址: https://www.tuoluo.cn/article/detail-56615.html

免责声明:
1、本文版权归原作者所有,仅代表作者本人观点,不代表陀螺科技观点或立场。
2、如发现文章、图片等侵权行为,侵权责任将由作者本人承担。

相关文章