区块类
接下来我们将创建区块类。在项目目录中,创建一个文件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以检查事务池是否为空。
您还可以为提交,准备和消息池创建更多此类末端。