在前面的章节中,我们构建了一个由五个节点组成的网络。每个节点都知道网络中的所有其他节点,从而创建了一个分布式区块链网络。我们现在需要创建一个同步网络,这样每个节点上的区块链都是相同的,数据在整个过程中是一致的。我们负担不起在不同节点上运行不同版本的区块链,因为这将彻底破坏拥有区块链的目的。每个节点上应该只有一个版本的区块链是一致的。因此,在本章中,让我们同步我们在第 4 章中构建的网络,创建一个分布式区块链网络。我们将通过广播在网络中所有节点上挖掘的事务和新块来实现这一点。
本章将介绍以下主题:
让我们开始同步网络
让我们试着理解为什么网络需要同步。区块链网络目前由五个分布式节点组成。这些节点之间的数据不一致;每个节点上的数据可能会有所不同,这将导致无法实现区块链的目的。让我们通过一个例子来尝试了解这种情况。转到 Postman 并发送一个示例事务,如以下屏幕截图所示:
点击发送按钮,将此事务发送到localhost:3001
上托管的节点。此交易将出现在localhost:3001/blockchain
的pendingTransactions
数组中,您可以在下面的屏幕截图中看到:
现在,转到任何其他节点并检查发送的事务。我们将无法查看这些节点上的pendingTransactions
数组中的事务。发送的示例事务将仅出现在localhost:3001
上的节点中。它不会广播到网络中的任何其他节点
在本章中,您将要做的是重构/transaction
端点,以便无论何时创建事务,它都会广播到所有节点。这意味着所有节点将拥有相同的数据。我们需要做同样的事情来挖掘一个块。让我们重构/mine
端点,这样每当挖掘新块时,它也会在整个网络中广播。这意味着整个网络是同步的,并且具有完全相同的块数。通过网络同步数据是区块链技术的一个重要特征
在本节中,让我们通过将createNewTransaction
方法拆分为两个单独的部分来重构它。一部分将简单地创建一个新事务,然后返回该事务,另一部分将新事务推送到pendingTransactions
数组中。我们将通过为其创建一个单独的方法来完成后一部分。我们还创建了一个名为/transaction/broadcast
的新事务端点。该端点将允许我们在整个区块链网络中广播交易,以便每个节点都有相同的数据,从而使整个网络同步。
在这里,我们将createNewTransaction
方法分成两个单独的方法,修改如下:
dev/blockchain.js
文件中的createNewTransaction
方法,我们在第二章中构建了该方法,在创建 createNewTransaction 方法一节中构建区块链中构建了该方法,请参考以下createNewTransaction
方法:Blockchain.prototype.createNewTransaction = function (amount, sender, recipient) {
const newTransaction = {
amount: amount,
sender: sender,
recipient: recipient,
};
this.newTransactions.push(newTransaction);
return.this.getlastBlock() ['index'] + 1;
}
Blockchain.prototype.createNewTransaction = function (amount, sender, recipient) {
const newTransaction = {
amount: amount,
sender: sender,
recipient: recipient,
transactionId: uuid().split('-').join('')
};
return newTransaction;
}
在这里,每个事务都会添加一个 ID。为了创建这个 ID,使用了一个唯一的字符串,这非常类似于我们在第 3 章、中通过 API 访问区块链时创建节点地址所使用的字符串。
uuid
库创建的。因此,在定义了所有常量的dev/blockchain.js
文件的开头,您需要添加以下代码行,以便在我们的项目中使用uuid
库:const uuid = require('uuid/v1');
在修改后的方法中,您可以看到添加了以下代码行以为transactionId
值创建唯一的字符串。这就是使用uuid
库的地方:
transactionId: uuid().split('-').join('')
这里,.split()
函数将去掉添加到唯一字符串中的破折号,.join()
函数将重新连接字符串,为每个事务提供唯一的Id
输出
接下来,我们需要推送返回到区块链pendingTransactions
阵列的newTransaction
。因此,让我们创建另一个方法,称为addTransactionToPendingTransactions
:
dev/blockchain.js
文件中,将addTransactionToPendingTransactions
方法定义如下:Blockchain.prototype.addTransactionToPendingTransactions = function(transactionObj) {
};
transactionObj
推到区块链的pendingTransactions
阵列:Blockchain.prototype.addTransactionToPendingTransaction = function(transactionObj) {
this.pendingTransactions.push(transactionObj);
};
Blockchain.prototype.addTransactionToPendingTransaction = function(transactionObj) {
this.pendingTransaction.push(transactionObj);
return this.getLastBlock()['index'] + 1;
};
为了快速回顾,我们修改了createNewTransaction
方法,该方法创建一个新事务,并返回该新事务。然后我们创建了一个新方法,名为addTransactionToPendingTransactions
。此方法接收一个transactionObj
并将其添加到区块链上的pendingTransactions
阵列中。之后,我们只返回添加了新事务的块的索引。
在本节中,让我们构建一个名为/transaction/broadcast
的新端点。从现在开始,任何时候我们想要创建一个新事务,我们都将访问这个/transaction/broadcast
端点。该端点将完成两件事:
让我们通过以下步骤来创建端点:
dev/networkNode.js
文件,我们在其中定义了所有端点,并按如下方式添加此新端点:app.post('/transaction/broadcast', function(req, res) ) {
});
app.post('/transaction/broadcast', function(req, res) ) {
const newTransaction = bitcoin.createNewTransaction();
});
此处的createNewTransaction()
方法是上一节中的修改方法。
createNewTransaction()
方法采用amount
、sender
和recipient
参数。对于我们的端点,假设所有这些数据都是通过req.body
发送的。因此,这些参数将定义为以下代码中突出显示的参数:app.post('/transaction/broadcast', function(req, res) ) {
const newTransaction = bitcoin.createNewTransaction(req.body.amount, req.body.sender, req.body.recipient);
});
addTransactionToPendingTransactions
方法将newTransaction
变量添加到节点上的pendingTransactions
数组中。因此,在前一行代码之后,添加以下行:bitcoin.addTransactionToPendingTransactions (newTransaction);
bitcoin.netowrkNodes.forEach(networkNodeUrl => {
//...
});
forEach
循环中,让我们定义用于广播事务的代码。为此,向网络中所有其他节点上的/transaction
端点发出请求。因此,请为这些请求定义一些选项。在循环内部,让我们添加以下行:const requestOptions = {
};
const requestOptions = {
uri: networkNodeUrl + '/transaction',
method: 'POST',
body: newTransaction,
json: true
};
forEach
循环之前定义数组,如下所示:const requestPromises = [];
requestPromises.push(rp(requestOptions));
在前面这行代码中,我们将把所有请求推送到requestPromises
数组中。在forEach
循环运行之后,我们应该拥有requestPromises
数组中定义的所有请求。
forEach
循环之后,添加以下行:promise.all(requestPromises)
.then(data => {
});
.then(data => {
res.json({ note: 'Transaction created and broadcast successfully.'})
});
通过添加前一行代码,我们成功地完成了构建/transaction/broadcast
端点的工作。
我们将在本节中重构/transaction
端点,使其与新的/transaction/broadcast
端点完美配合。让我们应用以下步骤来修改端点:
dev/networkNode.js
文件并删除/transaction
端点中的所有内容。只有在广播发生时才会命中/transaction
端点。当命中/transaction
端点时,newTransaction
变量将作为数据发送。该条件可定义如下:app.post('/transaction', function(req, res) {
const newTransaction = req.body;
};
在前面突出显示的行中,newTransaction
变量在req.body
的帮助下发送到/transaction
端点
pendingTransactions
数组中。为此,将使用新的addTransactionToPendingTransactions
方法。因此,在前一行代码之后,添加以下行:bitcoin.addTransactionToPendingTransactions();
newTransaction
变量:bitcoin.addTransactionToPendingTransactions(newTransaction);
addTransactionToPendingTransactions
方法,我们得到了将添加事务的块的索引。让我们将此块索引保存在新的/transaction
端点中。在前一行代码的开头添加变量,如下所示:const blockIndex = bitcoin.addTransactionToPendingTransactions(newTransaction);
res.json({ note: 'Transaction will be added in block ${blockIndex}.'});
我们现在已经完成了对/transaction
端点的重构
让我们测试/transaction/broadcast
和/transaction
端点,以确保它们一起正确工作。
对于这个测试,我们需要做的第一件事是将所有节点连接在一起,形成一个网络。您可能还记得如何做到这一点,正如我们在第 4 章创建分布式区块链网络中了解到的。无论如何,我们将快速完成这些步骤,以刷新您的记忆。
查看以下步骤以了解如何连接所有节点:
/register-and-broadcast-node
路线。这可以在任何节点上完成。在我们的示例中,让我们使用localhost:3001
。localhost:3001/blockchain
,然后按回车。您将看到networkNodes
数组中的所有节点现在区块链网络已经建立,让我们测试一下我们在前面几节中创建的端点
让我们创建一个事务并将其发送到/transaction/broadcast
端点。返回 Postman 并点击节点上的/transaction/broadcast
端点,该节点位于端口3001
上。在此,将一些数据作为事务发送,如以下屏幕截图所示:
您正在发送的事务数据可以是任何随机数据。我们所需要的只是金额、发件人和收件人。添加事务数据后,让我们通过单击发送按钮发送此请求。如果事务发送成功,将收到一个响应,显示事务创建并广播成功。
现在,转到浏览器,您应该能够看到我们在网络的每个节点上创建的事务。让我们检查一下这是否有效。在浏览器的地址栏中,键入localhost:3001/blockchain
,然后按输入。您应该可以看到pendingTransactions
数组中的事务数据,如下图所示:
这里,pendingTransactions
数组中的事务现在也有一个transactionId
值,该值以随机散列开始。
接下来,打开另一个选项卡,在地址栏中键入localhost:3002/blockchain
,然后按进入。您可以看到,在阵列中可以看到相同的事务数据:
如果转到网络中的其他节点,则可以对所有剩余节点执行类似的检查。您可以在每个节点的pendingTransactions
数组中观察到相同的事务数据。区块链网络中的每个节点现在都知道创建了一个新的交易。
您也可以尝试使用其他事务数据测试端点。尝试将金额更改为500
,将发送方和接收方的地址更改为随机哈希字符串,并尝试将此请求发送到托管在localhost:3004
上的节点。这应该没有什么区别,因为广播端点将事务数据发送到网络中的所有节点。因此,这个请求应该与上一个请求一样工作。在浏览器上检查响应时,您应该能够看到具有不同事务 ID 的两个事务
Try experimenting with different transaction data to gain a clear understanding of how the /transaction
and /transaction/broadcast
endpoints work.
从测试中,我们可以得出结论,/transaction/broadcast
端点和/transaction
端点都像我们预期的那样正常工作
在下一节中,我们将通过重构/mine
端点来继续同步网络,这样它将向整个网络广播创建的新块。
同步网络所需的下一件事是更新/mine
端点。我们还将添加一个新端点,称为/receive-new-block
。需要更新/mine
端点,以便每当节点创建新块时,该新块将广播到网络中的所有其他节点。这意味着网络上的每个节点都知道已创建了一个新块,并且承载区块链的所有节点保持同步。
无论何时挖掘新块,都将在特定节点上进行挖掘。为了理解更新的挖掘过程,假设我们需要一个托管在端口3001
上的节点为区块链挖掘一个新块:
首先,在所选节点上点击/mine
端点。当到达/mine
端点时,通过工作证明创建一个新块。
创建新块后,它将广播到网络中的所有其他节点。所有其他节点将在其/receive-new-block
端点接收该新块,如下图所示:
另一件需要注意的事情是,当一个新块被广播并且一个节点接收到它时,在链验证该块是合法的之后,该新块将被添加到链中。然后,节点清除其pendingTransactions
数组,因为所有挂起的事务现在都在它们刚刚接收到的新块中。
在接下来的几节中,我们将逐步构建整个流程。当我们构建这些步骤中的每一步时,应该更容易看到所有东西是如何协同工作的。
让我们通过实现以下步骤重构/mine
端点:
dev/networkNode.js
档案。在/mine
端点中,在定义newBlock
变量的部分下面,让我们添加将新块广播到网络中所有其他节点的功能。要做到这一点,请遵循我们在前面章节中介绍的相同过程,即循环网络中的所有其他节点,向节点发出请求,并将newBlock
变量作为数据发送:bitcoin.networkNodes.forEach(networkNodeUrl => {
})
前一行提到,对于每个networkNodes
,我们将发出请求并发送newBlock
bitcoin.networkNodes.forEach(networkNodeUrl => {
const requestOptions = {
};
})
uri
。我们要向其发送请求的uri
将是networkNodeUrl
和我们要创建的新端点,即/receive-new-block
。我们将在下一节中研究此端点:bitcoin.networkNodes.forEach(networkNodeUrl => {
const requestOptions = {
uri: networkNodeUrl + '/receive-new-block',
};
})
POST
方法:bitcoin.networkNodes.forEach(networkNodeUrl => {
const requestOptions = {
uri: networkNodeUrl + '/receive-new-block', method: 'POST',
};
})
newBlock
实例:bitcoin.networkNodes.forEach(networkNodeUrl => {
const requestOptions = {
uri: networkNodeUrl + '/receive-new-block',method: 'POST', body: { newBlock: newBlock }
};
})
body
之后,将json
设置为true
,如下所示:bitcoin.networkNodes.forEach(networkNodeUrl => {
const requestOptions = {
uri: networkNodeUrl + '/receive-new-block',method: 'POST', body: { newBlock: newBlock },
json: true
};
})
bitcoin.networkNodes.forEach(networkNodeUrl => {
const requestOptions = {
uri: networkNodeUrl + '/receive-new-block',method: 'POST', body: { newBlock: newBlock },
json: true
};
rp(requestOptions)
})
const requestPromises = [];
bitcoin.networkNodes.forEach(networkNodeUrl => {
const requestOptions = {
uri: networkNodeUrl + '/receive-new-block',method: 'POST', body: { newBlock: newBlock },
json: true
};
requestPromises.push(rp(requestOptions));
});
在forEach
循环运行之后,我们应该有一个充满承诺的数组。
forEach
块之后,添加以下代码:Promise.all(requestPromises)
.then(data => {
// ....
})
在所有请求运行后,我们希望在.then(data => { })
内进行另一次计算。如果您还记得,当创建新交易时,挖掘奖励交易代码bitcoin.createNewTransaction(12.5, "00" , nodeAddress);
需要在整个区块链网络中广播。此时,当挖掘一个新块时,我们创建一个挖掘奖励事务,但它不会广播到整个网络。要广播它,请求将被发送到/transaction/broadcast
端点,因为它已经具有广播事务的功能。我们将使用传入的挖掘奖励事务数据调用此端点。
Promise.all(requestPromises)
.then(data => {
const requestOptions = {
uri: bitcoin.currentNodeUrl + '/transaction/broadcast',
method: 'POST',
};
})
body
数据将作为对象发送。在body
中,我们添加挖掘奖励交易数据:Promise.all(requestPromises)
.then(data => {
const requestOptions = {
uri: bitcoin.currentNodeUrl + '/transaction/broadcast',
method: 'POST',
body: {
amount: 12.5,
sender:"00",
recipient: nodeAddress
}
};
})
body
之后,通过添加以下行将json
设置为true
:json: true
requestOptions
之后,我们发送以下请求:return rp(requestOptions);
此时,在/mine
端点内,正在进行一系列计算以创建新块。然后,一旦创建了新的块,它将被广播到网络中的所有其他节点。在.then
块内的广播完成后,向/transaction/broadcast
端点发出新的请求。此请求将创建挖掘奖励交易,然后节点将其广播到整个区块链网络。然后,在请求运行且所有计算完成后,发送响应:New block mined successfully。
You can view the complete updated mine endpoint code at https://github.com/PacktPublishing/Learn-Blockchain-Programming-with-JavaScript/blob/master/dev/networkNode.js.
我们要做的下一件事是构建我们在更新的/mine
端点中使用的/receive-new-block
端点。让我们开始构建端点:
dev/networkNode.js
文件中,在/register-and-broadcast-node
端点之前,定义/receive-new-block
端点如下:app.post('/receive-new-block', function(req, res) {
};
app.post('/receive-new-block', function(req, res) {
const newBlock = req.body.newBlock;
};
newBlock
上的previousBlockHash
以确保它等于链中最后一个块上的哈希值。为此,需要访问链中的最后一个块:app.post('/receive-new-block', function(req, res) {
const newBlock = req.body.newBlock;
const lastBlock = bitcoin.getLastBlock();
};
newBlock
实例中的previousBlockHash
: lastBlock.hash === newBlock.previousBlockHash;
newBlock
确实紧跟在链中的lastBlock
之后。前面定义的语句将返回true
或false
。true
或false
值将保存在correctHash
变量中:const correctHash = lastBlock.hash === newBlock.previousBlockHash;
newBlock
具有正确的索引。这意味着newBlock
应该是链中lastBlock
之上的一个索引。添加以下复选框:const correctIndex = lastBlock['index'] + 1 === newBlock['index'];
newBlock
是否合法,需要采取两种不同的行动。如果newBlock
合法,则应接受并添加到链中。如果它不应该被拒绝,那么它就应该被拒绝。为了定义这个条件,让我们使用一个if
-else
语句:if (correctHash && correctIndex) {
bitcoin.chain.push(newBlock);
}
newBlock
已添加到链中,pendingTransactions
数组需要清除,因为挂起的事务现在位于新块中。因此,在if
语句中,需要添加的下一个条件如下:bitcoin.pendingTransaction = [];
if
语句中,在前一行下方添加响应,如下所示:res.json({
note: 'New block received and accepted.',
newBlock: newBlock
})
newBlock
不合法且未通过前面定义的任何一项测试,则在else
语句中发送一个响应,指示该块被拒绝:else{
res.json({
note:'New block rejected.',
newBlock: newBlock
});
}
加上前面的条件,我们已经完成了/receive-new-block
端点的构建。
让我们测试更新的/mine
端点和我们刚刚创建的/receive-new-block
端点。基本上,/mine
端点将为我们挖掘新块。它还将获取该块并在整个区块链网络中广播,以便每个节点都是同步的,并且所有节点都具有相同的块和相同的数据。这是我们在测试/mine
终点时预期观察到的结果:
localhost:3001/mine
,然后点击输入。您将获得如下输出:看起来矿井的端点工作得很好。响应表明新块已成功挖掘和广播。您还可以在前面的屏幕截图中看到新块及其索引
localhost:3001/blockchain
,然后按进入。您可以看到,新块已添加到网络中,如下所示:在前面的屏幕截图中,您可能还注意到pendingTransactions
数组中存在一些事务。这个挂起的事务实际上是我们刚刚挖掘的块的挖掘奖励。更新后的/mine
端点定义在创建新块后应广播挖掘奖励事务。
从现在开始,每当创建一个新的区块时,该区块的采矿奖励将进入pendingTransactions
数组并添加到下一个区块。这就是挖掘奖励在比特币区块链中的工作方式。当我们在前两章中首次创建区块链时,我们将挖掘奖励放在我们挖掘的区块中。现在区块链更加先进,我们有了一个分布式网络,我们必须遵循最佳实践,并将采矿奖励投入下一个区块。
让我们回到/mine
端点,继续测试。让我们检查网络中的其他节点,并验证挖掘的新块是否添加到这些节点。另外,让我们检查生成的挖掘奖励是否也广播到网络中的其他节点。
打开另一个选项卡,在地址栏中键入localhost:3002/blockchain
,然后按进入。您将看到以下输出:
在前面的屏幕截图中,您可以看到端口3002
上的节点收到了新挖掘的块以及挖掘奖励事务。您可以为网络中的其余节点验证这一点
现在让我们从另一个节点挖掘另一个块。在浏览器的地址栏中输入localhost:3004/mine
而不是进入localhost:3001
,然后按进入。新区块将被开采;输出如下所示:
从前面的屏幕截图中,您可以看到这是第三个块。这是正确的,因为我们已经开采了两个区块。在该区块的transactions
阵列中,您可以看到我们获得了上一个区块的采矿奖励。此事务是端口3001
上的节点挖掘上一个块时生成的挖掘奖励。
让我们转到localhost:3001/blockchain
并验证我们刚刚挖掘的这个新区块是否已添加到网络中。您将看到以下响应:
在此屏幕截图中,您可以观察到已挖掘的新块已添加到托管在3001
上的节点。此块的事务数组由上一块的挖掘奖励组成。我们现在在pendingTransactions
数组中还有一个新的采矿奖励,它是在第三个区块被开采时生成的。按照之前使用的类似验证过程,您可以检查我们挖掘的第三个块是否已添加到所有剩余节点
从这些测试来看,/mine
端点似乎正常工作。它正在创建新的区块,并向整个网络广播。这意味着整个网络是同步的,并且具有完全相同的区块链数据,这对于区块链正常工作非常重要。
让我们进一步测试端点。前往邮递员处,创建两个交易,然后广播它们。之后,让我们挖掘一个新区块,看看新交易是否已正确添加到区块链中:
/transaction/broadcast
端点。您可以将此事务数据发送到任何节点,并将其广播到整个网络。在我们的示例中,让我们将此事务发送到端口3002
上的节点:您也可以尝试进行其他交易,就像我们以前做的那样,通过更改金额值以及发件人和收件人的地址。另一个测试是将事务数据发送到不同的节点
3001
,现在让我们刷新它。您应该获得以下输出:从前面的屏幕截图中,您可以看到该节点拥有我们创建的所有三个事务,加上它拥有pendingTransactions
数组中前一个块的挖掘奖励。类似地,您可以验证剩余节点的pendingTransaction
数组。因此,我们可以得出结论,我们创建的所有事务都可以完美地广播到整个网络
现在,让我们挖掘一个新块,以验证是否已将所有挂起的事务添加到新块中。对于本例,让我们通过在新选项卡的地址栏中键入localhost:3003/mine
来挖掘节点3003
上的新块。响应将表明该区块已成功挖掘和广播:
从前面的屏幕截图来看,在transactions
数组中,我们创建的所有事务似乎都存在于新挖掘的块中。让我们转到所有节点,并验证我们创建的事务是否已添加到新块中。在localhost:3001
上,您可以观察到以下输出:
从这个屏幕截图中,我们可以看到,我们现在有第四个块,其中包含我们发送的所有事务。然后,如果您检查pendingTransactions
数组,您可以看到交易数据已被清除,并且其中存在新的挖掘奖励:
在本节中,我们在不同的节点上创建了两个新事务。然后成功地向整个网络广播。然后,我们挖掘了一个新块,我们创建的所有事务都成功地添加到该新块中。除此之外,我们新开采的区块被广播到区块链网络中的所有节点。我们整个网络中的所有节点现在都是同步的,并且都包含相同的区块链数据。
到目前为止,你在这本书中取得了很多成就。您已经创建了一个分布式区块链网络,目前运行在五个节点上,并且您构建了同步整个网络的功能,以便所有节点都具有完全相同的数据。这反映了区块链在现实应用程序中的功能。
在本章中,我们通过重构端点将数据广播到网络中的所有节点,成功地同步了整个区块链网络。我们首先将/createNewTransaction
方法的功能分为两个独立的部分:/createNewTransaction
方法和addTransactionToPendingTransactions
方法。然后,我们构建/transaction/broadcast
端点,将新创建的事务广播到网络中的所有节点。我们还重构了/transaction
端点,以便/transaction/broadcast
端点和/transaction
端点可以一起工作。在本章后面,我们重构了/mine
端点,并构建了一个新端点/receive-new-block
。在这些端点的帮助下,新创建的块可以广播到网络中的所有节点。
在下一章中,我们将构建共识算法,以确保我们网络中的所有节点都能就区块链中要保存的正确数据达成一致。