使用 NodeJS 和 Socket.io 的区块链

2025-05-26

使用 NodeJS 和 Socket.io 的区块链

我对区块链的概念非常着迷,它是一个基于去中心化信任的系统,用于传输和验证通过网络发送的数据。它所基于的概念非常简单,所以为了理解其中涉及的概念,我决定创建自己的区块链,但很快,这些概念就变得说起来容易做起来难。那么,让我来详细讲解一下区块链究竟是什么,以及如何创建它。

区块链101

首先,区块链并非加密货币。区块链是加密货币背后的技术,就像互联网是电子邮件背后的技术一样。区块链是一种维护分布式数字账本的过程,其中包含一系列相互关联且不可篡改的记录。对于一项革命性技术来说,这是一个相当简单的定义。它已经颠覆了许多领域,例如医药、物流、教育和金融(主要是金融)。让我们来看看区块链的组成部分。

区块链的组成部分

  1. 块:它是数据的集合,通常受数据单元的大小或数量的上限限制。

  2. 链:它是通过使用前一个区块的信息的算法链接在一起的区块的集合。

  3. 节点:区块链中的一个系统,用于验证区块的真实性以及维护按时间顺序记录所有区块的数字分类账。

这些只是区块链的组成部分。真正确保区块链安全的是其中涉及的流程。

挖矿

挖矿是生成新区块并将其添加到网络的过程,它涉及两个过程。

  1. 工作量证明生成:
    如前所述,区块通过一种算法连接起来,该算法利用前一个区块的信息来生成下一个区块。这种算法被称为工作量证明,其设计通常使得输出难以生成,但验证输出却相当容易。

  2. 工作量证明验证:
    当网络中的一个节点成功生成区块时,网络中的其他节点必须验证该证明和链的真实性。因此,节点会验证该证明是否正确,并检查链的真实性。如果一切正常,该区块将被挖出,所有其他节点都会更新其账本,以包含新挖出的区块。

区块链事件流

让我们看看当你将数据添加到区块链时事件如何展开

  1. 发送方向链中的某个节点发出了交换数据​​的请求。

  2. 然后,该节点向其他节点广播有关传入其他节点的数据,并将其添加到当前交易池中。

  3. 一旦达到区块的限制(大小或单位数量),节点就会开始挖掘该区块。

  4. 节点之间相互竞争,寻找工作量证明的解决方案。当其中一个节点成功挖矿时,它会广播该解决方案。

  5. 然后,其他节点验证输出是否有效。然后,它们验证链上的区块,并添加新挖出的区块。

创建你自己的区块链

现在我们已经掌握了基础知识,让我们开始创建我们自己的区块链。我决定使用 Socket.io 来构建跨节点的实时通信系统。让我们继续创建模型。

模型

交易模型:

class Transaction {
  constructor(sender, receiver, amount) {
    this.sender = sender;
    this.receiver = receiver;
    this.amount = amount;
    this.timestamp = Date.now();
  }

  /* Stringfying and Parser functions */ 
}

module.exports = Transaction;
Enter fullscreen mode Exit fullscreen mode

这个模型非常简单,我们有特定的数据,如发送者、接收者、金额和时间戳。

块模型:

const crypto = require('crypto');

const Transaction = require('./transaction');

class Block {
  constructor(index, previousBlockHash, previousProof, transactions) {
    this.index = index;
    this.proof = previousProof;
    this.previousBlockHash = previousBlockHash;
    this.transactions = transactions;
    this.timestamp = Date.now();
  }

  hashValue() {
    const { index, proof, transactions, timestamp } = this;
    const blockString= `${index}-${proof}-${JSON.stringify(transactions)}-${timestamp}`;
    const hashFunction = crypto.createHash('sha256');
    hashFunction.update(blockString);
    return hashFunction.digest('hex');
  }

  setProof(proof) {
    this.proof = proof;
  }

  getProof() {
    return this.proof;
  }

  getIndex() {
    return this.index;
  }

  getPreviousBlockHash() {
    return this.previousBlockHash;
  }

  /* Stringify and Parsing functions */
}

module.exports = Block;
Enter fullscreen mode Exit fullscreen mode

Block 中最重要的部分是hashValue()previousBlockHashhashValue()负责创建区块的哈希值。它生成区块的字符串表达式,并将其发送到 NodeJScrypto模块的createHash()函数,并使用指定的算法创建哈希值sha256。生成的哈希值随后存储在 中,用于下一个区块previousBlockHash

链条型号:

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

const actions = require('../constants');

const { generateProof, isProofValid } = require('../utils/proof');

class Blockchain {
  constructor(blocks, io) {
    this.blocks = blocks || [new Block(0, 1, 0, [])];
    this.currentTransactions = [];
    this.nodes = [];
    this.io = io;
  }

  addNode(node) {
    this.nodes.push(node);
  }

  mineBlock(block) {
    this.blocks.push(block);
    console.log('Mined Successfully');
    this.io.emit(actions.END_MINING, this.toArray());
  }

  async newTransaction(transaction) {
    this.currentTransactions.push(transaction);
    if (this.currentTransactions.length === 2) {
      console.info('Starting mining block...');
      const previousBlock = this.lastBlock();
      process.env.BREAK = false;
      const block = new Block(previousBlock.getIndex() + 1, previousBlock.hashValue(), previousBlock.getProof(), this.currentTransactions);
      const { proof, dontMine } = await generateProof(previousBlock.getProof());
      block.setProof(proof);
      this.currentTransactions = [];
      if (dontMine !== 'true') {
        this.mineBlock(block);
      }
    }
  }

  lastBlock() {
    return this.blocks[this.blocks.length - 1];
  }

  getLength() {
    return this.blocks.length;
  }

  checkValidity() {
    const { blocks } = this;
    let previousBlock = blocks[0];
    for (let index = 1; index < blocks.length; index++) {
      const currentBlock = blocks[index];
      if (currentBlock.getPreviousBlockHash() !== previousBlock.hashValue()) {
        return false;
      }
      if (!isProofValid(previousBlock.getProof(), currentBlock.getProof())) {
        return false;
      }
      previousBlock = currentBlock;
    }
    return true;
  }

  /* Stringify and Parsing functions */
}

module.exports = Blockchain;
Enter fullscreen mode Exit fullscreen mode

该链由两个主要部分组成currentTransactions:和blockscurrentTransactions包含尚未被挖出区块的交易列表。blocks包含链中所有区块的列表,按挖出时间排序。上述链的区块大小也为 2 个交易。

在构造函数中,我们将 设置blocks为初始只有一个包含 和 的块index0 previousBlockHash1称为初始块。但是我们有一个传递给链的块列表,我们只是用它接收到的值proof设置它。0blocks

addNode()函数负责将当前节点与区块链网络的其他节点连接起来。该mineBlock()函数将挖出的区块添加到链中,并向其他区块发出信号以结束挖矿。

最重要的方法是newTransaction()checkValidity()newTransaction()当节点收到交易请求时,会调用该方法。我们将交易推送到currentTransactions池中。如果池的大小currentTransaction为 2,我们就开始挖掘区块。我们首先获取当前链的最新区块。我们根据最新区块的hashValueindex和池子创建一个区块。然后,我们通过传递最新区块的to方法currentTransactions来生成工作量证明的解决方案(我们稍后会研究这种实现)。一旦得到解决方案,我们就会设置新创建的区块的证明。然后,我们重置池子,并使用标志检查是否可以使用该区块进行挖掘。如果可以挖掘,我们继续挖掘该区块。proofgenerateProof()currentTransactiondontMine

checkValidity()方法从初始块开始检查链的有效性。我们获取currentBlockpreviousBlock,然后检查当前块是否previousHash与前一个块的相同hashValue。如果它们不匹配,则拒绝。然后,我们检查当前块和前一个块之间的证明的有效性。如果它们也不匹配,则拒绝该链。然后,我们检查是否currentBlockpreviousBlock。我们这样做直到链的末尾,如果没有发现差异,则返回 true。

上述区块验证机制使得区块链无法被攻破和篡改。如果攻击者想要更改currentBlocks数据,他必须更改previousBlocks数据本身,因为我们的哈希计算是基于数据的。如果数据发生变化,哈希值也会随之改变,因此攻击者必须一直这样做直到生成初始区块。另一个安全方面来自证明生成。如果攻击者更改了区块的篡改记录,证明方案也会随之改变,因此攻击者必须再次从初始区块到被篡改区块生成证明,这可能需要耗费大量时间,因为证明计算并非易事。

工作量证明的生成和验证

const crypto = require('crypto');


const generateProof = (previousProof) => new Promise((resolve) => {
  setImmediate(async () => {
    let proof = Math.random() * 10000000001;
    const dontMine = process.env.BREAK;
    if (isProofValid(previousProof, proof) || dontMine === 'true') {
      resolve({ proof, dontMine });
    } else  {
      resolve(await generateProof(previousProof));
    }
  });
});

const isProofValid = (previousProof, currentProof) => {
  const difference = currentProof - previousProof;
  const proofString = `difference-${difference}`;
  const hashFunction = crypto.createHash('sha256');
  hashFunction.update(proofString);
  const hexString = hashFunction.digest('hex');
  if (hexString.includes('000000')) {
    return true;
  }
  return false;
};

exports.generateProof = generateProof;
exports.isProofValid = isProofValid;
Enter fullscreen mode Exit fullscreen mode

这是任何区块链挖矿中最耗时且至关重要的部分。工作量证明可以解决一些更难解决但更容易验证的问题。例如,生成两个大素数,使它们相乘后可以被 5 整除。找到这两个大素数是一项艰巨的任务,我们必须遍历数百万种组合才能找到其中一个可能的解。但验证这两个大素数的乘积是否能被 5 整除却很容易。

我们将生成算法包装在一个setImmediate函数中,并将其进一步包装在下Promise。好的,现在你一定想知道为什么要将它包装在下setImmediate。原因是我有一个环境变量,它表示挖掘过程的结束。true如果网络中的任何其他节点已完成挖掘一个块,我会将该变量设置为。如果我将生成算法包装在一个while循环中,它会阻止事件循环,并且永远不会检查环境变量的状态,直到它完成得出解决方案为止。setImmediate允许它绕过它,因为它会等到当前进程执行完毕后再进行下一次生成。这允许我的程序中的另一个模块去更改环境变量的状态。它还允许我在调用递归函数时绕过调用堆栈限制。

我们的工作量证明问题很简单:当前证明与先前证明之差的哈希值必须包含六个连续的零。我们先选择一个随机数,并将其乘以一个大数。然后,我们验证证明是否满足条件,并验证是否已设置挖矿结束时间。如果满足条件,则解析该值,否则再次尝试。重复此过程,直到获得证明。

服务器应用程序

太好了,我们有了模型和生成设置,我们需要的是一个工作服务器来协调动作并与区块链交互。

const app = require('express')();
const bodyParser = require('body-parser');
const httpServer = require('http').Server(app);
const axios = require('axios');
const io = require('socket.io')(httpServer);
const client = require('socket.io-client');

const BlockChain = require('./models/chain');
const SocketActions  = require('./constants');

const socketListeners = require('./socketListeners');

const { PORT } = process.env;

const blockChain = new BlockChain(null, io);

app.use(bodyParser.json());

app.post('/nodes', (req, res) => {
  const { host, port } = req.body;
  const { callback } = req.query;
  const node = `http://${host}:${port}`;
  const socketNode = socketListeners(client(node), blockChain);
  blockChain.addNode(socketNode, blockChain);
  if (callback === 'true') {
    console.info(`Added node ${node} back`);
    res.json({ status: 'Added node Back' }).end();
  } else {
    axios.post(`${node}/nodes?callback=true`, {
      host: req.hostname,
      port: PORT,
    });
    console.info(`Added node ${node}`);
    res.json({ status: 'Added node' }).end();
  }
});

app.post('/transaction', (req, res) => {
  const { sender, receiver, amount } = req.body;
  io.emit(SocketActions.ADD_TRANSACTION, sender, receiver, amount);
  res.json({ message: 'transaction success' }).end();
});

app.get('/chain', (req, res) => {
  res.json(blockChain.toArray()).end();
});

io.on('connection', (socket) => {
  console.info(`Socket connected, ID: ${socket.id}`);
  socket.on('disconnect', () => {
    console.log(`Socket disconnected, ID: ${socket.id}`);
  });
});

blockChain.addNode(socketListeners(client(`http://localhost:${PORT}`), blockChain));

httpServer.listen(PORT, () => console.info(`Express server running on ${PORT}...`));
Enter fullscreen mode Exit fullscreen mode

该服务器由 Express 和 Socket 应用组成,它们绑定到在特定端口上运行的 HTTP 服务器。/nodes端点允许我们连接到另一个节点的 Socket 应用,并发送信息以便另一个节点进行连接。我们还将 Socket 监听器绑定到创建的 Socket 连接。/transaction端点接收交易请求并将交易信息广播给其他节点。/chain端点列出了区块链的详细信息。此外,还有一个 Socket 连接监听器,它会主动记录 ID 并监控节点之间的连接状态。最后,我们让服务器监听特定的端口。

套接字监听器

服务器应用程序仅充当套接字监听器的外观,仅用于促进节点之间的连接。套接字监听器负责触发事件,例如将交易添加到区块链、挖掘新区块以及向区块链网络中的其他节点发送节点成功挖矿的信号。

const SocketActions = require('./constants');

const Transaction = require('./models/transaction');
const Blockchain = require('./models/chain');

const socketListeners = (socket, chain) => {
  socket.on(SocketActions.ADD_TRANSACTION, (sender, receiver, amount) => {
    const transaction = new Transaction(sender, receiver, amount);
    chain.newTransaction(transaction);
    console.info(`Added transaction: ${JSON.stringify(transaction.getDetails(), null, '\t')}`);
  });

  socket.on(SocketActions.END_MINING, (newChain) => {
    console.log('End Mining encountered');
    process.env.BREAK = true;
    const blockChain = new Blockchain();
    blockChain.parseChain(newChain);
    if (blockChain.checkValidity() && blockChain.getLength() >= chain.getLength()) {
      chain.blocks = blockChain.blocks;
    }
  });

  return socket;
};

module.exports = socketListeners;
Enter fullscreen mode Exit fullscreen mode

套接字监听两个事件ADD_TRANSACTION以及END_MINING其他节点发出的事件。ADD_TRANSACTION监听器主动监听网络上任何节点触发的传入交易事件。通过调用链的newTransaction方法将其添加到区块链。

END_MINING当其中一个节点成功挖出区块时,会触发该事件。它会将BREAK标志设置为 true,告知网络上的其他节点停止挖矿并开始验证解决方案。我们将字符串化的链解析回正确的区块链,并调用checkValidity()已解析链的方法。我们还会检查已解析链的长度是否大于当前链的长度。如果成功,则继续进行,并用新链替换,否则,我们将拒绝并保留旧链。

现在我们已经设置了链的代码,让我们运行它并查看输出。

运行它...

我使用 PM2 在不同端口上生成应用程序的实例。因此,一旦两个实例启动并运行,我就会触发/nodes其中一个节点的端点连接到另一个节点,输出如下:

初始状态

输出表明两个节点已成功建立它们之间的 websocket 连接。

然后我从其中一个节点触发/transaction端点。然后它向另一个节点发出有关传入交易的信号,然后它们都将该交易添加到各自的交易池中。

第一笔交易

然后我再次触发/transaction,由于我们的区块大小为 2,因此两个节点都会开始挖矿。当其中一个节点成功挖出区块时,它表示挖矿结束并启动验证过程。验证完成后,新的链将在整个网络中替换。

挖矿区块

然后,当我到达/chain端点时,我会收到链中的块列表。

链

就这样,我们使用 NodeJS 和 Socket.io 创建了自己的区块链

结论

我们创建的是一个简单的区块链。本文旨在通过实际实现来总结管理和驱动区块链的基本流程。区块链分叉等概念我尚未涉及,但建议您阅读一下。区块链交易中还涉及一些其他流程,本文未作介绍,因为其他文章和帖子已经深入探讨了区块链架构。我已经将代码提交到GitHub,您可以克隆代码库并尝试添加新流程 ;)。

最后,当您想要了解某些东西时,尝试学习概念并自行实施,这将帮助您更深入地了解和掌握所涉及的技术。

文章来源:https://dev.to/sadarshannaiynar/blockchain-using-nodejs-and-socketio-5gbe
PREV
揭秘 Webpack
NEXT
2022 年前端开发面试清单和路线图